Skip to content

Commit ff58a2e

Browse files
committed
chore(solana): replug MMR verification logic
1 parent f1cece8 commit ff58a2e

4 files changed

Lines changed: 227 additions & 11 deletions

File tree

solana/programs/portal/src/instructions/base_to_solana/prove_call.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anchor_lang::{prelude::*, solana_program::keccak};
22

33
use crate::{
44
constants::REMOTE_CALL_SEED,
5+
internal::{merkle_utils, Proof},
56
state::{OutputRoot, RemoteCall},
67
};
78

@@ -15,8 +16,9 @@ pub struct ProveCall<'info> {
1516
init,
1617
payer = payer,
1718
space = 8 + RemoteCall::INIT_SPACE,
18-
seeds = [REMOTE_CALL_SEED, &call_hash],
19-
bump
19+
// NOTE: We check that the PDA derivation is correct in the handler to optimize the CPI.
20+
// seeds = [REMOTE_CALL_SEED, &call_hash],
21+
// bump
2022
)]
2123
pub remote_call: Account<'info, RemoteCall>,
2224

@@ -27,18 +29,24 @@ pub struct ProveCall<'info> {
2729

2830
pub fn prove_call_handler(
2931
ctx: Context<ProveCall>,
30-
call_hash: [u8; 32],
3132
nonce: [u8; 32],
3233
sender: [u8; 20],
3334
data: Vec<u8>,
35+
proof: Proof,
3436
) -> Result<()> {
37+
// Hash the call
38+
let call_hash = hash_call(&nonce, &sender, &data);
39+
40+
// Verify the PDA derivation is correct
41+
let (remote_call_pda, _) =
42+
Pubkey::find_program_address(&[REMOTE_CALL_SEED, &call_hash], ctx.program_id);
3543
require!(
36-
hash_call(&nonce, &sender, &data) == call_hash,
37-
ProveCallError::InvalidCallHash
44+
remote_call_pda == ctx.accounts.remote_call.key(),
45+
ProveCallError::InvalidPda
3846
);
3947

40-
// TODO: MMR proof verification
41-
require!(true, ProveCallError::InvalidProof);
48+
// Verify the merkle proof to ensure the transaction exists on the source chain
49+
merkle_utils::verify_mmr_proof(&ctx.accounts.output_root.root, &call_hash, &proof)?;
4250

4351
ctx.accounts.remote_call.executed = false;
4452
ctx.accounts.remote_call.sender = sender;
@@ -58,8 +66,8 @@ fn hash_call(nonce: &[u8; 32], sender: &[u8; 20], data: &[u8]) -> [u8; 32] {
5866

5967
#[error_code]
6068
pub enum ProveCallError {
61-
#[msg("Invalid call hash")]
62-
InvalidCallHash,
69+
#[msg("Invalid PDA derivation")]
70+
InvalidPda,
6371
#[msg("Invalid proof")]
6472
InvalidProof,
6573
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use anchor_lang::{prelude::*, solana_program::keccak};
2+
3+
#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
4+
pub struct Proof {
5+
pub proof: Vec<[u8; 32]>,
6+
pub leaf_index: u64,
7+
pub total_leaf_count: u64,
8+
}
9+
10+
/// Verifies an MMR proof.
11+
///
12+
/// The proof consists of sibling hashes along the path from the leaf to its
13+
/// mountain's peak, followed by the hashes of all other mountain peaks (right-to-left).
14+
///
15+
/// # Arguments
16+
/// * `proof` - The proof elements.
17+
/// * `root` - The expected MMR root.
18+
/// * `leaf_hash` - The hash of the leaf being verified.
19+
/// * `leaf_index` - The 0-indexed position of the leaf in the MMR.
20+
/// * `total_leaf_count` - The total number of leaves in the MMR when the proof was generated.
21+
///
22+
/// # Returns
23+
/// `true` if the proof is valid, `false` otherwise.
24+
pub fn verify_mmr_proof(
25+
expected_root: &[u8; 32],
26+
leaf_hash: &[u8; 32],
27+
proof: &Proof,
28+
) -> Result<()> {
29+
let Proof {
30+
proof,
31+
leaf_index,
32+
total_leaf_count,
33+
} = proof;
34+
35+
if *total_leaf_count == 0 {
36+
require!(proof.is_empty(), MerkleUtilsError::MmrShouldBeEmpty);
37+
require!(*expected_root == [0u8; 32], MerkleUtilsError::InvalidProof);
38+
return Ok(());
39+
}
40+
41+
require!(
42+
leaf_index < total_leaf_count,
43+
MerkleUtilsError::InvalidProof
44+
);
45+
46+
let calculated_root =
47+
calculate_mmr_root_from_proof(proof, leaf_hash, *leaf_index, *total_leaf_count)?;
48+
49+
require!(
50+
calculated_root == *expected_root,
51+
MerkleUtilsError::InvalidProof
52+
);
53+
54+
Ok(())
55+
}
56+
57+
/// Calculates the MMR root given a leaf, its proof, and the MMR structure.
58+
///
59+
/// This function reconstructs the peaks of the MMR based on the provided leaf and its proof,
60+
/// then bags these peaks together to form the final MMR root.
61+
fn calculate_mmr_root_from_proof(
62+
proof: &[[u8; 32]],
63+
leaf_hash: &[u8; 32],
64+
leaf_idx: u64, // 0-indexed leaf position
65+
total_leaf_count: u64,
66+
) -> Result<[u8; 32]> {
67+
require!(total_leaf_count > 0, MerkleUtilsError::EmptyMmr);
68+
69+
// 1. Determine the mountain structure and the leaf's mountain details.
70+
let mut mountains: Vec<(u32, u64, bool)> = Vec::new(); // (height, num_leaves_in_mountain, is_leafs_mountain)
71+
let mut temp_leaf_count = total_leaf_count;
72+
let mut current_leaf_offset: u64 = 0; // Tracks leaves before the current mountain being considered
73+
let mut leaf_s_mountain_details: Option<(u32, u64)> = None; // (height, leaf_idx_in_mountain)
74+
75+
let max_h = if total_leaf_count > 0 {
76+
64 - total_leaf_count.leading_zeros() - 1
77+
} else {
78+
0
79+
};
80+
81+
for h_idx in 0..=max_h {
82+
let h = max_h - h_idx; // Iterate from largest height downwards
83+
if (temp_leaf_count >> h) & 1 == 1 {
84+
let leaves_in_this_mountain = 1u64 << h;
85+
let is_leafs_m = leaf_idx >= current_leaf_offset
86+
&& leaf_idx < current_leaf_offset + leaves_in_this_mountain;
87+
mountains.push((h, leaves_in_this_mountain, is_leafs_m));
88+
if is_leafs_m {
89+
leaf_s_mountain_details = Some((h, leaf_idx - current_leaf_offset));
90+
}
91+
92+
current_leaf_offset += leaves_in_this_mountain;
93+
temp_leaf_count -= leaves_in_this_mountain;
94+
}
95+
96+
if temp_leaf_count == 0 {
97+
break;
98+
}
99+
}
100+
101+
let (leaf_mountain_height, _leaf_idx_in_mountain) =
102+
leaf_s_mountain_details.ok_or(error!(MerkleUtilsError::LeafMountainNotFound))?;
103+
104+
// 2. Calculate the peak of the leaf's mountain.
105+
let mut current_computed_hash = *leaf_hash;
106+
let mut proof_idx_offset = 0; // Tracks how many proof elements we've used for intra-mountain
107+
108+
require!(
109+
leaf_mountain_height as usize <= proof.len() || leaf_mountain_height == 0,
110+
MerkleUtilsError::InsufficientProofElementsForIntraMountainPath
111+
);
112+
113+
for _h_climb in 0..leaf_mountain_height {
114+
let sibling_hash = proof[proof_idx_offset];
115+
proof_idx_offset += 1;
116+
current_computed_hash = commutative_keccak256(current_computed_hash, sibling_hash);
117+
}
118+
let leaf_mountain_peak_hash = current_computed_hash;
119+
120+
// 3. Collect all peak hashes (leaf's calculated peak + other peaks from proof).
121+
let mut all_peak_hashes: Vec<[u8; 32]> = Vec::new();
122+
let mut remaining_proof_idx = proof_idx_offset;
123+
124+
// Peaks are needed in right-to-left order for bagging.
125+
// The `mountains` vector is currently left-to-right.
126+
for (_height, _num_leaves, is_leafs_m) in mountains.iter().rev() {
127+
if *is_leafs_m {
128+
all_peak_hashes.push(leaf_mountain_peak_hash);
129+
} else {
130+
require!(
131+
remaining_proof_idx < proof.len(),
132+
MerkleUtilsError::InsufficientProofElementsForOtherMountainPeaks
133+
);
134+
135+
all_peak_hashes.push(proof[remaining_proof_idx]);
136+
remaining_proof_idx += 1;
137+
}
138+
}
139+
140+
require!(
141+
remaining_proof_idx == proof.len(),
142+
MerkleUtilsError::UnusedProofElementsRemaining
143+
);
144+
145+
// 4. Bag the peaks (right-to-left).
146+
// `all_peak_hashes` is already in right-to-left mountain order because we iterated `mountains.iter().rev()`.
147+
if all_peak_hashes.is_empty() {
148+
// Should be caught by total_leaf_count == 0 earlier, but as a safeguard.
149+
require!(
150+
total_leaf_count == 0,
151+
MerkleUtilsError::NoPeaksFoundForNonEmptyMmr
152+
);
153+
154+
// If total_leaf_count is 0, what should an empty root be? Let's assume [0u8;32]
155+
return Ok([0u8; 32]);
156+
}
157+
158+
let mut current_root = all_peak_hashes[0]; // Start with the rightmost peak.
159+
for peak_hash in all_peak_hashes.iter().skip(1) {
160+
// next_peak_hash is to the left of current_root.
161+
// Hashing order for bagging: H(LeftPeak, H(MiddlePeak, RightPeak))
162+
// So, current_root is the right operand, all_peak_hashes[i] is the left.
163+
current_root = commutative_keccak256(*peak_hash, current_root);
164+
}
165+
166+
Ok(current_root)
167+
}
168+
169+
// Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs.
170+
// NOTE: Equivalent to the `standardNodeHash` in our https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
171+
fn commutative_keccak256(a: [u8; 32], b: [u8; 32]) -> [u8; 32] {
172+
if a < b {
173+
efficient_keccak256(&a, &b)
174+
} else {
175+
efficient_keccak256(&b, &a)
176+
}
177+
}
178+
179+
fn efficient_keccak256(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
180+
let mut data_to_hash = Vec::new();
181+
data_to_hash.extend_from_slice(a);
182+
data_to_hash.extend_from_slice(b);
183+
keccak::hash(&data_to_hash).to_bytes()
184+
}
185+
186+
#[error_code]
187+
pub enum MerkleUtilsError {
188+
#[msg("Invalid proof")]
189+
InvalidProof,
190+
#[msg("MMR should be empty")]
191+
MmrShouldBeEmpty,
192+
#[msg("MMR is empty")]
193+
EmptyMmr,
194+
#[msg("Leaf's mountain not found")]
195+
LeafMountainNotFound,
196+
#[msg("Insufficient proof elements for intra-mountain path")]
197+
InsufficientProofElementsForIntraMountainPath,
198+
#[msg("Insufficient proof elements for other mountain peaks")]
199+
InsufficientProofElementsForOtherMountainPeaks,
200+
#[msg("Unused proof elements remaining")]
201+
UnusedProofElementsRemaining,
202+
#[msg("No peaks found for non-empty MMR")]
203+
NoPeaksFoundForNonEmptyMmr,
204+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
pub mod ix;
2+
pub mod merkle_utils;
23

34
pub use ix::*;
5+
pub use merkle_utils::*;

solana/programs/portal/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ pub mod state;
1111
pub mod test_utils;
1212

1313
use instructions::*;
14+
use internal::Proof;
1415

1516
declare_id!("4aRCwRtUjaoNA34AVLUmYVsyPRph2fNcAhXUxwHKUGtn");
1617

1718
#[program]
1819
pub mod portal {
20+
1921
use super::*;
2022

2123
// Portal instructions
@@ -44,12 +46,12 @@ pub mod portal {
4446

4547
pub fn prove_call(
4648
ctx: Context<ProveCall>,
47-
call_hash: [u8; 32],
4849
nonce: [u8; 32],
4950
sender: [u8; 20],
5051
data: Vec<u8>,
52+
proof: Proof,
5153
) -> Result<()> {
52-
prove_call_handler(ctx, call_hash, nonce, sender, data)
54+
prove_call_handler(ctx, nonce, sender, data, proof)
5355
}
5456

5557
pub fn relay_call<'a, 'info>(

0 commit comments

Comments
 (0)