pool/
merkle_with_history.rs

1//! Merkle Tree with History Module
2//!
3//! This module implements a fixed-depth binary Merkle tree with root history
4//! for privacy-preserving transactions. It uses the Poseidon2 hash function
5//! for ZK-circuit compatibility.
6//!
7//! - Maintains a ring buffer of recent roots for membership proof verification
8//! - Compatible with the ASP membership Merkle tree implementation
9//!
10//! This module is designed to be used internally by the pool contract.
11//! Authorization should be handled by the calling main contract before invoking
12//! these functions.
13
14use soroban_sdk::{Env, U256, Vec, contracttype};
15use soroban_utils::{get_zeroes, poseidon2_compress};
16
17/// Number of roots kept in history for proof verification
18const ROOT_HISTORY_SIZE: u32 = 90;
19
20// Errors
21#[derive(Clone, Debug)]
22pub enum Error {
23    AlreadyInitialized,
24    WrongLevels,
25    MerkleTreeFull,
26    NextIndexNotEven,
27    NotInitialized,
28}
29
30/// Storage keys for Merkle tree persistent data
31#[contracttype]
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum MerkleDataKey {
34    /// Number of levels in the Merkle tree
35    Levels,
36    /// Current position in the root history ring buffer
37    CurrentRootIndex,
38    /// Next available index for leaf insertion
39    NextIndex,
40    /// Subtree hashes at each level (indexed by level)
41    FilledSubtree(u32),
42    /// Zero hash values for each level (indexed by level)
43    Zeroes(u32),
44    /// Historical roots ring buffer
45    Root(u32),
46}
47
48/// Merkle Tree with root history for privacy-preserving transactions
49///
50/// This struct provides methods to manage a fixed-depth binary Merkle tree
51/// that maintains a history of recent roots. When the tree is modified,
52/// it automatically preserves previous roots for membership proof verification.
53pub struct MerkleTreeWithHistory;
54
55impl MerkleTreeWithHistory {
56    /// Initialize the Merkle tree with history
57    ///
58    /// Creates a new Merkle tree with the specified number of levels. The tree
59    /// is initialized with precomputed zero hashes at each level, and the
60    /// initial root is set to the zero hash at the top level.
61    ///
62    /// # Arguments
63    ///
64    /// * `env` - The Soroban environment
65    /// * `levels` - Number of levels in the Merkle tree (must be in range
66    ///   [1..32])
67    pub fn init(env: &Env, levels: u32) -> Result<(), Error> {
68        if levels == 0 || levels > 32 {
69            return Err(Error::WrongLevels);
70        }
71        let storage = env.storage().persistent();
72
73        // Prevent reinitialization
74        if storage.has(&MerkleDataKey::CurrentRootIndex) {
75            return Err(Error::AlreadyInitialized);
76        }
77
78        // Store levels
79        storage.set(&MerkleDataKey::Levels, &levels);
80
81        // Initialize with precomputed zero hashes
82        let zeros: Vec<U256> = get_zeroes(env);
83
84        // Initialize filledSubtrees[i] = zeros(i) for each level
85        for i in 0..levels + 1 {
86            let z: U256 = zeros.get(i).ok_or(Error::NotInitialized)?;
87            storage.set(&MerkleDataKey::FilledSubtree(i), &z);
88            storage.set(&MerkleDataKey::Zeroes(i), &z);
89        }
90
91        // Set initial root to zero hash at top level
92        let root_0: U256 = zeros.get(levels).ok_or(Error::NotInitialized)?;
93        storage.set(&MerkleDataKey::Root(0), &root_0);
94        storage.set(&MerkleDataKey::CurrentRootIndex, &0u32);
95        storage.set(&MerkleDataKey::NextIndex, &0u64);
96
97        Ok(())
98    }
99
100    /// Insert two leaves into the Merkle tree as siblings
101    ///
102    /// Adds 2 new leaves to the Merkle tree and updates the root. The leaves
103    /// are inserted at the next available index, and the tree is updated
104    /// efficiently by only recomputing the hashes along the path to the
105    /// root.
106    ///
107    /// When the tree is modified, a new root is automatically created in
108    /// the next history slot. The previous root remains valid for proof
109    /// verification until it is overwritten after `ROOT_HISTORY_SIZE`
110    /// rotations.
111    ///
112    /// # Arguments
113    ///
114    /// * `env` - The Soroban environment
115    /// * `leaf_1` - The left leaf value to insert (at even index)
116    /// * `leaf_2` - The right leaf value to insert (at odd index)
117    ///
118    /// # Returns
119    ///
120    /// Returns the indexes where leaves were inserted
121    pub fn insert_two_leaves(env: &Env, leaf_1: U256, leaf_2: U256) -> Result<(u32, u32), Error> {
122        let storage = env.storage().persistent();
123
124        let levels: u32 = storage
125            .get(&MerkleDataKey::Levels)
126            .ok_or(Error::NotInitialized)?;
127        let next_index: u64 = storage
128            .get(&MerkleDataKey::NextIndex)
129            .ok_or(Error::NotInitialized)?;
130        let mut root_index: u32 = storage
131            .get(&MerkleDataKey::CurrentRootIndex)
132            .ok_or(Error::NotInitialized)?;
133        let max_leaves = 1u64.checked_shl(levels).ok_or(Error::WrongLevels)?;
134
135        // NextIndex must be even for two-leaf insertion
136        if !next_index.is_multiple_of(2) {
137            return Err(Error::NextIndexNotEven);
138        }
139
140        if (next_index + 2) > max_leaves {
141            return Err(Error::MerkleTreeFull);
142        }
143
144        // Hash the two leaves to form their parent node at level 1
145        let mut current_hash = poseidon2_compress(env, leaf_1, leaf_2);
146
147        // Calculate the parent index at level 1 (since we already hashed the two
148        // leaves)
149        let mut current_index = next_index >> 1;
150
151        // Update the tree by recomputing hashes along the path to root
152        // Start at level 1 since current_hash is already the parent of the two leaves
153        for lvl in 1..levels {
154            let is_right = current_index & 1 == 1;
155            if is_right {
156                // Leaf is right child, get the stored left sibling
157                let left: U256 = storage
158                    .get(&MerkleDataKey::FilledSubtree(lvl))
159                    .ok_or(Error::NotInitialized)?;
160                current_hash = poseidon2_compress(env, left, current_hash);
161            } else {
162                // Leaf is left child, store it and pair with zero hash
163                storage.set(&MerkleDataKey::FilledSubtree(lvl), &current_hash);
164                let zero_val: U256 = storage
165                    .get(&MerkleDataKey::Zeroes(lvl))
166                    .ok_or(Error::NotInitialized)?;
167                current_hash = poseidon2_compress(env, current_hash, zero_val);
168            }
169            current_index >>= 1;
170        }
171
172        // Update the root history index
173        root_index = (root_index + 1) % ROOT_HISTORY_SIZE;
174        // Update the root with the computed hash
175        storage.set(&MerkleDataKey::Root(root_index), &current_hash);
176        storage.set(&MerkleDataKey::CurrentRootIndex, &root_index);
177
178        // Update NextIndex
179        storage.set(&MerkleDataKey::NextIndex, &(next_index + 2));
180
181        // Return the index of the left leaf
182        Ok((next_index as u32, (next_index + 1) as u32))
183    }
184
185    /// Check if a root exists in the recent history
186    ///
187    /// Searches the root history ring buffer to verify if a given root is
188    /// valid. This allows proofs generated against recent tree states to be
189    /// verified, providing some tolerance for latency between proof
190    /// generation and submission.
191    ///
192    /// # Arguments
193    ///
194    /// * `env` - The Soroban environment
195    /// * `root` - The Merkle root to check
196    ///
197    /// # Returns
198    ///
199    /// Returns `true` if the root exists in the history buffer, `false`
200    /// otherwise. Zero root always returns `false`.
201    pub fn is_known_root(env: &Env, root: &U256) -> Result<bool, Error> {
202        // Zero root is never valid as define zero in a different way
203        if *root == U256::from_u32(env, 0u32) {
204            return Ok(false);
205        }
206
207        let storage = env.storage().persistent();
208        let current_root_index: u32 = storage
209            .get(&MerkleDataKey::CurrentRootIndex)
210            .ok_or(Error::NotInitialized)?;
211
212        // Search the ring buffer for the root
213        let mut i = current_root_index;
214        loop {
215            // roots[i]
216            if let Some(r) = storage.get::<MerkleDataKey, U256>(&MerkleDataKey::Root(i))
217                && &r == root
218            {
219                return Ok(true);
220            }
221            i = (i + 1) % ROOT_HISTORY_SIZE;
222            if i == current_root_index {
223                // Break after seeing all roots
224                break;
225            }
226        }
227        Ok(false)
228    }
229
230    /// Get the current Merkle root
231    ///
232    /// Returns the most recent root hash of the Merkle tree.
233    ///
234    /// # Arguments
235    ///
236    /// * `env` - The Soroban environment
237    ///
238    /// # Returns
239    ///
240    /// Returns the current Merkle root as U256
241    pub fn get_last_root(env: &Env) -> Result<U256, Error> {
242        let storage = env.storage().persistent();
243        let current_root_index: u32 = storage
244            .get(&MerkleDataKey::CurrentRootIndex)
245            .ok_or(Error::NotInitialized)?;
246
247        storage
248            .get(&MerkleDataKey::Root(current_root_index))
249            .ok_or(Error::NotInitialized)
250    }
251
252    /// Hash two U256 values using Poseidon2 compression
253    ///
254    /// Computes the Poseidon2 hash of two field elements in compression mode.
255    /// This is the core hashing function used for Merkle tree operations.
256    ///
257    /// # Arguments
258    /// * `env` - The Soroban environment
259    /// * `left` - Left input value
260    /// * `right` - Right input value
261    ///
262    /// # Returns
263    /// The Poseidon2 hash result as U256
264    pub fn hash_pair(env: &Env, left: U256, right: U256) -> U256 {
265        poseidon2_compress(env, left, right)
266    }
267}