Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/actions/e2e-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/tendermint-light-client/solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

220 changes: 177 additions & 43 deletions packages/tendermint-light-client/solana/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
}
Expand Down Expand Up @@ -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<u8> {
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());
}
}
1 change: 1 addition & 0 deletions programs/solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions programs/solana/packages/solana-ibc-constants/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
5 changes: 5 additions & 0 deletions programs/solana/packages/solana-ibc-types/src/ics07.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ pub struct SignatureData {
pub msg: Vec<u8>,
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;
10 changes: 7 additions & 3 deletions programs/solana/packages/solana-ibc-types/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
12 changes: 9 additions & 3 deletions programs/solana/programs/ics07-tendermint/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the compile time check for the discriminator length. One question. When can the lengths be different?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially anchor allows setting custom discriminators (non 8 bytes).
it's not widely used feature but still better to have this as safeguard

);

#[cfg(test)]
mod compatibility_tests {
use super::*;
Expand Down