Skip to main content
Technical proposal SIMD-0152 Activated

Precompiles

  1. Idea
  2. Draft
  3. Review
  4. Accept
  5. Implement
  6. Active

Decision brief

Why this proposal matters

Define a unified behavior for precompiles, and highlight a few minor changes to the existing precompiles to minimize differences.

Source-backed reading aid · source language: EN

Proposal at a glance

What changes

  • If instruction data is empty, return error.
  • The first byte of data is the number of signatures num_signatures.
  • If num_signatures is 0, return error.

Stakeholder map

Who is affected

Builders & client teams Medium impact

Reduce the complexity of existing precompiles, to simplify building different validator clients.

Action requirement unknown
Validators & operators Medium impact

Reduce the complexity of existing precompiles, to simplify building different validator clients.

Action requirement unknown
Users & stakers Impact unknown

No proposal-specific evidence was found for this group.

Action requirement unknown
Governance & ecosystem Impact unknown

No proposal-specific evidence was found for this group.

Action requirement unknown

Exact source revision

Full proposal document

Source-language document. Technical identifiers and evidence remain unchanged. 0616093b2952

Summary

Define a unified behavior for precompiles, and highlight a few minor changes to the existing precompiles to minimize differences.

Motivation

Precompiles are special native programs designed to verify additional transaction signatures. They run without the VM and without loading any account, and they can access data from other instructions, within the same transaction.

At the time of writing, two precompiles exist to verify Ed25519 and Ethereum-like Secp256k1 signatures, and another one is being proposed to support Secp256r1 signatures for FIDO Passkeys.

Historically, the two precompiles were built at different times and by different people, so naturally there are some subtle differences in how they behave, especially in edge cases.

The main goal of this document is to provide a specification for how a precompile should behave, remove differences and provide guidelines for future proposals.

In addition, we highlight 3 minor changes to the existing precompiles that will simplify their behavior and make it easier to develop alternative validator clients.

Alternatives Considered

Leave as is.

New Terminology

n/a

Detailed Design

We first propose a specification for precompiles. Then we propose changes to the existing precompiles with respect to the specification.

Precompile Specification

Precompiles are special native programs designed to verify additional signatures. Each precompile consists of a single verify instruction.

Precompiles are executed right after transaction signature verification, they run without the VM and without loading any account. From a cost perspective, they're included in the transaction fee (each signature to verify counts as a transaction signature), but don't require any compute units.

If a transaction contains more than 8 precompile signatures, it must fail.

MAX_ALLOWED_PRECOMPILE_SIGNATURES = 8

The precompile instruction verify accepts the following data:

struct PrecompileVerifyInstruction {
  num_signatures:  u8,                       // Number of signatures to verify
  padding:         u8,                       // Single byte padding
  offsets:         PrecompileOffsets[],      // Array of `num_signatures` offsets
  additionalData?: Bytes,                    // Optional additional data, e.g.
                                             // signatures included in the same
                                             // instruction
}

struct PrecompileOffsets {
  signature_offset: u16 LE,                  // Offset to signature (offset within
                                             // the specified instruction data)
  signature_instruction_index: u16 LE,       // Instruction index to signature
  public_key_offset: u16 LE,                 // Offset to public key
  public_key_instruction_index: u16 LE,      // Instruction index to  public key
  message_offset: u16 LE,                    // Offset to start of message data
  message_length: u16 LE,                    // Size of message data
  message_instruction_index: u16 LE,         // Instruction index to message
}

The behavior of the precompile instruction verify is as follow:

  1. If instruction data is empty, return error.
  2. The first byte of data is the number of signatures num_signatures.
  3. If num_signatures is 0, return error.
  4. Expect (enough bytes of data for) num_signatures instances of PrecompileOffsets.
  5. For each signature: a. Read offsets: an instance of PrecompileOffsets b. Based on the offsets, retrieve signature, public_key, and message bytes. If any of the three fails, return error. c. Invoke the actual sigverify function. If it fails, return error.

To retrieve signature, public_key, and message:

  1. Get the instruction_index-th instruction_data
    • The special value 0xFFFF means "current instruction"
    • If the index is invalid, return Error
  2. Return length bytes starting from offset
    • If this exceeds the instruction_data length, return Error

Note that fields (offsets) can overlap, for example the same public key or message can be referred to by multiple instances of PrecompileOffsets.

If the precompile verify function returns any error, the whole transaction should fail. Therefore, the type of error is irrelevant and is left as an implementation detail.

In pseudo-code:

fn verify() {
  if data_length == 0 {
    return Error
  }
  num_signatures = data[0]
  if num_signatures == 0 {
    return Error
  }
  if data_length < (2 + num_signatures * size_of_offsets) {
    return Error
  }

  all_tx_data = { data, instruction_datas }

  data_position = 2
  for i in 0..num_signatures {
    offsets = (PrecompileOffsets)
      data[data_position..data_position+size_of_offsets]
    data_position += size_of_offsets

    signature = get_data_slice(all_tx_data,
                               offsets.signature_instruction_index,
                               offsets.signature_offset
                               signature_length)
    if !signature {
      return Error
    }

    public_key = get_data_slice(all_tx_data,
                                offsets.public_key_instruction_index,
                                offsets.public_key_offset,
                                public_key_length)
    if !public_key {
      return Error
    }

    message = get_data_slice(all_tx_data,
                             offsets.message_instruction_index,
                             offsets.message_offset
                             offsets.message_length)
    if !message {
      return Error
    }

    // sigverify includes validating signature and public_key
    result = sigverify(signature, public_key, message)
    if result != Success {
      return Error
    }
  }
  return Success
}

