SIGNATURES — Solana × secp256r1 Precompile

Passkeys on Solana: Why Half of WebAuthn Signatures Fail

6 min read
SolanaPasskeysWebAuthn

A passkey wallet is the obvious future: no seed phrase, a thumbprint or a face, the device’s secure chip holding the key. Solana made it possible by adding native verification for the curve those chips use — P-256, called secp256r1 in the code — as a precompile, the same way it verifies Ethereum-style secp256k1 signatures.

Then developers wired it up and found that roughly every second signature failed. Nothing was wrong with the authenticator and nothing was wrong with the chain. The two disagree about what a valid signature looks like, and the chain’s rule is the stricter one. Here are the rules the precompile enforces, what each verification costs you in fee and block space, and the one-line fix for the failures.


What the precompile checks, in order

A secp256r1 instruction carries a count byte, a reserved byte, an offsets table, and then the signatures, public keys and messages it references. The verifier reads it in this order:

The count is data[0]. Zero is rejected. More than eight is rejected. That is the hard cap per instruction; a transaction needing more signatures needs more instructions.

data[1] is read by nobody. The source says so in a comment: “we do not check or use the byte at data[1]”. It is one free byte per instruction that the consensus ignores.

Each signature is checked for low S. The S component must be at most half the curve order; anything above is rejected as an invalid signature, even though it is mathematically valid.

Each public key is validated as a point on the curve, and the signature is verified against the message through OpenSSL.


Why half of them fail: high S

An ECDSA signature has two halves, R and S, and for any valid S there is a second valid value, the curve order minus S. Both verify. One is “low” (at most half the order) and one is “high”. A signer that does not normalise will produce each about half the time.

Bitcoin and Ethereum reject high-S signatures to prevent malleability — the ability to produce a second, different-looking signature for the same message — and Solana’s precompile does the same. WebAuthn authenticators do not normalise. They return whichever half the hardware produced, and about half the time that is the high one.

The fix is one line, and it belongs in the client before the transaction is built: if S is greater than half the order, replace it with the order minus S. The signature stays valid, the authenticator is none the wiser, and the precompile accepts it every time. A passkey wallet that does not do this will fail roughly half its transactions and blame the network.


What each signature costs

Two costs, charged in different places.

  • Fee. Every precompile signature counts as a signature for the base fee. The fee calculation sums transaction signatures, ed25519 signatures, secp256k1 signatures and secp256r1 signatures, and multiplies the total by the per-signature rate. A transaction with one fee-payer signature and one passkey signature pays two signatures’ worth of base fee — 10,000 lamports at the default rate, not 5,000.
  • Block space. The cost model charges 4,800 compute units per secp256r1 verification — 160 microseconds at the 30-units-per-microsecond ratio the model uses. For comparison an ed25519 precompile signature costs 2,400 and a secp256k1 one 6,690. Those units are added to your transaction’s cost, which is the denominator in the leader’s ranking formula, so a passkey transaction ranks lower than an identical ed25519 one at the same priority fee.

The eight-per-instruction cap interacts with the cost: the verifier sets up its curve context once per instruction, so eight signatures in one instruction cost less validator time than eight instructions of one signature, even though the cost model charges the same.


How a passkey transaction is actually built

The precompile verifies a signature; it does not authorise anything by itself. The pattern that makes a passkey a wallet is:

A smart-wallet program owns the user’s assets and stores the user’s P-256 public key.

A transaction carries a secp256r1 precompile instruction with the passkey signature over a message, followed by an instruction to the smart-wallet program.

The smart-wallet program reads the precompile instruction from the transaction’s instruction sysvar, confirms it verified the expected public key over the expected message, and only then moves assets.

A relayer or the user pays the fee with an ordinary ed25519 key, because the fee payer must still be a Solana keypair. That is why the base fee counts two signatures.

Two consequences follow. The passkey never signs a Solana transaction directly — it signs a message the program checks — so replay protection is the program’s job, not the runtime’s. And a transaction that references the wrong instruction index, or a message the program does not expect, fails in the program, not in the precompile.


The limits you will hit first

Eight signatures per instruction, and the instruction count and size limits of the transaction itself. A P-256 signature is 64 bytes, a compressed public key 33, and the message whatever the program expects — WebAuthn messages include the authenticator data and client data hash, which are not small. In a 1,232-byte legacy transaction, two or three passkey signatures fill it. Transaction v1’s 4,096-byte envelope is the comfortable home for multi-signature passkey flows.

The precompile is feature-gated, and on mainnet the gate has been active since epoch 800. Check its state on any other cluster you target before you ship a wallet that depends on it.


Passkeys: Questions People Actually Ask

Does Solana support passkeys natively?

Solana verifies P-256 (secp256r1) signatures through a precompile, which is the curve passkeys and WebAuthn authenticators use. A smart-wallet program reads the verified signature and decides what it authorises; the precompile is feature-gated, so check its state on your target cluster.

Why do my WebAuthn signatures fail on Solana?

The precompile rejects high-S signatures. Authenticators produce high S about half the time. Normalise S to the low half (order minus S when S exceeds half the order) before submitting.

How much does a passkey signature cost on Solana?

A full signature’s share of the base fee, plus 4,800 compute units of block-space cost per verification. A transaction with a fee payer and one passkey pays two signatures of base fee.

How many passkey signatures fit in one instruction?

Eight. The precompile rejects a count of zero or more than eight; additional signatures need additional instructions, and the transaction size limit applies.


The checklist for a passkey integration

Normalise S to the low half before building the transaction. This alone removes the “random” failures.

Budget two signatures of base fee per passkey transaction, and 4,800 compute units of block space per verification.

Keep to eight signatures per precompile instruction and read the transaction size as you add them.

Verify, in your program, that the precompile instruction you are trusting checked the key and message you expect. The precompile proves a signature exists; your program decides what it means.

Each passkey signature is block space like any other — the ranking formula counts its 4,800 units — and signed approvals that must wait for a human pair naturally with a durable nonce. Verification data fits more comfortably in a 4,096-byte v1 transaction.

Constants read from the Agave validator source at commit beee69b958: precompiles/src/secp256r1.rs, cost-model/src/block_cost_limits.rs and fee/src/lib.rs. The enable_secp256r1_precompile gate has been active on mainnet since slot 345,600,000, the first slot of epoch 800.

xroot.devOn-chain research, published in full
𝕏 @xrootdev
← Back to all notes