Skip to content

Support native secp256r1 and secp256k1 signature precompiles #668

@edo-chan

Description

@edo-chan

Summary

Surfpool currently appears to reject Solana's native signature verification precompiles for both secp256r1 and secp256k1. Transactions that begin with either precompile fail during simulation with UnsupportedProgramId before any downstream program instruction is executed.

This blocks local e2e testing for wallets or auth flows that use passkeys / WebAuthn (Secp256r1SigVerify1111111111111111111111111) or EVM signatures (KeccakSecp256k11111111111111111111111111111).

Environment

  • Surfpool image: surfpool/surfpool:latest
  • surfnet-version: 1.1.1
  • solana-core: 3.1.6
  • feature-set: 2086771155
  • Started with mainnet fork mode:
    surfpool start --port 8899 --ws-port 8900 --host 0.0.0.0 --network mainnet --no-tui --no-studio
  • Also reproduced with devnet mode and --features-all.

Reproduction

  1. Start Surfpool locally.
  2. Build and simulate a transaction whose first instruction is a valid native signature precompile instruction.
  3. Sign the transaction with a normal Solana fee payer.
  4. Simulate or send the transaction to Surfpool.

For secp256k1, a minimal JS repro is:

import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  Secp256k1Program,
  Transaction,
} from "@solana/web3.js";
import { Wallet, keccak256 } from "ethers";

const connection = new Connection("http://localhost:8899", "confirmed");
const payer = Keypair.generate();
const airdropSig = await connection.requestAirdrop(payer.publicKey, LAMPORTS_PER_SOL);
await connection.confirmTransaction(airdropSig, "confirmed");

const wallet = Wallet.createRandom();
const message = Buffer.from("surfpool secp256k1 precompile check");
const digest = keccak256(message);
const sig = wallet.signingKey.sign(digest);
const signature = Buffer.concat([
  Buffer.from(sig.r.slice(2), "hex"),
  Buffer.from(sig.s.slice(2), "hex"),
]);
const ix = Secp256k1Program.createInstructionWithEthAddress({
  ethAddress: Buffer.from(wallet.address.slice(2), "hex"),
  message,
  signature,
  recoveryId: Number(sig.yParity),
});

const blockhash = await connection.getLatestBlockhash("confirmed");
const tx = new Transaction({ feePayer: payer.publicKey, recentBlockhash: blockhash.blockhash }).add(ix);
tx.sign(payer);
console.log(ix.programId.toBase58());
console.log(await connection.simulateTransaction(tx));

Actual result

For secp256k1:

programId KeccakSecp256k11111111111111111111111111111
simulate err: InstructionError [0, "UnsupportedProgramId"]

For secp256r1, using a valid Secp256r1SigVerify1111111111111111111111111 instruction produced the same failure:

Transaction simulation failed: Error processing Instruction 0: Unsupported program id

Expected result

Surfpool should dispatch these native precompile programs the same way a Solana validator runtime does, so valid secp256r1/secp256k1 verification instructions can pass simulation and execution.

Forking mainnet/devnet does not work around this because these are native runtime precompiles, not BPF program accounts that can be cloned from an upstream cluster.

Why this matters

Without these precompiles, local Surfpool e2e tests cannot cover:

  • passkey/WebAuthn wallet authorities using secp256r1 verification
  • EVM wallet authorities using secp256k1 verification
  • downstream flows where the first instruction is a native signature verify precompile and the second instruction consumes the instructions sysvar

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions