Skip to content

solana-program/secp256k1

Repository files navigation

solana-secp256k1-program: on-chain signature verification for Solana

A minimal Solana SBF program that verifies secp256k1 ECDSA signatures on-chain without adding any new runtime syscalls.

Motivation

The goal is to migrate the native secp256k1 precompile to SBF so that it can be maintained and deployed like any other on-chain program. The instruction format is intentionally identical to the precompile, including parts that are not intuitive for general-purpose use, so that tools and clients built around the precompile require no changes.

The program is intended to run only as a transaction-level instruction. It rejects cross-program invocations (CPI) using sol_get_stack_height() so that programs cannot treat this verifier as a CPI authorization oracle.

Syscalls used

Verification relies only on existing Solana runtime syscalls:

Syscall SDK wrapper
sol_keccak256 solana_keccak_hasher::hash
sol_secp256k1_recover solana_secp256k1_recover::secp256k1_recover
sol_get_stack_height solana_define_syscall::definitions::sol_get_stack_height

Instruction format

[0]                   number of signatures (u8)
[1 .. 1 + 11*N]       N × SecpSignatureOffsets records (11 bytes each, LE)
[1 + 11*N ..]         payload: signatures, addresses, messages (order flexible)

Each 11-byte offset record matches SecpSignatureOffsets exposed by this crate:

[0..2]    signature_offset        - byte position of 64-byte r||s + 1-byte recovery id
[2]       signature_instruction_index
[3..5]    eth_address_offset      - byte position of 20-byte Ethereum address
[5]       eth_address_instruction_index
[6..8]    message_data_offset     - byte position of the raw message
[8..10]   message_data_size
[10]      message_instruction_index

Constraints

  • All instruction-index fields must be 0. An SBF program receives only its own instruction data; cross-instruction references require a future runtime change.
  • Recovery id must be 03. Values 2/3 are accepted for compatibility with legacy Solana secp256k1 instruction data and are passed through to recovery, where overflowing signatures generally fail as InvalidArgument. Ethereum-style 27/28 offsets are rejected at the wire level.
  • Zero-signature payloads (count == 0) are accepted only when the buffer is exactly 1 byte. Any trailing bytes are treated as malformed.
  • No accounts. The program takes no account arguments and returns InvalidArgument if any are supplied.
  • No CPI. The program returns InvalidArgument when stack height is greater than the transaction-level height (1).

Hashing

The program Keccak-256 hashes message before recovering the public key. Pass the exact bytes that should be hashed for your signing scheme. For personal_sign, include the "\x19Ethereum Signed Message:\n{len}" prefix; for EIP-712 typed data, pass "\x19\x01" || domain_separator || struct_hash (66 bytes total). The program hashes those bytes for you. Passing the 32-byte final digest would verify keccak256(digest), causing valid typed-data signatures to fail.

Cargo features

Feature Default Description
bincode off Enables SDK-compatible instruction construction helpers.
dev-context-only-utils off Backward-compatible alias for bincode, matching the upstream helper crate feature.
no-entrypoint off Omits the program entrypoint; use when embedding the crate in another program or in tests that call process_instruction directly.
custom-heap off Reserved for callers that provide a custom heap allocator.
serde off Derives serde traits for SecpSignatureOffsets.

Public API

The SDK helpers and layout constants are exposed from solana_secp256k1_program:

use solana_secp256k1_program::{
    eth_address_from_pubkey, eth_address_from_sec1_pubkey,
    new_secp256k1_instruction_with_signature, try_new_secp256k1_instruction_with_signature,
    SecpSignatureOffsets,
};

new_secp256k1_instruction_with_signature keeps the upstream SDK return type (Instruction) for source compatibility. Prefer try_new_secp256k1_instruction_with_signature when callers need construction errors instead of panics for invalid offsets, oversized messages, or invalid recovery ids.

Build and test

Stable Rust 1.93.1 is pinned in rust-toolchain.toml. Some make targets also require the nightly Rust chain nightly-2026-01-22 (clippy, format-check, rustdoc, feature-powerset).

# Unit tests (host, no SBF toolchain required)
cargo test --manifest-path program/Cargo.toml

# SBF build only
cargo build-sbf --no-rustup-override --arch v3 --manifest-path program/Cargo.toml

# SBF build via Makefile
make build-sbf-program

# Host unit tests, then SBF integration tests via Mollusk
make test-program

# Print Mollusk compute-unit measurements for the SBF program
make cu-program

# Lint / format
make clippy-program
make format-check-program

The Mollusk tests in program/tests/mollusk.rs execute the built target/deploy/solana_secp256k1_program.so artifact and report compute_units_consumed for one-signature and two-signature verification paths. Plain cargo test --manifest-path program/Cargo.toml still runs the host tests without requiring the SBF Rust chain; the Mollusk tests skip themselves unless SBF_OUT_DIR is set.

About

Solana program for secp256k1 curve operations and signature verification

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors