Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ jobs:
- name: Lint the code
run: bun run lint:sol

- uses: srdtrk/natlint@main
-- TODO: update original repo
- uses: vaporif/natlint@drop-forge-fmt
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest we solve this todo before merging.

Copy link
Member

Choose a reason for hiding this comment

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

@vaporif you didn't make a PR to natlint?

with:
include: 'contracts/**/*.sol'

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
Loading