diff --git a/.github/actions/e2e-setup/action.yml b/.github/actions/e2e-setup/action.yml index 218cfe613..d79fa0573 100644 --- a/.github/actions/e2e-setup/action.yml +++ b/.github/actions/e2e-setup/action.yml @@ -36,6 +36,9 @@ runs: ./programs/solana/target/sbpf-solana-solana/ key: ${{ runner.os }}-solana-${{ env.SOLANA_VERSION }}-${{ env.ANCHOR_VERSION }}-${{ hashFiles('programs/solana/**') }} + - name: Install just + uses: extractions/setup-just@v2 + - name: Install artifacts shell: bash run: | diff --git a/Cargo.lock b/Cargo.lock index b15a44d06..a4239727d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12735,6 +12735,7 @@ dependencies = [ "ics23", "sha2 0.10.9", "solana-account-info", + "solana-ibc-types", "solana-keccak-hasher", "solana-msg", "solana-pubkey", diff --git a/packages/tendermint-light-client/solana/Cargo.toml b/packages/tendermint-light-client/solana/Cargo.toml index 20345e37c..72649d461 100644 --- a/packages/tendermint-light-client/solana/Cargo.toml +++ b/packages/tendermint-light-client/solana/Cargo.toml @@ -18,4 +18,5 @@ solana-pubkey.workspace = true solana-sha256-hasher.workspace = true solana-keccak-hasher.workspace = true solana-msg.workspace = true +solana-ibc-types = { path = "../../../programs/solana/packages/solana-ibc-types" } diff --git a/packages/tendermint-light-client/solana/src/lib.rs b/packages/tendermint-light-client/solana/src/lib.rs index b1563e75c..46a4c48ca 100644 --- a/packages/tendermint-light-client/solana/src/lib.rs +++ b/packages/tendermint-light-client/solana/src/lib.rs @@ -104,50 +104,46 @@ impl<'a> SolanaSignatureVerifier<'a> { impl<'a> tendermint::crypto::signature::Verifier for SolanaSignatureVerifier<'a> { fn verify(&self, pubkey: PublicKey, msg: &[u8], signature: &Signature) -> Result<(), Error> { - match pubkey { - PublicKey::Ed25519(pk) => { - if !self.verification_accounts.is_empty() { - use solana_msg::msg; - - let sig_hash = - solana_sha256_hasher::hashv(&[pk.as_bytes(), msg, signature.as_bytes()]) - .to_bytes(); - - // PDA: [b"sig_verify", hash(pubkey || msg || signature)] - let (expected_pda, _) = - Pubkey::find_program_address(&[b"sig_verify", &sig_hash], self.program_id); - - for account in self.verification_accounts { - if account.key == &expected_pda { - let data = account.try_borrow_data().map_err(|_| { - msg!("Failed to borrow verification account data"); - Error::VerificationFailed - })?; - - if data.len() < 9 { - msg!("Verification account data too short"); - return Err(Error::VerificationFailed); - } - - let is_valid = data[8] != 0; - if !is_valid { - return Err(Error::VerificationFailed); - } - - return Ok(()); - } - } - - msg!("Pre-verification account not found, using brine-ed25519"); - } - - // Ed25519Program only verifies sigs in current tx, can't handle external Tendermint headers. - // Pre-verification (above) uses Ed25519Program via separate tx for FREE verification. - // Fallback: brine-ed25519 (~30k CU/sig, audited by OtterSec) - brine_ed25519::sig_verify(pk.as_bytes(), signature.as_bytes(), msg) - .map_err(|_| Error::VerificationFailed) + use solana_ibc_types::ics07::SIGNATURE_VERIFICATION_IS_VALID_OFFSET; + + let PublicKey::Ed25519(pk) = pubkey else { + return Err(Error::UnsupportedKeyType); + }; + + let sig_hash = + solana_sha256_hasher::hashv(&[pk.as_bytes(), msg, signature.as_bytes()]).to_bytes(); + let (expected_pda, _) = + Pubkey::find_program_address(&[b"sig_verify", &sig_hash], self.program_id); + + // Fallback: brine-ed25519 (~30k CU/sig, audited by OtterSec) + let fallback = || { + brine_ed25519::sig_verify(pk.as_bytes(), signature.as_bytes(), msg) + .map_err(|_| Error::VerificationFailed) + }; + + let Some(account) = self + .verification_accounts + .iter() + .find(|a| a.key == &expected_pda) + else { + return fallback(); + }; + + let Ok(data) = account.try_borrow_data() else { + return fallback(); + }; + + match data.get(SIGNATURE_VERIFICATION_IS_VALID_OFFSET) { + Some(&1) => Ok(()), + Some(&0) => Err(Error::VerificationFailed), + Some(v) => { + solana_msg::msg!("Unexpected verification value: {}", v); + fallback() + } + None => { + solana_msg::msg!("Verification account data too short"); + fallback() } - _ => Err(Error::UnsupportedKeyType), } } } @@ -222,3 +218,141 @@ impl ics23::HostFunctionsProvider for SolanaHostFunctionsManager { HostFunctionsManager::blake3(message) } } + +#[cfg(test)] +mod tests { + use super::*; + use solana_ibc_types::ics07::SIGNATURE_VERIFICATION_IS_VALID_OFFSET; + use std::cell::RefCell; + use std::rc::Rc; + use tendermint::crypto::signature::Verifier; + + fn create_verification_account_data(is_valid: u8) -> Vec { + let mut data = vec![0u8; SIGNATURE_VERIFICATION_IS_VALID_OFFSET + 1]; + data[SIGNATURE_VERIFICATION_IS_VALID_OFFSET] = is_valid; + data + } + + fn create_account_info<'a>( + key: &'a Pubkey, + data: &'a mut [u8], + lamports: &'a mut u64, + owner: &'a Pubkey, + ) -> AccountInfo<'a> { + AccountInfo { + key, + is_signer: false, + is_writable: false, + lamports: Rc::new(RefCell::new(lamports)), + data: Rc::new(RefCell::new(data)), + owner, + executable: false, + rent_epoch: 0, + } + } + + fn compute_sig_verify_pda( + pk: &[u8; 32], + msg: &[u8], + sig: &[u8; 64], + program_id: &Pubkey, + ) -> Pubkey { + let sig_hash = solana_sha256_hasher::hashv(&[pk, msg, sig]).to_bytes(); + let (pda, _) = Pubkey::find_program_address(&[b"sig_verify", &sig_hash], program_id); + pda + } + + #[test] + fn test_preverify_valid_signature() { + let program_id = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let pk_bytes = [1u8; 32]; + let msg = b"test message"; + let sig_bytes = [2u8; 64]; + let pda = compute_sig_verify_pda(&pk_bytes, msg, &sig_bytes, &program_id); + + let mut data = create_verification_account_data(1); + let mut lamports = 1_000_000u64; + let accounts = [create_account_info(&pda, &mut data, &mut lamports, &owner)]; + + let verifier = SolanaSignatureVerifier::new(&accounts, &program_id); + let pk = tendermint::PublicKey::from_raw_ed25519(&pk_bytes).unwrap(); + let sig = Signature::try_from(sig_bytes.as_slice()).unwrap(); + + assert!(verifier.verify(pk, msg, &sig).is_ok()); + } + + #[test] + fn test_preverify_invalid_signature() { + let program_id = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let pk_bytes = [1u8; 32]; + let msg = b"test message"; + let sig_bytes = [2u8; 64]; + let pda = compute_sig_verify_pda(&pk_bytes, msg, &sig_bytes, &program_id); + + let mut data = create_verification_account_data(0); + let mut lamports = 1_000_000u64; + let accounts = [create_account_info(&pda, &mut data, &mut lamports, &owner)]; + + let verifier = SolanaSignatureVerifier::new(&accounts, &program_id); + let pk = tendermint::PublicKey::from_raw_ed25519(&pk_bytes).unwrap(); + let sig = Signature::try_from(sig_bytes.as_slice()).unwrap(); + + assert!(verifier.verify(pk, msg, &sig).is_err()); + } + + #[test] + fn test_preverify_unexpected_value_falls_back() { + let program_id = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let pk_bytes = [1u8; 32]; + let msg = b"test message"; + let sig_bytes = [2u8; 64]; + let pda = compute_sig_verify_pda(&pk_bytes, msg, &sig_bytes, &program_id); + + let mut data = create_verification_account_data(42); + let mut lamports = 1_000_000u64; + let accounts = [create_account_info(&pda, &mut data, &mut lamports, &owner)]; + + let verifier = SolanaSignatureVerifier::new(&accounts, &program_id); + let pk = tendermint::PublicKey::from_raw_ed25519(&pk_bytes).unwrap(); + let sig = Signature::try_from(sig_bytes.as_slice()).unwrap(); + + assert!(verifier.verify(pk, msg, &sig).is_err()); + } + + #[test] + fn test_preverify_data_too_short_falls_back() { + let program_id = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let pk_bytes = [1u8; 32]; + let msg = b"test message"; + let sig_bytes = [2u8; 64]; + let pda = compute_sig_verify_pda(&pk_bytes, msg, &sig_bytes, &program_id); + + let mut data = vec![0u8; SIGNATURE_VERIFICATION_IS_VALID_OFFSET]; + let mut lamports = 1_000_000u64; + let accounts = [create_account_info(&pda, &mut data, &mut lamports, &owner)]; + + let verifier = SolanaSignatureVerifier::new(&accounts, &program_id); + let pk = tendermint::PublicKey::from_raw_ed25519(&pk_bytes).unwrap(); + let sig = Signature::try_from(sig_bytes.as_slice()).unwrap(); + + assert!(verifier.verify(pk, msg, &sig).is_err()); + } + + #[test] + fn test_no_preverify_account_falls_back() { + let program_id = Pubkey::new_unique(); + let pk_bytes = [1u8; 32]; + let msg = b"test message"; + let sig_bytes = [2u8; 64]; + + let verifier = SolanaSignatureVerifier::new(&[], &program_id); + let pk = tendermint::PublicKey::from_raw_ed25519(&pk_bytes).unwrap(); + let sig = Signature::try_from(sig_bytes.as_slice()).unwrap(); + + assert!(verifier.verify(pk, msg, &sig).is_err()); + } +} diff --git a/programs/solana/Cargo.lock b/programs/solana/Cargo.lock index 09ccc2a7d..3ed1fc6f0 100644 --- a/programs/solana/Cargo.lock +++ b/programs/solana/Cargo.lock @@ -5627,6 +5627,7 @@ dependencies = [ "ics23", "sha2 0.10.9", "solana-account-info", + "solana-ibc-types", "solana-keccak-hasher", "solana-msg", "solana-pubkey", diff --git a/programs/solana/packages/solana-ibc-constants/src/lib.rs b/programs/solana/packages/solana-ibc-constants/src/lib.rs index 7c6e76f8e..da5340d18 100644 --- a/programs/solana/packages/solana-ibc-constants/src/lib.rs +++ b/programs/solana/packages/solana-ibc-constants/src/lib.rs @@ -2,6 +2,9 @@ //! //! This crate provides all the program IDs and constants used by IBC on Solana +/// Anchor default discriminator length in bytes +pub const ANCHOR_DISCRIMINATOR_LEN: usize = 8; + /// ICS26 Router Program ID on Solana pub const ICS26_ROUTER_ID: &str = "FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx"; diff --git a/programs/solana/packages/solana-ibc-types/src/ics07.rs b/programs/solana/packages/solana-ibc-types/src/ics07.rs index b5c18c08c..a27a7527c 100644 --- a/programs/solana/packages/solana-ibc-types/src/ics07.rs +++ b/programs/solana/packages/solana-ibc-types/src/ics07.rs @@ -118,3 +118,8 @@ pub struct SignatureData { pub msg: Vec, pub signature: [u8; 64], } + +/// Offset of `is_valid` field in SignatureVerification account data. +/// Equals Anchor discriminator length since `is_valid` is the first field. +pub const SIGNATURE_VERIFICATION_IS_VALID_OFFSET: usize = + solana_ibc_constants::ANCHOR_DISCRIMINATOR_LEN; diff --git a/programs/solana/packages/solana-ibc-types/src/utils.rs b/programs/solana/packages/solana-ibc-types/src/utils.rs index a2f060d32..430c5be31 100644 --- a/programs/solana/packages/solana-ibc-types/src/utils.rs +++ b/programs/solana/packages/solana-ibc-types/src/utils.rs @@ -1,12 +1,16 @@ //! Utility functions for IBC on Solana +use solana_ibc_constants::ANCHOR_DISCRIMINATOR_LEN; + /// Compute Anchor instruction discriminator /// /// This computes the first 8 bytes of SHA256("global:{instruction_name}") /// following Anchor's discriminator calculation formula. -pub fn compute_discriminator(instruction_name: &str) -> [u8; 8] { +pub fn compute_discriminator(instruction_name: &str) -> [u8; ANCHOR_DISCRIMINATOR_LEN] { let preimage = format!("global:{instruction_name}"); - let mut hash_result = [0u8; 8]; - hash_result.copy_from_slice(&solana_sha256_hasher::hash(preimage.as_bytes()).to_bytes()[..8]); + let mut hash_result = [0u8; ANCHOR_DISCRIMINATOR_LEN]; + hash_result.copy_from_slice( + &solana_sha256_hasher::hash(preimage.as_bytes()).to_bytes()[..ANCHOR_DISCRIMINATOR_LEN], + ); hash_result } diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/pre_verify_signatures.rs b/programs/solana/programs/ics07-tendermint/src/instructions/pre_verify_signatures.rs index 0303cab1d..7e8075061 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/pre_verify_signatures.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/pre_verify_signatures.rs @@ -18,8 +18,8 @@ pub fn pre_verify_signature<'info>( &signature.signature, )?; - ctx.accounts.signature_verification.submitter = ctx.accounts.payer.key(); ctx.accounts.signature_verification.is_valid = is_valid; + ctx.accounts.signature_verification.submitter = ctx.accounts.payer.key(); Ok(()) } diff --git a/programs/solana/programs/ics07-tendermint/src/state.rs b/programs/solana/programs/ics07-tendermint/src/state.rs index 9278836d8..170465b16 100644 --- a/programs/solana/programs/ics07-tendermint/src/state.rs +++ b/programs/solana/programs/ics07-tendermint/src/state.rs @@ -41,20 +41,26 @@ impl MisbehaviourChunk { pub const SEED: &'static [u8] = b"misbehaviour_chunk"; } -/// Storage for Ed25519 signature verification results +/// Storage for Ed25519 signature verification results. +/// IMPORTANT: Field order matters: verifier reads `data[8]` for `is_valid`. #[account] #[derive(InitSpace)] pub struct SignatureVerification { - /// The submitter who created this verification - pub submitter: Pubkey, /// Whether the signature is valid pub is_valid: bool, + /// The submitter who created this verification + pub submitter: Pubkey, } impl SignatureVerification { pub const SEED: &'static [u8] = b"sig_verify"; } +// Compile-time verification that discriminator length matches the shared constant +const _: () = assert!( + SignatureVerification::DISCRIMINATOR.len() == solana_ibc_constants::ANCHOR_DISCRIMINATOR_LEN +); + #[cfg(test)] mod compatibility_tests { use super::*;