Skip to main content

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(&[&current_hash, &path_node]).to_bytes();
274            } else {
275                // Sibling is on the left: hash(path_node, current_hash)
276                current_hash = hashv(&[&path_node, &current_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}