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 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/Cargo.lock b/Cargo.lock index 3d38494..fc230f1 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,19 +2215,27 @@ checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" name = "solana-ed25519-program" version = "4.0.0" dependencies = [ - "bincode", "ed25519-dalek", "mollusk-svm", - "serde", - "serde_derive", - "solana-account-info", + "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 = [ + "ed25519-dalek", "solana-curve25519", + "solana-ed25519-verify", "solana-instruction", - "solana-program-entrypoint", "solana-program-error", - "solana-program-runtime", "solana-pubkey 4.1.0", - "solana-sdk-ids", "solana-sha512-hasher", ] @@ -2308,6 +2339,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..27f3676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["program"] +members = ["ed25519-verify", "program"] resolver = "2" [workspace.lints.rust] @@ -10,17 +10,16 @@ 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" 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" -solana-sdk-ids = "3.1.0" solana-sha512-hasher = { version = "1.0.1", features = ["sha2"] } 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 \ diff --git a/ed25519-verify/Cargo.toml b/ed25519-verify/Cargo.toml new file mode 100644 index 0000000..c912c53 --- /dev/null +++ b/ed25519-verify/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "solana-ed25519-verify" +version = "4.0.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[features] +default = [] +# Enables the client-side constructor for invoking the standalone program. +instruction = ["dep:solana-instruction", "dep:solana-pubkey"] + +[dependencies] +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 } + +[dev-dependencies] +ed25519-dalek = { workspace = true } +solana-ed25519-verify = { path = ".", features = ["instruction"] } +solana-pubkey = { workspace = true } + +[lints] +workspace = true diff --git a/ed25519-verify/src/lib.rs b/ed25519-verify/src/lib.rs new file mode 100644 index 0000000..77dec64 --- /dev/null +++ b/ed25519-verify/src/lib.rs @@ -0,0 +1,24 @@ +#![no_std] + +//! Stateless Ed25519 verification utilities for Solana programs. +//! +//! 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. +//! +//! The verifier performs ZIP-215 verification with canonical `S`. + +#[cfg(feature = "instruction")] +extern crate alloc; + +#[cfg(feature = "instruction")] +pub mod program; +mod scalar; +mod verifier; + +#[cfg(feature = "instruction")] +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/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..f970c2d --- /dev/null +++ b/ed25519-verify/src/verifier.rs @@ -0,0 +1,82 @@ +use { + crate::{scalar, 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, +]); +/// 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, +]); + +/// 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 + } + + /// 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, 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); + } + + 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(()) + } +} + +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/verify_instruction.rs b/ed25519-verify/tests/verify_instruction.rs new file mode 100644 index 0000000..99db20c --- /dev/null +++ b/ed25519-verify/tests/verify_instruction.rs @@ -0,0 +1,191 @@ +use { + ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}, + solana_ed25519_verify::{ + ed25519_verify_instruction, Ed25519Verifier, PUBKEY_SERIALIZED_SIZE, + SIGNATURE_SERIALIZED_SIZE, + }, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, +}; + +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 message = b"hello ed25519"; + let (signature, public_key) = signed_payload(message); + + assert_eq!(verify_signature(&signature, &public_key, message), Ok(())); +} + +#[test] +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); + + 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] +fn accepts_zip215_small_order_public_key_vector_rejected_by_strict_verification() { + 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 dalek_key = VerifyingKey::from_bytes(&SMALL_ORDER_PUBLIC_KEY_COMPRESSED) + .expect("small-order key decompresses"); + let dalek_signature = Signature::from_bytes(&signature); + assert!(dalek_key.verify_strict(message, &dalek_signature).is_err()); + + assert_eq!( + verify_signature(&signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, message), + Ok(()) + ); +} + +#[test] +fn rejects_wrong_public_key() { + let message = b"hello ed25519"; + let (signature, mut public_key) = signed_payload(message); + public_key[0] ^= 1; + + assert_eq!( + verify_signature(&signature, &public_key, message), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn rejects_corrupted_signature() { + let message = b"hello ed25519"; + let (mut signature, public_key) = signed_payload(message); + signature[0] ^= 1; + + assert_eq!( + verify_signature(&signature, &public_key, message), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn rejects_tampered_message() { + let message = b"hello ed25519"; + let (signature, public_key) = signed_payload(message); + + assert_eq!( + verify_signature(&signature, &public_key, b"hello ed25518"), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn rejects_non_canonical_s_scalar() { + 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!( + verify_signature(&signature, &public_key, message), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn rejects_low_order_r() { + 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!( + verify_signature(&signature, &public_key, message), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn rejects_low_order_public_key() { + let message = b"hello ed25519"; + let (signature, _) = signed_payload(message); + + assert_eq!( + verify_signature(&signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, message), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn rejects_invalid_public_key() { + let message = b"hello ed25519"; + let (signature, _) = signed_payload(message); + + assert_eq!( + verify_signature(&signature, &[0xff; PUBKEY_SERIALIZED_SIZE], message), + Err(ProgramError::InvalidArgument) + ); +} + +#[test] +fn accepts_valid_zip215_pure_torsion_signature() { + 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]; + + assert_eq!( + verify_signature(&signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, &message), + Ok(()), + "message index {i} is failing" + ); + } +} 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: - - .: {} diff --git a/program/Cargo.toml b/program/Cargo.toml index 103fecc..35f5b7b 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-ed25519-verify = { workspace = true, features = ["instruction"] } 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/instruction.rs b/program/src/instruction.rs deleted file mode 100644 index 5371128..0000000 --- a/program/src/instruction.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Ed25519 instruction layout and construction helpers. -// This was adapted from `solana-sdk/ed25519_program`. - -#[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; - -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_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. -#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] -#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] -pub struct Ed25519SignatureOffsets { - /// Offset to 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. - 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. - pub message_data_offset: u16, - /// Size of 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. -#[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(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -/// 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(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -/// 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( - offsets: &[Ed25519SignatureOffsets], -) -> 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: solana_sdk_ids::ed25519_program::id(), - accounts: vec![], - data: instruction_data, - }) -} - -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -/// 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(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -/// 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( - 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 = Ed25519SignatureOffsets { - 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], - &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: solana_sdk_ids::ed25519_program::id(), - accounts: vec![], - data: instruction_data, - }) -} - -#[cfg(all( - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -fn serialize_signature_offsets( - output: &mut [u8], - offsets: &Ed25519SignatureOffsets, -) -> 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()); - - Ok(()) -} - -#[cfg(all( - test, - feature = "bincode", - not(any(target_os = "solana", target_arch = "bpf")) -))] -mod tests { - use super::*; - - fn read_first_offsets(input: &[u8]) -> Ed25519SignatureOffsets { - Ed25519SignatureOffsets { - 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()), - } - } - - #[test] - fn test_instruction_builder_keeps_legacy_return_type() { - let signature = [1; SIGNATURE_SERIALIZED_SIZE]; - let pubkey = [2; PUBKEY_SERIALIZED_SIZE]; - - let instruction = new_ed25519_instruction_with_signature(b"message", &signature, &pubkey); - 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 - ); - assert_eq!( - offsets.public_key_instruction_index, - CURRENT_INSTRUCTION_INDEX - ); - assert_eq!(offsets.message_instruction_index, CURRENT_INSTRUCTION_INDEX); - } - - #[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!( - try_new_ed25519_instruction_with_signature(&max_message, &signature, &pubkey).is_ok() - ); - 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()); - } -} diff --git a/program/src/instruction_data.rs b/program/src/instruction_data.rs deleted file mode 100644 index d94e435..0000000 --- a/program/src/instruction_data.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Parsing helpers for the ed25519 instruction data wire format. -//! -//! The on-wire layout matches the native ed25519 precompile: -//! -//! ```text -//! Byte 0 : num_signatures (u8) -//! Byte 1 : padding, ignored -//! Bytes 2 ... : num_signatures x Ed25519SignatureOffsets (14 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. - -use { - crate::{ - Ed25519SignatureOffsets, 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 a 14-byte `Ed25519SignatureOffsets` record from `input`. -fn unpack_signature_offsets(input: &[u8]) -> Result { - Ok(Ed25519SignatureOffsets { - 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)?, - }) -} - -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 Ed25519SignatureOffsets, -) -> 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 `Ed25519SignatureOffsets` 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. -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/program/src/lib.rs b/program/src/lib.rs index ab4294c..4e1c61a 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,161 +1,48 @@ -//! 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`]. #[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")) -))] -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, +extern crate std; + +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; -#[cfg(target_os = "solana")] -use solana_program_error::ProgramError; +const SIGNATURE_OFFSET: usize = PUBKEY_SERIALIZED_SIZE; +const MESSAGE_OFFSET: usize = PUBKEY_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE; -/// Program entry point for the version 2 instruction-data pointer interface. -/// -/// # Safety +#[cfg(any(target_os = "solana", target_arch = "bpf"))] +pinocchio::no_allocator!(); +#[cfg(any(target_os = "solana", target_arch = "bpf"))] +pinocchio::nostd_panic_handler!(); + +lazy_program_entrypoint!(process_instruction); + +/// Program entry point. /// -/// 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) - } - }; +/// 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); + } - match result { - Ok(()) => solana_program_entrypoint::SUCCESS, - Err(error) => error.into(), + let instruction_data = context.instruction_data()?; + if instruction_data.len() < MESSAGE_OFFSET { + return Err(ProgramError::InvalidInstructionData); } -} -#[cfg(not(feature = "no-entrypoint"))] -solana_program_entrypoint::custom_heap_default!(); -#[cfg(not(feature = "no-entrypoint"))] -solana_program_entrypoint::custom_panic_default!(); + 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..]; -#[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, - ) - } + 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 2030f6d..0000000 --- a/program/src/processor.rs +++ /dev/null @@ -1,118 +0,0 @@ -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, -}; - -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(()) -} - -/// 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) { - 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) -} diff --git a/program/tests/common/mod.rs b/program/tests/common/mod.rs deleted file mode 100644 index d6edddd..0000000 --- a/program/tests/common/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -use { - ed25519_dalek::{Signer, SigningKey}, - solana_ed25519_program::{ - 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 ee8ebe1..b3dd489 100644 --- a/program/tests/mollusk.rs +++ b/program/tests/mollusk.rs @@ -1,11 +1,11 @@ use { - common::{ - first_offsets, instruction_with_signature, signed_instruction, EDWARDS_IDENTITY_COMPRESSED, - SMALL_ORDER_PUBLIC_KEY_COMPRESSED, - }, + ed25519_dalek::{Signer, SigningKey}, mollusk_svm::Mollusk, - solana_ed25519_program::SIGNATURE_SERIALIZED_SIZE, - solana_instruction::Instruction, + solana_account::Account, + solana_ed25519_verify::{ + ed25519_verify_instruction, PUBKEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE, + }, + solana_instruction::{AccountMeta, Instruction}, solana_program_runtime::{ invoke_context::InvokeContext, solana_sbpf::{ @@ -18,11 +18,19 @@ 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"; +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)] @@ -142,12 +150,29 @@ fn instruction(program_id: Pubkey, data: Vec) -> Instruction { } } +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] 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!( @@ -163,13 +188,18 @@ fn verifies_single_signature_on_sbf_and_reports_compute_units() { } #[test] -fn verifies_multiple_signatures_on_sbf_and_reports_compute_units() { +fn accepts_zip215_small_order_public_key_vector_on_sbf() { let Some((mollusk, program_id)) = make_mollusk() else { return; }; - let ix = instruction( + 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_with_signature( program_id, - signed_instruction(&[SINGLE_MESSAGE, SECOND_MESSAGE]), + message, + &signature, + &SMALL_ORDER_PUBLIC_KEY_COMPRESSED, ); let result = mollusk.process_instruction(&ix, &[]); @@ -178,64 +208,69 @@ fn verifies_multiple_signatures_on_sbf_and_reports_compute_units() { "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() { +fn rejects_tampered_message_on_sbf() { let Some((mollusk, program_id)) = make_mollusk() else { return; }; - 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( - program_id, - instruction_with_signature(message, &signature, &SMALL_ORDER_PUBLIC_KEY_COMPRESSED), - ); + let mut ix = signed_instruction(program_id, SINGLE_MESSAGE); + ix.data[MESSAGE_OFFSET] ^= 1; + let result = mollusk.process_instruction(&ix, &[]); + assert!( + result.program_result.is_err(), + "expected failure on tampered message, got: {:?}", + result.program_result + ); +} +#[test] +fn rejects_tampered_public_key_on_sbf() { + let Some((mollusk, program_id)) = make_mollusk() else { + return; + }; + let mut ix = signed_instruction(program_id, SINGLE_MESSAGE); + ix.data[PUBLIC_KEY_OFFSET] ^= 1; + + let result = mollusk.process_instruction(&ix, &[]); assert!( - result.program_result.is_ok(), - "verify failed: {:?}", + result.program_result.is_err(), + "expected failure on tampered public key, got: {:?}", result.program_result ); } #[test] -fn rejects_tampered_message_on_sbf() { +fn rejects_accounts_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 account = Pubkey::new_unique(); + 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(&instruction(program_id, data), &[]); + let result = mollusk.process_instruction(&ix, &accounts); assert!( result.program_result.is_err(), - "expected failure on tampered message, got: {:?}", + "expected failure when accounts are provided, got: {:?}", result.program_result ); } #[test] -fn rejects_tampered_public_key_on_sbf() { +fn rejects_short_instruction_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 ix = instruction(program_id, vec![0; 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 public key, got: {:?}", + "expected failure on short instruction data, got: {:?}", result.program_result ); } diff --git a/program/tests/process_instruction.rs b/program/tests/process_instruction.rs deleted file mode 100644 index ef6ffce..0000000 --- a/program/tests/process_instruction.rs +++ /dev/null @@ -1,311 +0,0 @@ -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_program::{ - process_instruction, CURRENT_INSTRUCTION_INDEX, DATA_START, SIGNATURE_SERIALIZED_SIZE, - }, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, -}; - -mod common; - -#[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(())); -} - -#[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(())); -} - -#[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); - - let dalek_key = VerifyingKey::from_bytes(&SMALL_ORDER_PUBLIC_KEY_COMPRESSED) - .expect("small-order key decompresses"); - 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(&program_id, &[], &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), - 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), - 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), - Err(ProgramError::InvalidArgument) - ); -} - -#[test] -fn rejects_short_instruction() { - let program_id = Pubkey::default(); - - assert_eq!( - process_instruction(&program_id, &[], &[]), - Err(ProgramError::InvalidInstructionData) - ); - assert_eq!( - process_instruction(&program_id, &[], &[1]), - Err(ProgramError::InvalidInstructionData) - ); - assert_eq!( - process_instruction(&program_id, &[], &[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(&program_id, &[], &[0]), - Err(ProgramError::InvalidInstructionData) - ); - assert_eq!( - process_instruction(&program_id, &[], &[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), - 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(&program_id, &[], &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(&program_id, &[], &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), - 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; - instruction[s_offset..s_offset + 32].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(&program_id, &[], &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); - instruction[r_offset..r_offset + 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(&program_id, &[], &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); - instruction[public_key_offset..public_key_offset + 32] - .copy_from_slice(&SMALL_ORDER_PUBLIC_KEY_COMPRESSED); - - assert_eq!( - process_instruction(&program_id, &[], &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), - 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(())); -} - -#[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) - .expect("test instruction length fits u16"); - write_offsets(&mut instruction[2..DATA_START], &offsets); - - assert_eq!( - process_instruction(&program_id, &[], &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 - 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. - for i in 0..20 { - let message = vec![i as u8; 10]; - let instruction = instruction_with_signature(&message, &signature, &pubkey); - - assert_eq!( - process_instruction(&program_id, &[], &instruction), - Ok(()), - "message index {i} is failing" - ); - } -}