fn get_data_slice(all_tx_data, instruction_index, offset, length) {
  // Get the right instruction_data
  if instruction_index == 0xFFFF {
    instruction_data = all_tx_data.data
  } else {
    if instruction_index >= num_instructions {
      return Error
    }
    instruction_data = all_tx_data.instruction_datas[instruction_index]
  }

  start = offset
  end = offset + length
  if end > instruction_data_length {
    return Error
  }

  return instruction_data[start..end]
}

Changes to Ed25519SigVerify111111111111111111111111111

Summary.

  • Change #1. Replace sigverify function with Dalek strict_verify(), the same used for transactions sigverify.

  • Change #2. Implement step 3 above: "If num_signatures is 0, return error."

Context.

In Solana, transactions use Ed25199 signatures, and are validated using the so called strict verify. Compared to "RFC verify", strict verify enforces extra checks against (certain types of) malleability.

The Ed25199 precompile currently implements a non-strict verify, so with Change #1 we'll make it compatible with the way Solana verifies signatures.

Moreover, the Ed25519 precompile accepts a payload of [0, 0] as valid (for no good reason), so Change #2 will prevent this anomaly.

Finally, the Ed25519 precompile interleaves retrieving instruction data and parsing data types such as signatures and public keys. While this goes against this specification and creates unnecessary complexity in the return error code, we recommend to NOT change the internal behavior (as the return error code doesn't really matter).

FAQ.

  • Q: Why does the Ed25199 precompile currently use verify instead of strict_verify? A: No good reason, it was built without noticing the difference.

  • Q: If we switch to strict_verify, will some of the existing signatures break verification? A: All signatures created by a "regular" library, i.e. following RFC, pass both verify and strict_verify. Only carefully crafted signatures can pass verify and not strict_verify. So this won't break any honest use case.

  • Q: Why not leaving it as is? A: The verify is not well specified. In fact, it's behavior is slightly different in the older version of Dalek that Solana currently uses, versus the latest version of the same library. Trying to replicate all the edge cases is different validators is an unnecessary effort, not worth the risk of exposing different behaviors.

Changes to KeccakSecp256k11111111111111111111111111111

Summary.

  • Change #3. Implement step 3 above: "If num_signatures is 0, return error."

Context.

The KeccakSecp256k1 precompile currently accepts an input of [0], as in "verify 0 signatures", which is a useless instruction. With Change #3 we'll avoid this anomaly.

We note that the KeccakSecp256k1 precompile has a slightly different struct for offset, with instruction indexes of a single byte (and, as a result, no special value of 0xFFFF to indicate the "current instruction"). This is for historical reasons, and since modifying it would break existing users, we recommend to NOT change the existing behavior.

Impact

Reduce the complexity of existing precompiles, to simplify building different validator clients.

Security Considerations

All 3 changes are straightforward and have no impact on security.

Backwards Compatibility

All 3 changes require a feature gate.

Evidence graph

Related proposals and rollout

One or more sources are unavailable.

Upstream review record

Upstream discussion & review

GitHub review is editorial context, not evidence of on-chain support, voting, or outcome.

PR #152 · merged SIMD-0152: Precompiles 20 comments and reviews · Feb 2, 2026
@topointon-jump

Should we do these in one feature gate, or 3 separate ones? I think a single feature gate is probably easier for users to track.

GitHub ↗
@topointon-jump

Could we add the the reason behind the number 8 to this SIMD?

GitHub ↗
@0x0ece

No reason. 8 is what's implemented.

GitHub ↗
@0x0ece

🤷 It's called this way in the code, I don't think it's worth changing everywhere in the doc.

GitHub ↗
@0x0ece

There's no alignment, this is in code, not in vm. Or am I missing something? Added note on size.

GitHub ↗
@samkim-crypto

@jacobcreech, it seems like we have reached consensus, but I cannot seem to merge it. Can you help with merging 🙏 ?

GitHub ↗
@lheeger-jump

Yay great work @0x0ece!

GitHub ↗

simd.watch community discussion · SIMD-0152

Powered by Giscus · Sign in with GitHub to comment

Community comments load when this section approaches the viewport.

Provenance

Evidence & technical details

Rollout or chain data, source revisions, freshness and integrity.
2

SIMD

Deployment Status

Document lifecycleActivated
CategoryCore protocol
Devnetactive Epoch 791
Testnetactive Epoch 707
Mainnet-Betaactive Epoch 714
ed9tNscbWLYBooxWA7FE2B5KHWs8A6sxfY8EzezEcoo

Exact source revision

Sources & integrity

  • Proposal document pinned_commit_blob
    0616093b2952ed6de52c4a27d66aadd11d48d4f9
  • simd-document document · stale · Sep 10, 2026
    0616093b2952ed6de52c4a27d66aadd11d48d4f9

One or more sources are unavailable.