From 4879837980a3201d578de43715460b94f0920c2b Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Mon, 22 Sep 2025 18:36:03 +0530 Subject: [PATCH 01/12] feat: ed25519 & secp256k1 sig verification helper modules --- lang/src/error.rs | 23 ++++ lang/src/lib.rs | 1 + lang/src/signature_verifications/ed25519.rs | 57 ++++++++ lang/src/signature_verifications/mod.rs | 16 +++ lang/src/signature_verifications/secp256k1.rs | 64 +++++++++ lang/tests/signature_verifications.rs | 123 ++++++++++++++++++ 6 files changed, 284 insertions(+) create mode 100644 lang/src/signature_verifications/ed25519.rs create mode 100644 lang/src/signature_verifications/mod.rs create mode 100644 lang/src/signature_verifications/secp256k1.rs create mode 100644 lang/tests/signature_verifications.rs diff --git a/lang/src/error.rs b/lang/src/error.rs index 967d6cdbc6..ec6a8765bd 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -178,6 +178,29 @@ pub enum ErrorCode { #[msg("A transfer hook extension transfer hook program id constraint was violated")] ConstraintMintTransferHookExtensionProgramId, + // Signature verification errors + /// 2040 - Invalid Ed25519 program id for signature verification + #[msg("Invalid Ed25519 program id for signature verification")] + Ed25519InvalidProgram, + /// 2041 - Invalid Secp256k1 program id for signature verification + #[msg("Invalid Secp256k1 program id for signature verification")] + Secp256k1InvalidProgram, + /// 2042 - Instruction unexpectedly had account metas + #[msg("Instruction unexpectedly had account metas")] + InstructionHasAccounts, + /// 2043 - Message length exceeds allowed maximum + #[msg("Message length exceeds allowed maximum")] + MessageTooLong, + /// 2044 - Instruction data length mismatch + #[msg("Instruction data length mismatch")] + DataLengthMismatch, + /// 2045 - Invalid Secp256k1 recovery id (must be 0 or 1) + #[msg("Invalid Secp256k1 recovery id")] + InvalidRecoveryId, + /// 2046 - Number of signatures and addresses mismatch in Secp256k1 header + #[msg("Number of signatures and addresses mismatch")] + NumSigsAddrsMismatch, + // Require /// 2500 - A require expression was violated #[msg("A require expression was violated")] diff --git a/lang/src/lib.rs b/lang/src/lib.rs index a13417f7f8..719311a566 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -43,6 +43,7 @@ pub mod error; pub mod event; #[doc(hidden)] pub mod idl; +pub mod signature_verifications; pub mod system_program; mod vec; diff --git a/lang/src/signature_verifications/ed25519.rs b/lang/src/signature_verifications/ed25519.rs new file mode 100644 index 0000000000..64c4e78dc0 --- /dev/null +++ b/lang/src/signature_verifications/ed25519.rs @@ -0,0 +1,57 @@ +use crate::error::ErrorCode; +use crate::prelude::*; +use crate::solana_program::instruction::Instruction; +use solana_sdk_ids::ed25519_program; + +/// Verify that `ix.data` is an Ed25519 syscall verifying `sig` over `msg` by `pubkey`. +pub fn verify_ed25519_ix( + ix: &Instruction, + pubkey: &[u8; 32], + msg: &[u8], + sig: &[u8; 64], +) -> Result<()> { + require_keys_eq!( + ix.program_id, + ed25519_program::id(), + ErrorCode::Ed25519InvalidProgram + ); + require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); + require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); + + let num_signatures: u8 = 1; + let padding: u8 = 0; + let header_len: usize = 2; + let offsets_len: usize = 14; // 7 * u16 + let base: usize = header_len + offsets_len; + + let signature_offset: u16 = base as u16; + let signature_instruction_index: u16 = u16::MAX; + let public_key_offset: u16 = (base + 64) as u16; + let public_key_instruction_index: u16 = u16::MAX; + let message_data_offset: u16 = (base + 64 + 32) as u16; // 32 bytes for pubkey + let message_data_size: u16 = msg.len() as u16; + let message_instruction_index: u16 = u16::MAX; + + // [header][offsets][signature][public_key][msg] + let mut expected = Vec::with_capacity(base + 64 + 32 + msg.len()); + expected.push(num_signatures); + expected.push(padding); + expected.extend_from_slice(&signature_offset.to_le_bytes()); + expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); + expected.extend_from_slice(&public_key_offset.to_le_bytes()); + expected.extend_from_slice(&public_key_instruction_index.to_le_bytes()); + expected.extend_from_slice(&message_data_offset.to_le_bytes()); + expected.extend_from_slice(&message_data_size.to_le_bytes()); + expected.extend_from_slice(&message_instruction_index.to_le_bytes()); + expected.extend_from_slice(sig); + expected.extend_from_slice(pubkey); + expected.extend_from_slice(msg); + + if ix.data.len() != expected.len() { + return Err(error!(ErrorCode::DataLengthMismatch)); + } + if ix.data != expected { + return Err(error!(ErrorCode::ConstraintRaw)); + } + Ok(()) +} \ No newline at end of file diff --git a/lang/src/signature_verifications/mod.rs b/lang/src/signature_verifications/mod.rs new file mode 100644 index 0000000000..b6d6544a1c --- /dev/null +++ b/lang/src/signature_verifications/mod.rs @@ -0,0 +1,16 @@ +use crate::prelude::*; +use crate::solana_program::instruction::Instruction; +use crate::solana_program::sysvar::instructions::load_instruction_at_checked; + +mod ed25519; +mod secp256k1; + +pub use ed25519::verify_ed25519_ix; +pub use secp256k1::verify_secp256k1_ix; + +/// Load an instruction from the Instructions sysvar at the given index. +pub fn load_instruction(index: usize, ix_sysvar: &AccountInfo<'_>) -> Result { + let ix = load_instruction_at_checked(index, ix_sysvar) + .map_err(|_| error!(error::ErrorCode::ConstraintRaw))?; + Ok(ix) +} diff --git a/lang/src/signature_verifications/secp256k1.rs b/lang/src/signature_verifications/secp256k1.rs new file mode 100644 index 0000000000..e2a502e306 --- /dev/null +++ b/lang/src/signature_verifications/secp256k1.rs @@ -0,0 +1,64 @@ +use crate::error::ErrorCode; +use crate::prelude::*; +use crate::solana_program::instruction::Instruction; +use solana_sdk_ids::secp256k1_program; + +/// Verify that `ix.data` is a Secp256k1 syscall verifying `sig` over `msg` by `eth_address` with `recovery_id`. +pub fn verify_secp256k1_ix( + ix: &Instruction, + eth_address: &[u8; 20], + msg: &[u8], + sig: &[u8; 64], + recovery_id: u8, +) -> Result<()> { + require_keys_eq!( + ix.program_id, + secp256k1_program::id(), + ErrorCode::Secp256k1InvalidProgram + ); + require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); + require!(recovery_id <= 1, ErrorCode::InvalidRecoveryId); + require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); + + let num_signatures: u8 = 1; + let num_eth_addresses: u8 = 1; + let header_len: usize = 2; + let offsets_len: usize = 18; // 9 * u16 + let base: usize = header_len + offsets_len; + + let signature_offset: u16 = base as u16; + let signature_instruction_index: u16 = u16::MAX; + let eth_address_offset: u16 = (base + 64) as u16; + let eth_address_instruction_index: u16 = u16::MAX; + let message_data_offset: u16 = (base + 64 + 20) as u16; // 20 bytes for eth_address + let message_data_size: u16 = msg.len() as u16; + let message_instruction_index: u16 = u16::MAX; + let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16; + let recovery_id_instruction_index: u16 = u16::MAX; + + // [header][offsets][signature][eth_address][msg][recovery_id] + let mut expected = Vec::with_capacity(base + 64 + 20 + msg.len() + 1); + expected.push(num_signatures); + expected.push(num_eth_addresses); + expected.extend_from_slice(&signature_offset.to_le_bytes()); + expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); + expected.extend_from_slice(ð_address_offset.to_le_bytes()); + expected.extend_from_slice(ð_address_instruction_index.to_le_bytes()); + expected.extend_from_slice(&message_data_offset.to_le_bytes()); + expected.extend_from_slice(&message_data_size.to_le_bytes()); + expected.extend_from_slice(&message_instruction_index.to_le_bytes()); + expected.extend_from_slice(&recovery_id_offset.to_le_bytes()); + expected.extend_from_slice(&recovery_id_instruction_index.to_le_bytes()); + expected.extend_from_slice(sig); + expected.extend_from_slice(eth_address); + expected.extend_from_slice(msg); + expected.push(recovery_id); + + if ix.data.len() != expected.len() { + return Err(error!(ErrorCode::DataLengthMismatch)); + } + if ix.data != expected { + return Err(error!(ErrorCode::ConstraintRaw)); + } + Ok(()) +} diff --git a/lang/tests/signature_verifications.rs b/lang/tests/signature_verifications.rs new file mode 100644 index 0000000000..b539f2f955 --- /dev/null +++ b/lang/tests/signature_verifications.rs @@ -0,0 +1,123 @@ +use anchor_lang::signature_verifications::{verify_ed25519_ix, verify_secp256k1_ix}; +use anchor_lang::solana_program::instruction::Instruction; +use solana_sdk_ids::{ed25519_program, secp256k1_program}; + +#[test] +fn test_verify_ed25519_matches() { + let pubkey = [7u8; 32]; + let msg = b"Testing #3944 with Ed25519 Signature".to_vec(); + let sig = [9u8; 64]; + + let num_signatures: u8 = 1; + let padding: u8 = 0; + let header_len: usize = 2; + let offsets_len: usize = 14; + let base: usize = header_len + offsets_len; + + let signature_offset: u16 = base as u16; + let signature_instruction_index: u16 = u16::MAX; + let public_key_offset: u16 = (base + 64) as u16; + let public_key_instruction_index: u16 = u16::MAX; + let message_data_offset: u16 = (base + 64 + 32) as u16; + let message_data_size: u16 = msg.len() as u16; + let message_instruction_index: u16 = u16::MAX; + + let mut data = Vec::with_capacity(base + 64 + 32 + msg.len()); + data.push(num_signatures); + data.push(padding); + data.extend_from_slice(&signature_offset.to_le_bytes()); + data.extend_from_slice(&signature_instruction_index.to_le_bytes()); + data.extend_from_slice(&public_key_offset.to_le_bytes()); + data.extend_from_slice(&public_key_instruction_index.to_le_bytes()); + data.extend_from_slice(&message_data_offset.to_le_bytes()); + data.extend_from_slice(&message_data_size.to_le_bytes()); + data.extend_from_slice(&message_instruction_index.to_le_bytes()); + data.extend_from_slice(&sig); + data.extend_from_slice(&pubkey); + data.extend_from_slice(&msg); + + let ix = Instruction { + program_id: ed25519_program::id(), + accounts: vec![], + data, + }; + assert!(verify_ed25519_ix(&ix, &pubkey, &msg, &sig).is_ok()); +} + +#[test] +fn test_verify_ed25519_mismatch() { + let pubkey = [7u8; 32]; + let msg = b"Testing #3944 with Ed25519 Signature".to_vec(); + let sig = [9u8; 64]; + + let data = vec![0u8; 10]; + let ix = Instruction { + program_id: ed25519_program::id(), + accounts: vec![], + data, + }; + assert!(verify_ed25519_ix(&ix, &pubkey, &msg, &sig).is_err()); +} + +#[test] +fn test_verify_secp256k1_matches() { + let eth_address = [0x11u8; 20]; + let msg = b"Testing #3944 with Secp256k1 Signature".to_vec(); + let sig = [0x22u8; 64]; + let recovery_id: u8 = 1; + + let num_signatures: u8 = 1; + let num_eth_addresses: u8 = 1; + let header_len: usize = 2; + let offsets_len: usize = 18; + let base: usize = header_len + offsets_len; + + let signature_offset: u16 = base as u16; + let signature_instruction_index: u16 = u16::MAX; + let eth_address_offset: u16 = (base + 64) as u16; + let eth_address_instruction_index: u16 = u16::MAX; + let message_data_offset: u16 = (base + 64 + 20) as u16; + let message_data_size: u16 = msg.len() as u16; + let message_instruction_index: u16 = u16::MAX; + let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16; + let recovery_id_instruction_index: u16 = u16::MAX; + + let mut data = Vec::with_capacity(base + 64 + 20 + msg.len() + 1); + data.push(num_signatures); + data.push(num_eth_addresses); + data.extend_from_slice(&signature_offset.to_le_bytes()); + data.extend_from_slice(&signature_instruction_index.to_le_bytes()); + data.extend_from_slice(ð_address_offset.to_le_bytes()); + data.extend_from_slice(ð_address_instruction_index.to_le_bytes()); + data.extend_from_slice(&message_data_offset.to_le_bytes()); + data.extend_from_slice(&message_data_size.to_le_bytes()); + data.extend_from_slice(&message_instruction_index.to_le_bytes()); + data.extend_from_slice(&recovery_id_offset.to_le_bytes()); + data.extend_from_slice(&recovery_id_instruction_index.to_le_bytes()); + data.extend_from_slice(&sig); + data.extend_from_slice(ð_address); + data.extend_from_slice(&msg); + data.push(recovery_id); + + let ix = Instruction { + program_id: secp256k1_program::id(), + accounts: vec![], + data, + }; + assert!(verify_secp256k1_ix(&ix, ð_address, &msg, &sig, recovery_id).is_ok()); +} + +#[test] +fn test_verify_secp256k1_mismatch() { + let eth_address = [0x11u8; 20]; + let msg = b"Testing #3944 with Secp256k1 Signature".to_vec(); + let sig = [0x22u8; 64]; + let recovery_id: u8 = 1; + + let ix = Instruction { + program_id: secp256k1_program::id(), + accounts: vec![], + data: vec![1, 2, 3], + }; + assert!(verify_secp256k1_ix(&ix, ð_address, &msg, &sig, recovery_id).is_err()); +} From d1d65e41f323804e308e447d7b70c0126e7f4171 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Mon, 22 Sep 2025 19:04:49 +0530 Subject: [PATCH 02/12] fix: fmt/clippy --- lang/src/signature_verifications/ed25519.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/src/signature_verifications/ed25519.rs b/lang/src/signature_verifications/ed25519.rs index 64c4e78dc0..78448c3376 100644 --- a/lang/src/signature_verifications/ed25519.rs +++ b/lang/src/signature_verifications/ed25519.rs @@ -54,4 +54,4 @@ pub fn verify_ed25519_ix( return Err(error!(ErrorCode::ConstraintRaw)); } Ok(()) -} \ No newline at end of file +} From ab3fcc3fddece4d17263e28e52ebb26cab9909a1 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Mon, 22 Sep 2025 23:52:31 +0530 Subject: [PATCH 03/12] feat: workflow tests --- .github/workflows/reusable-tests.yaml | 2 + lang/src/lib.rs | 2 +- lang/src/signature_verification/ed25519.rs | 57 +++++++++++ lang/src/signature_verification/mod.rs | 16 +++ lang/src/signature_verification/secp256k1.rs | 64 ++++++++++++ lang/tests/signature_verifications.rs | 2 +- tests/package.json | 3 +- tests/signature-verification/Anchor.toml | 9 ++ tests/signature-verification/Cargo.toml | 7 ++ tests/signature-verification/package.json | 19 ++++ .../signature-verification-test/Cargo.toml | 17 ++++ .../signature-verification-test/Xargo.toml | 2 + .../signature-verification-test/src/lib.rs | 70 +++++++++++++ .../tests/signature-verification-test.ts | 99 +++++++++++++++++++ tests/signature-verification/tsconfig.json | 13 +++ 15 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 lang/src/signature_verification/ed25519.rs create mode 100644 lang/src/signature_verification/mod.rs create mode 100644 lang/src/signature_verification/secp256k1.rs create mode 100644 tests/signature-verification/Anchor.toml create mode 100644 tests/signature-verification/Cargo.toml create mode 100644 tests/signature-verification/package.json create mode 100644 tests/signature-verification/programs/signature-verification-test/Cargo.toml create mode 100644 tests/signature-verification/programs/signature-verification-test/Xargo.toml create mode 100644 tests/signature-verification/programs/signature-verification-test/src/lib.rs create mode 100644 tests/signature-verification/tests/signature-verification-test.ts create mode 100644 tests/signature-verification/tsconfig.json diff --git a/.github/workflows/reusable-tests.yaml b/.github/workflows/reusable-tests.yaml index 7b8af78491..7b08ec2045 100644 --- a/.github/workflows/reusable-tests.yaml +++ b/.github/workflows/reusable-tests.yaml @@ -462,6 +462,8 @@ jobs: path: tests/idl - cmd: cd tests/lazy-account && anchor test path: tests/lazy-account + - cmd: cd tests/signature-verification && anchor test --skip-lint + path: tests/signature-verification steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup/ diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 719311a566..143f0d4131 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -43,7 +43,7 @@ pub mod error; pub mod event; #[doc(hidden)] pub mod idl; -pub mod signature_verifications; +pub mod signature_verification; pub mod system_program; mod vec; diff --git a/lang/src/signature_verification/ed25519.rs b/lang/src/signature_verification/ed25519.rs new file mode 100644 index 0000000000..78448c3376 --- /dev/null +++ b/lang/src/signature_verification/ed25519.rs @@ -0,0 +1,57 @@ +use crate::error::ErrorCode; +use crate::prelude::*; +use crate::solana_program::instruction::Instruction; +use solana_sdk_ids::ed25519_program; + +/// Verify that `ix.data` is an Ed25519 syscall verifying `sig` over `msg` by `pubkey`. +pub fn verify_ed25519_ix( + ix: &Instruction, + pubkey: &[u8; 32], + msg: &[u8], + sig: &[u8; 64], +) -> Result<()> { + require_keys_eq!( + ix.program_id, + ed25519_program::id(), + ErrorCode::Ed25519InvalidProgram + ); + require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); + require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); + + let num_signatures: u8 = 1; + let padding: u8 = 0; + let header_len: usize = 2; + let offsets_len: usize = 14; // 7 * u16 + let base: usize = header_len + offsets_len; + + let signature_offset: u16 = base as u16; + let signature_instruction_index: u16 = u16::MAX; + let public_key_offset: u16 = (base + 64) as u16; + let public_key_instruction_index: u16 = u16::MAX; + let message_data_offset: u16 = (base + 64 + 32) as u16; // 32 bytes for pubkey + let message_data_size: u16 = msg.len() as u16; + let message_instruction_index: u16 = u16::MAX; + + // [header][offsets][signature][public_key][msg] + let mut expected = Vec::with_capacity(base + 64 + 32 + msg.len()); + expected.push(num_signatures); + expected.push(padding); + expected.extend_from_slice(&signature_offset.to_le_bytes()); + expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); + expected.extend_from_slice(&public_key_offset.to_le_bytes()); + expected.extend_from_slice(&public_key_instruction_index.to_le_bytes()); + expected.extend_from_slice(&message_data_offset.to_le_bytes()); + expected.extend_from_slice(&message_data_size.to_le_bytes()); + expected.extend_from_slice(&message_instruction_index.to_le_bytes()); + expected.extend_from_slice(sig); + expected.extend_from_slice(pubkey); + expected.extend_from_slice(msg); + + if ix.data.len() != expected.len() { + return Err(error!(ErrorCode::DataLengthMismatch)); + } + if ix.data != expected { + return Err(error!(ErrorCode::ConstraintRaw)); + } + Ok(()) +} diff --git a/lang/src/signature_verification/mod.rs b/lang/src/signature_verification/mod.rs new file mode 100644 index 0000000000..b6d6544a1c --- /dev/null +++ b/lang/src/signature_verification/mod.rs @@ -0,0 +1,16 @@ +use crate::prelude::*; +use crate::solana_program::instruction::Instruction; +use crate::solana_program::sysvar::instructions::load_instruction_at_checked; + +mod ed25519; +mod secp256k1; + +pub use ed25519::verify_ed25519_ix; +pub use secp256k1::verify_secp256k1_ix; + +/// Load an instruction from the Instructions sysvar at the given index. +pub fn load_instruction(index: usize, ix_sysvar: &AccountInfo<'_>) -> Result { + let ix = load_instruction_at_checked(index, ix_sysvar) + .map_err(|_| error!(error::ErrorCode::ConstraintRaw))?; + Ok(ix) +} diff --git a/lang/src/signature_verification/secp256k1.rs b/lang/src/signature_verification/secp256k1.rs new file mode 100644 index 0000000000..e2a502e306 --- /dev/null +++ b/lang/src/signature_verification/secp256k1.rs @@ -0,0 +1,64 @@ +use crate::error::ErrorCode; +use crate::prelude::*; +use crate::solana_program::instruction::Instruction; +use solana_sdk_ids::secp256k1_program; + +/// Verify that `ix.data` is a Secp256k1 syscall verifying `sig` over `msg` by `eth_address` with `recovery_id`. +pub fn verify_secp256k1_ix( + ix: &Instruction, + eth_address: &[u8; 20], + msg: &[u8], + sig: &[u8; 64], + recovery_id: u8, +) -> Result<()> { + require_keys_eq!( + ix.program_id, + secp256k1_program::id(), + ErrorCode::Secp256k1InvalidProgram + ); + require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); + require!(recovery_id <= 1, ErrorCode::InvalidRecoveryId); + require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); + + let num_signatures: u8 = 1; + let num_eth_addresses: u8 = 1; + let header_len: usize = 2; + let offsets_len: usize = 18; // 9 * u16 + let base: usize = header_len + offsets_len; + + let signature_offset: u16 = base as u16; + let signature_instruction_index: u16 = u16::MAX; + let eth_address_offset: u16 = (base + 64) as u16; + let eth_address_instruction_index: u16 = u16::MAX; + let message_data_offset: u16 = (base + 64 + 20) as u16; // 20 bytes for eth_address + let message_data_size: u16 = msg.len() as u16; + let message_instruction_index: u16 = u16::MAX; + let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16; + let recovery_id_instruction_index: u16 = u16::MAX; + + // [header][offsets][signature][eth_address][msg][recovery_id] + let mut expected = Vec::with_capacity(base + 64 + 20 + msg.len() + 1); + expected.push(num_signatures); + expected.push(num_eth_addresses); + expected.extend_from_slice(&signature_offset.to_le_bytes()); + expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); + expected.extend_from_slice(ð_address_offset.to_le_bytes()); + expected.extend_from_slice(ð_address_instruction_index.to_le_bytes()); + expected.extend_from_slice(&message_data_offset.to_le_bytes()); + expected.extend_from_slice(&message_data_size.to_le_bytes()); + expected.extend_from_slice(&message_instruction_index.to_le_bytes()); + expected.extend_from_slice(&recovery_id_offset.to_le_bytes()); + expected.extend_from_slice(&recovery_id_instruction_index.to_le_bytes()); + expected.extend_from_slice(sig); + expected.extend_from_slice(eth_address); + expected.extend_from_slice(msg); + expected.push(recovery_id); + + if ix.data.len() != expected.len() { + return Err(error!(ErrorCode::DataLengthMismatch)); + } + if ix.data != expected { + return Err(error!(ErrorCode::ConstraintRaw)); + } + Ok(()) +} diff --git a/lang/tests/signature_verifications.rs b/lang/tests/signature_verifications.rs index b539f2f955..fd08439d4b 100644 --- a/lang/tests/signature_verifications.rs +++ b/lang/tests/signature_verifications.rs @@ -1,4 +1,4 @@ -use anchor_lang::signature_verifications::{verify_ed25519_ix, verify_secp256k1_ix}; +use anchor_lang::signature_verification::{verify_ed25519_ix, verify_secp256k1_ix}; use anchor_lang::solana_program::instruction::Instruction; use solana_sdk_ids::{ed25519_program, secp256k1_program}; diff --git a/tests/package.json b/tests/package.json index 41c258ceb9..adb0de4fd1 100644 --- a/tests/package.json +++ b/tests/package.json @@ -50,7 +50,8 @@ "cpi-returns", "multiple-suites", "multiple-suites-run-single", - "bpf-upgradeable-state" + "bpf-upgradeable-state", + "signature-verification" ], "dependencies": { "@project-serum/common": "^0.0.1-beta.3", diff --git a/tests/signature-verification/Anchor.toml b/tests/signature-verification/Anchor.toml new file mode 100644 index 0000000000..8e72bc9bbf --- /dev/null +++ b/tests/signature-verification/Anchor.toml @@ -0,0 +1,9 @@ +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[programs.localnet] +signature_verification_test = "9q9StGMtVHQtz14vH8YhPtED9MxnBj2mnVpBhYeTeRhj" + +[scripts] +test = "yarn --cwd ../.. run ts-mocha -t 1000000 tests/*.ts" diff --git a/tests/signature-verification/Cargo.toml b/tests/signature-verification/Cargo.toml new file mode 100644 index 0000000000..3c29fab173 --- /dev/null +++ b/tests/signature-verification/Cargo.toml @@ -0,0 +1,7 @@ + +[workspace] +members = ["programs/signature-verification-test"] +resolver = "2" + +[profile.release] +overflow-checks = true diff --git a/tests/signature-verification/package.json b/tests/signature-verification/package.json new file mode 100644 index 0000000000..0a24d7e233 --- /dev/null +++ b/tests/signature-verification/package.json @@ -0,0 +1,19 @@ +{ + "name": "signature-verifications", + "version": "0.31.1", + "license": "(MIT OR Apache-2.0)", + "homepage": "https://github.com/coral-xyz/anchor#readme", + "bugs": { + "url": "https://github.com/coral-xyz/anchor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/coral-xyz/anchor.git" + }, + "engines": { + "node": ">=17" + }, + "scripts": { + "test": "anchor test" + } +} diff --git a/tests/signature-verification/programs/signature-verification-test/Cargo.toml b/tests/signature-verification/programs/signature-verification-test/Cargo.toml new file mode 100644 index 0000000000..68bf649926 --- /dev/null +++ b/tests/signature-verification/programs/signature-verification-test/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "signature-verification-test" +version = "0.1.0" +description = "A test program for signature verification" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "signature_verification_test" + +[features] +no-entrypoint = [] +cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/signature-verification/programs/signature-verification-test/Xargo.toml b/tests/signature-verification/programs/signature-verification-test/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/tests/signature-verification/programs/signature-verification-test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/tests/signature-verification/programs/signature-verification-test/src/lib.rs b/tests/signature-verification/programs/signature-verification-test/src/lib.rs new file mode 100644 index 0000000000..e162f49b83 --- /dev/null +++ b/tests/signature-verification/programs/signature-verification-test/src/lib.rs @@ -0,0 +1,70 @@ +use anchor_lang::prelude::*; +use anchor_lang::signature_verification::{ + load_instruction, verify_ed25519_ix, verify_secp256k1_ix, +}; + +declare_id!("9q9StGMtVHQtz14vH8YhPtED9MxnBj2mnVpBhYeTeRhj"); + +#[program] +pub mod signature_verification_test { + use super::*; + + pub fn verify_ed25519_signature( + ctx: Context, + message: Vec, + signature: [u8; 64], + ) -> Result<()> { + let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?; + verify_ed25519_ix( + &ix, + &ctx.accounts.signer.key().to_bytes(), + &message, + &signature, + )?; + + msg!("Ed25519 signature verified successfully using custom helper!"); + msg!("Signer: {}", ctx.accounts.signer.key()); + msg!("Message length: {}", message.len()); + + Ok(()) + } + + pub fn verify_secp256k1_signature( + ctx: Context, + message_hash: [u8; 32], + signature: [u8; 64], + recovery_id: u8, + eth_address: [u8; 20], + ) -> Result<()> { + let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?; + verify_secp256k1_ix(&ix, ð_address, &message_hash, &signature, recovery_id)?; + + msg!("Secp256k1 signature verified successfully using custom helper!"); + msg!("Eth address: {:?}", eth_address); + msg!("Message hash: {:?}", message_hash); + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct VerifyEd25519Signature<'info> { + /// CHECK: This account represents the signer's public key + pub signer: AccountInfo<'info>, + /// CHECK: Instructions sysvar account + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub ix_sysvar: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct VerifySecp256k1Signature<'info> { + /// CHECK: Instructions sysvar account + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub ix_sysvar: AccountInfo<'info>, +} + +#[error_code] +pub enum ErrorCode { + #[msg("Signature verification failed")] + SignatureVerificationFailed, +} diff --git a/tests/signature-verification/tests/signature-verification-test.ts b/tests/signature-verification/tests/signature-verification-test.ts new file mode 100644 index 0000000000..3af475820c --- /dev/null +++ b/tests/signature-verification/tests/signature-verification-test.ts @@ -0,0 +1,99 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { SignatureVerificationTest } from "../target/types/signature_verification_test"; +import { Buffer } from "buffer"; +import { PublicKey, Keypair, Transaction, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, Ed25519Program, Secp256k1Program } from "@solana/web3.js"; +import * as crypto from "crypto"; + +describe("signature-verification-test", () => { + const provider = anchor.AnchorProvider.local(); + anchor.setProvider(provider); + const program = anchor.workspace.SignatureVerificationTest as Program; + + it("Verify Ed25519 signature with actual signature", async () => { + // Create a keypair for testing + const signer = Keypair.generate(); + + // Create a test message + const message = Buffer.from("Hello, Anchor Signature Verification Test!"); + + // Create a mock signature (in real implementation, this would be properly signed) + const signature = new Uint8Array(64).fill(1); // Mock signature + + // Create instruction to call the program + const instruction = await program.methods + .verifyEd25519Signature(message, Array.from(signature) as [number, ...number[]]) + .accounts({ + signer: signer.publicKey, + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction(); + + // Create transaction with the signature verification instruction + const transaction = new Transaction().add(instruction); + + // Add the Ed25519 signature instruction to the transaction + const ed25519Instruction = Ed25519Program.createInstructionWithPublicKey({ + publicKey: signer.publicKey.toBytes(), + message: message, + signature: signature, + }); + + transaction.add(ed25519Instruction); + + try { + await provider.sendAndConfirm(transaction, [signer]); + console.log("✅ Ed25519 signature verified successfully using custom helper!"); + } catch (error) { + console.log("❌ Ed25519 verification failed:", error.message); + // This test demonstrates the structure even if signature verification fails + } + }); + + it("Verify Secp256k1 signature with actual signature", async () => { + // Create ETH address (20 bytes) + const ethAddress = crypto.randomBytes(20); + + // Create a test message hash (32 bytes) + const messageHash = crypto.randomBytes(32); + + // Create a mock signature + const signature = new Uint8Array(64).fill(2); + const recoveryId = 0; + + // Create instruction to call the program + const instruction = await program.methods + .verifySecp256k1Signature( + Array.from(messageHash) as [number, ...number[]], + Array.from(signature) as [number, ...number[]], + recoveryId, + Array.from(ethAddress) as [number, ...number[]] + ) + .accounts({ + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction(); + + // Create transaction with the signature verification instruction + const transaction = new Transaction().add(instruction); + + // Add the Secp256k1 signature instruction to the transaction + const secp256k1Instruction = Secp256k1Program.createInstructionWithPublicKey({ + publicKey: Buffer.from(ethAddress), // Use ETH address as public key + message: messageHash, + signature: signature, + recoveryId: recoveryId, + }); + + transaction.add(secp256k1Instruction); + + try { + await provider.sendAndConfirm(transaction, []); + console.log("✅ Secp256k1 signature verified successfully using custom helper!"); + } catch (error) { + console.log("❌ Secp256k1 verification failed:", error.message); + // This test demonstrates the structure even if signature verification fails + } + }); + +}); \ No newline at end of file diff --git a/tests/signature-verification/tsconfig.json b/tests/signature-verification/tsconfig.json new file mode 100644 index 0000000000..4e62c98006 --- /dev/null +++ b/tests/signature-verification/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es6"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} From e6b4e2a9eb6e2acd11423a001d932e250bceaa59 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Mon, 22 Sep 2025 23:59:56 +0530 Subject: [PATCH 04/12] fix: yarn lint --- .../tests/signature-verification-test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/signature-verification/tests/signature-verification-test.ts b/tests/signature-verification/tests/signature-verification-test.ts index 3af475820c..5050e8a1fb 100644 --- a/tests/signature-verification/tests/signature-verification-test.ts +++ b/tests/signature-verification/tests/signature-verification-test.ts @@ -13,13 +13,13 @@ describe("signature-verification-test", () => { it("Verify Ed25519 signature with actual signature", async () => { // Create a keypair for testing const signer = Keypair.generate(); - + // Create a test message const message = Buffer.from("Hello, Anchor Signature Verification Test!"); - + // Create a mock signature (in real implementation, this would be properly signed) const signature = new Uint8Array(64).fill(1); // Mock signature - + // Create instruction to call the program const instruction = await program.methods .verifyEd25519Signature(message, Array.from(signature) as [number, ...number[]]) @@ -31,16 +31,16 @@ describe("signature-verification-test", () => { // Create transaction with the signature verification instruction const transaction = new Transaction().add(instruction); - + // Add the Ed25519 signature instruction to the transaction const ed25519Instruction = Ed25519Program.createInstructionWithPublicKey({ publicKey: signer.publicKey.toBytes(), message: message, signature: signature, }); - + transaction.add(ed25519Instruction); - + try { await provider.sendAndConfirm(transaction, [signer]); console.log("✅ Ed25519 signature verified successfully using custom helper!"); @@ -53,14 +53,14 @@ describe("signature-verification-test", () => { it("Verify Secp256k1 signature with actual signature", async () => { // Create ETH address (20 bytes) const ethAddress = crypto.randomBytes(20); - + // Create a test message hash (32 bytes) const messageHash = crypto.randomBytes(32); - + // Create a mock signature const signature = new Uint8Array(64).fill(2); const recoveryId = 0; - + // Create instruction to call the program const instruction = await program.methods .verifySecp256k1Signature( @@ -76,7 +76,7 @@ describe("signature-verification-test", () => { // Create transaction with the signature verification instruction const transaction = new Transaction().add(instruction); - + // Add the Secp256k1 signature instruction to the transaction const secp256k1Instruction = Secp256k1Program.createInstructionWithPublicKey({ publicKey: Buffer.from(ethAddress), // Use ETH address as public key @@ -84,9 +84,9 @@ describe("signature-verification-test", () => { signature: signature, recoveryId: recoveryId, }); - + transaction.add(secp256k1Instruction); - + try { await provider.sendAndConfirm(transaction, []); console.log("✅ Secp256k1 signature verified successfully using custom helper!"); From 05f0da48618655dc7a373f3ce82a1c21ad932ed0 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Tue, 23 Sep 2025 00:04:10 +0530 Subject: [PATCH 05/12] fix: yarn test --- tests/signature-verification/Anchor.toml | 2 +- tests/signature-verification/tsconfig.json | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/signature-verification/Anchor.toml b/tests/signature-verification/Anchor.toml index 8e72bc9bbf..d327a0e4bb 100644 --- a/tests/signature-verification/Anchor.toml +++ b/tests/signature-verification/Anchor.toml @@ -6,4 +6,4 @@ wallet = "~/.config/solana/id.json" signature_verification_test = "9q9StGMtVHQtz14vH8YhPtED9MxnBj2mnVpBhYeTeRhj" [scripts] -test = "yarn --cwd ../.. run ts-mocha -t 1000000 tests/*.ts" +test = "yarn run ts-mocha -t 1000000 tests/*.ts" diff --git a/tests/signature-verification/tsconfig.json b/tests/signature-verification/tsconfig.json index 4e62c98006..95e0193a7d 100644 --- a/tests/signature-verification/tsconfig.json +++ b/tests/signature-verification/tsconfig.json @@ -1,13 +1,11 @@ { "compilerOptions": { - "types": ["mocha", "chai"], + "types": ["mocha", "chai", "node"], "typeRoots": ["./node_modules/@types"], - "lib": ["es6"], + "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, "skipLibCheck": true } -} +} \ No newline at end of file From 8b394981712caa630dc3513e92106eb5d1c48cb2 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Tue, 23 Sep 2025 11:28:09 +0530 Subject: [PATCH 06/12] fix: workflow tests with ts-mocha & naming errors --- lang/src/error.rs | 9 +- lang/src/signature_verification/ed25519.rs | 50 +-- lang/src/signature_verification/secp256k1.rs | 58 ++- lang/tests/signature_verifications.rs | 123 ------ tests/signature-verification/Anchor.toml | 9 +- tests/signature-verification/package.json | 3 + .../signature-verification-test/src/lib.rs | 21 +- .../tests/signature-verification-test.ts | 385 +++++++++++++++--- 8 files changed, 391 insertions(+), 267 deletions(-) delete mode 100644 lang/tests/signature_verifications.rs diff --git a/lang/src/error.rs b/lang/src/error.rs index ec6a8765bd..12f6a2e128 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -191,15 +191,12 @@ pub enum ErrorCode { /// 2043 - Message length exceeds allowed maximum #[msg("Message length exceeds allowed maximum")] MessageTooLong, - /// 2044 - Instruction data length mismatch - #[msg("Instruction data length mismatch")] - DataLengthMismatch, /// 2045 - Invalid Secp256k1 recovery id (must be 0 or 1) #[msg("Invalid Secp256k1 recovery id")] InvalidRecoveryId, - /// 2046 - Number of signatures and addresses mismatch in Secp256k1 header - #[msg("Number of signatures and addresses mismatch")] - NumSigsAddrsMismatch, + /// 2047 - Signature verification failed + #[msg("Signature verification failed")] + SignatureVerificationFailed, // Require /// 2500 - A require expression was violated diff --git a/lang/src/signature_verification/ed25519.rs b/lang/src/signature_verification/ed25519.rs index 78448c3376..9dc73cd14e 100644 --- a/lang/src/signature_verification/ed25519.rs +++ b/lang/src/signature_verification/ed25519.rs @@ -3,7 +3,6 @@ use crate::prelude::*; use crate::solana_program::instruction::Instruction; use solana_sdk_ids::ed25519_program; -/// Verify that `ix.data` is an Ed25519 syscall verifying `sig` over `msg` by `pubkey`. pub fn verify_ed25519_ix( ix: &Instruction, pubkey: &[u8; 32], @@ -18,40 +17,33 @@ pub fn verify_ed25519_ix( require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); - let num_signatures: u8 = 1; - let padding: u8 = 0; - let header_len: usize = 2; - let offsets_len: usize = 14; // 7 * u16 - let base: usize = header_len + offsets_len; + const DATA_START: usize = 16; // 2 header + 14 offset bytes + let pubkey_len = pubkey.len() as u16; + let sig_len = sig.len() as u16; + let msg_len = msg.len() as u16; - let signature_offset: u16 = base as u16; - let signature_instruction_index: u16 = u16::MAX; - let public_key_offset: u16 = (base + 64) as u16; - let public_key_instruction_index: u16 = u16::MAX; - let message_data_offset: u16 = (base + 64 + 32) as u16; // 32 bytes for pubkey - let message_data_size: u16 = msg.len() as u16; - let message_instruction_index: u16 = u16::MAX; + let sig_offset: u16 = DATA_START as u16; + let pubkey_offset: u16 = sig_offset + sig_len; + let msg_offset: u16 = pubkey_offset + pubkey_len; + + let mut expected = Vec::with_capacity(DATA_START + sig.len() + pubkey.len() + msg.len()); + + expected.push(1u8); // num signatures + expected.push(0u8); // padding + expected.extend_from_slice(&sig_offset.to_le_bytes()); + expected.push(0u8); // sig ix idx + expected.extend_from_slice(&pubkey_offset.to_le_bytes()); + expected.push(0u8); // pubkey ix idx + expected.extend_from_slice(&msg_offset.to_le_bytes()); + expected.extend_from_slice(&msg_len.to_le_bytes()); + expected.push(0u8); // msg ix idx - // [header][offsets][signature][public_key][msg] - let mut expected = Vec::with_capacity(base + 64 + 32 + msg.len()); - expected.push(num_signatures); - expected.push(padding); - expected.extend_from_slice(&signature_offset.to_le_bytes()); - expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); - expected.extend_from_slice(&public_key_offset.to_le_bytes()); - expected.extend_from_slice(&public_key_instruction_index.to_le_bytes()); - expected.extend_from_slice(&message_data_offset.to_le_bytes()); - expected.extend_from_slice(&message_data_size.to_le_bytes()); - expected.extend_from_slice(&message_instruction_index.to_le_bytes()); expected.extend_from_slice(sig); expected.extend_from_slice(pubkey); expected.extend_from_slice(msg); - if ix.data.len() != expected.len() { - return Err(error!(ErrorCode::DataLengthMismatch)); - } - if ix.data != expected { - return Err(error!(ErrorCode::ConstraintRaw)); + if expected != ix.data { + return Err(ErrorCode::SignatureVerificationFailed.into()); } Ok(()) } diff --git a/lang/src/signature_verification/secp256k1.rs b/lang/src/signature_verification/secp256k1.rs index e2a502e306..ca8595076c 100644 --- a/lang/src/signature_verification/secp256k1.rs +++ b/lang/src/signature_verification/secp256k1.rs @@ -3,7 +3,6 @@ use crate::prelude::*; use crate::solana_program::instruction::Instruction; use solana_sdk_ids::secp256k1_program; -/// Verify that `ix.data` is a Secp256k1 syscall verifying `sig` over `msg` by `eth_address` with `recovery_id`. pub fn verify_secp256k1_ix( ix: &Instruction, eth_address: &[u8; 20], @@ -20,45 +19,34 @@ pub fn verify_secp256k1_ix( require!(recovery_id <= 1, ErrorCode::InvalidRecoveryId); require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); - let num_signatures: u8 = 1; - let num_eth_addresses: u8 = 1; - let header_len: usize = 2; - let offsets_len: usize = 18; // 9 * u16 - let base: usize = header_len + offsets_len; + const DATA_START: usize = 12; // 1 header + 11 offset bytes + let eth_len = eth_address.len() as u16; + let sig_len = sig.len() as u16; + let msg_len = msg.len() as u16; - let signature_offset: u16 = base as u16; - let signature_instruction_index: u16 = u16::MAX; - let eth_address_offset: u16 = (base + 64) as u16; - let eth_address_instruction_index: u16 = u16::MAX; - let message_data_offset: u16 = (base + 64 + 20) as u16; // 20 bytes for eth_address - let message_data_size: u16 = msg.len() as u16; - let message_instruction_index: u16 = u16::MAX; - let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16; - let recovery_id_instruction_index: u16 = u16::MAX; + let eth_offset: u16 = DATA_START as u16; + let sig_offset: u16 = eth_offset + eth_len; + let msg_offset: u16 = sig_offset + sig_len + 1; // +1 for recovery id + + let mut expected = + Vec::with_capacity(DATA_START + eth_address.len() + sig.len() + 1 + msg.len()); + + expected.push(1u8); // num signatures + expected.extend_from_slice(&sig_offset.to_le_bytes()); + expected.push(0u8); // sig ix idx + expected.extend_from_slice(ð_offset.to_le_bytes()); + expected.push(0u8); // eth ix idx + expected.extend_from_slice(&msg_offset.to_le_bytes()); + expected.extend_from_slice(&msg_len.to_le_bytes()); + expected.push(0u8); // msg ix idx - // [header][offsets][signature][eth_address][msg][recovery_id] - let mut expected = Vec::with_capacity(base + 64 + 20 + msg.len() + 1); - expected.push(num_signatures); - expected.push(num_eth_addresses); - expected.extend_from_slice(&signature_offset.to_le_bytes()); - expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); - expected.extend_from_slice(ð_address_offset.to_le_bytes()); - expected.extend_from_slice(ð_address_instruction_index.to_le_bytes()); - expected.extend_from_slice(&message_data_offset.to_le_bytes()); - expected.extend_from_slice(&message_data_size.to_le_bytes()); - expected.extend_from_slice(&message_instruction_index.to_le_bytes()); - expected.extend_from_slice(&recovery_id_offset.to_le_bytes()); - expected.extend_from_slice(&recovery_id_instruction_index.to_le_bytes()); - expected.extend_from_slice(sig); expected.extend_from_slice(eth_address); - expected.extend_from_slice(msg); + expected.extend_from_slice(sig); expected.push(recovery_id); + expected.extend_from_slice(msg); - if ix.data.len() != expected.len() { - return Err(error!(ErrorCode::DataLengthMismatch)); - } - if ix.data != expected { - return Err(error!(ErrorCode::ConstraintRaw)); + if expected != ix.data { + return Err(ErrorCode::SignatureVerificationFailed.into()); } Ok(()) } diff --git a/lang/tests/signature_verifications.rs b/lang/tests/signature_verifications.rs deleted file mode 100644 index fd08439d4b..0000000000 --- a/lang/tests/signature_verifications.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anchor_lang::signature_verification::{verify_ed25519_ix, verify_secp256k1_ix}; -use anchor_lang::solana_program::instruction::Instruction; -use solana_sdk_ids::{ed25519_program, secp256k1_program}; - -#[test] -fn test_verify_ed25519_matches() { - let pubkey = [7u8; 32]; - let msg = b"Testing #3944 with Ed25519 Signature".to_vec(); - let sig = [9u8; 64]; - - let num_signatures: u8 = 1; - let padding: u8 = 0; - let header_len: usize = 2; - let offsets_len: usize = 14; - let base: usize = header_len + offsets_len; - - let signature_offset: u16 = base as u16; - let signature_instruction_index: u16 = u16::MAX; - let public_key_offset: u16 = (base + 64) as u16; - let public_key_instruction_index: u16 = u16::MAX; - let message_data_offset: u16 = (base + 64 + 32) as u16; - let message_data_size: u16 = msg.len() as u16; - let message_instruction_index: u16 = u16::MAX; - - let mut data = Vec::with_capacity(base + 64 + 32 + msg.len()); - data.push(num_signatures); - data.push(padding); - data.extend_from_slice(&signature_offset.to_le_bytes()); - data.extend_from_slice(&signature_instruction_index.to_le_bytes()); - data.extend_from_slice(&public_key_offset.to_le_bytes()); - data.extend_from_slice(&public_key_instruction_index.to_le_bytes()); - data.extend_from_slice(&message_data_offset.to_le_bytes()); - data.extend_from_slice(&message_data_size.to_le_bytes()); - data.extend_from_slice(&message_instruction_index.to_le_bytes()); - data.extend_from_slice(&sig); - data.extend_from_slice(&pubkey); - data.extend_from_slice(&msg); - - let ix = Instruction { - program_id: ed25519_program::id(), - accounts: vec![], - data, - }; - assert!(verify_ed25519_ix(&ix, &pubkey, &msg, &sig).is_ok()); -} - -#[test] -fn test_verify_ed25519_mismatch() { - let pubkey = [7u8; 32]; - let msg = b"Testing #3944 with Ed25519 Signature".to_vec(); - let sig = [9u8; 64]; - - let data = vec![0u8; 10]; - let ix = Instruction { - program_id: ed25519_program::id(), - accounts: vec![], - data, - }; - assert!(verify_ed25519_ix(&ix, &pubkey, &msg, &sig).is_err()); -} - -#[test] -fn test_verify_secp256k1_matches() { - let eth_address = [0x11u8; 20]; - let msg = b"Testing #3944 with Secp256k1 Signature".to_vec(); - let sig = [0x22u8; 64]; - let recovery_id: u8 = 1; - - let num_signatures: u8 = 1; - let num_eth_addresses: u8 = 1; - let header_len: usize = 2; - let offsets_len: usize = 18; - let base: usize = header_len + offsets_len; - - let signature_offset: u16 = base as u16; - let signature_instruction_index: u16 = u16::MAX; - let eth_address_offset: u16 = (base + 64) as u16; - let eth_address_instruction_index: u16 = u16::MAX; - let message_data_offset: u16 = (base + 64 + 20) as u16; - let message_data_size: u16 = msg.len() as u16; - let message_instruction_index: u16 = u16::MAX; - let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16; - let recovery_id_instruction_index: u16 = u16::MAX; - - let mut data = Vec::with_capacity(base + 64 + 20 + msg.len() + 1); - data.push(num_signatures); - data.push(num_eth_addresses); - data.extend_from_slice(&signature_offset.to_le_bytes()); - data.extend_from_slice(&signature_instruction_index.to_le_bytes()); - data.extend_from_slice(ð_address_offset.to_le_bytes()); - data.extend_from_slice(ð_address_instruction_index.to_le_bytes()); - data.extend_from_slice(&message_data_offset.to_le_bytes()); - data.extend_from_slice(&message_data_size.to_le_bytes()); - data.extend_from_slice(&message_instruction_index.to_le_bytes()); - data.extend_from_slice(&recovery_id_offset.to_le_bytes()); - data.extend_from_slice(&recovery_id_instruction_index.to_le_bytes()); - data.extend_from_slice(&sig); - data.extend_from_slice(ð_address); - data.extend_from_slice(&msg); - data.push(recovery_id); - - let ix = Instruction { - program_id: secp256k1_program::id(), - accounts: vec![], - data, - }; - assert!(verify_secp256k1_ix(&ix, ð_address, &msg, &sig, recovery_id).is_ok()); -} - -#[test] -fn test_verify_secp256k1_mismatch() { - let eth_address = [0x11u8; 20]; - let msg = b"Testing #3944 with Secp256k1 Signature".to_vec(); - let sig = [0x22u8; 64]; - let recovery_id: u8 = 1; - - let ix = Instruction { - program_id: secp256k1_program::id(), - accounts: vec![], - data: vec![1, 2, 3], - }; - assert!(verify_secp256k1_ix(&ix, ð_address, &msg, &sig, recovery_id).is_err()); -} diff --git a/tests/signature-verification/Anchor.toml b/tests/signature-verification/Anchor.toml index d327a0e4bb..608470da46 100644 --- a/tests/signature-verification/Anchor.toml +++ b/tests/signature-verification/Anchor.toml @@ -2,8 +2,13 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" +[features] +seeds = false +resolution = true +skip-lint = false + [programs.localnet] -signature_verification_test = "9q9StGMtVHQtz14vH8YhPtED9MxnBj2mnVpBhYeTeRhj" +signature_verification_test = "9P8zSbNRQkwDrjCmqsHHcU1GTk5npaKYgKHroAkupbLG" [scripts] -test = "yarn run ts-mocha -t 1000000 tests/*.ts" +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.ts" diff --git a/tests/signature-verification/package.json b/tests/signature-verification/package.json index 0a24d7e233..902716905b 100644 --- a/tests/signature-verification/package.json +++ b/tests/signature-verification/package.json @@ -15,5 +15,8 @@ }, "scripts": { "test": "anchor test" + }, + "dependencies": { + "ethers": "^5.7.2" } } diff --git a/tests/signature-verification/programs/signature-verification-test/src/lib.rs b/tests/signature-verification/programs/signature-verification-test/src/lib.rs index e162f49b83..4ac9b5c54a 100644 --- a/tests/signature-verification/programs/signature-verification-test/src/lib.rs +++ b/tests/signature-verification/programs/signature-verification-test/src/lib.rs @@ -3,7 +3,7 @@ use anchor_lang::signature_verification::{ load_instruction, verify_ed25519_ix, verify_secp256k1_ix, }; -declare_id!("9q9StGMtVHQtz14vH8YhPtED9MxnBj2mnVpBhYeTeRhj"); +declare_id!("9P8zSbNRQkwDrjCmqsHHcU1GTk5npaKYgKHroAkupbLG"); #[program] pub mod signature_verification_test { @@ -23,25 +23,20 @@ pub mod signature_verification_test { )?; msg!("Ed25519 signature verified successfully using custom helper!"); - msg!("Signer: {}", ctx.accounts.signer.key()); - msg!("Message length: {}", message.len()); - Ok(()) } - pub fn verify_secp256k1_signature( + pub fn verify_secp( ctx: Context, - message_hash: [u8; 32], + message: Vec, signature: [u8; 64], recovery_id: u8, eth_address: [u8; 20], ) -> Result<()> { let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?; - verify_secp256k1_ix(&ix, ð_address, &message_hash, &signature, recovery_id)?; + verify_secp256k1_ix(&ix, ð_address, &message, &signature, recovery_id)?; msg!("Secp256k1 signature verified successfully using custom helper!"); - msg!("Eth address: {:?}", eth_address); - msg!("Message hash: {:?}", message_hash); Ok(()) } @@ -49,7 +44,7 @@ pub mod signature_verification_test { #[derive(Accounts)] pub struct VerifyEd25519Signature<'info> { - /// CHECK: This account represents the signer's public key + /// CHECK: Signer account pub signer: AccountInfo<'info>, /// CHECK: Instructions sysvar account #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] @@ -62,9 +57,3 @@ pub struct VerifySecp256k1Signature<'info> { #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] pub ix_sysvar: AccountInfo<'info>, } - -#[error_code] -pub enum ErrorCode { - #[msg("Signature verification failed")] - SignatureVerificationFailed, -} diff --git a/tests/signature-verification/tests/signature-verification-test.ts b/tests/signature-verification/tests/signature-verification-test.ts index 5050e8a1fb..e3aab1a436 100644 --- a/tests/signature-verification/tests/signature-verification-test.ts +++ b/tests/signature-verification/tests/signature-verification-test.ts @@ -1,99 +1,372 @@ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; -import { SignatureVerificationTest } from "../target/types/signature_verification_test"; +import * as fs from "fs"; +const signatureVerificationTestIDL = JSON.parse( + fs.readFileSync("./target/idl/signature_verification_test.json", "utf8"), +); import { Buffer } from "buffer"; -import { PublicKey, Keypair, Transaction, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, Ed25519Program, Secp256k1Program } from "@solana/web3.js"; +import { + PublicKey, + Keypair, + Transaction, + SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, + Ed25519Program, + Secp256k1Program, +} from "@solana/web3.js"; import * as crypto from "crypto"; +import { ethers } from "ethers"; +import * as assert from "assert"; +import { sign } from "@noble/ed25519"; describe("signature-verification-test", () => { - const provider = anchor.AnchorProvider.local(); + const provider = anchor.AnchorProvider.local(undefined, { + commitment: `confirmed`, + }); + anchor.setProvider(provider); - const program = anchor.workspace.SignatureVerificationTest as Program; + const program = new anchor.Program( + signatureVerificationTestIDL as anchor.Idl, + provider, + ); - it("Verify Ed25519 signature with actual signature", async () => { - // Create a keypair for testing + it("Verify Ed25519 signature with valid signature", async () => { const signer = Keypair.generate(); + const message = Buffer.from( + "Hello, Anchor Signature Verification Test with valid signature!", + ); + const signature = await sign(message, signer.secretKey.slice(0, 32)); - // Create a test message - const message = Buffer.from("Hello, Anchor Signature Verification Test!"); + // Create transaction with just the Ed25519Program instruction + const ed25519Instruction = Ed25519Program.createInstructionWithPublicKey({ + publicKey: signer.publicKey.toBytes(), + message: message, + signature: signature, + }); - // Create a mock signature (in real implementation, this would be properly signed) - const signature = new Uint8Array(64).fill(1); // Mock signature + const transaction = new Transaction().add(ed25519Instruction); - // Create instruction to call the program - const instruction = await program.methods - .verifyEd25519Signature(message, Array.from(signature) as [number, ...number[]]) - .accounts({ - signer: signer.publicKey, - ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, - }) - .instruction(); + try { + await provider.sendAndConfirm(transaction, []); + console.log("Ed25519 signature verified successfully!"); + } catch (error) { + assert.fail("Valid Ed25519 signature should be verified"); + } + }); - // Create transaction with the signature verification instruction - const transaction = new Transaction().add(instruction); + it("Verify Ed25519 signature with invalid signature", async () => { + const signer = Keypair.generate(); + const message = Buffer.from( + "Hello, Anchor Signature Verification Test with invalid signature!", + ); + // Create a fake signature (all zeros) + const fakeSignature = new Uint8Array(64).fill(0); - // Add the Ed25519 signature instruction to the transaction + // Create transaction with just the Ed25519Program instruction const ed25519Instruction = Ed25519Program.createInstructionWithPublicKey({ publicKey: signer.publicKey.toBytes(), message: message, - signature: signature, + signature: fakeSignature, }); - transaction.add(ed25519Instruction); + const transaction = new Transaction().add(ed25519Instruction); + // This should fail try { - await provider.sendAndConfirm(transaction, [signer]); - console.log("✅ Ed25519 signature verified successfully using custom helper!"); + await provider.sendAndConfirm(transaction, []); + assert.fail("Invalid Signature of Ed25519 should not be verified"); } catch (error) { - console.log("❌ Ed25519 verification failed:", error.message); - // This test demonstrates the structure even if signature verification fails + console.log("Invalid Signature of Ed25519 is not verified"); } }); - it("Verify Secp256k1 signature with actual signature", async () => { - // Create ETH address (20 bytes) - const ethAddress = crypto.randomBytes(20); + it("Verify Ethereum Secp256k1 signature with valid signature", async () => { + const ethSigner: ethers.Wallet = ethers.Wallet.createRandom(); + const PERSON = { name: "ben", age: 49 }; + + // keccak256(name, age) + const messageHashHex: string = ethers.utils.solidityKeccak256( + ["string", "uint16"], + [PERSON.name, PERSON.age], + ); + const messageHashBytes: Uint8Array = ethers.utils.arrayify(messageHashHex); - // Create a test message hash (32 bytes) - const messageHash = crypto.randomBytes(32); + // Sign with Ethereum prefix + const fullSig: string = await ethSigner.signMessage(messageHashBytes); + const fullSigBytes = ethers.utils.arrayify(fullSig); + const signature = fullSigBytes.slice(0, 64); + const recoveryId = fullSigBytes[64] - 27; - // Create a mock signature - const signature = new Uint8Array(64).fill(2); - const recoveryId = 0; + const actualMessage = Buffer.concat([ + Buffer.from("\x19Ethereum Signed Message:\n32"), + Buffer.from(messageHashBytes), + ]); - // Create instruction to call the program - const instruction = await program.methods - .verifySecp256k1Signature( - Array.from(messageHash) as [number, ...number[]], - Array.from(signature) as [number, ...number[]], + // 20-byte ETH address (hex without 0x) + const ethAddressHexNo0x = ethers.utils + .computeAddress(ethSigner.publicKey) + .slice(2); + const ethAddressBytes = Array.from( + ethers.utils.arrayify("0x" + ethAddressHexNo0x), + ) as [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ]; + + const verifyIx = await program.methods + .verifySecp( + actualMessage, + Array.from(signature) as [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ], recoveryId, - Array.from(ethAddress) as [number, ...number[]] + ethAddressBytes, ) .accounts({ ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, }) .instruction(); - // Create transaction with the signature verification instruction - const transaction = new Transaction().add(instruction); + // Secp precompile verification against ETH address + const secpIx = Secp256k1Program.createInstructionWithEthAddress({ + ethAddress: ethAddressHexNo0x, + message: actualMessage, + signature: Uint8Array.from(signature), + recoveryId, + }); - // Add the Secp256k1 signature instruction to the transaction - const secp256k1Instruction = Secp256k1Program.createInstructionWithPublicKey({ - publicKey: Buffer.from(ethAddress), // Use ETH address as public key - message: messageHash, - signature: signature, - recoveryId: recoveryId, + const tx = new Transaction().add(secpIx).add(verifyIx); + // This should succeed + try { + await provider.sendAndConfirm(tx, []); + console.log("Ethereum Secp256k1 signature verified successfully!"); + } catch (error) { + assert.fail("Valid Signature of Ethereum Secp256k1 should be verified"); + } + }); + + it("Verify Ethereum Secp256k1 signature with invalid signature", async () => { + const ethSigner: ethers.Wallet = ethers.Wallet.createRandom(); + const PERSON = { name: "ben", age: 49 }; + + // keccak256(name, age) + const messageHashHex: string = ethers.utils.solidityKeccak256( + ["string", "uint16"], + [PERSON.name, PERSON.age], + ); + const messageHashBytes: Uint8Array = ethers.utils.arrayify(messageHashHex); + + // Create a fake signature (all zeros) + const fakeSignature = new Uint8Array(64).fill(0); + const fakeRecoveryId = 0; + + const actualMessage = Buffer.concat([ + Buffer.from("\x19Ethereum Signed Message:\n32"), + Buffer.from(messageHashBytes), + ]); + + const ethAddressHexNo0x = ethers.utils + .computeAddress(ethSigner.publicKey) + .slice(2); + const ethAddressBytes = Array.from( + ethers.utils.arrayify("0x" + ethAddressHexNo0x), + ) as [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ]; + + const verifyIx = await program.methods + .verifySecp( + actualMessage, + Array.from(fakeSignature) as [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ], + fakeRecoveryId, + ethAddressBytes, + ) + .accounts({ + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction(); + const secpIx = Secp256k1Program.createInstructionWithEthAddress({ + ethAddress: ethAddressHexNo0x, + message: actualMessage, + signature: fakeSignature, + recoveryId: fakeRecoveryId, }); - transaction.add(secp256k1Instruction); + const tx = new Transaction().add(secpIx).add(verifyIx); + // This should fail try { - await provider.sendAndConfirm(transaction, []); - console.log("✅ Secp256k1 signature verified successfully using custom helper!"); + await provider.sendAndConfirm(tx, []); + assert.fail("Expected transaction to fail with invalid signature"); } catch (error) { - console.log("❌ Secp256k1 verification failed:", error.message); - // This test demonstrates the structure even if signature verification fails + console.log( + "Ethereum Secp256k1 verification correctly failed with invalid signature", + ); } }); - -}); \ No newline at end of file +}); From 7fb9709971e621ac424ba91608eb94cbb73097ef Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Tue, 23 Sep 2025 11:41:24 +0530 Subject: [PATCH 07/12] fix: yarn lint --- .../tests/signature-verification-test.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/signature-verification/tests/signature-verification-test.ts b/tests/signature-verification/tests/signature-verification-test.ts index e3aab1a436..a0bf85f0e0 100644 --- a/tests/signature-verification/tests/signature-verification-test.ts +++ b/tests/signature-verification/tests/signature-verification-test.ts @@ -2,7 +2,7 @@ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import * as fs from "fs"; const signatureVerificationTestIDL = JSON.parse( - fs.readFileSync("./target/idl/signature_verification_test.json", "utf8"), + fs.readFileSync("./target/idl/signature_verification_test.json", "utf8") ); import { Buffer } from "buffer"; import { @@ -27,13 +27,13 @@ describe("signature-verification-test", () => { anchor.setProvider(provider); const program = new anchor.Program( signatureVerificationTestIDL as anchor.Idl, - provider, + provider ); it("Verify Ed25519 signature with valid signature", async () => { const signer = Keypair.generate(); const message = Buffer.from( - "Hello, Anchor Signature Verification Test with valid signature!", + "Hello, Anchor Signature Verification Test with valid signature!" ); const signature = await sign(message, signer.secretKey.slice(0, 32)); @@ -57,7 +57,7 @@ describe("signature-verification-test", () => { it("Verify Ed25519 signature with invalid signature", async () => { const signer = Keypair.generate(); const message = Buffer.from( - "Hello, Anchor Signature Verification Test with invalid signature!", + "Hello, Anchor Signature Verification Test with invalid signature!" ); // Create a fake signature (all zeros) const fakeSignature = new Uint8Array(64).fill(0); @@ -87,7 +87,7 @@ describe("signature-verification-test", () => { // keccak256(name, age) const messageHashHex: string = ethers.utils.solidityKeccak256( ["string", "uint16"], - [PERSON.name, PERSON.age], + [PERSON.name, PERSON.age] ); const messageHashBytes: Uint8Array = ethers.utils.arrayify(messageHashHex); @@ -107,7 +107,7 @@ describe("signature-verification-test", () => { .computeAddress(ethSigner.publicKey) .slice(2); const ethAddressBytes = Array.from( - ethers.utils.arrayify("0x" + ethAddressHexNo0x), + ethers.utils.arrayify("0x" + ethAddressHexNo0x) ) as [ number, number, @@ -128,7 +128,7 @@ describe("signature-verification-test", () => { number, number, number, - number, + number ]; const verifyIx = await program.methods @@ -198,10 +198,10 @@ describe("signature-verification-test", () => { number, number, number, - number, + number ], recoveryId, - ethAddressBytes, + ethAddressBytes ) .accounts({ ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, @@ -233,7 +233,7 @@ describe("signature-verification-test", () => { // keccak256(name, age) const messageHashHex: string = ethers.utils.solidityKeccak256( ["string", "uint16"], - [PERSON.name, PERSON.age], + [PERSON.name, PERSON.age] ); const messageHashBytes: Uint8Array = ethers.utils.arrayify(messageHashHex); @@ -250,7 +250,7 @@ describe("signature-verification-test", () => { .computeAddress(ethSigner.publicKey) .slice(2); const ethAddressBytes = Array.from( - ethers.utils.arrayify("0x" + ethAddressHexNo0x), + ethers.utils.arrayify("0x" + ethAddressHexNo0x) ) as [ number, number, @@ -271,7 +271,7 @@ describe("signature-verification-test", () => { number, number, number, - number, + number ]; const verifyIx = await program.methods @@ -341,10 +341,10 @@ describe("signature-verification-test", () => { number, number, number, - number, + number ], fakeRecoveryId, - ethAddressBytes, + ethAddressBytes ) .accounts({ ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, @@ -365,7 +365,7 @@ describe("signature-verification-test", () => { assert.fail("Expected transaction to fail with invalid signature"); } catch (error) { console.log( - "Ethereum Secp256k1 verification correctly failed with invalid signature", + "Ethereum Secp256k1 verification correctly failed with invalid signature" ); } }); From 84efe872946b157f24d6f935e39235b4efd56141 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Wed, 24 Sep 2025 08:33:41 +0530 Subject: [PATCH 08/12] fix: review comments --- .github/workflows/reusable-tests.yaml | 2 +- lang/src/signature_verifications/ed25519.rs | 57 ----------------- lang/src/signature_verifications/mod.rs | 16 ----- lang/src/signature_verifications/secp256k1.rs | 64 ------------------- tests/signature-verification/package.json | 2 +- 5 files changed, 2 insertions(+), 139 deletions(-) delete mode 100644 lang/src/signature_verifications/ed25519.rs delete mode 100644 lang/src/signature_verifications/mod.rs delete mode 100644 lang/src/signature_verifications/secp256k1.rs diff --git a/.github/workflows/reusable-tests.yaml b/.github/workflows/reusable-tests.yaml index 7b08ec2045..02d869e8e6 100644 --- a/.github/workflows/reusable-tests.yaml +++ b/.github/workflows/reusable-tests.yaml @@ -462,7 +462,7 @@ jobs: path: tests/idl - cmd: cd tests/lazy-account && anchor test path: tests/lazy-account - - cmd: cd tests/signature-verification && anchor test --skip-lint + - cmd: cd tests/signature-verification && anchor test path: tests/signature-verification steps: - uses: actions/checkout@v3 diff --git a/lang/src/signature_verifications/ed25519.rs b/lang/src/signature_verifications/ed25519.rs deleted file mode 100644 index 78448c3376..0000000000 --- a/lang/src/signature_verifications/ed25519.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::error::ErrorCode; -use crate::prelude::*; -use crate::solana_program::instruction::Instruction; -use solana_sdk_ids::ed25519_program; - -/// Verify that `ix.data` is an Ed25519 syscall verifying `sig` over `msg` by `pubkey`. -pub fn verify_ed25519_ix( - ix: &Instruction, - pubkey: &[u8; 32], - msg: &[u8], - sig: &[u8; 64], -) -> Result<()> { - require_keys_eq!( - ix.program_id, - ed25519_program::id(), - ErrorCode::Ed25519InvalidProgram - ); - require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); - require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); - - let num_signatures: u8 = 1; - let padding: u8 = 0; - let header_len: usize = 2; - let offsets_len: usize = 14; // 7 * u16 - let base: usize = header_len + offsets_len; - - let signature_offset: u16 = base as u16; - let signature_instruction_index: u16 = u16::MAX; - let public_key_offset: u16 = (base + 64) as u16; - let public_key_instruction_index: u16 = u16::MAX; - let message_data_offset: u16 = (base + 64 + 32) as u16; // 32 bytes for pubkey - let message_data_size: u16 = msg.len() as u16; - let message_instruction_index: u16 = u16::MAX; - - // [header][offsets][signature][public_key][msg] - let mut expected = Vec::with_capacity(base + 64 + 32 + msg.len()); - expected.push(num_signatures); - expected.push(padding); - expected.extend_from_slice(&signature_offset.to_le_bytes()); - expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); - expected.extend_from_slice(&public_key_offset.to_le_bytes()); - expected.extend_from_slice(&public_key_instruction_index.to_le_bytes()); - expected.extend_from_slice(&message_data_offset.to_le_bytes()); - expected.extend_from_slice(&message_data_size.to_le_bytes()); - expected.extend_from_slice(&message_instruction_index.to_le_bytes()); - expected.extend_from_slice(sig); - expected.extend_from_slice(pubkey); - expected.extend_from_slice(msg); - - if ix.data.len() != expected.len() { - return Err(error!(ErrorCode::DataLengthMismatch)); - } - if ix.data != expected { - return Err(error!(ErrorCode::ConstraintRaw)); - } - Ok(()) -} diff --git a/lang/src/signature_verifications/mod.rs b/lang/src/signature_verifications/mod.rs deleted file mode 100644 index b6d6544a1c..0000000000 --- a/lang/src/signature_verifications/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::prelude::*; -use crate::solana_program::instruction::Instruction; -use crate::solana_program::sysvar::instructions::load_instruction_at_checked; - -mod ed25519; -mod secp256k1; - -pub use ed25519::verify_ed25519_ix; -pub use secp256k1::verify_secp256k1_ix; - -/// Load an instruction from the Instructions sysvar at the given index. -pub fn load_instruction(index: usize, ix_sysvar: &AccountInfo<'_>) -> Result { - let ix = load_instruction_at_checked(index, ix_sysvar) - .map_err(|_| error!(error::ErrorCode::ConstraintRaw))?; - Ok(ix) -} diff --git a/lang/src/signature_verifications/secp256k1.rs b/lang/src/signature_verifications/secp256k1.rs deleted file mode 100644 index e2a502e306..0000000000 --- a/lang/src/signature_verifications/secp256k1.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::error::ErrorCode; -use crate::prelude::*; -use crate::solana_program::instruction::Instruction; -use solana_sdk_ids::secp256k1_program; - -/// Verify that `ix.data` is a Secp256k1 syscall verifying `sig` over `msg` by `eth_address` with `recovery_id`. -pub fn verify_secp256k1_ix( - ix: &Instruction, - eth_address: &[u8; 20], - msg: &[u8], - sig: &[u8; 64], - recovery_id: u8, -) -> Result<()> { - require_keys_eq!( - ix.program_id, - secp256k1_program::id(), - ErrorCode::Secp256k1InvalidProgram - ); - require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts); - require!(recovery_id <= 1, ErrorCode::InvalidRecoveryId); - require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong); - - let num_signatures: u8 = 1; - let num_eth_addresses: u8 = 1; - let header_len: usize = 2; - let offsets_len: usize = 18; // 9 * u16 - let base: usize = header_len + offsets_len; - - let signature_offset: u16 = base as u16; - let signature_instruction_index: u16 = u16::MAX; - let eth_address_offset: u16 = (base + 64) as u16; - let eth_address_instruction_index: u16 = u16::MAX; - let message_data_offset: u16 = (base + 64 + 20) as u16; // 20 bytes for eth_address - let message_data_size: u16 = msg.len() as u16; - let message_instruction_index: u16 = u16::MAX; - let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16; - let recovery_id_instruction_index: u16 = u16::MAX; - - // [header][offsets][signature][eth_address][msg][recovery_id] - let mut expected = Vec::with_capacity(base + 64 + 20 + msg.len() + 1); - expected.push(num_signatures); - expected.push(num_eth_addresses); - expected.extend_from_slice(&signature_offset.to_le_bytes()); - expected.extend_from_slice(&signature_instruction_index.to_le_bytes()); - expected.extend_from_slice(ð_address_offset.to_le_bytes()); - expected.extend_from_slice(ð_address_instruction_index.to_le_bytes()); - expected.extend_from_slice(&message_data_offset.to_le_bytes()); - expected.extend_from_slice(&message_data_size.to_le_bytes()); - expected.extend_from_slice(&message_instruction_index.to_le_bytes()); - expected.extend_from_slice(&recovery_id_offset.to_le_bytes()); - expected.extend_from_slice(&recovery_id_instruction_index.to_le_bytes()); - expected.extend_from_slice(sig); - expected.extend_from_slice(eth_address); - expected.extend_from_slice(msg); - expected.push(recovery_id); - - if ix.data.len() != expected.len() { - return Err(error!(ErrorCode::DataLengthMismatch)); - } - if ix.data != expected { - return Err(error!(ErrorCode::ConstraintRaw)); - } - Ok(()) -} diff --git a/tests/signature-verification/package.json b/tests/signature-verification/package.json index 902716905b..3fd5b5db57 100644 --- a/tests/signature-verification/package.json +++ b/tests/signature-verification/package.json @@ -1,5 +1,5 @@ { - "name": "signature-verifications", + "name": "signature-verification", "version": "0.31.1", "license": "(MIT OR Apache-2.0)", "homepage": "https://github.com/coral-xyz/anchor#readme", From 4a5903a6d993798337312663ac1055387d4250c2 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Fri, 3 Oct 2025 18:13:52 +0530 Subject: [PATCH 09/12] fix: ref ix data itself for indecies --- lang/src/signature_verification/ed25519.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/src/signature_verification/ed25519.rs b/lang/src/signature_verification/ed25519.rs index 9dc73cd14e..b5cd7047a1 100644 --- a/lang/src/signature_verification/ed25519.rs +++ b/lang/src/signature_verification/ed25519.rs @@ -31,12 +31,12 @@ pub fn verify_ed25519_ix( expected.push(1u8); // num signatures expected.push(0u8); // padding expected.extend_from_slice(&sig_offset.to_le_bytes()); - expected.push(0u8); // sig ix idx + expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); expected.extend_from_slice(&pubkey_offset.to_le_bytes()); - expected.push(0u8); // pubkey ix idx + expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); expected.extend_from_slice(&msg_offset.to_le_bytes()); expected.extend_from_slice(&msg_len.to_le_bytes()); - expected.push(0u8); // msg ix idx + expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); expected.extend_from_slice(sig); expected.extend_from_slice(pubkey); From 818104576e71d86b6b8f16d0c60f0c9b12f1f868 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Fri, 7 Nov 2025 08:53:55 +0530 Subject: [PATCH 10/12] cargo fmt -- --check --- lang/src/signature_verification/ed25519.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/src/signature_verification/ed25519.rs b/lang/src/signature_verification/ed25519.rs index b5cd7047a1..962697f68d 100644 --- a/lang/src/signature_verification/ed25519.rs +++ b/lang/src/signature_verification/ed25519.rs @@ -31,12 +31,12 @@ pub fn verify_ed25519_ix( expected.push(1u8); // num signatures expected.push(0u8); // padding expected.extend_from_slice(&sig_offset.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); + expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); expected.extend_from_slice(&pubkey_offset.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); + expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); expected.extend_from_slice(&msg_offset.to_le_bytes()); expected.extend_from_slice(&msg_len.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); + expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); expected.extend_from_slice(sig); expected.extend_from_slice(pubkey); From 7fcb8fd120742713098da70e80d71735ce337bd4 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Fri, 7 Nov 2025 09:07:49 +0530 Subject: [PATCH 11/12] cargo clippy --- lang/src/signature_verification/ed25519.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/src/signature_verification/ed25519.rs b/lang/src/signature_verification/ed25519.rs index 962697f68d..fa865551a3 100644 --- a/lang/src/signature_verification/ed25519.rs +++ b/lang/src/signature_verification/ed25519.rs @@ -31,12 +31,12 @@ pub fn verify_ed25519_ix( expected.push(1u8); // num signatures expected.push(0u8); // padding expected.extend_from_slice(&sig_offset.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); + expected.extend_from_slice(&(u16::MAX).to_le_bytes()); expected.extend_from_slice(&pubkey_offset.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); + expected.extend_from_slice(&(u16::MAX).to_le_bytes()); expected.extend_from_slice(&msg_offset.to_le_bytes()); expected.extend_from_slice(&msg_len.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX as u16).to_le_bytes()); + expected.extend_from_slice(&(u16::MAX).to_le_bytes()); expected.extend_from_slice(sig); expected.extend_from_slice(pubkey); From c122c3f58d7398a4328e32d3e1447bb6b99cc6f6 Mon Sep 17 00:00:00 2001 From: Akash Thota Date: Fri, 14 Nov 2025 22:18:57 +0530 Subject: [PATCH 12/12] chore(feat): requseted changes for seperate current index and ix params functions --- lang/src/signature_verification/ed25519.rs | 20 ++++++++-- lang/src/signature_verification/mod.rs | 40 +++++++++++++++++-- lang/src/signature_verification/secp256k1.rs | 22 ++++++++-- .../signature-verification-test/src/lib.rs | 15 +++++-- 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/lang/src/signature_verification/ed25519.rs b/lang/src/signature_verification/ed25519.rs index fa865551a3..17a11d5baf 100644 --- a/lang/src/signature_verification/ed25519.rs +++ b/lang/src/signature_verification/ed25519.rs @@ -3,11 +3,25 @@ use crate::prelude::*; use crate::solana_program::instruction::Instruction; use solana_sdk_ids::ed25519_program; +/// Verifies an Ed25519 signature instruction assuming the signature, public key, +/// and message bytes are embedded directly inside the instruction data (Solana's +/// default encoding). Prefer [`verify_ed25519_ix_with_instruction_index`] when +/// working with custom instructions that point at external instruction data. pub fn verify_ed25519_ix( ix: &Instruction, pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64], +) -> Result<()> { + verify_ed25519_ix_with_instruction_index(ix, u16::MAX, pubkey, msg, sig) +} + +pub fn verify_ed25519_ix_with_instruction_index( + ix: &Instruction, + instruction_index: u16, + pubkey: &[u8; 32], + msg: &[u8], + sig: &[u8; 64], ) -> Result<()> { require_keys_eq!( ix.program_id, @@ -31,12 +45,12 @@ pub fn verify_ed25519_ix( expected.push(1u8); // num signatures expected.push(0u8); // padding expected.extend_from_slice(&sig_offset.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX).to_le_bytes()); + expected.extend_from_slice(&instruction_index.to_le_bytes()); expected.extend_from_slice(&pubkey_offset.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX).to_le_bytes()); + expected.extend_from_slice(&instruction_index.to_le_bytes()); expected.extend_from_slice(&msg_offset.to_le_bytes()); expected.extend_from_slice(&msg_len.to_le_bytes()); - expected.extend_from_slice(&(u16::MAX).to_le_bytes()); + expected.extend_from_slice(&instruction_index.to_le_bytes()); expected.extend_from_slice(sig); expected.extend_from_slice(pubkey); diff --git a/lang/src/signature_verification/mod.rs b/lang/src/signature_verification/mod.rs index b6d6544a1c..cd2a9d8d17 100644 --- a/lang/src/signature_verification/mod.rs +++ b/lang/src/signature_verification/mod.rs @@ -1,12 +1,15 @@ use crate::prelude::*; use crate::solana_program::instruction::Instruction; -use crate::solana_program::sysvar::instructions::load_instruction_at_checked; +use crate::solana_program::sysvar::instructions::{ + load_current_index_checked, load_instruction_at_checked, +}; +use core::convert::TryFrom; mod ed25519; mod secp256k1; -pub use ed25519::verify_ed25519_ix; -pub use secp256k1::verify_secp256k1_ix; +pub use ed25519::{verify_ed25519_ix, verify_ed25519_ix_with_instruction_index}; +pub use secp256k1::{verify_secp256k1_ix, verify_secp256k1_ix_with_instruction_index}; /// Load an instruction from the Instructions sysvar at the given index. pub fn load_instruction(index: usize, ix_sysvar: &AccountInfo<'_>) -> Result { @@ -14,3 +17,34 @@ pub fn load_instruction(index: usize, ix_sysvar: &AccountInfo<'_>) -> Result, + pubkey: &[u8; 32], + msg: &[u8], + sig: &[u8; 64], +) -> Result<()> { + let idx = load_current_index_checked(ix_sysvar) + .map_err(|_| error!(error::ErrorCode::ConstraintRaw))?; + let ix = load_instruction(idx as usize, ix_sysvar)?; + verify_ed25519_ix_with_instruction_index(&ix, idx, pubkey, msg, sig) +} + +/// Loads the instruction currently executing in this transaction and verifies it +/// as a Secp256k1 signature instruction. +pub fn verify_current_secp256k1_instruction( + ix_sysvar: &AccountInfo<'_>, + eth_address: &[u8; 20], + msg: &[u8], + sig: &[u8; 64], + recovery_id: u8, +) -> Result<()> { + let idx_u16 = load_current_index_checked(ix_sysvar) + .map_err(|_| error!(error::ErrorCode::ConstraintRaw))?; + let idx_u8 = + u8::try_from(idx_u16).map_err(|_| error!(error::ErrorCode::InvalidNumericConversion))?; + let ix = load_instruction(idx_u16 as usize, ix_sysvar)?; + verify_secp256k1_ix_with_instruction_index(&ix, idx_u8, eth_address, msg, sig, recovery_id) +} diff --git a/lang/src/signature_verification/secp256k1.rs b/lang/src/signature_verification/secp256k1.rs index ca8595076c..8189f81e80 100644 --- a/lang/src/signature_verification/secp256k1.rs +++ b/lang/src/signature_verification/secp256k1.rs @@ -3,12 +3,28 @@ use crate::prelude::*; use crate::solana_program::instruction::Instruction; use solana_sdk_ids::secp256k1_program; +/// Verifies a Secp256k1 instruction created under the assumption that the +/// signature, address, and message bytes all live inside the same instruction +/// (i.e. the signature ix is placed at index `0`). Prefer +/// [`verify_secp256k1_ix_with_instruction_index`] and pass the actual signature +/// instruction index instead of relying on this default. pub fn verify_secp256k1_ix( ix: &Instruction, eth_address: &[u8; 20], msg: &[u8], sig: &[u8; 64], recovery_id: u8, +) -> Result<()> { + verify_secp256k1_ix_with_instruction_index(ix, 0, eth_address, msg, sig, recovery_id) +} + +pub fn verify_secp256k1_ix_with_instruction_index( + ix: &Instruction, + instruction_index: u8, + eth_address: &[u8; 20], + msg: &[u8], + sig: &[u8; 64], + recovery_id: u8, ) -> Result<()> { require_keys_eq!( ix.program_id, @@ -33,12 +49,12 @@ pub fn verify_secp256k1_ix( expected.push(1u8); // num signatures expected.extend_from_slice(&sig_offset.to_le_bytes()); - expected.push(0u8); // sig ix idx + expected.push(instruction_index); // sig ix idx expected.extend_from_slice(ð_offset.to_le_bytes()); - expected.push(0u8); // eth ix idx + expected.push(instruction_index); // eth ix idx expected.extend_from_slice(&msg_offset.to_le_bytes()); expected.extend_from_slice(&msg_len.to_le_bytes()); - expected.push(0u8); // msg ix idx + expected.push(instruction_index); // msg ix idx expected.extend_from_slice(eth_address); expected.extend_from_slice(sig); diff --git a/tests/signature-verification/programs/signature-verification-test/src/lib.rs b/tests/signature-verification/programs/signature-verification-test/src/lib.rs index 4ac9b5c54a..51265b9172 100644 --- a/tests/signature-verification/programs/signature-verification-test/src/lib.rs +++ b/tests/signature-verification/programs/signature-verification-test/src/lib.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_lang::signature_verification::{ - load_instruction, verify_ed25519_ix, verify_secp256k1_ix, + load_instruction, verify_ed25519_ix_with_instruction_index, + verify_secp256k1_ix_with_instruction_index, }; declare_id!("9P8zSbNRQkwDrjCmqsHHcU1GTk5npaKYgKHroAkupbLG"); @@ -15,8 +16,9 @@ pub mod signature_verification_test { signature: [u8; 64], ) -> Result<()> { let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?; - verify_ed25519_ix( + verify_ed25519_ix_with_instruction_index( &ix, + u16::MAX, &ctx.accounts.signer.key().to_bytes(), &message, &signature, @@ -34,7 +36,14 @@ pub mod signature_verification_test { eth_address: [u8; 20], ) -> Result<()> { let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?; - verify_secp256k1_ix(&ix, ð_address, &message, &signature, recovery_id)?; + verify_secp256k1_ix_with_instruction_index( + &ix, + 0, + ð_address, + &message, + &signature, + recovery_id, + )?; msg!("Secp256k1 signature verified successfully using custom helper!");