From 1f252e12e83fa0685a646f5018a91e0d44699028 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 14:57:03 -0400 Subject: [PATCH 1/9] refactor --- Cargo.lock | 55 +++++- Cargo.toml | 7 +- README.md | 45 ++++- ed25519-verify/Cargo.toml | 30 +++ .../src/instruction.rs | 44 +---- .../src/instruction_data.rs | 0 ed25519-verify/src/lib.rs | 44 +++++ {program => ed25519-verify}/src/scalar.rs | 0 ed25519-verify/src/verifier.rs | 121 ++++++++++++ ed25519-verify/tests/common/mod.rs | 137 +++++++++++++ .../tests/verify_instruction.rs | 98 +++------- program/Cargo.toml | 23 +-- program/src/lib.rs | 180 +++--------------- program/src/processor.rs | 115 +---------- program/tests/common/mod.rs | 2 +- program/tests/mollusk.rs | 30 ++- 16 files changed, 529 insertions(+), 402 deletions(-) create mode 100644 ed25519-verify/Cargo.toml rename {program => ed25519-verify}/src/instruction.rs (92%) rename {program => ed25519-verify}/src/instruction_data.rs (100%) create mode 100644 ed25519-verify/src/lib.rs rename {program => ed25519-verify}/src/scalar.rs (100%) create mode 100644 ed25519-verify/src/verifier.rs create mode 100644 ed25519-verify/tests/common/mod.rs rename program/tests/process_instruction.rs => ed25519-verify/tests/verify_instruction.rs (72%) diff --git a/Cargo.lock b/Cargo.lock index 3d38494..03b0aab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1569,6 +1569,19 @@ dependencies = [ "num", ] +[[package]] +name = "pinocchio" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cababcb62a2e739c7078ae16eda02789a6e3edd21bbdded864e85c38a7914d" +dependencies = [ + "solana-account-view", + "solana-address 2.6.0", + "solana-define-syscall 5.1.0", + "solana-instruction-view", + "solana-program-error", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2003,6 +2016,16 @@ dependencies = [ "solana-program-memory", ] +[[package]] +name = "solana-account-view" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc141b940560430425ebaadb7645496c45f6a10fad9911d719bd03eab7f4d422" +dependencies = [ + "solana-address 2.6.0", + "solana-program-error", +] + [[package]] name = "solana-address" version = "1.1.0" @@ -2192,18 +2215,28 @@ checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" name = "solana-ed25519-program" version = "4.0.0" dependencies = [ - "bincode", "ed25519-dalek", "mollusk-svm", + "pinocchio", + "solana-account", + "solana-ed25519-verify", + "solana-instruction", + "solana-program-runtime", + "solana-pubkey 4.1.0", + "solana-sha512-hasher", +] + +[[package]] +name = "solana-ed25519-verify" +version = "4.0.0" +dependencies = [ + "bincode", + "ed25519-dalek", "serde", "serde_derive", - "solana-account-info", "solana-curve25519", "solana-instruction", - "solana-program-entrypoint", "solana-program-error", - "solana-program-runtime", - "solana-pubkey 4.1.0", "solana-sdk-ids", "solana-sha512-hasher", ] @@ -2308,6 +2341,18 @@ dependencies = [ "solana-program-error", ] +[[package]] +name = "solana-instruction-view" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab7a27d0c4214b9f7389c3dd00b68c93093a67f1dcc5b7893aebe299bbcbb47" +dependencies = [ + "solana-account-view", + "solana-address 2.6.0", + "solana-define-syscall 5.1.0", + "solana-program-error", +] + [[package]] name = "solana-instructions-sysvar" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index 234c520..b17ac6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["program"] +members = ["ed25519-verify", "program"] resolver = "2" [workspace.lints.rust] @@ -13,12 +13,13 @@ unexpected_cfgs = { level = "warn", check-cfg = [ bincode = "1.3.3" ed25519-dalek = "2.1.1" mollusk-svm = "0.13.1" +pinocchio = "0.11.2" serde = { version = "1.0.228", default-features = false } serde_derive = "1.0.228" -solana-account-info = "3.1.1" +solana-account = "3.4.0" solana-curve25519 = "4.0.1" +solana-ed25519-verify = { path = "ed25519-verify", version = "4.0.0", default-features = false } solana-instruction = "3.3.0" -solana-program-entrypoint = "3.1.1" solana-program-error = "3.0.1" solana-program-runtime = { version = "4.0.0", features = ["agave-unstable-api"] } solana-pubkey = "4.1.0" diff --git a/README.md b/README.md index 79e968f..4a19252 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ -# solana-ed25519-program: on-chain signature verification for Solana +# solana-ed25519: on-chain signature verification for Solana -A minimal Solana SBF program that re-verifies Ed25519 signatures on-chain using -the Curve25519 and SHA-512 syscalls. +This workspace provides two crates: + +- `solana-ed25519-verify`: a no-std, stateless Ed25519 verification library. +- `solana-ed25519-program`: a minimal Pinocchio SBF program that calls the + library and returns an explicit pass/fail result. + +Both use the Curve25519 and SHA-512 syscalls. The program entrypoint uses +Pinocchio's lazy instruction context to avoid up-front account parsing. ## Motivation @@ -10,10 +16,10 @@ maintained and deployed like any other on-chain program. The instruction format is intentionally identical to the precompile for current-instruction data, so clients can reuse the standard Ed25519 instruction layout. -Being a regular SBF program also unlocks CPI: another program can invoke this -one and act on the explicit pass/fail result, rather than relying on -`sysvar::instructions` inspection to confirm a parallel precompile instruction -succeeded. +Programs can either depend on `solana-ed25519-verify` directly or invoke +`solana-ed25519-program` by CPI and act on the explicit pass/fail result, +rather than relying on `sysvar::instructions` inspection to confirm a parallel +precompile instruction succeeded. [ed25519 precompile]: https://docs.solanalabs.com/runtime/programs#ed25519-program @@ -70,6 +76,29 @@ Each offset record matches `Ed25519SignatureOffsets` exposed by this crate: - **No accounts.** The program takes no account arguments and returns `InvalidArgument` if any are supplied. +## Cargo features + +| Feature | Default | Description | +|---|---|---| +| `instruction` | off | Enables alloc-based `Instruction` construction helpers. | +| `bincode` | off | Backward-compatible alias for SDK-style helper APIs; also enables `instruction`. | +| `dev-context-only-utils` | off | Backward-compatible alias for `bincode`, matching upstream helper crates. | +| `serde` | off | Derives serde traits for `Ed25519SignatureOffsets`. | + +`solana-ed25519-program` only exposes `no-entrypoint`, which omits the +Pinocchio entrypoint when embedding the program crate in tests or another +program. + +## Public API + +`solana-ed25519-verify` exposes the stateless `Ed25519Verifier`, layout +constants, `Ed25519SignatureOffsets`, and, with the `instruction` feature, +SDK-compatible instruction constructors such as +`new_ed25519_instruction_with_signature` and +`try_new_ed25519_instruction_with_signature`. + +`solana-ed25519-program` calls the library from its Pinocchio processor. + ## Build and test Stable Rust `1.93.1` is pinned in `rust-toolchain.toml`. Some make targets @@ -77,7 +106,7 @@ also require the nightly Rust chain `nightly-2026-01-22`. ```sh # Unit tests (host, no SBF toolchain required) -cargo test --manifest-path program/Cargo.toml +cargo test --workspace # SBF build only cargo build-sbf --arch v2 --manifest-path program/Cargo.toml diff --git a/ed25519-verify/Cargo.toml b/ed25519-verify/Cargo.toml new file mode 100644 index 0000000..e8ae48c --- /dev/null +++ b/ed25519-verify/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "solana-ed25519-verify" +version = "4.0.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[features] +default = [] +bincode = ["dep:bincode", "instruction"] +dev-context-only-utils = ["bincode"] +instruction = ["dep:solana-instruction", "dep:solana-sdk-ids", "serde"] +serde = ["dep:serde", "dep:serde_derive"] + +[dependencies] +bincode = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_derive = { workspace = true, optional = true } +solana-curve25519 = { workspace = true } +solana-instruction = { workspace = true, optional = true } +solana-program-error = { workspace = true } +solana-sdk-ids = { workspace = true, optional = true } +solana-sha512-hasher = { workspace = true } + +[target.'cfg(not(any(target_os = "solana", target_arch = "bpf")))'.dependencies] +ed25519-dalek = { workspace = true } + +[lints] +workspace = true diff --git a/program/src/instruction.rs b/ed25519-verify/src/instruction.rs similarity index 92% rename from program/src/instruction.rs rename to ed25519-verify/src/instruction.rs index 5371128..1d13dce 100644 --- a/program/src/instruction.rs +++ b/ed25519-verify/src/instruction.rs @@ -3,16 +3,8 @@ #[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -use solana_instruction::Instruction; -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -use solana_program_error::ProgramError; +#[cfg(feature = "instruction")] +use {alloc::vec, solana_instruction::Instruction, solana_program_error::ProgramError}; pub const PUBKEY_SERIALIZED_SIZE: usize = 32; pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; @@ -54,10 +46,7 @@ pub fn sign_message( signing_key.sign(message).to_bytes() } -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] +#[cfg(feature = "instruction")] /// Encode just the signature offsets in a single ed25519 instruction. /// /// This preserves the upstream SDK helper API by returning [`Instruction`] @@ -72,10 +61,7 @@ pub fn offsets_to_ed25519_instruction(offsets: &[Ed25519SignatureOffsets]) -> In try_offsets_to_ed25519_instruction(offsets).expect("invalid ed25519 instruction offsets") } -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] +#[cfg(feature = "instruction")] /// Encode just the signature offsets in a single ed25519 instruction with /// checked inputs. /// @@ -108,10 +94,7 @@ pub fn try_offsets_to_ed25519_instruction( }) } -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] +#[cfg(feature = "instruction")] /// Builds a single-signature ed25519 instruction. /// /// This preserves the upstream SDK helper API by returning [`Instruction`] @@ -131,10 +114,7 @@ pub fn new_ed25519_instruction_with_signature( .expect("invalid ed25519 instruction inputs") } -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] +#[cfg(feature = "instruction")] /// Builds a single-signature ed25519 instruction with checked inputs. /// /// Returns an error if the message length or any offset cannot be represented @@ -199,10 +179,7 @@ pub fn try_new_ed25519_instruction_with_signature( }) } -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] +#[cfg(feature = "instruction")] fn serialize_signature_offsets( output: &mut [u8], offsets: &Ed25519SignatureOffsets, @@ -222,13 +199,10 @@ fn serialize_signature_offsets( Ok(()) } -#[cfg(all( - test, - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] +#[cfg(all(test, feature = "instruction"))] mod tests { use super::*; + use alloc::vec; fn read_first_offsets(input: &[u8]) -> Ed25519SignatureOffsets { Ed25519SignatureOffsets { diff --git a/program/src/instruction_data.rs b/ed25519-verify/src/instruction_data.rs similarity index 100% rename from program/src/instruction_data.rs rename to ed25519-verify/src/instruction_data.rs diff --git a/ed25519-verify/src/lib.rs b/ed25519-verify/src/lib.rs new file mode 100644 index 0000000..78ca6cf --- /dev/null +++ b/ed25519-verify/src/lib.rs @@ -0,0 +1,44 @@ +#![no_std] + +//! Stateless Ed25519 verification utilities for Solana programs. +//! +//! This crate contains the reusable verifier and instruction-data helpers used +//! by `solana-ed25519-program`. It is intended for programs that want to verify +//! Ed25519 signatures directly without invoking the standalone verifier +//! program. +//! +//! Instruction data mirrors the native ed25519 precompile format: +//! +//! ```text +//! [num_signatures: u8] +//! [padding: u8] +//! [Ed25519SignatureOffsets x num_signatures] (14 bytes each, little-endian) +//! [public key || signature || message ...] (payload, order flexible) +//! ``` +//! +//! The verifier accepts only current-instruction references +//! (`CURRENT_INSTRUCTION_INDEX`, `u16::MAX`) and performs ZIP-215 verification +//! with canonical `S`. + +#[cfg(feature = "instruction")] +extern crate alloc; +#[cfg(test)] +extern crate std; + +mod instruction; +mod instruction_data; +mod scalar; +mod verifier; + +#[cfg(not(any(target_os = "solana", target_arch = "bpf")))] +pub use instruction::sign_message; +#[cfg(feature = "instruction")] +pub use instruction::{ + new_ed25519_instruction_with_signature, offsets_to_ed25519_instruction, + try_new_ed25519_instruction_with_signature, try_offsets_to_ed25519_instruction, +}; +pub use instruction::{ + Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, + SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, +}; +pub use verifier::Ed25519Verifier; diff --git a/program/src/scalar.rs b/ed25519-verify/src/scalar.rs similarity index 100% rename from program/src/scalar.rs rename to ed25519-verify/src/scalar.rs diff --git a/ed25519-verify/src/verifier.rs b/ed25519-verify/src/verifier.rs new file mode 100644 index 0000000..43bec88 --- /dev/null +++ b/ed25519-verify/src/verifier.rs @@ -0,0 +1,121 @@ +use { + crate::{ + instruction_data::{get_signature_fields, iter_signature_offsets, SignatureFields}, + scalar, Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, PUBKEY_SERIALIZED_SIZE, + SIGNATURE_SERIALIZED_SIZE, + }, + solana_curve25519::{ + edwards::{ + multiply_edwards, multiscalar_multiply_edwards, subtract_edwards, PodEdwardsPoint, + }, + scalar::PodScalar, + }, + solana_program_error::ProgramError, +}; + +const ED25519_BASEPOINT_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([ + 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, +]); +const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); +const EIGHT_SCALAR: PodScalar = PodScalar([ + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); + +/// Stateless, zero-allocation Ed25519 verifier. +#[derive(Debug, Clone, Copy, Default)] +pub struct Ed25519Verifier; + +impl Ed25519Verifier { + /// Initializes a new verifier. + pub const fn new() -> Self { + Self + } + + /// Parses `instruction_data` and verifies every ed25519 signature it + /// describes, returning an error on the first failure. + pub fn verify_instruction(&self, instruction_data: &[u8]) -> Result<(), ProgramError> { + for offsets in iter_signature_offsets(instruction_data)? { + self.verify_signature_offsets(instruction_data, &offsets?)?; + } + + Ok(()) + } + + /// Performs ZIP-215 Ed25519 verification for one signature. + /// + /// Uses the cofactored equation `[8](S*B - H(R || A || M)*A) == [8]R`. + /// The combined multiply-add minus `R` is performed first, then multiplied + /// by 8 and compared with the identity, matching the ed25519-zebra batch + /// verification shape. Canonical `S` is still required. + pub fn verify_signature( + &self, + signature: &[u8; SIGNATURE_SERIALIZED_SIZE], + public_key: &[u8; PUBKEY_SERIALIZED_SIZE], + message: &[u8], + ) -> Result<(), ProgramError> { + let r_bytes: &[u8; 32] = signature[..32] + .try_into() + .map_err(|_| ProgramError::InvalidArgument)?; + let s_bytes: &[u8; 32] = signature[32..] + .try_into() + .map_err(|_| ProgramError::InvalidArgument)?; + if !scalar::is_canonical_scalar(s_bytes) { + return Err(ProgramError::InvalidArgument); + } + + let r_point = PodEdwardsPoint(*r_bytes); + let public_key_point = PodEdwardsPoint(*public_key); + + let challenge = compute_challenge(r_bytes, public_key, message); + let minus_challenge = scalar::negate(&challenge); + let lhs = multiscalar_multiply_edwards( + &[PodScalar(*s_bytes), PodScalar(minus_challenge)], + &[ED25519_BASEPOINT_COMPRESSED, public_key_point], + ) + .ok_or(ProgramError::InvalidArgument)?; + let difference = subtract_edwards(&lhs, &r_point).ok_or(ProgramError::InvalidArgument)?; + let difference_cofactored = + multiply_edwards(&EIGHT_SCALAR, &difference).ok_or(ProgramError::InvalidArgument)?; + + if difference_cofactored != EDWARDS_IDENTITY_COMPRESSED { + return Err(ProgramError::InvalidArgument); + } + + Ok(()) + } + + /// Validates a single signature entry described by `offsets`. + fn verify_signature_offsets( + &self, + instruction_data: &[u8], + offsets: &Ed25519SignatureOffsets, + ) -> Result<(), ProgramError> { + if !references_current_instruction(offsets) { + return Err(ProgramError::InvalidInstructionData); + } + + let fields = get_signature_fields(instruction_data, offsets)?; + self.verify_signature_fields(&fields) + } + + fn verify_signature_fields(&self, fields: &SignatureFields) -> Result<(), ProgramError> { + self.verify_signature(fields.signature, fields.public_key, fields.message) + } +} + +/// Returns `true` when every offset field references the current instruction. +fn references_current_instruction(offsets: &Ed25519SignatureOffsets) -> bool { + offsets.signature_instruction_index == CURRENT_INSTRUCTION_INDEX + && offsets.public_key_instruction_index == CURRENT_INSTRUCTION_INDEX + && offsets.message_instruction_index == CURRENT_INSTRUCTION_INDEX +} + +fn compute_challenge(signature_r: &[u8; 32], public_key: &[u8; 32], message: &[u8]) -> [u8; 32] { + let digest = solana_sha512_hasher::hashv(&[signature_r, public_key, message]).to_bytes(); + scalar::reduce_wide(&digest) +} diff --git a/ed25519-verify/tests/common/mod.rs b/ed25519-verify/tests/common/mod.rs new file mode 100644 index 0000000..2cd443a --- /dev/null +++ b/ed25519-verify/tests/common/mod.rs @@ -0,0 +1,137 @@ +use { + ed25519_dalek::{Signer, SigningKey}, + solana_ed25519_verify::{ + Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, + SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, + }, +}; + +pub(crate) const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; +pub(crate) const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ + 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, +]; + +/// Holds all cryptographic material for a single signed message. +struct SignedPayload<'a> { + signature: [u8; SIGNATURE_SERIALIZED_SIZE], + pubkey: [u8; PUBKEY_SERIALIZED_SIZE], + message: &'a [u8], +} + +/// Signs `message` with `signing_key`. +fn signed_payload<'a>(signing_key: &SigningKey, message: &'a [u8]) -> SignedPayload<'a> { + SignedPayload { + signature: signing_key.sign(message).to_bytes(), + pubkey: signing_key.verifying_key().to_bytes(), + message, + } +} + +/// Builds a valid ed25519 instruction buffer containing one entry per message, +/// all signed by a fixed test key. +pub(crate) fn signed_instruction(messages: &[&[u8]]) -> Vec { + let signing_key = SigningKey::from_bytes(&[7; 32]); + let payloads = messages + .iter() + .map(|message| signed_payload(&signing_key, message)) + .collect::>(); + let offsets_len = payloads.len() * SIGNATURE_OFFSETS_SERIALIZED_SIZE; + let mut instruction = vec![0; SIGNATURE_OFFSETS_START + offsets_len]; + instruction[0] = payloads.len() as u8; + + for (index, payload) in payloads.iter().enumerate() { + let public_key_offset = instruction.len(); + instruction.extend_from_slice(&payload.pubkey); + + let signature_offset = instruction.len(); + instruction.extend_from_slice(&payload.signature); + + let message_data_offset = instruction.len(); + instruction.extend_from_slice(payload.message); + + let offsets = Ed25519SignatureOffsets { + signature_offset: u16::try_from(signature_offset).unwrap(), + signature_instruction_index: CURRENT_INSTRUCTION_INDEX, + public_key_offset: u16::try_from(public_key_offset).unwrap(), + public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, + message_data_offset: u16::try_from(message_data_offset).unwrap(), + message_data_size: u16::try_from(payload.message.len()).unwrap(), + message_instruction_index: CURRENT_INSTRUCTION_INDEX, + }; + write_offsets( + &mut instruction[SIGNATURE_OFFSETS_START + index * SIGNATURE_OFFSETS_SERIALIZED_SIZE + ..SIGNATURE_OFFSETS_START + (index + 1) * SIGNATURE_OFFSETS_SERIALIZED_SIZE], + &offsets, + ); + } + + instruction +} + +/// Builds a single-entry instruction from caller-provided signature material. +pub(crate) fn instruction_with_signature( + message: &[u8], + signature: &[u8; SIGNATURE_SERIALIZED_SIZE], + pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], +) -> Vec { + let mut instruction = vec![0; DATA_START]; + instruction[0] = 1; + + let public_key_offset = instruction.len(); + instruction.extend_from_slice(pubkey); + + let signature_offset = instruction.len(); + instruction.extend_from_slice(signature); + + let message_data_offset = instruction.len(); + instruction.extend_from_slice(message); + + let offsets = Ed25519SignatureOffsets { + signature_offset: u16::try_from(signature_offset).unwrap(), + signature_instruction_index: CURRENT_INSTRUCTION_INDEX, + public_key_offset: u16::try_from(public_key_offset).unwrap(), + public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, + message_data_offset: u16::try_from(message_data_offset).unwrap(), + message_data_size: u16::try_from(message.len()).unwrap(), + message_instruction_index: CURRENT_INSTRUCTION_INDEX, + }; + write_offsets( + &mut instruction[SIGNATURE_OFFSETS_START..DATA_START], + &offsets, + ); + + instruction +} + +/// Parses and returns the first `Ed25519SignatureOffsets` entry from `instruction`. +pub(crate) fn first_offsets(instruction: &[u8]) -> Ed25519SignatureOffsets { + read_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) +} + +/// Deserializes the 14-byte little-endian wire format. +fn read_offsets(input: &[u8]) -> Ed25519SignatureOffsets { + Ed25519SignatureOffsets { + signature_offset: u16::from_le_bytes(input[0..2].try_into().unwrap()), + signature_instruction_index: u16::from_le_bytes(input[2..4].try_into().unwrap()), + public_key_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), + public_key_instruction_index: u16::from_le_bytes(input[6..8].try_into().unwrap()), + message_data_offset: u16::from_le_bytes(input[8..10].try_into().unwrap()), + message_data_size: u16::from_le_bytes(input[10..12].try_into().unwrap()), + message_instruction_index: u16::from_le_bytes(input[12..14].try_into().unwrap()), + } +} + +/// Serializes `offsets` into the 14-byte little-endian wire format in `output`. +pub(crate) fn write_offsets(output: &mut [u8], offsets: &Ed25519SignatureOffsets) { + output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); + output[2..4].copy_from_slice(&offsets.signature_instruction_index.to_le_bytes()); + output[4..6].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); + output[6..8].copy_from_slice(&offsets.public_key_instruction_index.to_le_bytes()); + output[8..10].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); + output[10..12].copy_from_slice(&offsets.message_data_size.to_le_bytes()); + output[12..14].copy_from_slice(&offsets.message_instruction_index.to_le_bytes()); +} diff --git a/program/tests/process_instruction.rs b/ed25519-verify/tests/verify_instruction.rs similarity index 72% rename from program/tests/process_instruction.rs rename to ed25519-verify/tests/verify_instruction.rs index ef6ffce..6e5833a 100644 --- a/program/tests/process_instruction.rs +++ b/ed25519-verify/tests/verify_instruction.rs @@ -4,34 +4,34 @@ use { EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, }, ed25519_dalek::{Signature, VerifyingKey}, - solana_ed25519_program::{ - process_instruction, CURRENT_INSTRUCTION_INDEX, DATA_START, SIGNATURE_SERIALIZED_SIZE, + solana_ed25519_verify::{ + Ed25519Verifier, CURRENT_INSTRUCTION_INDEX, DATA_START, SIGNATURE_SERIALIZED_SIZE, }, solana_program_error::ProgramError, - solana_pubkey::Pubkey, }; mod common; +fn process_instruction(instruction_data: &[u8]) -> Result<(), ProgramError> { + Ed25519Verifier::new().verify_instruction(instruction_data) +} + #[test] fn verifies_matching_signature() { - let program_id = Pubkey::default(); let instruction = signed_instruction(&[b"hello ed25519"]); - assert_eq!(process_instruction(&program_id, &[], &instruction), Ok(())); + assert_eq!(process_instruction(&instruction), Ok(())); } #[test] fn verifies_multiple_signatures() { - let program_id = Pubkey::default(); let instruction = signed_instruction(&[b"hello ed25519", b"second message"]); - assert_eq!(process_instruction(&program_id, &[], &instruction), Ok(())); + assert_eq!(process_instruction(&instruction), Ok(())); } #[test] fn accepts_zip215_small_order_public_key_vector_rejected_by_strict_verification() { - let program_id = Pubkey::default(); let message = b"zip215 low-order public key vector"; let mut signature = [0; SIGNATURE_SERIALIZED_SIZE]; signature[..EDWARDS_IDENTITY_COMPRESSED.len()].copy_from_slice(&EDWARDS_IDENTITY_COMPRESSED); @@ -43,91 +43,83 @@ fn accepts_zip215_small_order_public_key_vector_rejected_by_strict_verification( let instruction = instruction_with_signature(message, &signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED); - assert_eq!(process_instruction(&program_id, &[], &instruction), Ok(())); + assert_eq!(process_instruction(&instruction), Ok(())); } #[test] fn rejects_wrong_public_key() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); instruction[usize::from(offsets.public_key_offset)] ^= 1; assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_corrupted_signature() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); instruction[usize::from(offsets.signature_offset)] ^= 1; assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_tampered_message() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); instruction[usize::from(offsets.message_data_offset)] ^= 1; assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_short_instruction() { - let program_id = Pubkey::default(); - assert_eq!( - process_instruction(&program_id, &[], &[]), + process_instruction(&[]), Err(ProgramError::InvalidInstructionData) ); assert_eq!( - process_instruction(&program_id, &[], &[1]), + process_instruction(&[1]), Err(ProgramError::InvalidInstructionData) ); assert_eq!( - process_instruction(&program_id, &[], &[1, 0]), + process_instruction(&[1, 0]), Err(ProgramError::InvalidInstructionData) ); } #[test] fn accepts_zero_signatures_only_when_data_has_just_header() { - let program_id = Pubkey::default(); - - assert_eq!(process_instruction(&program_id, &[], &[0, 0]), Ok(())); + assert_eq!(process_instruction(&[0, 0]), Ok(())); assert_eq!( - process_instruction(&program_id, &[], &[0]), + process_instruction(&[0]), Err(ProgramError::InvalidInstructionData) ); assert_eq!( - process_instruction(&program_id, &[], &[0, 0, 0]), + process_instruction(&[0, 0, 0]), Err(ProgramError::InvalidInstructionData) ); } #[test] fn rejects_offsets_to_other_instructions() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let mut offsets = first_offsets(&instruction); offsets.signature_instruction_index = 0; write_offsets(&mut instruction[2..DATA_START], &offsets); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidInstructionData) ); @@ -135,7 +127,7 @@ fn rejects_offsets_to_other_instructions() { offsets.public_key_instruction_index = 0; write_offsets(&mut instruction[2..DATA_START], &offsets); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidInstructionData) ); @@ -143,28 +135,26 @@ fn rejects_offsets_to_other_instructions() { offsets.message_instruction_index = 0; write_offsets(&mut instruction[2..DATA_START], &offsets); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidInstructionData) ); } #[test] fn rejects_out_of_bounds_offsets() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let mut offsets = first_offsets(&instruction); offsets.message_data_size = u16::MAX; write_offsets(&mut instruction[2..DATA_START], &offsets); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidInstructionData) ); } #[test] fn rejects_non_canonical_s_scalar() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); let s_offset = usize::from(offsets.signature_offset) + 32; @@ -175,14 +165,13 @@ fn rejects_non_canonical_s_scalar() { ]); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_low_order_r() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); let r_offset = usize::from(offsets.signature_offset); @@ -193,14 +182,13 @@ fn rejects_low_order_r() { ]); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_low_order_public_key() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); let public_key_offset = usize::from(offsets.public_key_offset); @@ -208,60 +196,34 @@ fn rejects_low_order_public_key() { .copy_from_slice(&SMALL_ORDER_PUBLIC_KEY_COMPRESSED); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_invalid_public_key() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let offsets = first_offsets(&instruction); let public_key_offset = usize::from(offsets.public_key_offset); instruction[public_key_offset..public_key_offset + 32].copy_from_slice(&[0xff; 32]); assert_eq!( - process_instruction(&program_id, &[], &instruction), - Err(ProgramError::InvalidArgument) - ); -} - -#[test] -fn rejects_accounts() { - let program_id = Pubkey::default(); - let instruction = signed_instruction(&[b"hello ed25519"]); - let key = Pubkey::new_unique(); - let mut lamports = 0; - let mut data = []; - let account = solana_account_info::AccountInfo::new( - &key, - false, - false, - &mut lamports, - &mut data, - &program_id, - false, - ); - - assert_eq!( - process_instruction(&program_id, &[account], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidArgument) ); } #[test] fn ignores_padding_byte() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); instruction[1] = 0xff; - assert_eq!(process_instruction(&program_id, &[], &instruction), Ok(())); + assert_eq!(process_instruction(&instruction), Ok(())); } #[test] fn signature_offset_points_to_exactly_64_bytes() { - let program_id = Pubkey::default(); let mut instruction = signed_instruction(&[b"hello ed25519"]); let mut offsets = first_offsets(&instruction); offsets.signature_offset = u16::try_from(instruction.len() - SIGNATURE_SERIALIZED_SIZE + 1) @@ -269,15 +231,13 @@ fn signature_offset_points_to_exactly_64_bytes() { write_offsets(&mut instruction[2..DATA_START], &offsets); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Err(ProgramError::InvalidInstructionData) ); } #[test] fn accepts_valid_zip215_pure_torsion_signature() { - let program_id = Pubkey::default(); - // R = Identity Point let signature_r = EDWARDS_IDENTITY_COMPRESSED; // S = Zero Scalar @@ -303,7 +263,7 @@ fn accepts_valid_zip215_pure_torsion_signature() { let instruction = instruction_with_signature(&message, &signature, &pubkey); assert_eq!( - process_instruction(&program_id, &[], &instruction), + process_instruction(&instruction), Ok(()), "message index {i} is failing" ); diff --git a/program/Cargo.toml b/program/Cargo.toml index 103fecc..5f4dc61 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -8,33 +8,20 @@ crate-type = ["cdylib", "rlib"] [features] default = [] -bincode = ["dep:bincode", "dep:solana-instruction", "dep:solana-sdk-ids", "serde"] -custom-heap = [] -dev-context-only-utils = ["bincode"] no-entrypoint = [] -serde = ["dep:serde", "dep:serde_derive"] [dependencies] -bincode = { workspace = true, optional = true } -serde = { workspace = true, optional = true } -serde_derive = { workspace = true, optional = true } -solana-account-info = { workspace = true } -solana-curve25519 = { workspace = true } -solana-instruction = { workspace = true, optional = true } -solana-program-entrypoint = { workspace = true } -solana-program-error = { workspace = true } -solana-pubkey = { workspace = true } -solana-sdk-ids = { workspace = true, optional = true } -solana-sha512-hasher = { workspace = true } - -[target.'cfg(not(any(target_os = "solana", target_arch = "bpf")))'.dependencies] -ed25519-dalek = { workspace = true } +pinocchio = { workspace = true } +solana-ed25519-verify = { workspace = true, default-features = false } [dev-dependencies] ed25519-dalek = { workspace = true } mollusk-svm = { workspace = true } +solana-account = { workspace = true } solana-instruction = { workspace = true } solana-program-runtime = { workspace = true } +solana-pubkey = { workspace = true } +solana-sha512-hasher = { workspace = true } [lints] workspace = true diff --git a/program/src/lib.rs b/program/src/lib.rs index ab4294c..2e50618 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,161 +1,33 @@ -//! Instructions and on-chain verification for the [`ed25519` native program][np]. -//! -//! [np]: https://solana.com/docs/core/programs/precompiles#verify-ed25519-signatures -//! -//! This crate contains the on-chain processor that re-verifies Ed25519 -//! signatures inside a Solana program, and re-exports the shared Ed25519 -//! instruction types and client-side builders from the upstream SDK crate. -//! -//! _This crate exposes low-level signature-checking building blocks. Read this -//! documentation carefully and validate instruction layout assumptions in any -//! program that depends on signature verification for safety._ -//! -//! The native ed25519 precompile validates signatures at the transaction level. -//! The shared API re-exported by this crate mirrors that native instruction -//! format so clients can build compatible instructions, while this crate's -//! processor lets other programs CPI into a verification program and trust the -//! explicit pass/fail result. -//! -//! # Current crate structure -//! -//! This crate intentionally separates the shared client-facing wire definitions -//! from the on-chain verification implementation: -//! -//! - The re-exported SDK surface provides types like -//! [`Ed25519SignatureOffsets`], layout constants, and instruction builders. -//! - The `processor` module contains the on-chain verification logic. -//! - The `instruction_data` module contains parser helpers for the 14-byte -//! offset records and instruction payload slices. -//! - The `scalar` module contains scalar arithmetic helpers used for canonical -//! `S` checks and challenge reduction. -//! -//! The crate root remains thin and contains only documentation, re-exports, and -//! the Solana entry point. -//! -//! # Instruction data layout -//! -//! The instruction data mirrors the layout consumed by the native ed25519 -//! precompile: -//! -//! ```text -//! [num_signatures: u8] -//! [padding: u8] -//! [Ed25519SignatureOffsets x num_signatures] (14 bytes each, little-endian) -//! [public key || signature || message ...] (payload, order flexible) -//! ``` -//! -//! The payload bytes can be arranged however the client wants, as long as each -//! [`Ed25519SignatureOffsets`] record points at the correct byte ranges. -//! -//! All data references inside [`Ed25519SignatureOffsets`] must use the native -//! "current instruction" sentinel (`u16::MAX`) when processed by this crate; -//! cross-instruction references are rejected. -//! -//! # ZIP-215 verification behavior -//! -//! This crate verifies signatures with the cofactored ZIP-215 equation -//! `[8](S*B - H(R || A || M)*A) == [8]R`. Verification fails if any of the -//! following are true: -//! -//! - The signature scalar `S` is non-canonical. -//! - The signature point `R` cannot be decompressed. -//! - The compressed public key cannot be decompressed. -//! - The cofactored signature equation does not hold. -//! - The instruction data is empty, truncated, or contains out-of-bounds -//! offsets. -//! - Any offset record references an instruction index other than `u16::MAX`. -//! -//! Small-order `R` and public-key points are not rejected solely because they -//! are small order; their torsion components are removed by multiplication by -//! 8. -//! -//! # Additional security considerations -//! -//! Most programs should be conservative about what instruction shapes they -//! accept. Desirable checks often include: -//! -//! - The number of signatures is exactly what the program expects. -//! - Every instruction index field is exactly where the program expects the -//! signature material to live. -//! - The signed messages are domain-separated and cannot be replayed across -//! unrelated instructions or protocols. -//! - The verification program ID is the expected one, so a malicious program -//! cannot fake a successful verification path. +#![no_std] -mod instruction; -mod instruction_data; -mod processor; -mod scalar; +//! Pinocchio SBF wrapper for [`solana_ed25519_verify`]. +//! +//! The reusable verifier, instruction layout, and instruction constructors live +//! in `solana-ed25519-verify`. This crate keeps only the standalone program +//! entrypoint. -#[cfg(not(any(target_os = "solana", target_arch = "bpf")))] -pub use instruction::sign_message; #[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) + not(feature = "no-entrypoint"), + any(target_os = "solana", target_arch = "bpf") ))] -pub use instruction::{ - new_ed25519_instruction_with_signature, offsets_to_ed25519_instruction, - try_new_ed25519_instruction_with_signature, try_offsets_to_ed25519_instruction, -}; -pub use instruction::{ - Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, -}; -pub use processor::process_instruction; - -#[cfg(target_os = "solana")] -use solana_program_error::ProgramError; - -/// Program entry point for the version 2 instruction-data pointer interface. -/// -/// # Safety -/// -/// The Solana runtime must pass `input` as the serialized accounts buffer and -/// `instruction_data_addr` as the pointer to instruction data with its length -/// stored in the preceding 8 bytes. -#[cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entrypoint(input: *mut u8, instruction_data_addr: *const u8) -> u64 { - let result = unsafe { - let num_accounts = *(input as *const u64); - if num_accounts != 0 { - Err(ProgramError::InvalidArgument) - } else { - let Some(instruction_data_len_addr) = - (instruction_data_addr as usize).checked_sub(core::mem::size_of::()) - else { - return ProgramError::InvalidInstructionData.into(); - }; - let instruction_data_len = *(instruction_data_len_addr as *const u64); - let instruction_data = - core::slice::from_raw_parts(instruction_data_addr, instruction_data_len as usize); - processor::verify_ed25519_instruction(instruction_data) - } - }; +use pinocchio::{lazy_program_entrypoint, no_allocator, nostd_panic_handler}; - match result { - Ok(()) => solana_program_entrypoint::SUCCESS, - Err(error) => error.into(), - } -} +mod processor; -#[cfg(not(feature = "no-entrypoint"))] -solana_program_entrypoint::custom_heap_default!(); -#[cfg(not(feature = "no-entrypoint"))] -solana_program_entrypoint::custom_panic_default!(); +pub use processor::process_instruction; -#[cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] -#[unsafe(no_mangle)] -pub extern "C" fn abort() -> ! { - let message = "abort"; - let file = file!(); - unsafe { - solana_program_entrypoint::__log(message.as_ptr(), message.len() as u64); - solana_program_entrypoint::__panic( - file.as_ptr(), - file.len() as u64, - line!() as u64, - column!() as u64, - ) - } -} +#[cfg(all( + not(feature = "no-entrypoint"), + any(target_os = "solana", target_arch = "bpf") +))] +lazy_program_entrypoint!(process_instruction); +#[cfg(all( + not(feature = "no-entrypoint"), + any(target_os = "solana", target_arch = "bpf") +))] +no_allocator!(); +#[cfg(all( + not(feature = "no-entrypoint"), + any(target_os = "solana", target_arch = "bpf") +))] +nostd_panic_handler!(); diff --git a/program/src/processor.rs b/program/src/processor.rs index 2030f6d..25e9fd0 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1,118 +1,19 @@ use { - crate::{ - instruction::CURRENT_INSTRUCTION_INDEX, - instruction_data::{get_signature_fields, iter_signature_offsets, SignatureFields}, - scalar, Ed25519SignatureOffsets, - }, - solana_account_info::AccountInfo, - solana_curve25519::{ - edwards::{ - multiply_edwards, multiscalar_multiply_edwards, subtract_edwards, PodEdwardsPoint, - }, - scalar::PodScalar, - }, - solana_program_entrypoint::ProgramResult, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, + pinocchio::{entrypoint::InstructionContext, error::ProgramError, ProgramResult}, + solana_ed25519_verify::Ed25519Verifier, }; -const ED25519_BASEPOINT_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([ - 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, - 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, -]); -const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([ - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]); -const EIGHT_SCALAR: PodScalar = PodScalar([ - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]); - -/// Parses `instruction_data` and verifies every ed25519 signature it -/// describes, returning an error on the first failure. -pub(crate) fn verify_ed25519_instruction(instruction_data: &[u8]) -> ProgramResult { - for offsets in iter_signature_offsets(instruction_data)? { - verify_signature(instruction_data, &offsets?)?; - } - - Ok(()) +fn verify_ed25519_instruction(instruction_data: &[u8]) -> ProgramResult { + Ed25519Verifier::new().verify_instruction(instruction_data) } /// Program entry point. /// -/// Expects no accounts and instruction data in the ed25519 precompile -/// format. Returns [`ProgramError::InvalidArgument`] if any accounts are -/// provided, or propagates errors from signature verification. -pub fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if !accounts.is_empty() { - return Err(ProgramError::InvalidArgument); - } - - verify_ed25519_instruction(instruction_data) -} - -/// Returns `true` when every offset field references the current instruction. -fn references_current_instruction(offsets: &Ed25519SignatureOffsets) -> bool { - offsets.signature_instruction_index == CURRENT_INSTRUCTION_INDEX - && offsets.public_key_instruction_index == CURRENT_INSTRUCTION_INDEX - && offsets.message_instruction_index == CURRENT_INSTRUCTION_INDEX -} - -/// Validates a single signature entry described by `offsets`. -fn verify_signature(instruction_data: &[u8], offsets: &Ed25519SignatureOffsets) -> ProgramResult { - if !references_current_instruction(offsets) { - return Err(ProgramError::InvalidInstructionData); - } - - let fields = get_signature_fields(instruction_data, offsets)?; - verify_signature_fields(&fields) -} - -/// Performs ZIP-215 Ed25519 verification for one entry. -/// -/// Uses the cofactored equation `[8](S*B - H(R || A || M)*A) == [8]R`. -/// The combined multiply-add minus `R` is performed first, then multiplied by -/// 8 and compared with the identity, matching the ed25519-zebra batch -/// verification shape. -/// Canonical `S` is still required. -fn verify_signature_fields(fields: &SignatureFields) -> ProgramResult { - let r_bytes: &[u8; 32] = fields.signature[..32] - .try_into() - .map_err(|_| ProgramError::InvalidArgument)?; - let s_bytes: &[u8; 32] = fields.signature[32..] - .try_into() - .map_err(|_| ProgramError::InvalidArgument)?; - if !scalar::is_canonical_scalar(s_bytes) { +/// Expects no accounts and instruction data in the ed25519 precompile format. +pub fn process_instruction(context: InstructionContext) -> ProgramResult { + if context.remaining() > 0 { return Err(ProgramError::InvalidArgument); } - let r_point = PodEdwardsPoint(*r_bytes); - let public_key_point = PodEdwardsPoint(*fields.public_key); - - let challenge = compute_challenge(r_bytes, fields.public_key, fields.message); - let minus_challenge = scalar::negate(&challenge); - let lhs = multiscalar_multiply_edwards( - &[PodScalar(*s_bytes), PodScalar(minus_challenge)], - &[ED25519_BASEPOINT_COMPRESSED, public_key_point], - ) - .ok_or(ProgramError::InvalidArgument)?; - let difference = subtract_edwards(&lhs, &r_point).ok_or(ProgramError::InvalidArgument)?; - let difference_cofactored = - multiply_edwards(&EIGHT_SCALAR, &difference).ok_or(ProgramError::InvalidArgument)?; - - if difference_cofactored != EDWARDS_IDENTITY_COMPRESSED { - return Err(ProgramError::InvalidArgument); - } - - Ok(()) -} - -fn compute_challenge(signature_r: &[u8; 32], public_key: &[u8; 32], message: &[u8]) -> [u8; 32] { - let digest = solana_sha512_hasher::hashv(&[signature_r, public_key, message]).to_bytes(); - scalar::reduce_wide(&digest) + verify_ed25519_instruction(context.instruction_data()?) } diff --git a/program/tests/common/mod.rs b/program/tests/common/mod.rs index d6edddd..2cd443a 100644 --- a/program/tests/common/mod.rs +++ b/program/tests/common/mod.rs @@ -1,6 +1,6 @@ use { ed25519_dalek::{Signer, SigningKey}, - solana_ed25519_program::{ + solana_ed25519_verify::{ Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, }, diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index ee8ebe1..8055d80 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -4,8 +4,9 @@ use { SMALL_ORDER_PUBLIC_KEY_COMPRESSED, }, mollusk_svm::Mollusk, - solana_ed25519_program::SIGNATURE_SERIALIZED_SIZE, - solana_instruction::Instruction, + solana_account::Account, + solana_ed25519_verify::SIGNATURE_SERIALIZED_SIZE, + solana_instruction::{AccountMeta, Instruction}, solana_program_runtime::{ invoke_context::InvokeContext, solana_sbpf::{ @@ -142,6 +143,14 @@ fn instruction(program_id: Pubkey, data: Vec) -> Instruction { } } +fn instruction_with_account(program_id: Pubkey, data: Vec, account: Pubkey) -> Instruction { + Instruction { + program_id, + accounts: vec![AccountMeta::new_readonly(account, false)], + data, + } +} + #[test] fn verifies_single_signature_on_sbf_and_reports_compute_units() { let Some((mollusk, program_id)) = make_mollusk() else { @@ -239,3 +248,20 @@ fn rejects_tampered_public_key_on_sbf() { result.program_result ); } + +#[test] +fn rejects_accounts_on_sbf() { + let Some((mollusk, program_id)) = make_mollusk() else { + return; + }; + let account = Pubkey::new_unique(); + let ix = instruction_with_account(program_id, signed_instruction(&[SINGLE_MESSAGE]), account); + let accounts = [(account, Account::default())]; + + let result = mollusk.process_instruction(&ix, &accounts); + assert!( + result.program_result.is_err(), + "expected failure when accounts are provided, got: {:?}", + result.program_result + ); +} From 23a41c2f7a8b7550015db91acfe1b8871f6e09d5 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 15:04:20 -0400 Subject: [PATCH 2/9] refactoring --- Cargo.lock | 2 +- README.md | 2 +- ed25519-verify/Cargo.toml | 3 + ed25519-verify/src/lib.rs | 2 + .../common/mod.rs => src/test_utils.rs} | 24 ++- ed25519-verify/src/verifier.rs | 6 +- ed25519-verify/tests/verify_instruction.rs | 10 +- program/Cargo.toml | 2 +- program/src/processor.rs | 6 +- program/tests/common/mod.rs | 137 ------------------ program/tests/mollusk.rs | 14 +- 11 files changed, 37 insertions(+), 171 deletions(-) rename ed25519-verify/{tests/common/mod.rs => src/test_utils.rs} (88%) delete mode 100644 program/tests/common/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 03b0aab..e05a693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2215,7 +2215,6 @@ checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" name = "solana-ed25519-program" version = "4.0.0" dependencies = [ - "ed25519-dalek", "mollusk-svm", "pinocchio", "solana-account", @@ -2235,6 +2234,7 @@ dependencies = [ "serde", "serde_derive", "solana-curve25519", + "solana-ed25519-verify", "solana-instruction", "solana-program-error", "solana-sdk-ids", diff --git a/README.md b/README.md index 4a19252..7cccf42 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Each offset record matches `Ed25519SignatureOffsets` exposed by this crate: |---|---|---| | `instruction` | off | Enables alloc-based `Instruction` construction helpers. | | `bincode` | off | Backward-compatible alias for SDK-style helper APIs; also enables `instruction`. | -| `dev-context-only-utils` | off | Backward-compatible alias for `bincode`, matching upstream helper crates. | +| `dev-context-only-utils` | off | Backward-compatible alias for `bincode`; also exposes `test_utils`, the instruction builders shared by this crate's and `solana-ed25519-program`'s tests. | | `serde` | off | Derives serde traits for `Ed25519SignatureOffsets`. | `solana-ed25519-program` only exposes `no-entrypoint`, which omits the diff --git a/ed25519-verify/Cargo.toml b/ed25519-verify/Cargo.toml index e8ae48c..bc86957 100644 --- a/ed25519-verify/Cargo.toml +++ b/ed25519-verify/Cargo.toml @@ -26,5 +26,8 @@ solana-sha512-hasher = { workspace = true } [target.'cfg(not(any(target_os = "solana", target_arch = "bpf")))'.dependencies] ed25519-dalek = { workspace = true } +[dev-dependencies] +solana-ed25519-verify = { path = ".", features = ["dev-context-only-utils"] } + [lints] workspace = true diff --git a/ed25519-verify/src/lib.rs b/ed25519-verify/src/lib.rs index 78ca6cf..18d9898 100644 --- a/ed25519-verify/src/lib.rs +++ b/ed25519-verify/src/lib.rs @@ -28,6 +28,8 @@ extern crate std; mod instruction; mod instruction_data; mod scalar; +#[cfg(feature = "dev-context-only-utils")] +pub mod test_utils; mod verifier; #[cfg(not(any(target_os = "solana", target_arch = "bpf")))] diff --git a/ed25519-verify/tests/common/mod.rs b/ed25519-verify/src/test_utils.rs similarity index 88% rename from ed25519-verify/tests/common/mod.rs rename to ed25519-verify/src/test_utils.rs index 2cd443a..ab755bf 100644 --- a/ed25519-verify/tests/common/mod.rs +++ b/ed25519-verify/src/test_utils.rs @@ -1,16 +1,24 @@ +//! Test-only ed25519 instruction builders. +//! +//! Shared by this crate's own integration tests and by `solana-ed25519-program`'s, +//! so the wire-format builders live in one place instead of being duplicated +//! per crate. Gated behind `dev-context-only-utils` so none of this ships in +//! on-chain builds. + use { - ed25519_dalek::{Signer, SigningKey}, - solana_ed25519_verify::{ + crate::{ Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, }, + alloc::{vec, vec::Vec}, + ed25519_dalek::{Signer, SigningKey}, }; -pub(crate) const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ +pub const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; -pub(crate) const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ +pub const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, ]; @@ -33,7 +41,7 @@ fn signed_payload<'a>(signing_key: &SigningKey, message: &'a [u8]) -> SignedPayl /// Builds a valid ed25519 instruction buffer containing one entry per message, /// all signed by a fixed test key. -pub(crate) fn signed_instruction(messages: &[&[u8]]) -> Vec { +pub fn signed_instruction(messages: &[&[u8]]) -> Vec { let signing_key = SigningKey::from_bytes(&[7; 32]); let payloads = messages .iter() @@ -73,7 +81,7 @@ pub(crate) fn signed_instruction(messages: &[&[u8]]) -> Vec { } /// Builds a single-entry instruction from caller-provided signature material. -pub(crate) fn instruction_with_signature( +pub fn instruction_with_signature( message: &[u8], signature: &[u8; SIGNATURE_SERIALIZED_SIZE], pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], @@ -108,7 +116,7 @@ pub(crate) fn instruction_with_signature( } /// Parses and returns the first `Ed25519SignatureOffsets` entry from `instruction`. -pub(crate) fn first_offsets(instruction: &[u8]) -> Ed25519SignatureOffsets { +pub fn first_offsets(instruction: &[u8]) -> Ed25519SignatureOffsets { read_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) } @@ -126,7 +134,7 @@ fn read_offsets(input: &[u8]) -> Ed25519SignatureOffsets { } /// Serializes `offsets` into the 14-byte little-endian wire format in `output`. -pub(crate) fn write_offsets(output: &mut [u8], offsets: &Ed25519SignatureOffsets) { +pub fn write_offsets(output: &mut [u8], offsets: &Ed25519SignatureOffsets) { output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); output[2..4].copy_from_slice(&offsets.signature_instruction_index.to_le_bytes()); output[4..6].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); diff --git a/ed25519-verify/src/verifier.rs b/ed25519-verify/src/verifier.rs index 43bec88..cfa1112 100644 --- a/ed25519-verify/src/verifier.rs +++ b/ed25519-verify/src/verifier.rs @@ -1,6 +1,6 @@ use { crate::{ - instruction_data::{get_signature_fields, iter_signature_offsets, SignatureFields}, + instruction_data::{get_signature_fields, iter_signature_offsets}, scalar, Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE, }, @@ -100,10 +100,6 @@ impl Ed25519Verifier { } let fields = get_signature_fields(instruction_data, offsets)?; - self.verify_signature_fields(&fields) - } - - fn verify_signature_fields(&self, fields: &SignatureFields) -> Result<(), ProgramError> { self.verify_signature(fields.signature, fields.public_key, fields.message) } } diff --git a/ed25519-verify/tests/verify_instruction.rs b/ed25519-verify/tests/verify_instruction.rs index 6e5833a..d2909b8 100644 --- a/ed25519-verify/tests/verify_instruction.rs +++ b/ed25519-verify/tests/verify_instruction.rs @@ -1,17 +1,15 @@ use { - common::{ - first_offsets, instruction_with_signature, signed_instruction, write_offsets, - EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, - }, ed25519_dalek::{Signature, VerifyingKey}, solana_ed25519_verify::{ + test_utils::{ + first_offsets, instruction_with_signature, signed_instruction, write_offsets, + EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, + }, Ed25519Verifier, CURRENT_INSTRUCTION_INDEX, DATA_START, SIGNATURE_SERIALIZED_SIZE, }, solana_program_error::ProgramError, }; -mod common; - fn process_instruction(instruction_data: &[u8]) -> Result<(), ProgramError> { Ed25519Verifier::new().verify_instruction(instruction_data) } diff --git a/program/Cargo.toml b/program/Cargo.toml index 5f4dc61..e106f89 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -15,9 +15,9 @@ pinocchio = { workspace = true } solana-ed25519-verify = { workspace = true, default-features = false } [dev-dependencies] -ed25519-dalek = { workspace = true } mollusk-svm = { workspace = true } solana-account = { workspace = true } +solana-ed25519-verify = { workspace = true, features = ["dev-context-only-utils"] } solana-instruction = { workspace = true } solana-program-runtime = { workspace = true } solana-pubkey = { workspace = true } diff --git a/program/src/processor.rs b/program/src/processor.rs index 25e9fd0..4a127d1 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -3,10 +3,6 @@ use { solana_ed25519_verify::Ed25519Verifier, }; -fn verify_ed25519_instruction(instruction_data: &[u8]) -> ProgramResult { - Ed25519Verifier::new().verify_instruction(instruction_data) -} - /// Program entry point. /// /// Expects no accounts and instruction data in the ed25519 precompile format. @@ -15,5 +11,5 @@ pub fn process_instruction(context: InstructionContext) -> ProgramResult { return Err(ProgramError::InvalidArgument); } - verify_ed25519_instruction(context.instruction_data()?) + Ed25519Verifier::new().verify_instruction(context.instruction_data()?) } diff --git a/program/tests/common/mod.rs b/program/tests/common/mod.rs deleted file mode 100644 index 2cd443a..0000000 --- a/program/tests/common/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -use { - ed25519_dalek::{Signer, SigningKey}, - solana_ed25519_verify::{ - Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, - }, -}; - -pub(crate) const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]; -pub(crate) const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ - 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, -]; - -/// Holds all cryptographic material for a single signed message. -struct SignedPayload<'a> { - signature: [u8; SIGNATURE_SERIALIZED_SIZE], - pubkey: [u8; PUBKEY_SERIALIZED_SIZE], - message: &'a [u8], -} - -/// Signs `message` with `signing_key`. -fn signed_payload<'a>(signing_key: &SigningKey, message: &'a [u8]) -> SignedPayload<'a> { - SignedPayload { - signature: signing_key.sign(message).to_bytes(), - pubkey: signing_key.verifying_key().to_bytes(), - message, - } -} - -/// Builds a valid ed25519 instruction buffer containing one entry per message, -/// all signed by a fixed test key. -pub(crate) fn signed_instruction(messages: &[&[u8]]) -> Vec { - let signing_key = SigningKey::from_bytes(&[7; 32]); - let payloads = messages - .iter() - .map(|message| signed_payload(&signing_key, message)) - .collect::>(); - let offsets_len = payloads.len() * SIGNATURE_OFFSETS_SERIALIZED_SIZE; - let mut instruction = vec![0; SIGNATURE_OFFSETS_START + offsets_len]; - instruction[0] = payloads.len() as u8; - - for (index, payload) in payloads.iter().enumerate() { - let public_key_offset = instruction.len(); - instruction.extend_from_slice(&payload.pubkey); - - let signature_offset = instruction.len(); - instruction.extend_from_slice(&payload.signature); - - let message_data_offset = instruction.len(); - instruction.extend_from_slice(payload.message); - - let offsets = Ed25519SignatureOffsets { - signature_offset: u16::try_from(signature_offset).unwrap(), - signature_instruction_index: CURRENT_INSTRUCTION_INDEX, - public_key_offset: u16::try_from(public_key_offset).unwrap(), - public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, - message_data_offset: u16::try_from(message_data_offset).unwrap(), - message_data_size: u16::try_from(payload.message.len()).unwrap(), - message_instruction_index: CURRENT_INSTRUCTION_INDEX, - }; - write_offsets( - &mut instruction[SIGNATURE_OFFSETS_START + index * SIGNATURE_OFFSETS_SERIALIZED_SIZE - ..SIGNATURE_OFFSETS_START + (index + 1) * SIGNATURE_OFFSETS_SERIALIZED_SIZE], - &offsets, - ); - } - - instruction -} - -/// Builds a single-entry instruction from caller-provided signature material. -pub(crate) fn instruction_with_signature( - message: &[u8], - signature: &[u8; SIGNATURE_SERIALIZED_SIZE], - pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], -) -> Vec { - let mut instruction = vec![0; DATA_START]; - instruction[0] = 1; - - let public_key_offset = instruction.len(); - instruction.extend_from_slice(pubkey); - - let signature_offset = instruction.len(); - instruction.extend_from_slice(signature); - - let message_data_offset = instruction.len(); - instruction.extend_from_slice(message); - - let offsets = Ed25519SignatureOffsets { - signature_offset: u16::try_from(signature_offset).unwrap(), - signature_instruction_index: CURRENT_INSTRUCTION_INDEX, - public_key_offset: u16::try_from(public_key_offset).unwrap(), - public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, - message_data_offset: u16::try_from(message_data_offset).unwrap(), - message_data_size: u16::try_from(message.len()).unwrap(), - message_instruction_index: CURRENT_INSTRUCTION_INDEX, - }; - write_offsets( - &mut instruction[SIGNATURE_OFFSETS_START..DATA_START], - &offsets, - ); - - instruction -} - -/// Parses and returns the first `Ed25519SignatureOffsets` entry from `instruction`. -pub(crate) fn first_offsets(instruction: &[u8]) -> Ed25519SignatureOffsets { - read_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) -} - -/// Deserializes the 14-byte little-endian wire format. -fn read_offsets(input: &[u8]) -> Ed25519SignatureOffsets { - Ed25519SignatureOffsets { - signature_offset: u16::from_le_bytes(input[0..2].try_into().unwrap()), - signature_instruction_index: u16::from_le_bytes(input[2..4].try_into().unwrap()), - public_key_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), - public_key_instruction_index: u16::from_le_bytes(input[6..8].try_into().unwrap()), - message_data_offset: u16::from_le_bytes(input[8..10].try_into().unwrap()), - message_data_size: u16::from_le_bytes(input[10..12].try_into().unwrap()), - message_instruction_index: u16::from_le_bytes(input[12..14].try_into().unwrap()), - } -} - -/// Serializes `offsets` into the 14-byte little-endian wire format in `output`. -pub(crate) fn write_offsets(output: &mut [u8], offsets: &Ed25519SignatureOffsets) { - output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); - output[2..4].copy_from_slice(&offsets.signature_instruction_index.to_le_bytes()); - output[4..6].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); - output[6..8].copy_from_slice(&offsets.public_key_instruction_index.to_le_bytes()); - output[8..10].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); - output[10..12].copy_from_slice(&offsets.message_data_size.to_le_bytes()); - output[12..14].copy_from_slice(&offsets.message_instruction_index.to_le_bytes()); -} diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index 8055d80..0b94bea 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -1,11 +1,13 @@ use { - common::{ - first_offsets, instruction_with_signature, signed_instruction, EDWARDS_IDENTITY_COMPRESSED, - SMALL_ORDER_PUBLIC_KEY_COMPRESSED, - }, mollusk_svm::Mollusk, solana_account::Account, - solana_ed25519_verify::SIGNATURE_SERIALIZED_SIZE, + solana_ed25519_verify::{ + test_utils::{ + first_offsets, instruction_with_signature, signed_instruction, + EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, + }, + SIGNATURE_SERIALIZED_SIZE, + }, solana_instruction::{AccountMeta, Instruction}, solana_program_runtime::{ invoke_context::InvokeContext, @@ -19,8 +21,6 @@ use { std::{env, error::Error, io, mem::size_of, path::PathBuf, slice}, }; -mod common; - const PROGRAM_SO_STEM: &str = "solana_ed25519_program"; const SINGLE_MESSAGE: &[u8] = b"deterministic ed25519 verify benchmark"; const SECOND_MESSAGE: &[u8] = b"second deterministic ed25519 verify benchmark"; From 072b222bc2ace9f62b2905e04f0598dddf6f6d5f Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 15:07:29 -0400 Subject: [PATCH 3/9] clean up --- Cargo.lock | 1 - Cargo.toml | 1 - README.md | 8 ++--- ed25519-verify/Cargo.toml | 4 +-- ed25519-verify/src/instruction.rs | 57 +++++-------------------------- ed25519-verify/src/lib.rs | 5 +-- 6 files changed, 14 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e05a693..ee1d908 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2229,7 +2229,6 @@ dependencies = [ name = "solana-ed25519-verify" version = "4.0.0" dependencies = [ - "bincode", "ed25519-dalek", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index b17ac6a..93e4ea4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ unexpected_cfgs = { level = "warn", check-cfg = [ ] } [workspace.dependencies] -bincode = "1.3.3" ed25519-dalek = "2.1.1" mollusk-svm = "0.13.1" pinocchio = "0.11.2" diff --git a/README.md b/README.md index 7cccf42..abf199c 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,7 @@ Each offset record matches `Ed25519SignatureOffsets` exposed by this crate: | Feature | Default | Description | |---|---|---| | `instruction` | off | Enables alloc-based `Instruction` construction helpers. | -| `bincode` | off | Backward-compatible alias for SDK-style helper APIs; also enables `instruction`. | -| `dev-context-only-utils` | off | Backward-compatible alias for `bincode`; also exposes `test_utils`, the instruction builders shared by this crate's and `solana-ed25519-program`'s tests. | +| `dev-context-only-utils` | off | Enables `instruction` and exposes `test_utils`, the instruction builders shared by this crate's and `solana-ed25519-program`'s tests. | | `serde` | off | Derives serde traits for `Ed25519SignatureOffsets`. | `solana-ed25519-program` only exposes `no-entrypoint`, which omits the @@ -93,9 +92,8 @@ program. `solana-ed25519-verify` exposes the stateless `Ed25519Verifier`, layout constants, `Ed25519SignatureOffsets`, and, with the `instruction` feature, -SDK-compatible instruction constructors such as -`new_ed25519_instruction_with_signature` and -`try_new_ed25519_instruction_with_signature`. +fallible instruction constructors `new_ed25519_instruction_with_signature` +and `offsets_to_ed25519_instruction`. `solana-ed25519-program` calls the library from its Pinocchio processor. diff --git a/ed25519-verify/Cargo.toml b/ed25519-verify/Cargo.toml index bc86957..188f7ab 100644 --- a/ed25519-verify/Cargo.toml +++ b/ed25519-verify/Cargo.toml @@ -8,13 +8,11 @@ crate-type = ["rlib"] [features] default = [] -bincode = ["dep:bincode", "instruction"] -dev-context-only-utils = ["bincode"] +dev-context-only-utils = ["instruction"] instruction = ["dep:solana-instruction", "dep:solana-sdk-ids", "serde"] serde = ["dep:serde", "dep:serde_derive"] [dependencies] -bincode = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_derive = { workspace = true, optional = true } solana-curve25519 = { workspace = true } diff --git a/ed25519-verify/src/instruction.rs b/ed25519-verify/src/instruction.rs index 1d13dce..620fda6 100644 --- a/ed25519-verify/src/instruction.rs +++ b/ed25519-verify/src/instruction.rs @@ -49,25 +49,9 @@ pub fn sign_message( #[cfg(feature = "instruction")] /// Encode just the signature offsets in a single ed25519 instruction. /// -/// This preserves the upstream SDK helper API by returning [`Instruction`] -/// directly. For fallible construction with explicit overflow errors, use -/// [`try_offsets_to_ed25519_instruction`]. -/// -/// # Panics -/// -/// Panics if `offsets.len()` cannot fit in the native program's one-byte -/// signature count field. -pub fn offsets_to_ed25519_instruction(offsets: &[Ed25519SignatureOffsets]) -> Instruction { - try_offsets_to_ed25519_instruction(offsets).expect("invalid ed25519 instruction offsets") -} - -#[cfg(feature = "instruction")] -/// Encode just the signature offsets in a single ed25519 instruction with -/// checked inputs. -/// /// Returns an error if `offsets.len()` cannot fit in the native program's /// one-byte signature count field. -pub fn try_offsets_to_ed25519_instruction( +pub fn offsets_to_ed25519_instruction( offsets: &[Ed25519SignatureOffsets], ) -> Result { let num_signatures = @@ -97,29 +81,9 @@ pub fn try_offsets_to_ed25519_instruction( #[cfg(feature = "instruction")] /// Builds a single-signature ed25519 instruction. /// -/// This preserves the upstream SDK helper API by returning [`Instruction`] -/// directly. For fallible construction with explicit overflow errors, use -/// [`try_new_ed25519_instruction_with_signature`]. -/// -/// # Panics -/// -/// Panics if the message length or any offset cannot be represented in the -/// 16-bit wire fields. -pub fn new_ed25519_instruction_with_signature( - message: &[u8], - signature: &[u8; SIGNATURE_SERIALIZED_SIZE], - pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], -) -> Instruction { - try_new_ed25519_instruction_with_signature(message, signature, pubkey) - .expect("invalid ed25519 instruction inputs") -} - -#[cfg(feature = "instruction")] -/// Builds a single-signature ed25519 instruction with checked inputs. -/// /// Returns an error if the message length or any offset cannot be represented /// in the 16-bit wire fields. -pub fn try_new_ed25519_instruction_with_signature( +pub fn new_ed25519_instruction_with_signature( message: &[u8], signature: &[u8; SIGNATURE_SERIALIZED_SIZE], pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], @@ -217,11 +181,12 @@ mod tests { } #[test] - fn test_instruction_builder_keeps_legacy_return_type() { + fn test_instruction_builder_produces_current_instruction_offsets() { let signature = [1; SIGNATURE_SERIALIZED_SIZE]; let pubkey = [2; PUBKEY_SERIALIZED_SIZE]; - let instruction = new_ed25519_instruction_with_signature(b"message", &signature, &pubkey); + let instruction = new_ed25519_instruction_with_signature(b"message", &signature, &pubkey) + .expect("valid inputs"); let offsets = read_first_offsets(&instruction.data); assert_eq!(instruction.accounts.len(), 0); @@ -245,21 +210,17 @@ mod tests { let max_message = vec![3; u16::MAX as usize]; let oversized_message = vec![3; u16::MAX as usize + 1]; + assert!(new_ed25519_instruction_with_signature(&max_message, &signature, &pubkey).is_ok()); assert!( - try_new_ed25519_instruction_with_signature(&max_message, &signature, &pubkey).is_ok() + new_ed25519_instruction_with_signature(&oversized_message, &signature, &pubkey) + .is_err() ); - assert!(try_new_ed25519_instruction_with_signature( - &oversized_message, - &signature, - &pubkey - ) - .is_err()); } #[test] fn test_offsets_builder_rejects_too_many_signatures() { let offsets = vec![Ed25519SignatureOffsets::default(); u8::MAX as usize + 1]; - assert!(try_offsets_to_ed25519_instruction(&offsets).is_err()); + assert!(offsets_to_ed25519_instruction(&offsets).is_err()); } } diff --git a/ed25519-verify/src/lib.rs b/ed25519-verify/src/lib.rs index 18d9898..380024a 100644 --- a/ed25519-verify/src/lib.rs +++ b/ed25519-verify/src/lib.rs @@ -35,10 +35,7 @@ mod verifier; #[cfg(not(any(target_os = "solana", target_arch = "bpf")))] pub use instruction::sign_message; #[cfg(feature = "instruction")] -pub use instruction::{ - new_ed25519_instruction_with_signature, offsets_to_ed25519_instruction, - try_new_ed25519_instruction_with_signature, try_offsets_to_ed25519_instruction, -}; +pub use instruction::{new_ed25519_instruction_with_signature, offsets_to_ed25519_instruction}; pub use instruction::{ Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, From a2bff4855f8936f19e96f00542b84e1f33b933b4 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 15:10:06 -0400 Subject: [PATCH 4/9] CI --- .github/workflows/main.yml | 5 +++-- package.json | 5 ----- pnpm-lock.yaml | 9 --------- 3 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 package.json delete mode 100644 pnpm-lock.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 392548f..192ef69 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,8 +7,8 @@ on: branches: [main] env: - SBPF_PROGRAM_PACKAGES: "['program']" - RUST_PACKAGES: "['program']" + SBPF_PROGRAM_PACKAGES: "['program', 'ed25519-verify']" + RUST_PACKAGES: "['program', 'ed25519-verify']" jobs: set_env: @@ -41,3 +41,4 @@ jobs: rustfmt-toolchain: ${{ needs.set_env.outputs.RUST_TOOLCHAIN_NIGHTLY }} clippy-toolchain: ${{ needs.set_env.outputs.RUST_TOOLCHAIN_NIGHTLY }} solana-cli-version: ${{ needs.set_env.outputs.SOLANA_CLI_VERSION }} + generate-clients: false diff --git a/package.json b/package.json deleted file mode 100644 index 6ddaa67..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "private": true, - "packageManager": "pnpm@10.25.0" -} - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 9b60ae1..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,9 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: {} From 6ee189bbaba0de1732de7a794ecf2c62001a85bc Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 15:12:32 -0400 Subject: [PATCH 5/9] Update Makefile --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index 3967ee7..715e609 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ RUST_TOOLCHAIN_NIGHTLY = nightly-2026-01-22 SOLANA_CLI_VERSION = v3.1.10 SBF_ARCH = v2 -PROGRAM_SO = solana_ed25519_program.so nightly = +${RUST_TOOLCHAIN_NIGHTLY} @@ -47,8 +46,6 @@ build-sbf-%: cargo build-sbf --arch $(SBF_ARCH) --manifest-path $(call make-path,$*)/Cargo.toml -- --locked $(ARGS) test-%: - @test -f target/deploy/$(PROGRAM_SO) || \ - (echo "SBF artifact not found: run make build-sbf-$* first" >&2; exit 1) SBF_OUT_DIR=$(PWD)/target/deploy cargo test \ --locked \ --manifest-path $(call make-path,$*)/Cargo.toml \ From 2ba055eb985c6b4a128d8c7f0bbe5e817db1b7ef Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 15:31:42 -0400 Subject: [PATCH 6/9] Update spellcheck.dic --- .config/spellcheck.dic | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.config/spellcheck.dic b/.config/spellcheck.dic index 3934b40..74c3c84 100644 --- a/.config/spellcheck.dic +++ b/.config/spellcheck.dic @@ -1,4 +1,4 @@ -16 +20 CPI Ed25519 Ed25519SignatureOffsets @@ -8,10 +8,14 @@ SDK SVM Solana cofactored +cryptographic +deserializes ed25519 +entrypoint malleability precompile precompiles runtime syscall syscalls +verifier From e04fb09142717afc01d5354ac8e7e16340c02639 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 15:44:14 -0400 Subject: [PATCH 7/9] refactor instruction data --- Cargo.lock | 2 +- Cargo.toml | 1 - README.md | 47 ++++---- ed25519-verify/Cargo.toml | 4 +- ed25519-verify/src/instruction.rs | 121 +++++++++++---------- ed25519-verify/src/instruction_data.rs | 36 +++--- ed25519-verify/src/lib.rs | 16 +-- ed25519-verify/src/test_utils.rs | 46 +++----- ed25519-verify/src/verifier.rs | 28 +---- ed25519-verify/tests/verify_instruction.rs | 31 +----- 10 files changed, 134 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee1d908..a5b94d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2236,7 +2236,7 @@ dependencies = [ "solana-ed25519-verify", "solana-instruction", "solana-program-error", - "solana-sdk-ids", + "solana-pubkey 4.1.0", "solana-sha512-hasher", ] diff --git a/Cargo.toml b/Cargo.toml index 93e4ea4..27f3676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,4 @@ solana-instruction = "3.3.0" solana-program-error = "3.0.1" solana-program-runtime = { version = "4.0.0", features = ["agave-unstable-api"] } solana-pubkey = "4.1.0" -solana-sdk-ids = "3.1.0" solana-sha512-hasher = { version = "1.0.1", features = ["sha2"] } diff --git a/README.md b/README.md index abf199c..882e2ee 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,12 @@ Pinocchio's lazy instruction context to avoid up-front account parsing. ## Motivation The goal is to migrate the native [ed25519 precompile] to SBF so it can be -maintained and deployed like any other on-chain program. The instruction format -is intentionally identical to the precompile for current-instruction data, so -clients can reuse the standard Ed25519 instruction layout. +maintained and deployed like any other on-chain program. The precompile is +expected to be removed from the runtime, so this program does not preserve +byte-for-byte compatibility with its instruction format: since an SBF program +only ever receives its own instruction data, the precompile's +cross-instruction reference fields have no meaning here and are dropped +entirely, giving a more compact encoding. Programs can either depend on `solana-ed25519-verify` directly or invoke `solana-ed25519-program` by CPI and act on the explicit pass/fail result, @@ -38,30 +41,27 @@ feature before SBF execution will work. ## Instruction format ```text -[0] number of signatures (u8) -[1] padding, ignored -[2 .. 2 + 14*N] N x Ed25519SignatureOffsets records (14 bytes each, LE) -[2 + 14*N ..] payload: public keys, signatures, messages (order flexible) +[0] number of signatures (u8) +[1] padding, ignored +[2 .. 2 + 8*N] N x SignatureOffsets records (8 bytes each, LE) +[2 + 8*N ..] payload: public keys, signatures, messages (order flexible) ``` -Each offset record matches `Ed25519SignatureOffsets` exposed by this crate: +Each offset record matches `SignatureOffsets` exposed by this crate: ```text [0..2] signature_offset -[2..4] signature_instruction_index -[4..6] public_key_offset -[6..8] public_key_instruction_index -[8..10] message_data_offset -[10..12] message_data_size -[12..14] message_instruction_index +[2..4] public_key_offset +[4..6] message_data_offset +[6..8] message_data_size ``` +Every offset implicitly refers to this instruction's own data. Unlike the +native precompile, there is no wire representation for referencing another +instruction in the transaction. + ### Constraints -- **All instruction-index fields must be `u16::MAX`.** The native precompile - uses this sentinel for the current instruction. An SBF program receives only - its own instruction data; cross-instruction references require a future - runtime change. - **ZIP-215 verification.** The program uses the cofactored equation `[8](S·B − H(R‖A‖M)·A) == [8]R` with canonical `S`, following [ZIP-215](https://zips.z.cash/zip-0215). Small-order `R` and public-key @@ -82,7 +82,7 @@ Each offset record matches `Ed25519SignatureOffsets` exposed by this crate: |---|---|---| | `instruction` | off | Enables alloc-based `Instruction` construction helpers. | | `dev-context-only-utils` | off | Enables `instruction` and exposes `test_utils`, the instruction builders shared by this crate's and `solana-ed25519-program`'s tests. | -| `serde` | off | Derives serde traits for `Ed25519SignatureOffsets`. | +| `serde` | off | Derives serde traits for `SignatureOffsets`. | `solana-ed25519-program` only exposes `no-entrypoint`, which omits the Pinocchio entrypoint when embedding the program crate in tests or another @@ -91,9 +91,12 @@ program. ## Public API `solana-ed25519-verify` exposes the stateless `Ed25519Verifier`, layout -constants, `Ed25519SignatureOffsets`, and, with the `instruction` feature, -fallible instruction constructors `new_ed25519_instruction_with_signature` -and `offsets_to_ed25519_instruction`. +constants, `SignatureOffsets`, and, with the `instruction` feature, fallible +instruction constructors `new_ed25519_instruction_with_signature` and +`offsets_to_ed25519_instruction`. Both constructors take the target +`program_id` explicitly, since this format is specific to wherever this +program (or one embedding this library) is deployed rather than the fixed +native precompile address. `solana-ed25519-program` calls the library from its Pinocchio processor. diff --git a/ed25519-verify/Cargo.toml b/ed25519-verify/Cargo.toml index 188f7ab..4340fb1 100644 --- a/ed25519-verify/Cargo.toml +++ b/ed25519-verify/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["rlib"] [features] default = [] dev-context-only-utils = ["instruction"] -instruction = ["dep:solana-instruction", "dep:solana-sdk-ids", "serde"] +instruction = ["dep:solana-instruction", "dep:solana-pubkey"] serde = ["dep:serde", "dep:serde_derive"] [dependencies] @@ -18,7 +18,7 @@ serde_derive = { workspace = true, optional = true } solana-curve25519 = { workspace = true } solana-instruction = { workspace = true, optional = true } solana-program-error = { workspace = true } -solana-sdk-ids = { workspace = true, optional = true } +solana-pubkey = { workspace = true, optional = true } solana-sha512-hasher = { workspace = true } [target.'cfg(not(any(target_os = "solana", target_arch = "bpf")))'.dependencies] diff --git a/ed25519-verify/src/instruction.rs b/ed25519-verify/src/instruction.rs index 620fda6..b7ad4e0 100644 --- a/ed25519-verify/src/instruction.rs +++ b/ed25519-verify/src/instruction.rs @@ -1,37 +1,35 @@ //! Ed25519 instruction layout and construction helpers. -// This was adapted from `solana-sdk/ed25519_program`. #[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; #[cfg(feature = "instruction")] -use {alloc::vec, solana_instruction::Instruction, solana_program_error::ProgramError}; +use { + alloc::vec, solana_instruction::Instruction, solana_program_error::ProgramError, + solana_pubkey::Pubkey, +}; pub const PUBKEY_SERIALIZED_SIZE: usize = 32; pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; -pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14; -/// The second header byte is padding; the native precompile ignores it. +pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 8; +/// The second header byte is padding, kept so the offset records start on an +/// even byte. pub const SIGNATURE_OFFSETS_START: usize = 2; pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; -pub const CURRENT_INSTRUCTION_INDEX: u16 = u16::MAX; -/// Offsets of signature data within an ed25519 instruction. +/// Offsets of one signature's fields within the instruction data. Every +/// offset is implicitly into this instruction's own data; there is no wire +/// representation for referencing another instruction. #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] -pub struct Ed25519SignatureOffsets { - /// Offset to 64-byte ed25519 signature. +pub struct SignatureOffsets { + /// Offset to the 64-byte ed25519 signature. pub signature_offset: u16, - /// Instruction index that contains the signature, or `u16::MAX` for this instruction. - pub signature_instruction_index: u16, - /// Offset to 32-byte public key. + /// Offset to the 32-byte public key. pub public_key_offset: u16, - /// Instruction index that contains the public key, or `u16::MAX` for this instruction. - pub public_key_instruction_index: u16, - /// Offset to start of message data. + /// Offset to the start of the message data. pub message_data_offset: u16, - /// Size of message data in bytes. + /// Size of the message data in bytes. pub message_data_size: u16, - /// Instruction index that contains the message, or `u16::MAX` for this instruction. - pub message_instruction_index: u16, } /// Signs a message from the given private key bytes. @@ -47,12 +45,14 @@ pub fn sign_message( } #[cfg(feature = "instruction")] -/// Encode just the signature offsets in a single ed25519 instruction. +/// Encode just the signature offsets in a single ed25519 instruction +/// targeting `program_id`. /// -/// Returns an error if `offsets.len()` cannot fit in the native program's -/// one-byte signature count field. +/// Returns an error if `offsets.len()` cannot fit in the one-byte signature +/// count field. pub fn offsets_to_ed25519_instruction( - offsets: &[Ed25519SignatureOffsets], + program_id: Pubkey, + offsets: &[SignatureOffsets], ) -> Result { let num_signatures = u8::try_from(offsets.len()).map_err(|_| ProgramError::InvalidInstructionData)?; @@ -72,18 +72,19 @@ pub fn offsets_to_ed25519_instruction( } Ok(Instruction { - program_id: solana_sdk_ids::ed25519_program::id(), + program_id, accounts: vec![], data: instruction_data, }) } #[cfg(feature = "instruction")] -/// Builds a single-signature ed25519 instruction. +/// Builds a single-signature ed25519 instruction targeting `program_id`. /// /// Returns an error if the message length or any offset cannot be represented /// in the 16-bit wire fields. pub fn new_ed25519_instruction_with_signature( + program_id: Pubkey, message: &[u8], signature: &[u8; SIGNATURE_SERIALIZED_SIZE], pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], @@ -111,14 +112,11 @@ pub fn new_ed25519_instruction_with_signature( let mut instruction_data = vec![0; message_data_end]; instruction_data[0] = 1; - let offsets = Ed25519SignatureOffsets { + let offsets = SignatureOffsets { signature_offset, - signature_instruction_index: CURRENT_INSTRUCTION_INDEX, public_key_offset, - public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, message_data_offset, message_data_size, - message_instruction_index: CURRENT_INSTRUCTION_INDEX, }; serialize_signature_offsets( &mut instruction_data[SIGNATURE_OFFSETS_START..DATA_START], @@ -137,7 +135,7 @@ pub fn new_ed25519_instruction_with_signature( instruction_data[message_data_start..message_data_end].copy_from_slice(message); Ok(Instruction { - program_id: solana_sdk_ids::ed25519_program::id(), + program_id, accounts: vec![], data: instruction_data, }) @@ -146,19 +144,16 @@ pub fn new_ed25519_instruction_with_signature( #[cfg(feature = "instruction")] fn serialize_signature_offsets( output: &mut [u8], - offsets: &Ed25519SignatureOffsets, + offsets: &SignatureOffsets, ) -> Result<(), ProgramError> { if output.len() != SIGNATURE_OFFSETS_SERIALIZED_SIZE { return Err(ProgramError::InvalidInstructionData); } output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); - output[2..4].copy_from_slice(&offsets.signature_instruction_index.to_le_bytes()); - output[4..6].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); - output[6..8].copy_from_slice(&offsets.public_key_instruction_index.to_le_bytes()); - output[8..10].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); - output[10..12].copy_from_slice(&offsets.message_data_size.to_le_bytes()); - output[12..14].copy_from_slice(&offsets.message_instruction_index.to_le_bytes()); + output[2..4].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); + output[4..6].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); + output[6..8].copy_from_slice(&offsets.message_data_size.to_le_bytes()); Ok(()) } @@ -168,39 +163,36 @@ mod tests { use super::*; use alloc::vec; - fn read_first_offsets(input: &[u8]) -> Ed25519SignatureOffsets { - Ed25519SignatureOffsets { + fn read_first_offsets(input: &[u8]) -> SignatureOffsets { + SignatureOffsets { signature_offset: u16::from_le_bytes(input[2..4].try_into().unwrap()), - signature_instruction_index: u16::from_le_bytes(input[4..6].try_into().unwrap()), - public_key_offset: u16::from_le_bytes(input[6..8].try_into().unwrap()), - public_key_instruction_index: u16::from_le_bytes(input[8..10].try_into().unwrap()), - message_data_offset: u16::from_le_bytes(input[10..12].try_into().unwrap()), - message_data_size: u16::from_le_bytes(input[12..14].try_into().unwrap()), - message_instruction_index: u16::from_le_bytes(input[14..16].try_into().unwrap()), + public_key_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), + message_data_offset: u16::from_le_bytes(input[6..8].try_into().unwrap()), + message_data_size: u16::from_le_bytes(input[8..10].try_into().unwrap()), } } #[test] - fn test_instruction_builder_produces_current_instruction_offsets() { + fn test_instruction_builder_produces_valid_offsets() { let signature = [1; SIGNATURE_SERIALIZED_SIZE]; let pubkey = [2; PUBKEY_SERIALIZED_SIZE]; - let instruction = new_ed25519_instruction_with_signature(b"message", &signature, &pubkey) - .expect("valid inputs"); + let instruction = new_ed25519_instruction_with_signature( + Pubkey::default(), + b"message", + &signature, + &pubkey, + ) + .expect("valid inputs"); let offsets = read_first_offsets(&instruction.data); assert_eq!(instruction.accounts.len(), 0); assert_eq!(instruction.data[0], 1); assert_eq!(instruction.data[1], 0); assert_eq!( - offsets.signature_instruction_index, - CURRENT_INSTRUCTION_INDEX + offsets.public_key_offset, + u16::try_from(DATA_START).unwrap() ); - assert_eq!( - offsets.public_key_instruction_index, - CURRENT_INSTRUCTION_INDEX - ); - assert_eq!(offsets.message_instruction_index, CURRENT_INSTRUCTION_INDEX); } #[test] @@ -210,17 +202,26 @@ mod tests { let max_message = vec![3; u16::MAX as usize]; let oversized_message = vec![3; u16::MAX as usize + 1]; - assert!(new_ed25519_instruction_with_signature(&max_message, &signature, &pubkey).is_ok()); - assert!( - new_ed25519_instruction_with_signature(&oversized_message, &signature, &pubkey) - .is_err() - ); + assert!(new_ed25519_instruction_with_signature( + Pubkey::default(), + &max_message, + &signature, + &pubkey + ) + .is_ok()); + assert!(new_ed25519_instruction_with_signature( + Pubkey::default(), + &oversized_message, + &signature, + &pubkey + ) + .is_err()); } #[test] fn test_offsets_builder_rejects_too_many_signatures() { - let offsets = vec![Ed25519SignatureOffsets::default(); u8::MAX as usize + 1]; + let offsets = vec![SignatureOffsets::default(); u8::MAX as usize + 1]; - assert!(offsets_to_ed25519_instruction(&offsets).is_err()); + assert!(offsets_to_ed25519_instruction(Pubkey::default(), &offsets).is_err()); } } diff --git a/ed25519-verify/src/instruction_data.rs b/ed25519-verify/src/instruction_data.rs index d94e435..5cb925c 100644 --- a/ed25519-verify/src/instruction_data.rs +++ b/ed25519-verify/src/instruction_data.rs @@ -1,21 +1,20 @@ //! Parsing helpers for the ed25519 instruction data wire format. //! -//! The on-wire layout matches the native ed25519 precompile: +//! The on-wire layout: //! //! ```text //! Byte 0 : num_signatures (u8) //! Byte 1 : padding, ignored -//! Bytes 2 ... : num_signatures x Ed25519SignatureOffsets (14 bytes each, LE) +//! Bytes 2 ... : num_signatures x SignatureOffsets (8 bytes each, LE) //! Remaining bytes : raw payload (public keys, signatures, messages) //! ``` //! -//! The native precompile treats instruction index `u16::MAX` as "current -//! instruction". This SBF program receives only its own instruction data, so -//! all index fields must use that sentinel. +//! Every offset implicitly refers to this instruction's own data; there is no +//! wire representation for referencing another instruction. use { crate::{ - Ed25519SignatureOffsets, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, + SignatureOffsets, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, }, solana_program_error::ProgramError, @@ -31,16 +30,13 @@ pub(crate) struct SignatureFields<'a> { pub(crate) message: &'a [u8], } -/// Parses a 14-byte `Ed25519SignatureOffsets` record from `input`. -fn unpack_signature_offsets(input: &[u8]) -> Result { - Ok(Ed25519SignatureOffsets { +/// Parses an 8-byte `SignatureOffsets` record from `input`. +fn unpack_signature_offsets(input: &[u8]) -> Result { + Ok(SignatureOffsets { signature_offset: decode_u16(input, 0)?, - signature_instruction_index: decode_u16(input, 2)?, - public_key_offset: decode_u16(input, 4)?, - public_key_instruction_index: decode_u16(input, 6)?, - message_data_offset: decode_u16(input, 8)?, - message_data_size: decode_u16(input, 10)?, - message_instruction_index: decode_u16(input, 12)?, + public_key_offset: decode_u16(input, 2)?, + message_data_offset: decode_u16(input, 4)?, + message_data_size: decode_u16(input, 6)?, }) } @@ -80,7 +76,7 @@ fn get_instruction_data_array( /// Extracts all signature fields for one entry from raw instruction data. pub(crate) fn get_signature_fields<'a>( instruction_data: &'a [u8], - offsets: &'a Ed25519SignatureOffsets, + offsets: &'a SignatureOffsets, ) -> Result, ProgramError> { Ok(SignatureFields { signature: get_instruction_data_array(instruction_data, offsets.signature_offset)?, @@ -94,15 +90,13 @@ pub(crate) fn get_signature_fields<'a>( } /// Parses the leading `num_signatures` byte and returns an iterator that yields -/// one `Ed25519SignatureOffsets` per entry. +/// one `SignatureOffsets` per entry. /// /// `num_signatures == 0` is valid only when the buffer is exactly the 2-byte -/// header. The padding byte is intentionally ignored for nonzero counts, -/// matching the native precompile. +/// header. The padding byte is intentionally ignored for nonzero counts. pub(crate) fn iter_signature_offsets( input: &[u8], -) -> Result> + '_, ProgramError> -{ +) -> Result> + '_, ProgramError> { if input.len() < SIGNATURE_OFFSETS_START { return Err(ProgramError::InvalidInstructionData); } diff --git a/ed25519-verify/src/lib.rs b/ed25519-verify/src/lib.rs index 380024a..b62e097 100644 --- a/ed25519-verify/src/lib.rs +++ b/ed25519-verify/src/lib.rs @@ -7,18 +7,18 @@ //! Ed25519 signatures directly without invoking the standalone verifier //! program. //! -//! Instruction data mirrors the native ed25519 precompile format: +//! Instruction data layout: //! //! ```text //! [num_signatures: u8] //! [padding: u8] -//! [Ed25519SignatureOffsets x num_signatures] (14 bytes each, little-endian) -//! [public key || signature || message ...] (payload, order flexible) +//! [SignatureOffsets x num_signatures] (8 bytes each, little-endian) +//! [public key || signature || message ...] (payload, order flexible) //! ``` //! -//! The verifier accepts only current-instruction references -//! (`CURRENT_INSTRUCTION_INDEX`, `u16::MAX`) and performs ZIP-215 verification -//! with canonical `S`. +//! Every offset implicitly refers to this instruction's own data. There is no +//! cross-instruction reference, unlike the native ed25519 precompile. The +//! verifier performs ZIP-215 verification with canonical `S`. #[cfg(feature = "instruction")] extern crate alloc; @@ -37,7 +37,7 @@ pub use instruction::sign_message; #[cfg(feature = "instruction")] pub use instruction::{new_ed25519_instruction_with_signature, offsets_to_ed25519_instruction}; pub use instruction::{ - Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, + SignatureOffsets, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, + SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, }; pub use verifier::Ed25519Verifier; diff --git a/ed25519-verify/src/test_utils.rs b/ed25519-verify/src/test_utils.rs index ab755bf..c2c4732 100644 --- a/ed25519-verify/src/test_utils.rs +++ b/ed25519-verify/src/test_utils.rs @@ -7,8 +7,8 @@ use { crate::{ - Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, DATA_START, PUBKEY_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, + SignatureOffsets, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, + SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, }, alloc::{vec, vec::Vec}, ed25519_dalek::{Signer, SigningKey}, @@ -61,14 +61,11 @@ pub fn signed_instruction(messages: &[&[u8]]) -> Vec { let message_data_offset = instruction.len(); instruction.extend_from_slice(payload.message); - let offsets = Ed25519SignatureOffsets { + let offsets = SignatureOffsets { signature_offset: u16::try_from(signature_offset).unwrap(), - signature_instruction_index: CURRENT_INSTRUCTION_INDEX, public_key_offset: u16::try_from(public_key_offset).unwrap(), - public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, message_data_offset: u16::try_from(message_data_offset).unwrap(), message_data_size: u16::try_from(payload.message.len()).unwrap(), - message_instruction_index: CURRENT_INSTRUCTION_INDEX, }; write_offsets( &mut instruction[SIGNATURE_OFFSETS_START + index * SIGNATURE_OFFSETS_SERIALIZED_SIZE @@ -98,14 +95,11 @@ pub fn instruction_with_signature( let message_data_offset = instruction.len(); instruction.extend_from_slice(message); - let offsets = Ed25519SignatureOffsets { + let offsets = SignatureOffsets { signature_offset: u16::try_from(signature_offset).unwrap(), - signature_instruction_index: CURRENT_INSTRUCTION_INDEX, public_key_offset: u16::try_from(public_key_offset).unwrap(), - public_key_instruction_index: CURRENT_INSTRUCTION_INDEX, message_data_offset: u16::try_from(message_data_offset).unwrap(), message_data_size: u16::try_from(message.len()).unwrap(), - message_instruction_index: CURRENT_INSTRUCTION_INDEX, }; write_offsets( &mut instruction[SIGNATURE_OFFSETS_START..DATA_START], @@ -115,31 +109,25 @@ pub fn instruction_with_signature( instruction } -/// Parses and returns the first `Ed25519SignatureOffsets` entry from `instruction`. -pub fn first_offsets(instruction: &[u8]) -> Ed25519SignatureOffsets { +/// Parses and returns the first `SignatureOffsets` entry from `instruction`. +pub fn first_offsets(instruction: &[u8]) -> SignatureOffsets { read_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) } -/// Deserializes the 14-byte little-endian wire format. -fn read_offsets(input: &[u8]) -> Ed25519SignatureOffsets { - Ed25519SignatureOffsets { +/// Deserializes the 8-byte little-endian wire format. +fn read_offsets(input: &[u8]) -> SignatureOffsets { + SignatureOffsets { signature_offset: u16::from_le_bytes(input[0..2].try_into().unwrap()), - signature_instruction_index: u16::from_le_bytes(input[2..4].try_into().unwrap()), - public_key_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), - public_key_instruction_index: u16::from_le_bytes(input[6..8].try_into().unwrap()), - message_data_offset: u16::from_le_bytes(input[8..10].try_into().unwrap()), - message_data_size: u16::from_le_bytes(input[10..12].try_into().unwrap()), - message_instruction_index: u16::from_le_bytes(input[12..14].try_into().unwrap()), + public_key_offset: u16::from_le_bytes(input[2..4].try_into().unwrap()), + message_data_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), + message_data_size: u16::from_le_bytes(input[6..8].try_into().unwrap()), } } -/// Serializes `offsets` into the 14-byte little-endian wire format in `output`. -pub fn write_offsets(output: &mut [u8], offsets: &Ed25519SignatureOffsets) { +/// Serializes `offsets` into the 8-byte little-endian wire format in `output`. +pub fn write_offsets(output: &mut [u8], offsets: &SignatureOffsets) { output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); - output[2..4].copy_from_slice(&offsets.signature_instruction_index.to_le_bytes()); - output[4..6].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); - output[6..8].copy_from_slice(&offsets.public_key_instruction_index.to_le_bytes()); - output[8..10].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); - output[10..12].copy_from_slice(&offsets.message_data_size.to_le_bytes()); - output[12..14].copy_from_slice(&offsets.message_instruction_index.to_le_bytes()); + output[2..4].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); + output[4..6].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); + output[6..8].copy_from_slice(&offsets.message_data_size.to_le_bytes()); } diff --git a/ed25519-verify/src/verifier.rs b/ed25519-verify/src/verifier.rs index cfa1112..dde68da 100644 --- a/ed25519-verify/src/verifier.rs +++ b/ed25519-verify/src/verifier.rs @@ -1,8 +1,7 @@ use { crate::{ instruction_data::{get_signature_fields, iter_signature_offsets}, - scalar, Ed25519SignatureOffsets, CURRENT_INSTRUCTION_INDEX, PUBKEY_SERIALIZED_SIZE, - SIGNATURE_SERIALIZED_SIZE, + scalar, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE, }, solana_curve25519::{ edwards::{ @@ -40,7 +39,9 @@ impl Ed25519Verifier { /// describes, returning an error on the first failure. pub fn verify_instruction(&self, instruction_data: &[u8]) -> Result<(), ProgramError> { for offsets in iter_signature_offsets(instruction_data)? { - self.verify_signature_offsets(instruction_data, &offsets?)?; + let offsets = offsets?; + let fields = get_signature_fields(instruction_data, &offsets)?; + self.verify_signature(fields.signature, fields.public_key, fields.message)?; } Ok(()) @@ -88,27 +89,6 @@ impl Ed25519Verifier { Ok(()) } - - /// Validates a single signature entry described by `offsets`. - fn verify_signature_offsets( - &self, - instruction_data: &[u8], - offsets: &Ed25519SignatureOffsets, - ) -> Result<(), ProgramError> { - if !references_current_instruction(offsets) { - return Err(ProgramError::InvalidInstructionData); - } - - let fields = get_signature_fields(instruction_data, offsets)?; - self.verify_signature(fields.signature, fields.public_key, fields.message) - } -} - -/// Returns `true` when every offset field references the current instruction. -fn references_current_instruction(offsets: &Ed25519SignatureOffsets) -> bool { - offsets.signature_instruction_index == CURRENT_INSTRUCTION_INDEX - && offsets.public_key_instruction_index == CURRENT_INSTRUCTION_INDEX - && offsets.message_instruction_index == CURRENT_INSTRUCTION_INDEX } fn compute_challenge(signature_r: &[u8; 32], public_key: &[u8; 32], message: &[u8]) -> [u8; 32] { diff --git a/ed25519-verify/tests/verify_instruction.rs b/ed25519-verify/tests/verify_instruction.rs index d2909b8..78b37b0 100644 --- a/ed25519-verify/tests/verify_instruction.rs +++ b/ed25519-verify/tests/verify_instruction.rs @@ -5,7 +5,7 @@ use { first_offsets, instruction_with_signature, signed_instruction, write_offsets, EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, }, - Ed25519Verifier, CURRENT_INSTRUCTION_INDEX, DATA_START, SIGNATURE_SERIALIZED_SIZE, + Ed25519Verifier, DATA_START, SIGNATURE_SERIALIZED_SIZE, }, solana_program_error::ProgramError, }; @@ -109,35 +109,6 @@ fn accepts_zero_signatures_only_when_data_has_just_header() { ); } -#[test] -fn rejects_offsets_to_other_instructions() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let mut offsets = first_offsets(&instruction); - offsets.signature_instruction_index = 0; - write_offsets(&mut instruction[2..DATA_START], &offsets); - - assert_eq!( - process_instruction(&instruction), - Err(ProgramError::InvalidInstructionData) - ); - - offsets.signature_instruction_index = CURRENT_INSTRUCTION_INDEX; - offsets.public_key_instruction_index = 0; - write_offsets(&mut instruction[2..DATA_START], &offsets); - assert_eq!( - process_instruction(&instruction), - Err(ProgramError::InvalidInstructionData) - ); - - offsets.public_key_instruction_index = CURRENT_INSTRUCTION_INDEX; - offsets.message_instruction_index = 0; - write_offsets(&mut instruction[2..DATA_START], &offsets); - assert_eq!( - process_instruction(&instruction), - Err(ProgramError::InvalidInstructionData) - ); -} - #[test] fn rejects_out_of_bounds_offsets() { let mut instruction = signed_instruction(&[b"hello ed25519"]); From b11e9aeea2ba1fe52da835d2b4612c1b788a303b Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 9 Jul 2026 16:04:18 -0400 Subject: [PATCH 8/9] clean up --- ed25519-verify/src/instruction.rs | 2 +- ed25519-verify/src/instruction_data.rs | 2 +- ed25519-verify/src/test_utils.rs | 54 ++++++-------------------- ed25519-verify/src/verifier.rs | 16 ++++---- 4 files changed, 21 insertions(+), 53 deletions(-) diff --git a/ed25519-verify/src/instruction.rs b/ed25519-verify/src/instruction.rs index b7ad4e0..5ce1b36 100644 --- a/ed25519-verify/src/instruction.rs +++ b/ed25519-verify/src/instruction.rs @@ -142,7 +142,7 @@ pub fn new_ed25519_instruction_with_signature( } #[cfg(feature = "instruction")] -fn serialize_signature_offsets( +pub(crate) fn serialize_signature_offsets( output: &mut [u8], offsets: &SignatureOffsets, ) -> Result<(), ProgramError> { diff --git a/ed25519-verify/src/instruction_data.rs b/ed25519-verify/src/instruction_data.rs index 5cb925c..3cc8c86 100644 --- a/ed25519-verify/src/instruction_data.rs +++ b/ed25519-verify/src/instruction_data.rs @@ -31,7 +31,7 @@ pub(crate) struct SignatureFields<'a> { } /// Parses an 8-byte `SignatureOffsets` record from `input`. -fn unpack_signature_offsets(input: &[u8]) -> Result { +pub(crate) fn unpack_signature_offsets(input: &[u8]) -> Result { Ok(SignatureOffsets { signature_offset: decode_u16(input, 0)?, public_key_offset: decode_u16(input, 2)?, diff --git a/ed25519-verify/src/test_utils.rs b/ed25519-verify/src/test_utils.rs index c2c4732..fca01c2 100644 --- a/ed25519-verify/src/test_utils.rs +++ b/ed25519-verify/src/test_utils.rs @@ -7,17 +7,18 @@ use { crate::{ + instruction::serialize_signature_offsets, instruction_data::unpack_signature_offsets, + new_ed25519_instruction_with_signature, verifier::EDWARDS_IDENTITY_COMPRESSED_BYTES, SignatureOffsets, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, }, alloc::{vec, vec::Vec}, ed25519_dalek::{Signer, SigningKey}, + solana_pubkey::Pubkey, }; -pub const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]; +pub const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = + EDWARDS_IDENTITY_COMPRESSED_BYTES; pub const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, @@ -83,51 +84,18 @@ pub fn instruction_with_signature( signature: &[u8; SIGNATURE_SERIALIZED_SIZE], pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], ) -> Vec { - let mut instruction = vec![0; DATA_START]; - instruction[0] = 1; - - let public_key_offset = instruction.len(); - instruction.extend_from_slice(pubkey); - - let signature_offset = instruction.len(); - instruction.extend_from_slice(signature); - - let message_data_offset = instruction.len(); - instruction.extend_from_slice(message); - - let offsets = SignatureOffsets { - signature_offset: u16::try_from(signature_offset).unwrap(), - public_key_offset: u16::try_from(public_key_offset).unwrap(), - message_data_offset: u16::try_from(message_data_offset).unwrap(), - message_data_size: u16::try_from(message.len()).unwrap(), - }; - write_offsets( - &mut instruction[SIGNATURE_OFFSETS_START..DATA_START], - &offsets, - ); - - instruction + new_ed25519_instruction_with_signature(Pubkey::default(), message, signature, pubkey) + .expect("valid test inputs") + .data } /// Parses and returns the first `SignatureOffsets` entry from `instruction`. pub fn first_offsets(instruction: &[u8]) -> SignatureOffsets { - read_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) -} - -/// Deserializes the 8-byte little-endian wire format. -fn read_offsets(input: &[u8]) -> SignatureOffsets { - SignatureOffsets { - signature_offset: u16::from_le_bytes(input[0..2].try_into().unwrap()), - public_key_offset: u16::from_le_bytes(input[2..4].try_into().unwrap()), - message_data_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), - message_data_size: u16::from_le_bytes(input[6..8].try_into().unwrap()), - } + unpack_signature_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) + .expect("well-formed test instruction") } /// Serializes `offsets` into the 8-byte little-endian wire format in `output`. pub fn write_offsets(output: &mut [u8], offsets: &SignatureOffsets) { - output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); - output[2..4].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); - output[4..6].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); - output[6..8].copy_from_slice(&offsets.message_data_size.to_le_bytes()); + serialize_signature_offsets(output, offsets).expect("output is exactly 8 bytes"); } diff --git a/ed25519-verify/src/verifier.rs b/ed25519-verify/src/verifier.rs index dde68da..8e97bd7 100644 --- a/ed25519-verify/src/verifier.rs +++ b/ed25519-verify/src/verifier.rs @@ -16,10 +16,13 @@ const ED25519_BASEPOINT_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([ 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, ]); -const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint = PodEdwardsPoint([ +/// Identity point of the Edwards curve, in compressed form. +pub(crate) const EDWARDS_IDENTITY_COMPRESSED_BYTES: [u8; PUBKEY_SERIALIZED_SIZE] = [ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]); +]; +const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint = + PodEdwardsPoint(EDWARDS_IDENTITY_COMPRESSED_BYTES); const EIGHT_SCALAR: PodScalar = PodScalar([ 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -59,12 +62,9 @@ impl Ed25519Verifier { public_key: &[u8; PUBKEY_SERIALIZED_SIZE], message: &[u8], ) -> Result<(), ProgramError> { - let r_bytes: &[u8; 32] = signature[..32] - .try_into() - .map_err(|_| ProgramError::InvalidArgument)?; - let s_bytes: &[u8; 32] = signature[32..] - .try_into() - .map_err(|_| ProgramError::InvalidArgument)?; + let (r_bytes, s_bytes) = signature.split_at(32); + let r_bytes: &[u8; 32] = r_bytes.try_into().unwrap(); + let s_bytes: &[u8; 32] = s_bytes.try_into().unwrap(); if !scalar::is_canonical_scalar(s_bytes) { return Err(ProgramError::InvalidArgument); } From 3be55e0adadc217e6d947ae6f753179d1c8bb2bd Mon Sep 17 00:00:00 2001 From: zz-sol Date: Fri, 10 Jul 2026 07:40:40 -0400 Subject: [PATCH 9/9] address comments --- Cargo.lock | 3 +- README.md | 84 +++----- ed25519-verify/Cargo.toml | 12 +- ed25519-verify/src/instruction.rs | 227 --------------------- ed25519-verify/src/instruction_data.rs | 128 ------------ ed25519-verify/src/lib.rs | 39 +--- ed25519-verify/src/program.rs | 24 +++ ed25519-verify/src/test_utils.rs | 101 --------- ed25519-verify/src/verifier.rs | 17 +- ed25519-verify/tests/verify_instruction.rs | 221 ++++++++------------ program/Cargo.toml | 4 +- program/src/lib.rs | 65 +++--- program/src/processor.rs | 15 -- program/tests/mollusk.rs | 103 +++++----- 14 files changed, 251 insertions(+), 792 deletions(-) delete mode 100644 ed25519-verify/src/instruction.rs delete mode 100644 ed25519-verify/src/instruction_data.rs create mode 100644 ed25519-verify/src/program.rs delete mode 100644 ed25519-verify/src/test_utils.rs delete mode 100644 program/src/processor.rs diff --git a/Cargo.lock b/Cargo.lock index a5b94d1..fc230f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2215,6 +2215,7 @@ checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" name = "solana-ed25519-program" version = "4.0.0" dependencies = [ + "ed25519-dalek", "mollusk-svm", "pinocchio", "solana-account", @@ -2230,8 +2231,6 @@ name = "solana-ed25519-verify" version = "4.0.0" dependencies = [ "ed25519-dalek", - "serde", - "serde_derive", "solana-curve25519", "solana-ed25519-verify", "solana-instruction", diff --git a/README.md b/README.md index 882e2ee..79e968f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,19 @@ -# solana-ed25519: on-chain signature verification for Solana +# solana-ed25519-program: on-chain signature verification for Solana -This workspace provides two crates: - -- `solana-ed25519-verify`: a no-std, stateless Ed25519 verification library. -- `solana-ed25519-program`: a minimal Pinocchio SBF program that calls the - library and returns an explicit pass/fail result. - -Both use the Curve25519 and SHA-512 syscalls. The program entrypoint uses -Pinocchio's lazy instruction context to avoid up-front account parsing. +A minimal Solana SBF program that re-verifies Ed25519 signatures on-chain using +the Curve25519 and SHA-512 syscalls. ## Motivation The goal is to migrate the native [ed25519 precompile] to SBF so it can be -maintained and deployed like any other on-chain program. The precompile is -expected to be removed from the runtime, so this program does not preserve -byte-for-byte compatibility with its instruction format: since an SBF program -only ever receives its own instruction data, the precompile's -cross-instruction reference fields have no meaning here and are dropped -entirely, giving a more compact encoding. - -Programs can either depend on `solana-ed25519-verify` directly or invoke -`solana-ed25519-program` by CPI and act on the explicit pass/fail result, -rather than relying on `sysvar::instructions` inspection to confirm a parallel -precompile instruction succeeded. +maintained and deployed like any other on-chain program. The instruction format +is intentionally identical to the precompile for current-instruction data, so +clients can reuse the standard Ed25519 instruction layout. + +Being a regular SBF program also unlocks CPI: another program can invoke this +one and act on the explicit pass/fail result, rather than relying on +`sysvar::instructions` inspection to confirm a parallel precompile instruction +succeeded. [ed25519 precompile]: https://docs.solanalabs.com/runtime/programs#ed25519-program @@ -41,27 +32,30 @@ feature before SBF execution will work. ## Instruction format ```text -[0] number of signatures (u8) -[1] padding, ignored -[2 .. 2 + 8*N] N x SignatureOffsets records (8 bytes each, LE) -[2 + 8*N ..] payload: public keys, signatures, messages (order flexible) +[0] number of signatures (u8) +[1] padding, ignored +[2 .. 2 + 14*N] N x Ed25519SignatureOffsets records (14 bytes each, LE) +[2 + 14*N ..] payload: public keys, signatures, messages (order flexible) ``` -Each offset record matches `SignatureOffsets` exposed by this crate: +Each offset record matches `Ed25519SignatureOffsets` exposed by this crate: ```text [0..2] signature_offset -[2..4] public_key_offset -[4..6] message_data_offset -[6..8] message_data_size +[2..4] signature_instruction_index +[4..6] public_key_offset +[6..8] public_key_instruction_index +[8..10] message_data_offset +[10..12] message_data_size +[12..14] message_instruction_index ``` -Every offset implicitly refers to this instruction's own data. Unlike the -native precompile, there is no wire representation for referencing another -instruction in the transaction. - ### Constraints +- **All instruction-index fields must be `u16::MAX`.** The native precompile + uses this sentinel for the current instruction. An SBF program receives only + its own instruction data; cross-instruction references require a future + runtime change. - **ZIP-215 verification.** The program uses the cofactored equation `[8](S·B − H(R‖A‖M)·A) == [8]R` with canonical `S`, following [ZIP-215](https://zips.z.cash/zip-0215). Small-order `R` and public-key @@ -76,30 +70,6 @@ instruction in the transaction. - **No accounts.** The program takes no account arguments and returns `InvalidArgument` if any are supplied. -## Cargo features - -| Feature | Default | Description | -|---|---|---| -| `instruction` | off | Enables alloc-based `Instruction` construction helpers. | -| `dev-context-only-utils` | off | Enables `instruction` and exposes `test_utils`, the instruction builders shared by this crate's and `solana-ed25519-program`'s tests. | -| `serde` | off | Derives serde traits for `SignatureOffsets`. | - -`solana-ed25519-program` only exposes `no-entrypoint`, which omits the -Pinocchio entrypoint when embedding the program crate in tests or another -program. - -## Public API - -`solana-ed25519-verify` exposes the stateless `Ed25519Verifier`, layout -constants, `SignatureOffsets`, and, with the `instruction` feature, fallible -instruction constructors `new_ed25519_instruction_with_signature` and -`offsets_to_ed25519_instruction`. Both constructors take the target -`program_id` explicitly, since this format is specific to wherever this -program (or one embedding this library) is deployed rather than the fixed -native precompile address. - -`solana-ed25519-program` calls the library from its Pinocchio processor. - ## Build and test Stable Rust `1.93.1` is pinned in `rust-toolchain.toml`. Some make targets @@ -107,7 +77,7 @@ also require the nightly Rust chain `nightly-2026-01-22`. ```sh # Unit tests (host, no SBF toolchain required) -cargo test --workspace +cargo test --manifest-path program/Cargo.toml # SBF build only cargo build-sbf --arch v2 --manifest-path program/Cargo.toml diff --git a/ed25519-verify/Cargo.toml b/ed25519-verify/Cargo.toml index 4340fb1..c912c53 100644 --- a/ed25519-verify/Cargo.toml +++ b/ed25519-verify/Cargo.toml @@ -8,24 +8,20 @@ crate-type = ["rlib"] [features] default = [] -dev-context-only-utils = ["instruction"] +# Enables the client-side constructor for invoking the standalone program. instruction = ["dep:solana-instruction", "dep:solana-pubkey"] -serde = ["dep:serde", "dep:serde_derive"] [dependencies] -serde = { workspace = true, optional = true } -serde_derive = { workspace = true, optional = true } solana-curve25519 = { workspace = true } solana-instruction = { workspace = true, optional = true } solana-program-error = { workspace = true } solana-pubkey = { workspace = true, optional = true } solana-sha512-hasher = { workspace = true } -[target.'cfg(not(any(target_os = "solana", target_arch = "bpf")))'.dependencies] -ed25519-dalek = { workspace = true } - [dev-dependencies] -solana-ed25519-verify = { path = ".", features = ["dev-context-only-utils"] } +ed25519-dalek = { workspace = true } +solana-ed25519-verify = { path = ".", features = ["instruction"] } +solana-pubkey = { workspace = true } [lints] workspace = true diff --git a/ed25519-verify/src/instruction.rs b/ed25519-verify/src/instruction.rs deleted file mode 100644 index 5ce1b36..0000000 --- a/ed25519-verify/src/instruction.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Ed25519 instruction layout and construction helpers. - -#[cfg(feature = "serde")] -use serde_derive::{Deserialize, Serialize}; -#[cfg(feature = "instruction")] -use { - alloc::vec, solana_instruction::Instruction, solana_program_error::ProgramError, - solana_pubkey::Pubkey, -}; - -pub const PUBKEY_SERIALIZED_SIZE: usize = 32; -pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; -pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 8; -/// The second header byte is padding, kept so the offset records start on an -/// even byte. -pub const SIGNATURE_OFFSETS_START: usize = 2; -pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; - -/// Offsets of one signature's fields within the instruction data. Every -/// offset is implicitly into this instruction's own data; there is no wire -/// representation for referencing another instruction. -#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] -#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] -pub struct SignatureOffsets { - /// Offset to the 64-byte ed25519 signature. - pub signature_offset: u16, - /// Offset to the 32-byte public key. - pub public_key_offset: u16, - /// Offset to the start of the message data. - pub message_data_offset: u16, - /// Size of the message data in bytes. - pub message_data_size: u16, -} - -/// Signs a message from the given private key bytes. -#[cfg(not(any(target_os = "solana", target_arch = "bpf")))] -pub fn sign_message( - priv_key_bytes: &[u8; PUBKEY_SERIALIZED_SIZE], - message: &[u8], -) -> [u8; SIGNATURE_SERIALIZED_SIZE] { - use ed25519_dalek::{Signer, SigningKey}; - - let signing_key = SigningKey::from_bytes(priv_key_bytes); - signing_key.sign(message).to_bytes() -} - -#[cfg(feature = "instruction")] -/// Encode just the signature offsets in a single ed25519 instruction -/// targeting `program_id`. -/// -/// Returns an error if `offsets.len()` cannot fit in the one-byte signature -/// count field. -pub fn offsets_to_ed25519_instruction( - program_id: Pubkey, - offsets: &[SignatureOffsets], -) -> Result { - let num_signatures = - u8::try_from(offsets.len()).map_err(|_| ProgramError::InvalidInstructionData)?; - let offsets_len = SIGNATURE_OFFSETS_SERIALIZED_SIZE - .checked_mul(offsets.len()) - .ok_or(ProgramError::InvalidInstructionData)?; - let instruction_data_len = SIGNATURE_OFFSETS_START - .checked_add(offsets_len) - .ok_or(ProgramError::InvalidInstructionData)?; - let mut instruction_data = vec![0; instruction_data_len]; - instruction_data[0] = num_signatures; - - for (index, offsets) in offsets.iter().enumerate() { - let start = SIGNATURE_OFFSETS_START + index * SIGNATURE_OFFSETS_SERIALIZED_SIZE; - let end = start + SIGNATURE_OFFSETS_SERIALIZED_SIZE; - serialize_signature_offsets(&mut instruction_data[start..end], offsets)?; - } - - Ok(Instruction { - program_id, - accounts: vec![], - data: instruction_data, - }) -} - -#[cfg(feature = "instruction")] -/// Builds a single-signature ed25519 instruction targeting `program_id`. -/// -/// Returns an error if the message length or any offset cannot be represented -/// in the 16-bit wire fields. -pub fn new_ed25519_instruction_with_signature( - program_id: Pubkey, - message: &[u8], - signature: &[u8; SIGNATURE_SERIALIZED_SIZE], - pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], -) -> Result { - let public_key_offset = DATA_START; - let signature_offset = public_key_offset - .checked_add(pubkey.len()) - .ok_or(ProgramError::InvalidInstructionData)?; - let message_data_offset = signature_offset - .checked_add(signature.len()) - .ok_or(ProgramError::InvalidInstructionData)?; - let message_data_end = message_data_offset - .checked_add(message.len()) - .ok_or(ProgramError::InvalidInstructionData)?; - - let public_key_offset = - u16::try_from(public_key_offset).map_err(|_| ProgramError::InvalidInstructionData)?; - let signature_offset = - u16::try_from(signature_offset).map_err(|_| ProgramError::InvalidInstructionData)?; - let message_data_offset = - u16::try_from(message_data_offset).map_err(|_| ProgramError::InvalidInstructionData)?; - let message_data_size = - u16::try_from(message.len()).map_err(|_| ProgramError::InvalidInstructionData)?; - - let mut instruction_data = vec![0; message_data_end]; - instruction_data[0] = 1; - - let offsets = SignatureOffsets { - signature_offset, - public_key_offset, - message_data_offset, - message_data_size, - }; - serialize_signature_offsets( - &mut instruction_data[SIGNATURE_OFFSETS_START..DATA_START], - &offsets, - )?; - - let public_key_start = usize::from(public_key_offset); - let public_key_end = public_key_start + pubkey.len(); - instruction_data[public_key_start..public_key_end].copy_from_slice(pubkey); - - let signature_start = usize::from(signature_offset); - let signature_end = signature_start + signature.len(); - instruction_data[signature_start..signature_end].copy_from_slice(signature); - - let message_data_start = usize::from(message_data_offset); - instruction_data[message_data_start..message_data_end].copy_from_slice(message); - - Ok(Instruction { - program_id, - accounts: vec![], - data: instruction_data, - }) -} - -#[cfg(feature = "instruction")] -pub(crate) fn serialize_signature_offsets( - output: &mut [u8], - offsets: &SignatureOffsets, -) -> Result<(), ProgramError> { - if output.len() != SIGNATURE_OFFSETS_SERIALIZED_SIZE { - return Err(ProgramError::InvalidInstructionData); - } - - output[0..2].copy_from_slice(&offsets.signature_offset.to_le_bytes()); - output[2..4].copy_from_slice(&offsets.public_key_offset.to_le_bytes()); - output[4..6].copy_from_slice(&offsets.message_data_offset.to_le_bytes()); - output[6..8].copy_from_slice(&offsets.message_data_size.to_le_bytes()); - - Ok(()) -} - -#[cfg(all(test, feature = "instruction"))] -mod tests { - use super::*; - use alloc::vec; - - fn read_first_offsets(input: &[u8]) -> SignatureOffsets { - SignatureOffsets { - signature_offset: u16::from_le_bytes(input[2..4].try_into().unwrap()), - public_key_offset: u16::from_le_bytes(input[4..6].try_into().unwrap()), - message_data_offset: u16::from_le_bytes(input[6..8].try_into().unwrap()), - message_data_size: u16::from_le_bytes(input[8..10].try_into().unwrap()), - } - } - - #[test] - fn test_instruction_builder_produces_valid_offsets() { - let signature = [1; SIGNATURE_SERIALIZED_SIZE]; - let pubkey = [2; PUBKEY_SERIALIZED_SIZE]; - - let instruction = new_ed25519_instruction_with_signature( - Pubkey::default(), - b"message", - &signature, - &pubkey, - ) - .expect("valid inputs"); - let offsets = read_first_offsets(&instruction.data); - - assert_eq!(instruction.accounts.len(), 0); - assert_eq!(instruction.data[0], 1); - assert_eq!(instruction.data[1], 0); - assert_eq!( - offsets.public_key_offset, - u16::try_from(DATA_START).unwrap() - ); - } - - #[test] - fn test_instruction_builder_rejects_oversized_messages() { - let signature = [1; SIGNATURE_SERIALIZED_SIZE]; - let pubkey = [2; PUBKEY_SERIALIZED_SIZE]; - let max_message = vec![3; u16::MAX as usize]; - let oversized_message = vec![3; u16::MAX as usize + 1]; - - assert!(new_ed25519_instruction_with_signature( - Pubkey::default(), - &max_message, - &signature, - &pubkey - ) - .is_ok()); - assert!(new_ed25519_instruction_with_signature( - Pubkey::default(), - &oversized_message, - &signature, - &pubkey - ) - .is_err()); - } - - #[test] - fn test_offsets_builder_rejects_too_many_signatures() { - let offsets = vec![SignatureOffsets::default(); u8::MAX as usize + 1]; - - assert!(offsets_to_ed25519_instruction(Pubkey::default(), &offsets).is_err()); - } -} diff --git a/ed25519-verify/src/instruction_data.rs b/ed25519-verify/src/instruction_data.rs deleted file mode 100644 index 3cc8c86..0000000 --- a/ed25519-verify/src/instruction_data.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Parsing helpers for the ed25519 instruction data wire format. -//! -//! The on-wire layout: -//! -//! ```text -//! Byte 0 : num_signatures (u8) -//! Byte 1 : padding, ignored -//! Bytes 2 ... : num_signatures x SignatureOffsets (8 bytes each, LE) -//! Remaining bytes : raw payload (public keys, signatures, messages) -//! ``` -//! -//! Every offset implicitly refers to this instruction's own data; there is no -//! wire representation for referencing another instruction. - -use { - crate::{ - SignatureOffsets, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, - }, - solana_program_error::ProgramError, -}; - -/// Borrowed views into the raw signature fields for one entry. -pub(crate) struct SignatureFields<'a> { - /// 64-byte ed25519 signature (`R || S`). - pub(crate) signature: &'a [u8; SIGNATURE_SERIALIZED_SIZE], - /// 32-byte compressed Edwards public key. - pub(crate) public_key: &'a [u8; PUBKEY_SERIALIZED_SIZE], - /// Raw message bytes that were signed. - pub(crate) message: &'a [u8], -} - -/// Parses an 8-byte `SignatureOffsets` record from `input`. -pub(crate) fn unpack_signature_offsets(input: &[u8]) -> Result { - Ok(SignatureOffsets { - signature_offset: decode_u16(input, 0)?, - public_key_offset: decode_u16(input, 2)?, - message_data_offset: decode_u16(input, 4)?, - message_data_size: decode_u16(input, 6)?, - }) -} - -fn decode_u16(input: &[u8], index: usize) -> Result { - let bytes: [u8; 2] = input - .get(index..index + 2) - .ok_or(ProgramError::InvalidInstructionData)? - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?; - Ok(u16::from_le_bytes(bytes)) -} - -/// Returns `input[offset .. offset + length]`, checking bounds on both ends. -fn get_instruction_data_slice( - input: &[u8], - offset: u16, - length: usize, -) -> Result<&[u8], ProgramError> { - let offset = usize::from(offset); - let end = offset - .checked_add(length) - .ok_or(ProgramError::InvalidInstructionData)?; - input - .get(offset..end) - .ok_or(ProgramError::InvalidInstructionData) -} - -fn get_instruction_data_array( - input: &[u8], - offset: u16, -) -> Result<&[u8; N], ProgramError> { - get_instruction_data_slice(input, offset, N)? - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData) -} - -/// Extracts all signature fields for one entry from raw instruction data. -pub(crate) fn get_signature_fields<'a>( - instruction_data: &'a [u8], - offsets: &'a SignatureOffsets, -) -> Result, ProgramError> { - Ok(SignatureFields { - signature: get_instruction_data_array(instruction_data, offsets.signature_offset)?, - public_key: get_instruction_data_array(instruction_data, offsets.public_key_offset)?, - message: get_instruction_data_slice( - instruction_data, - offsets.message_data_offset, - usize::from(offsets.message_data_size), - )?, - }) -} - -/// Parses the leading `num_signatures` byte and returns an iterator that yields -/// one `SignatureOffsets` per entry. -/// -/// `num_signatures == 0` is valid only when the buffer is exactly the 2-byte -/// header. The padding byte is intentionally ignored for nonzero counts. -pub(crate) fn iter_signature_offsets( - input: &[u8], -) -> Result> + '_, ProgramError> { - if input.len() < SIGNATURE_OFFSETS_START { - return Err(ProgramError::InvalidInstructionData); - } - - let num_signatures = input[0]; - if num_signatures == 0 { - if input.len() == SIGNATURE_OFFSETS_START { - return Ok(input[SIGNATURE_OFFSETS_START..SIGNATURE_OFFSETS_START] - .chunks_exact(SIGNATURE_OFFSETS_SERIALIZED_SIZE) - .map(unpack_signature_offsets)); - } - - return Err(ProgramError::InvalidInstructionData); - } - - let all_offsets_size = SIGNATURE_OFFSETS_SERIALIZED_SIZE - .checked_mul(usize::from(num_signatures)) - .ok_or(ProgramError::InvalidInstructionData)?; - let all_offsets_end = SIGNATURE_OFFSETS_START - .checked_add(all_offsets_size) - .ok_or(ProgramError::InvalidInstructionData)?; - let all_offsets = input - .get(SIGNATURE_OFFSETS_START..all_offsets_end) - .ok_or(ProgramError::InvalidInstructionData)?; - - Ok(all_offsets - .chunks_exact(SIGNATURE_OFFSETS_SERIALIZED_SIZE) - .map(unpack_signature_offsets)) -} diff --git a/ed25519-verify/src/lib.rs b/ed25519-verify/src/lib.rs index b62e097..77dec64 100644 --- a/ed25519-verify/src/lib.rs +++ b/ed25519-verify/src/lib.rs @@ -2,42 +2,23 @@ //! Stateless Ed25519 verification utilities for Solana programs. //! -//! This crate contains the reusable verifier and instruction-data helpers used -//! by `solana-ed25519-program`. It is intended for programs that want to verify -//! Ed25519 signatures directly without invoking the standalone verifier -//! program. +//! This crate contains the reusable verifier used by +//! `solana-ed25519-program`. Programs can also depend on it directly to verify +//! Ed25519 signatures without invoking the standalone verifier program. //! -//! Instruction data layout: -//! -//! ```text -//! [num_signatures: u8] -//! [padding: u8] -//! [SignatureOffsets x num_signatures] (8 bytes each, little-endian) -//! [public key || signature || message ...] (payload, order flexible) -//! ``` -//! -//! Every offset implicitly refers to this instruction's own data. There is no -//! cross-instruction reference, unlike the native ed25519 precompile. The -//! verifier performs ZIP-215 verification with canonical `S`. +//! The verifier performs ZIP-215 verification with canonical `S`. #[cfg(feature = "instruction")] extern crate alloc; -#[cfg(test)] -extern crate std; -mod instruction; -mod instruction_data; +#[cfg(feature = "instruction")] +pub mod program; mod scalar; -#[cfg(feature = "dev-context-only-utils")] -pub mod test_utils; mod verifier; -#[cfg(not(any(target_os = "solana", target_arch = "bpf")))] -pub use instruction::sign_message; #[cfg(feature = "instruction")] -pub use instruction::{new_ed25519_instruction_with_signature, offsets_to_ed25519_instruction}; -pub use instruction::{ - SignatureOffsets, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, -}; +pub use program::ed25519_verify_instruction; pub use verifier::Ed25519Verifier; + +pub const PUBKEY_SERIALIZED_SIZE: usize = 32; +pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; diff --git a/ed25519-verify/src/program.rs b/ed25519-verify/src/program.rs new file mode 100644 index 0000000..c2bcf65 --- /dev/null +++ b/ed25519-verify/src/program.rs @@ -0,0 +1,24 @@ +extern crate alloc; + +use { + crate::{PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE}, + alloc::{vec, vec::Vec}, + solana_instruction::Instruction, + solana_pubkey::Pubkey, +}; + +/// Constructs an on-chain instruction to invoke `solana-ed25519-program`. +pub fn ed25519_verify_instruction( + program_id: &Pubkey, + public_key: &[u8; PUBKEY_SERIALIZED_SIZE], + signature: &[u8; SIGNATURE_SERIALIZED_SIZE], + message: &[u8], +) -> Instruction { + let mut data = + Vec::with_capacity(PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE + message.len()); + data.extend_from_slice(public_key); + data.extend_from_slice(signature); + data.extend_from_slice(message); + + Instruction::new_with_bytes(*program_id, &data, vec![]) +} diff --git a/ed25519-verify/src/test_utils.rs b/ed25519-verify/src/test_utils.rs deleted file mode 100644 index fca01c2..0000000 --- a/ed25519-verify/src/test_utils.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Test-only ed25519 instruction builders. -//! -//! Shared by this crate's own integration tests and by `solana-ed25519-program`'s, -//! so the wire-format builders live in one place instead of being duplicated -//! per crate. Gated behind `dev-context-only-utils` so none of this ships in -//! on-chain builds. - -use { - crate::{ - instruction::serialize_signature_offsets, instruction_data::unpack_signature_offsets, - new_ed25519_instruction_with_signature, verifier::EDWARDS_IDENTITY_COMPRESSED_BYTES, - SignatureOffsets, DATA_START, PUBKEY_SERIALIZED_SIZE, SIGNATURE_OFFSETS_SERIALIZED_SIZE, - SIGNATURE_OFFSETS_START, SIGNATURE_SERIALIZED_SIZE, - }, - alloc::{vec, vec::Vec}, - ed25519_dalek::{Signer, SigningKey}, - solana_pubkey::Pubkey, -}; - -pub const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = - EDWARDS_IDENTITY_COMPRESSED_BYTES; -pub const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ - 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, -]; - -/// Holds all cryptographic material for a single signed message. -struct SignedPayload<'a> { - signature: [u8; SIGNATURE_SERIALIZED_SIZE], - pubkey: [u8; PUBKEY_SERIALIZED_SIZE], - message: &'a [u8], -} - -/// Signs `message` with `signing_key`. -fn signed_payload<'a>(signing_key: &SigningKey, message: &'a [u8]) -> SignedPayload<'a> { - SignedPayload { - signature: signing_key.sign(message).to_bytes(), - pubkey: signing_key.verifying_key().to_bytes(), - message, - } -} - -/// Builds a valid ed25519 instruction buffer containing one entry per message, -/// all signed by a fixed test key. -pub fn signed_instruction(messages: &[&[u8]]) -> Vec { - let signing_key = SigningKey::from_bytes(&[7; 32]); - let payloads = messages - .iter() - .map(|message| signed_payload(&signing_key, message)) - .collect::>(); - let offsets_len = payloads.len() * SIGNATURE_OFFSETS_SERIALIZED_SIZE; - let mut instruction = vec![0; SIGNATURE_OFFSETS_START + offsets_len]; - instruction[0] = payloads.len() as u8; - - for (index, payload) in payloads.iter().enumerate() { - let public_key_offset = instruction.len(); - instruction.extend_from_slice(&payload.pubkey); - - let signature_offset = instruction.len(); - instruction.extend_from_slice(&payload.signature); - - let message_data_offset = instruction.len(); - instruction.extend_from_slice(payload.message); - - let offsets = SignatureOffsets { - signature_offset: u16::try_from(signature_offset).unwrap(), - public_key_offset: u16::try_from(public_key_offset).unwrap(), - message_data_offset: u16::try_from(message_data_offset).unwrap(), - message_data_size: u16::try_from(payload.message.len()).unwrap(), - }; - write_offsets( - &mut instruction[SIGNATURE_OFFSETS_START + index * SIGNATURE_OFFSETS_SERIALIZED_SIZE - ..SIGNATURE_OFFSETS_START + (index + 1) * SIGNATURE_OFFSETS_SERIALIZED_SIZE], - &offsets, - ); - } - - instruction -} - -/// Builds a single-entry instruction from caller-provided signature material. -pub fn instruction_with_signature( - message: &[u8], - signature: &[u8; SIGNATURE_SERIALIZED_SIZE], - pubkey: &[u8; PUBKEY_SERIALIZED_SIZE], -) -> Vec { - new_ed25519_instruction_with_signature(Pubkey::default(), message, signature, pubkey) - .expect("valid test inputs") - .data -} - -/// Parses and returns the first `SignatureOffsets` entry from `instruction`. -pub fn first_offsets(instruction: &[u8]) -> SignatureOffsets { - unpack_signature_offsets(&instruction[SIGNATURE_OFFSETS_START..DATA_START]) - .expect("well-formed test instruction") -} - -/// Serializes `offsets` into the 8-byte little-endian wire format in `output`. -pub fn write_offsets(output: &mut [u8], offsets: &SignatureOffsets) { - serialize_signature_offsets(output, offsets).expect("output is exactly 8 bytes"); -} diff --git a/ed25519-verify/src/verifier.rs b/ed25519-verify/src/verifier.rs index 8e97bd7..f970c2d 100644 --- a/ed25519-verify/src/verifier.rs +++ b/ed25519-verify/src/verifier.rs @@ -1,8 +1,5 @@ use { - crate::{ - instruction_data::{get_signature_fields, iter_signature_offsets}, - scalar, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE, - }, + crate::{scalar, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE}, solana_curve25519::{ edwards::{ multiply_edwards, multiscalar_multiply_edwards, subtract_edwards, PodEdwardsPoint, @@ -38,18 +35,6 @@ impl Ed25519Verifier { Self } - /// Parses `instruction_data` and verifies every ed25519 signature it - /// describes, returning an error on the first failure. - pub fn verify_instruction(&self, instruction_data: &[u8]) -> Result<(), ProgramError> { - for offsets in iter_signature_offsets(instruction_data)? { - let offsets = offsets?; - let fields = get_signature_fields(instruction_data, &offsets)?; - self.verify_signature(fields.signature, fields.public_key, fields.message)?; - } - - Ok(()) - } - /// Performs ZIP-215 Ed25519 verification for one signature. /// /// Uses the cofactored equation `[8](S*B - H(R || A || M)*A) == [8]R`. diff --git a/ed25519-verify/tests/verify_instruction.rs b/ed25519-verify/tests/verify_instruction.rs index 78b37b0..99db20c 100644 --- a/ed25519-verify/tests/verify_instruction.rs +++ b/ed25519-verify/tests/verify_instruction.rs @@ -1,31 +1,71 @@ use { - ed25519_dalek::{Signature, VerifyingKey}, + ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}, solana_ed25519_verify::{ - test_utils::{ - first_offsets, instruction_with_signature, signed_instruction, write_offsets, - EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, - }, - Ed25519Verifier, DATA_START, SIGNATURE_SERIALIZED_SIZE, + ed25519_verify_instruction, Ed25519Verifier, PUBKEY_SERIALIZED_SIZE, + SIGNATURE_SERIALIZED_SIZE, }, solana_program_error::ProgramError, + solana_pubkey::Pubkey, }; -fn process_instruction(instruction_data: &[u8]) -> Result<(), ProgramError> { - Ed25519Verifier::new().verify_instruction(instruction_data) +const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; +const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ + 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, +]; + +fn signed_payload( + message: &[u8], +) -> ( + [u8; SIGNATURE_SERIALIZED_SIZE], + [u8; PUBKEY_SERIALIZED_SIZE], +) { + let signing_key = SigningKey::from_bytes(&[7; 32]); + ( + signing_key.sign(message).to_bytes(), + signing_key.verifying_key().to_bytes(), + ) +} + +fn verify_signature( + signature: &[u8; SIGNATURE_SERIALIZED_SIZE], + public_key: &[u8; PUBKEY_SERIALIZED_SIZE], + message: &[u8], +) -> Result<(), ProgramError> { + Ed25519Verifier::new().verify_signature(signature, public_key, message) } #[test] fn verifies_matching_signature() { - let instruction = signed_instruction(&[b"hello ed25519"]); + let message = b"hello ed25519"; + let (signature, public_key) = signed_payload(message); - assert_eq!(process_instruction(&instruction), Ok(())); + assert_eq!(verify_signature(&signature, &public_key, message), Ok(())); } #[test] -fn verifies_multiple_signatures() { - let instruction = signed_instruction(&[b"hello ed25519", b"second message"]); +fn constructs_program_instruction_with_direct_layout() { + let program_id = Pubkey::new_unique(); + let message = b"hello ed25519"; + let (signature, public_key) = signed_payload(message); - assert_eq!(process_instruction(&instruction), Ok(())); + let instruction = ed25519_verify_instruction(&program_id, &public_key, &signature, message); + + assert_eq!(instruction.program_id, program_id); + assert!(instruction.accounts.is_empty()); + assert_eq!(&instruction.data[..PUBKEY_SERIALIZED_SIZE], &public_key); + assert_eq!( + &instruction.data + [PUBKEY_SERIALIZED_SIZE..PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE], + &signature + ); + assert_eq!( + &instruction.data[PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE..], + message + ); } #[test] @@ -39,200 +79,111 @@ fn accepts_zip215_small_order_public_key_vector_rejected_by_strict_verification( let dalek_signature = Signature::from_bytes(&signature); assert!(dalek_key.verify_strict(message, &dalek_signature).is_err()); - let instruction = - instruction_with_signature(message, &signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED); - assert_eq!(process_instruction(&instruction), Ok(())); + assert_eq!( + verify_signature(&signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, message), + Ok(()) + ); } #[test] fn rejects_wrong_public_key() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - instruction[usize::from(offsets.public_key_offset)] ^= 1; + let message = b"hello ed25519"; + let (signature, mut public_key) = signed_payload(message); + public_key[0] ^= 1; assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &public_key, message), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_corrupted_signature() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - instruction[usize::from(offsets.signature_offset)] ^= 1; + let message = b"hello ed25519"; + let (mut signature, public_key) = signed_payload(message); + signature[0] ^= 1; assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &public_key, message), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_tampered_message() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - instruction[usize::from(offsets.message_data_offset)] ^= 1; + let message = b"hello ed25519"; + let (signature, public_key) = signed_payload(message); assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &public_key, b"hello ed25518"), Err(ProgramError::InvalidArgument) ); } -#[test] -fn rejects_short_instruction() { - assert_eq!( - process_instruction(&[]), - Err(ProgramError::InvalidInstructionData) - ); - assert_eq!( - process_instruction(&[1]), - Err(ProgramError::InvalidInstructionData) - ); - assert_eq!( - process_instruction(&[1, 0]), - Err(ProgramError::InvalidInstructionData) - ); -} - -#[test] -fn accepts_zero_signatures_only_when_data_has_just_header() { - assert_eq!(process_instruction(&[0, 0]), Ok(())); - assert_eq!( - process_instruction(&[0]), - Err(ProgramError::InvalidInstructionData) - ); - assert_eq!( - process_instruction(&[0, 0, 0]), - Err(ProgramError::InvalidInstructionData) - ); -} - -#[test] -fn rejects_out_of_bounds_offsets() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let mut offsets = first_offsets(&instruction); - offsets.message_data_size = u16::MAX; - write_offsets(&mut instruction[2..DATA_START], &offsets); - - assert_eq!( - process_instruction(&instruction), - Err(ProgramError::InvalidInstructionData) - ); -} - #[test] fn rejects_non_canonical_s_scalar() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - let s_offset = usize::from(offsets.signature_offset) + 32; - instruction[s_offset..s_offset + 32].copy_from_slice(&[ + let message = b"hello ed25519"; + let (mut signature, public_key) = signed_payload(message); + signature[32..64].copy_from_slice(&[ 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, ]); assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &public_key, message), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_low_order_r() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - let r_offset = usize::from(offsets.signature_offset); - instruction[r_offset..r_offset + 32].copy_from_slice(&[ + let message = b"hello ed25519"; + let (mut signature, public_key) = signed_payload(message); + signature[..32].copy_from_slice(&[ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &public_key, message), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_low_order_public_key() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - let public_key_offset = usize::from(offsets.public_key_offset); - instruction[public_key_offset..public_key_offset + 32] - .copy_from_slice(&SMALL_ORDER_PUBLIC_KEY_COMPRESSED); + let message = b"hello ed25519"; + let (signature, _) = signed_payload(message); assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, message), Err(ProgramError::InvalidArgument) ); } #[test] fn rejects_invalid_public_key() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let offsets = first_offsets(&instruction); - let public_key_offset = usize::from(offsets.public_key_offset); - instruction[public_key_offset..public_key_offset + 32].copy_from_slice(&[0xff; 32]); + let message = b"hello ed25519"; + let (signature, _) = signed_payload(message); assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &[0xff; PUBKEY_SERIALIZED_SIZE], message), Err(ProgramError::InvalidArgument) ); } -#[test] -fn ignores_padding_byte() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - instruction[1] = 0xff; - - assert_eq!(process_instruction(&instruction), Ok(())); -} - -#[test] -fn signature_offset_points_to_exactly_64_bytes() { - let mut instruction = signed_instruction(&[b"hello ed25519"]); - let mut offsets = first_offsets(&instruction); - offsets.signature_offset = u16::try_from(instruction.len() - SIGNATURE_SERIALIZED_SIZE + 1) - .expect("test instruction length fits u16"); - write_offsets(&mut instruction[2..DATA_START], &offsets); - - assert_eq!( - process_instruction(&instruction), - Err(ProgramError::InvalidInstructionData) - ); -} - #[test] fn accepts_valid_zip215_pure_torsion_signature() { - // R = Identity Point - let signature_r = EDWARDS_IDENTITY_COMPRESSED; - // S = Zero Scalar - let signature_s = [0u8; 32]; - - let mut signature = [0u8; 64]; - signature[..32].copy_from_slice(&signature_r); - signature[32..].copy_from_slice(&signature_s); - - // A = A non-identity pure torsion point. - let pubkey = SMALL_ORDER_PUBLIC_KEY_COMPRESSED; - - // Under ZIP-215: [8](S*B) = [8]R + [8](c*A) - // Since S=0 and R=O, this becomes O = O + c*[8]A. - // Because A is an 8-torsion point, [8]A = O. - // The equation is O = O + O, which is always true. - // This signature must be accepted for any message. - - // The buggy verification failed for some challenge values, so try a couple - // messages in a loop to cover multiple challenges. + let mut signature = [0u8; SIGNATURE_SERIALIZED_SIZE]; + signature[..32].copy_from_slice(&EDWARDS_IDENTITY_COMPRESSED); + for i in 0..20 { let message = vec![i as u8; 10]; - let instruction = instruction_with_signature(&message, &signature, &pubkey); assert_eq!( - process_instruction(&instruction), + verify_signature(&signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, &message), Ok(()), "message index {i} is failing" ); diff --git a/program/Cargo.toml b/program/Cargo.toml index e106f89..35f5b7b 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -8,16 +8,16 @@ crate-type = ["cdylib", "rlib"] [features] default = [] -no-entrypoint = [] [dependencies] pinocchio = { workspace = true } solana-ed25519-verify = { workspace = true, default-features = false } [dev-dependencies] +ed25519-dalek = { workspace = true } mollusk-svm = { workspace = true } solana-account = { workspace = true } -solana-ed25519-verify = { workspace = true, features = ["dev-context-only-utils"] } +solana-ed25519-verify = { workspace = true, features = ["instruction"] } solana-instruction = { workspace = true } solana-program-runtime = { workspace = true } solana-pubkey = { workspace = true } diff --git a/program/src/lib.rs b/program/src/lib.rs index 2e50618..4e1c61a 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,33 +1,48 @@ #![no_std] //! Pinocchio SBF wrapper for [`solana_ed25519_verify`]. -//! -//! The reusable verifier, instruction layout, and instruction constructors live -//! in `solana-ed25519-verify`. This crate keeps only the standalone program -//! entrypoint. -#[cfg(all( - not(feature = "no-entrypoint"), - any(target_os = "solana", target_arch = "bpf") -))] -use pinocchio::{lazy_program_entrypoint, no_allocator, nostd_panic_handler}; +#[cfg(not(any(target_os = "solana", target_arch = "bpf")))] +extern crate std; -mod processor; +use { + pinocchio::{ + entrypoint::InstructionContext, error::ProgramError, lazy_program_entrypoint, ProgramResult, + }, + solana_ed25519_verify::{Ed25519Verifier, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE}, +}; -pub use processor::process_instruction; +const SIGNATURE_OFFSET: usize = PUBKEY_SERIALIZED_SIZE; +const MESSAGE_OFFSET: usize = PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE; + +#[cfg(any(target_os = "solana", target_arch = "bpf"))] +pinocchio::no_allocator!(); +#[cfg(any(target_os = "solana", target_arch = "bpf"))] +pinocchio::nostd_panic_handler!(); -#[cfg(all( - not(feature = "no-entrypoint"), - any(target_os = "solana", target_arch = "bpf") -))] lazy_program_entrypoint!(process_instruction); -#[cfg(all( - not(feature = "no-entrypoint"), - any(target_os = "solana", target_arch = "bpf") -))] -no_allocator!(); -#[cfg(all( - not(feature = "no-entrypoint"), - any(target_os = "solana", target_arch = "bpf") -))] -nostd_panic_handler!(); + +/// Program entry point. +/// +/// Expects no accounts and instruction data encoded as +/// `public_key || signature || message`. +pub fn process_instruction(context: InstructionContext) -> ProgramResult { + if context.remaining() > 0 { + return Err(ProgramError::InvalidArgument); + } + + let instruction_data = context.instruction_data()?; + if instruction_data.len() < MESSAGE_OFFSET { + return Err(ProgramError::InvalidInstructionData); + } + + let public_key = instruction_data[..PUBKEY_SERIALIZED_SIZE] + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + let signature = instruction_data[SIGNATURE_OFFSET..MESSAGE_OFFSET] + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + let message = &instruction_data[MESSAGE_OFFSET..]; + + Ed25519Verifier::new().verify_signature(signature, public_key, message) +} diff --git a/program/src/processor.rs b/program/src/processor.rs deleted file mode 100644 index 4a127d1..0000000 --- a/program/src/processor.rs +++ /dev/null @@ -1,15 +0,0 @@ -use { - pinocchio::{entrypoint::InstructionContext, error::ProgramError, ProgramResult}, - solana_ed25519_verify::Ed25519Verifier, -}; - -/// Program entry point. -/// -/// Expects no accounts and instruction data in the ed25519 precompile format. -pub fn process_instruction(context: InstructionContext) -> ProgramResult { - if context.remaining() > 0 { - return Err(ProgramError::InvalidArgument); - } - - Ed25519Verifier::new().verify_instruction(context.instruction_data()?) -} diff --git a/program/tests/mollusk.rs b/program/tests/mollusk.rs index 0b94bea..b3dd489 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -1,12 +1,9 @@ use { + ed25519_dalek::{Signer, SigningKey}, mollusk_svm::Mollusk, solana_account::Account, solana_ed25519_verify::{ - test_utils::{ - first_offsets, instruction_with_signature, signed_instruction, - EDWARDS_IDENTITY_COMPRESSED, SMALL_ORDER_PUBLIC_KEY_COMPRESSED, - }, - SIGNATURE_SERIALIZED_SIZE, + ed25519_verify_instruction, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE, }, solana_instruction::{AccountMeta, Instruction}, solana_program_runtime::{ @@ -23,7 +20,17 @@ use { const PROGRAM_SO_STEM: &str = "solana_ed25519_program"; const SINGLE_MESSAGE: &[u8] = b"deterministic ed25519 verify benchmark"; -const SECOND_MESSAGE: &[u8] = b"second deterministic ed25519 verify benchmark"; +const PUBLIC_KEY_OFFSET: usize = 0; +const MESSAGE_OFFSET: usize = PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE; + +const EDWARDS_IDENTITY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; +const SMALL_ORDER_PUBLIC_KEY_COMPRESSED: [u8; PUBKEY_SERIALIZED_SIZE] = [ + 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, +]; #[repr(C)] #[derive(Clone, Copy)] @@ -143,12 +150,21 @@ fn instruction(program_id: Pubkey, data: Vec) -> Instruction { } } -fn instruction_with_account(program_id: Pubkey, data: Vec, account: Pubkey) -> Instruction { - Instruction { - program_id, - accounts: vec![AccountMeta::new_readonly(account, false)], - data, - } +fn signed_instruction(program_id: Pubkey, message: &[u8]) -> Instruction { + let signing_key = SigningKey::from_bytes(&[7; 32]); + let signature = signing_key.sign(message).to_bytes(); + let public_key = signing_key.verifying_key().to_bytes(); + + ed25519_verify_instruction(&program_id, &public_key, &signature, message) +} + +fn instruction_with_signature( + program_id: Pubkey, + message: &[u8], + signature: &[u8; SIGNATURE_SERIALIZED_SIZE], + public_key: &[u8; PUBKEY_SERIALIZED_SIZE], +) -> Instruction { + ed25519_verify_instruction(&program_id, public_key, signature, message) } #[test] @@ -156,7 +172,7 @@ fn verifies_single_signature_on_sbf_and_reports_compute_units() { let Some((mollusk, program_id)) = make_mollusk() else { return; }; - let ix = instruction(program_id, signed_instruction(&[SINGLE_MESSAGE])); + let ix = signed_instruction(program_id, SINGLE_MESSAGE); let result = mollusk.process_instruction(&ix, &[]); assert!( @@ -171,29 +187,6 @@ fn verifies_single_signature_on_sbf_and_reports_compute_units() { ); } -#[test] -fn verifies_multiple_signatures_on_sbf_and_reports_compute_units() { - let Some((mollusk, program_id)) = make_mollusk() else { - return; - }; - let ix = instruction( - program_id, - signed_instruction(&[SINGLE_MESSAGE, SECOND_MESSAGE]), - ); - let result = mollusk.process_instruction(&ix, &[]); - - assert!( - result.program_result.is_ok(), - "verify failed: {:?}", - result.program_result - ); - println!( - "ed25519 verify: 2 signatures, {} total message bytes, {} CUs", - SINGLE_MESSAGE.len() + SECOND_MESSAGE.len(), - result.compute_units_consumed - ); -} - #[test] fn accepts_zip215_small_order_public_key_vector_on_sbf() { let Some((mollusk, program_id)) = make_mollusk() else { @@ -202,9 +195,11 @@ fn accepts_zip215_small_order_public_key_vector_on_sbf() { let message = b"zip215 low-order public key vector"; let mut signature = [0; SIGNATURE_SERIALIZED_SIZE]; signature[..EDWARDS_IDENTITY_COMPRESSED.len()].copy_from_slice(&EDWARDS_IDENTITY_COMPRESSED); - let ix = instruction( + let ix = instruction_with_signature( program_id, - instruction_with_signature(message, &signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED), + message, + &signature, + &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, ); let result = mollusk.process_instruction(&ix, &[]); @@ -220,11 +215,10 @@ fn rejects_tampered_message_on_sbf() { let Some((mollusk, program_id)) = make_mollusk() else { return; }; - let mut data = signed_instruction(&[SINGLE_MESSAGE]); - let offsets = first_offsets(&data); - data[usize::from(offsets.message_data_offset)] ^= 1; + let mut ix = signed_instruction(program_id, SINGLE_MESSAGE); + ix.data[MESSAGE_OFFSET] ^= 1; - let result = mollusk.process_instruction(&instruction(program_id, data), &[]); + let result = mollusk.process_instruction(&ix, &[]); assert!( result.program_result.is_err(), "expected failure on tampered message, got: {:?}", @@ -237,11 +231,10 @@ fn rejects_tampered_public_key_on_sbf() { let Some((mollusk, program_id)) = make_mollusk() else { return; }; - let mut data = signed_instruction(&[SINGLE_MESSAGE]); - let offsets = first_offsets(&data); - data[usize::from(offsets.public_key_offset)] ^= 1; + let mut ix = signed_instruction(program_id, SINGLE_MESSAGE); + ix.data[PUBLIC_KEY_OFFSET] ^= 1; - let result = mollusk.process_instruction(&instruction(program_id, data), &[]); + let result = mollusk.process_instruction(&ix, &[]); assert!( result.program_result.is_err(), "expected failure on tampered public key, got: {:?}", @@ -255,7 +248,8 @@ fn rejects_accounts_on_sbf() { return; }; let account = Pubkey::new_unique(); - let ix = instruction_with_account(program_id, signed_instruction(&[SINGLE_MESSAGE]), account); + let mut ix = signed_instruction(program_id, SINGLE_MESSAGE); + ix.accounts = vec![AccountMeta::new_readonly(account, false)]; let accounts = [(account, Account::default())]; let result = mollusk.process_instruction(&ix, &accounts); @@ -265,3 +259,18 @@ fn rejects_accounts_on_sbf() { result.program_result ); } + +#[test] +fn rejects_short_instruction_on_sbf() { + let Some((mollusk, program_id)) = make_mollusk() else { + return; + }; + let ix = instruction(program_id, vec![0; MESSAGE_OFFSET - 1]); + + let result = mollusk.process_instruction(&ix, &[]); + assert!( + result.program_result.is_err(), + "expected failure on short instruction data, got: {:?}", + result.program_result + ); +}