airdrop_api/merkle.rs
1// Merkle proof verification utilities
2// Aligned with miracle-copy and escrow-copy's merkle tree construction approach
3// Our approach: Single merkle tree per campaign (simpler than dual merkle tree)
4
5use solana_program::hash::hashv;
6use solana_program::pubkey::Pubkey;
7
8/// Airdrop allocation data for merkle tree construction
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct AirdropAllocation {
11 /// Recipient wallet address
12 pub recipient: Pubkey,
13 /// Token amount to airdrop
14 pub amount: u64,
15}
16
17impl AirdropAllocation {
18 /// Create a new airdrop allocation
19 pub fn new(recipient: Pubkey, amount: u64) -> Self {
20 Self { recipient, amount }
21 }
22
23 /// Compute leaf hash for this allocation
24 pub fn compute_leaf_hash(&self) -> [u8; 32] {
25 compute_leaf_hash(&self.recipient, self.amount)
26 }
27}
28
29/// Merkle tree for airdrop campaign
30///
31/// ## Construction Rules (aligned with miracle-copy and escrow-copy):
32/// 1. **Single leaf case**: Duplicate the leaf before building tree
33/// - Root = hash(leaf, leaf) instead of root = leaf
34/// 2. **Odd nodes at level**: Hash last node with itself
35/// - Ensures consistent tree structure
36/// 3. **Deterministic**: Same allocations always produce same root
37#[derive(Clone, Debug)]
38pub struct MerkleTree {
39 /// Merkle root hash (32 bytes)
40 root: [u8; 32],
41 /// Tree levels (for proof generation)
42 /// tree[0] = leaf level, tree[last] = root level
43 tree: Vec<Vec<[u8; 32]>>,
44 /// Original allocations (sorted)
45 allocations: Vec<AirdropAllocation>,
46}
47
48impl MerkleTree {
49 /// Create a new merkle tree from airdrop allocations
50 ///
51 /// ## Parameters
52 /// - `allocations`: Vector of (recipient, amount) allocations
53 ///
54 /// ## Returns
55 /// - `Ok(MerkleTree)` if successful
56 /// - `Err(String)` if allocations is empty
57 ///
58 /// ## Single Leaf Handling
59 /// If only one allocation exists, the leaf is duplicated before building the tree.
60 /// This matches miracle-copy and escrow-copy's approach for consistency.
61 ///
62 /// ## Example
63 /// ```rust
64 /// use airdrop_api::merkle::{MerkleTree, AirdropAllocation};
65 /// use solana_program::pubkey::Pubkey;
66 ///
67 /// let allocations = vec![
68 /// AirdropAllocation::new(Pubkey::new_unique(), 1000),
69 /// AirdropAllocation::new(Pubkey::new_unique(), 2000),
70 /// ];
71 ///
72 /// let tree = MerkleTree::new(allocations)?;
73 /// println!("Merkle root: {:?}", tree.root());
74 /// ```
75 pub fn new(mut allocations: Vec<AirdropAllocation>) -> Result<Self, String> {
76 if allocations.is_empty() {
77 return Err("Cannot create merkle tree from empty allocations list".to_string());
78 }
79
80 // Sort allocations deterministically by recipient for consistent merkle tree construction
81 // This ensures that the same allocations always produce the same merkle root regardless of input order
82 allocations.sort_by(|a, b| a.recipient.cmp(&b.recipient));
83
84 // Generate leaf hashes from sorted allocations
85 let mut leaves: Vec<[u8; 32]> = allocations.iter().map(|a| a.compute_leaf_hash()).collect();
86
87 // CRITICAL: Handle single leaf case - duplicate the leaf to force one hash operation
88 // This matches miracle-copy and escrow-copy's logic exactly for consistency
89 if leaves.len() == 1 {
90 leaves.push(leaves[0]); // Duplicate the single leaf
91 }
92
93 // Build merkle tree bottom-up
94 let mut tree = vec![leaves.clone()];
95
96 while leaves.len() > 1 {
97 let mut new_level = Vec::new();
98 for chunk in leaves.chunks(2) {
99 if chunk.len() == 2 {
100 // Two nodes: hash(left, right)
101 let combined = hashv(&[&chunk[0], &chunk[1]]).to_bytes();
102 new_level.push(combined);
103 } else {
104 // Single element case - hash with itself for consistency
105 let combined = hashv(&[&chunk[0], &chunk[0]]).to_bytes();
106 new_level.push(combined);
107 }
108 }
109 leaves = new_level;
110 tree.push(leaves.clone());
111 }
112
113 let root = tree
114 .last()
115 .ok_or("Failed to get tree root: tree is empty")?
116 .first()
117 .ok_or("Failed to get tree root: root level is empty")?
118 .clone();
119
120 Ok(Self {
121 root,
122 tree,
123 allocations,
124 })
125 }
126
127 /// Get the merkle root hash
128 pub fn root(&self) -> [u8; 32] {
129 self.root
130 }
131
132 /// Generate a merkle proof for a specific allocation
133 ///
134 /// ## Parameters
135 /// - `allocation_index`: Index of the allocation in the original data
136 ///
137 /// ## Returns
138 /// - `Ok(MerkleProof)` if successful
139 /// - `Err(String)` if index is out of bounds
140 ///
141 /// ## Example
142 /// ```rust
143 /// let tree = MerkleTree::new(allocations)?;
144 /// let proof = tree.generate_proof(0)?;
145 /// ```
146 pub fn generate_proof(&self, allocation_index: usize) -> Result<MerkleProof, String> {
147 if allocation_index >= self.allocations.len() {
148 return Err(format!(
149 "Allocation index {} out of bounds (max: {})",
150 allocation_index,
151 self.allocations.len() - 1
152 ));
153 }
154
155 let mut path = Vec::new();
156 let mut indices = Vec::new();
157 let mut current_index = allocation_index;
158
159 // Traverse up the tree to build the proof
160 for level in 0..self.tree.len() - 1 {
161 let current_level = &self.tree[level];
162 let sibling_index = if current_index % 2 == 0 {
163 // Current is left, sibling is right
164 current_index + 1
165 } else {
166 // Current is right, sibling is left
167 current_index - 1
168 };
169
170 // Check if sibling exists (might not exist at odd levels)
171 if sibling_index < current_level.len() {
172 let sibling_hash = current_level[sibling_index];
173 path.push(sibling_hash);
174 // is_right = true if sibling is on the right (current_index is even)
175 indices.push(current_index % 2 == 0);
176 } else {
177 // No sibling (odd level), use current node as sibling (duplicate)
178 let current_hash = current_level[current_index];
179 path.push(current_hash);
180 // Always treat as right sibling for consistency
181 indices.push(true);
182 }
183
184 // Move to parent level
185 current_index /= 2;
186 }
187
188 Ok(MerkleProof::new(path, indices))
189 }
190
191 /// Find and generate proof for a specific recipient
192 ///
193 /// ## Parameters
194 /// - `recipient`: Recipient wallet address
195 ///
196 /// ## Returns
197 /// - `Some(Ok(MerkleProof))` if recipient found
198 /// - `Some(Err(String))` if found but proof generation failed
199 /// - `None` if recipient not found
200 pub fn find_and_generate_proof(
201 &self,
202 recipient: &Pubkey,
203 ) -> Option<Result<MerkleProof, String>> {
204 self.allocations
205 .iter()
206 .position(|a| &a.recipient == recipient)
207 .map(|index| self.generate_proof(index))
208 }
209
210 /// Get the number of allocations in the tree
211 pub fn allocation_count(&self) -> usize {
212 self.allocations.len()
213 }
214
215 /// Get allocation data by index
216 pub fn get_allocation(&self, index: usize) -> Option<&AirdropAllocation> {
217 self.allocations.get(index)
218 }
219
220 /// Get all allocation data
221 pub fn allocations(&self) -> &[AirdropAllocation] {
222 &self.allocations
223 }
224}
225
226/// Merkle proof structure for airdrop claims
227#[derive(Clone, Debug)]
228pub struct MerkleProof {
229 /// Proof path nodes (32-byte hashes)
230 pub path: Vec<[u8; 32]>,
231 /// Direction indices (false = left sibling, true = right sibling)
232 pub indices: Vec<bool>,
233}
234
235impl MerkleProof {
236 /// Create a new merkle proof
237 pub fn new(path: Vec<[u8; 32]>, indices: Vec<bool>) -> Self {
238 Self { path, indices }
239 }
240
241 /// Verify merkle proof for airdrop claim
242 ///
243 /// ## Parameters
244 /// - `recipient`: Recipient wallet address
245 /// - `amount`: Token amount to claim
246 /// - `merkle_root`: Campaign's merkle root
247 ///
248 /// ## Returns
249 /// - `true` if proof is valid
250 /// - `false` if proof is invalid
251 ///
252 /// ## Algorithm (from miracle-copy's essence)
253 /// 1. Compute leaf hash: hashv(&[recipient, amount])
254 /// 2. Traverse proof path
255 /// 3. For each step: hash(left, right) based on direction
256 /// 4. Final hash should match merkle_root
257 pub fn verify(&self, recipient: &Pubkey, amount: u64, merkle_root: [u8; 32]) -> bool {
258 // Validate proof structure
259 if self.path.len() != self.indices.len() {
260 return false;
261 }
262
263 // Compute leaf hash: hash(recipient, amount)
264 let leaf_hash = compute_leaf_hash(recipient, amount);
265
266 // Traverse proof path
267 let mut current_hash = leaf_hash;
268 for (i, &path_node) in self.path.iter().enumerate() {
269 let is_right = self.indices[i];
270
271 if is_right {
272 // Sibling is on the right: hash(current_hash, path_node)
273 current_hash = hashv(&[¤t_hash, &path_node]).to_bytes();
274 } else {
275 // Sibling is on the left: hash(path_node, current_hash)
276 current_hash = hashv(&[&path_node, ¤t_hash]).to_bytes();
277 }
278 }
279
280 current_hash == merkle_root
281 }
282}
283
284/// Compute leaf hash for airdrop allocation
285///
286/// ## Formula
287/// leaf_hash = hashv(&[recipient.as_ref(), &amount.to_le_bytes()])
288///
289/// ## Deterministic
290/// Same (recipient, amount) always produces same hash
291pub fn compute_leaf_hash(recipient: &Pubkey, amount: u64) -> [u8; 32] {
292 hashv(&[recipient.as_ref(), &amount.to_le_bytes()]).to_bytes()
293}
294
295/// Verify merkle proof for airdrop claim (standalone function)
296///
297/// ## Parameters
298/// - `proof`: Merkle proof structure
299/// - `recipient`: Recipient wallet address
300/// - `amount`: Token amount to claim
301/// - `merkle_root`: Campaign's merkle root
302///
303/// ## Returns
304/// - `true` if proof is valid
305/// - `false` if proof is invalid
306pub fn verify_merkle_proof(
307 proof: &MerkleProof,
308 recipient: &Pubkey,
309 amount: u64,
310 merkle_root: [u8; 32],
311) -> bool {
312 proof.verify(recipient, amount, merkle_root)
313}