All articles
solanaed25519precompilesecuritycryptography

Ed25519SigVerify111111111111111111111111111: Solana's signature-verification workhorse

The Ed25519 precompile verifies arbitrary signed messages inside a transaction — no accounts, no CPI, just instruction introspection. The wire format, the sysvar pattern, the security footguns, and when to use it.

Share
devrels.xyz/a/131short link

Solana already verifies Ed25519 signatures on every transaction — that's how fee payers and signers work. But those signatures cover the transaction message. The moment your program needs to verify a signature over arbitrary bytes — a signed airdrop voucher, an oracle price update, an intent signed by a key that isn't a transaction signer — you need Ed25519SigVerify111111111111111111111111111, the Ed25519 precompile.

It's one of the oldest and most-used pieces of Solana's crypto plumbing, and also one of the most misused. Here's the full picture.

What a precompile is

Precompiles aren't BPF programs. They execute natively in the validator, outside the SVM, before your transaction's instructions run. They take no accounts — everything is in instruction data — and if verification fails, the entire transaction fails before any program executes. Solana has three signature-verification precompiles:

text
Ed25519SigVerify111111111111111111111111111   Ed25519 (Solana-native curve)
KeccakSecp256k11111111111111111111111111111   secp256k1 (Ethereum/Bitcoin keys)
Secp256r1SigVerify1111111111111111111111111   secp256r1 / P-256 (passkeys, SIMD-0075)

Same architecture, three curves. The secp256k1 one lets you verify Ethereum-signed messages (bridges use this); the r1 one powers passkey wallets. Everything below about the Ed25519 wire format and introspection pattern applies to all three.

The wire format

One precompile instruction can verify multiple signatures. Instruction data is a count, padding, then an array of offset structs telling the runtime where to find each (signature, pubkey, message) triple:

text
byte 0     num_signatures (u8)
byte 1     padding
bytes 2+   Ed25519SignatureOffsets[num_signatures], each 14 bytes:
             signature_offset              u16  // 64-byte signature
             signature_instruction_index   u16
             public_key_offset             u16  // 32-byte pubkey
             public_key_instruction_index  u16
             message_data_offset           u16
             message_data_size             u16
             message_instruction_index     u16

instruction_index = 0xFFFF  →  "this instruction"
anything else               →  that instruction's data, by index

The instruction-index fields are the interesting part: the signature, pubkey, and message don't have to live in the precompile instruction itself — they can point into any instruction in the transaction. That enables compact layouts (point at data your program instruction already carries) and, as we'll see, it's also where the security bugs live.

Fees: precompile signatures are priced like transaction signatures — each verified signature adds to the transaction's signature fee. Cheap, but not free.

The introspection pattern

Here's the counterintuitive part: your program cannot call the precompile via CPI. Precompiles only run as top-level transaction instructions. So the pattern is always two instructions in one transaction:

text
ix 0   Ed25519SigVerify…   verifies sig(pubkey, message)
ix 1   your program        introspects ix 0, then acts on the message

Your program reads instruction 0 through the Instructions sysvar (Sysvar1nstructions1111111111111111111111111):

rust
use solana_program::sysvar::instructions::{load_instruction_at_checked};

let ix = load_instruction_at_checked(0, &instructions_sysvar)?;

// 1. It must actually be the precompile
require_keys_eq!(ix.program_id, solana_program::ed25519_program::ID);

// 2. Parse the offsets header and verify it references the
//    pubkey + message you expect — not just "a valid signature"
let (pubkey, message) = parse_ed25519_ix_data(&ix.data)?;
require_keys_eq!(pubkey, expected_signer);
require!(message == expected_message, ErrorCode::SigMismatch);

The logic: if your program's instruction is executing at all, the precompile instruction in the same transaction must have succeeded (a failed precompile kills the whole transaction). So "the precompile ran, on this exact pubkey and message" implies "this signature is valid." Your program never does curve math — it does bookkeeping.

The footguns

Every one of these is a real exploit class, not a theoretical one:

  • Checking that a precompile ran, but not what it verified. The attacker submits a valid signature from their own key over their own message. If your program only checks "instruction 0 is the Ed25519 program," you approved it. You must parse the offsets and compare pubkey and message byte-for-byte.
  • Trusting offsets blindly. The attacker builds the precompile instruction, so they control every offset — including instruction indexes pointing at other instructions. Parse defensively: exact header sizes, expected num_signatures, offsets pointing where your layout says they must (most programs require 0xFFFF/self-contained data and reject anything else).
  • Hardcoding instruction position without checking. Use load_instruction_at_checked against the sysvar and verify the program id — don't assume "the instruction before mine" is the precompile. A transaction can contain arbitrary extra instructions.
  • No replay protection. The precompile proves the message was signed — not that it was signed for this transaction. A signed voucher without a nonce/expiry/ domain-separator can be replayed forever. Put a nonce or a one-time account in the signed message and mark it consumed.
  • Verifying in-program instead. Doing Ed25519 verification inside your program with a dalek-style crate costs on the order of tens of thousands of CUs (~11k for the curve work plus ~8k per SHA-512 block of message) and bloats your program. The precompile exists so you don't do this.

What people build with it

  • Signed vouchers / claims — backend signs "wallet X may claim Y," program verifies via precompile. No per-user on-chain allowlist needed.
  • Oracle-style attestations — any off-chain service with an Ed25519 key can feed signed data to programs without being a transaction signer.
  • Relayed intents / session keys — user signs a message once; a relayer submits it. Same skeleton as the passkey smart-wallet pattern, just on Solana's native curve.
  • Cross-system verification — anything that signs Ed25519 off-chain (TEEs, other chains, hardware) can be verified on Solana for the price of a signature fee.

TL;DR

  • Ed25519SigVerify111… verifies signatures over arbitrary messages; transaction signatures only cover the transaction. One of three curve precompiles (k1 for Ethereum keys, r1 for passkeys).
  • Not CPI-able. The pattern is: precompile instruction + your instruction, glued by the Instructions sysvar.
  • The precompile proves "a valid signature exists in this transaction." Your program must prove it's the right signer, the right message, and not a replay — that's where audits find bugs.
  • Never verify Ed25519 in-program; the precompile does it at native speed for a signature fee.

Keep reading

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

Ed25519SigVerify111111111111111111111111111: Solana's signature-verification workhorse | devrels.xyz