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), ¤t_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), ¤t_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}