miden_node_store/accounts/
mod.rs

1//! Historical tracking for `AccountTree` via mutation overlays
2
3use std::collections::{BTreeMap, HashMap};
4
5use miden_objects::account::{AccountId, AccountIdPrefix};
6use miden_objects::block::account_tree::{AccountMutationSet, AccountTree};
7use miden_objects::block::{AccountWitness, BlockNumber};
8use miden_objects::crypto::merkle::{
9    EmptySubtreeRoots,
10    LargeSmt,
11    LeafIndex,
12    MemoryStorage,
13    MerkleError,
14    MerklePath,
15    NodeIndex,
16    NodeMutation,
17    SMT_DEPTH,
18    SmtLeaf,
19    SmtStorage,
20    SparseMerklePath,
21};
22use miden_objects::{AccountTreeError, EMPTY_WORD, Word};
23
24#[cfg(test)]
25mod tests;
26
27/// Convenience for an in-memory-only account tree.
28pub type InMemoryAccountTree = AccountTree<LargeSmt<MemoryStorage>>;
29
30// ACCOUNT TREE STORAGE TRAIT
31// ================================================================================================
32
33/// Trait abstracting operations over different account tree backends.
34pub trait AccountTreeStorage {
35    /// Returns the root hash of the tree.
36    fn root(&self) -> Word;
37
38    /// Returns the number of accounts in the tree.
39    fn num_accounts(&self) -> usize;
40
41    /// Opens an account and returns its witness.
42    fn open(&self, account_id: AccountId) -> AccountWitness;
43
44    /// Gets the account state commitment.
45    fn get(&self, account_id: AccountId) -> Word;
46
47    /// Computes mutations for applying account updates.
48    fn compute_mutations(
49        &self,
50        accounts: impl IntoIterator<Item = (AccountId, Word)>,
51    ) -> Result<AccountMutationSet, AccountTreeError>;
52
53    /// Applies mutations with reversion data.
54    fn apply_mutations_with_reversion(
55        &mut self,
56        mutations: AccountMutationSet,
57    ) -> Result<AccountMutationSet, AccountTreeError>;
58
59    /// Checks if the tree contains an account with the given prefix.
60    fn contains_account_id_prefix(&self, prefix: AccountIdPrefix) -> bool;
61}
62
63impl<S> AccountTreeStorage for AccountTree<LargeSmt<S>>
64where
65    S: SmtStorage,
66{
67    fn root(&self) -> Word {
68        self.root()
69    }
70
71    fn num_accounts(&self) -> usize {
72        self.num_accounts()
73    }
74
75    fn open(&self, account_id: AccountId) -> AccountWitness {
76        self.open(account_id)
77    }
78
79    fn get(&self, account_id: AccountId) -> Word {
80        self.get(account_id)
81    }
82
83    fn compute_mutations(
84        &self,
85        accounts: impl IntoIterator<Item = (AccountId, Word)>,
86    ) -> Result<AccountMutationSet, AccountTreeError> {
87        self.compute_mutations(accounts)
88    }
89
90    fn apply_mutations_with_reversion(
91        &mut self,
92        mutations: AccountMutationSet,
93    ) -> Result<AccountMutationSet, AccountTreeError> {
94        self.apply_mutations_with_reversion(mutations)
95    }
96
97    fn contains_account_id_prefix(&self, prefix: AccountIdPrefix) -> bool {
98        self.contains_account_id_prefix(prefix)
99    }
100}
101
102// HISTORICAL ERROR TYPES
103// ================================================================================================
104
105#[allow(missing_docs)]
106#[derive(thiserror::Error, Debug)]
107pub enum HistoricalError {
108    #[error(transparent)]
109    MerkleError(#[from] MerkleError),
110    #[error(transparent)]
111    AccountTreeError(#[from] AccountTreeError),
112}
113
114// HISTORICAL SELECTOR ENUM
115// ================================================================================================
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118enum HistoricalSelector {
119    /// The requested block is in the future (later than current block).
120    Future,
121    /// The requested block is available in history.
122    At(BlockNumber),
123    /// The requested block is the current/latest block.
124    Latest,
125    /// The requested block is too old and has been pruned from history.
126    TooAncient,
127}
128
129// HISTORICAL OVERLAY
130// ================================================================================================
131
132/// Captures reversion state for historical queries at a specific block.
133#[derive(Debug, Clone)]
134struct HistoricalOverlay {
135    block_number: BlockNumber,
136    root: Word,
137    node_mutations: HashMap<NodeIndex, Word>,
138    account_updates: HashMap<LeafIndex<SMT_DEPTH>, (Word, Word)>,
139}
140
141impl HistoricalOverlay {
142    fn new(block_number: BlockNumber, rev_set: AccountMutationSet) -> Self {
143        let root = rev_set.as_mutation_set().root();
144        let mut_set = rev_set.into_mutation_set();
145
146        let node_mutations =
147            HashMap::from_iter(mut_set.node_mutations().iter().map(|(node_index, mutation)| {
148                match mutation {
149                    NodeMutation::Addition(inner_node) => (*node_index, inner_node.hash()),
150                    NodeMutation::Removal => {
151                        // Store the actual empty subtree root for this depth
152                        // depth() is 1-indexed from leaf, so we use it directly for
153                        // EmptySubtreeRoots
154                        let empty_root = *EmptySubtreeRoots::entry(SMT_DEPTH, node_index.depth());
155                        (*node_index, empty_root)
156                    },
157                }
158            }));
159
160        let account_updates = HashMap::from_iter(
161            mut_set.new_pairs().iter().map(|(&k, &v)| (LeafIndex::from(k), (k, v))),
162        );
163
164        Self {
165            block_number,
166            root,
167            node_mutations,
168            account_updates,
169        }
170    }
171}
172
173// ACCOUNT TREE WITH HISTORY
174// ================================================================================================
175
176/// Wraps `AccountTree` with historical query support via reversion overlays.
177///
178/// This structure maintains a sliding window of historical account states by storing
179/// reversion data (mutations that undo changes). Historical witnesses are reconstructed
180/// by starting from the latest state and applying reversion overlays backwards in time.
181#[derive(Debug, Clone)]
182pub struct AccountTreeWithHistory<S>
183where
184    S: AccountTreeStorage,
185{
186    /// The current block number (latest state).
187    block_number: BlockNumber,
188    /// The latest account tree state.
189    latest: S,
190    /// Historical overlays indexed by block number, storing reversion data.
191    overlays: BTreeMap<BlockNumber, HistoricalOverlay>,
192}
193
194impl<S> AccountTreeWithHistory<S>
195where
196    S: AccountTreeStorage,
197{
198    /// Maximum number of historical blocks to maintain.
199    pub const MAX_HISTORY: usize = 33;
200
201    // CONSTRUCTORS
202    // --------------------------------------------------------------------------------------------
203
204    /// Creates a new historical tree starting at the given block number.
205    pub fn new(account_tree: S, block_number: BlockNumber) -> Self {
206        Self {
207            block_number,
208            latest: account_tree,
209            overlays: BTreeMap::new(),
210        }
211    }
212
213    /// Removes oldest overlays when exceeding the maximum history depth.
214    fn drain_excess(overlays: &mut BTreeMap<BlockNumber, HistoricalOverlay>) {
215        while overlays.len() > Self::MAX_HISTORY {
216            overlays.pop_first();
217        }
218    }
219
220    // PUBLIC ACCESSORS
221    // --------------------------------------------------------------------------------------------
222
223    /// Returns the latest block number.
224    pub fn block_number_latest(&self) -> BlockNumber {
225        self.block_number
226    }
227
228    /// Returns the root hash of the latest state.
229    pub fn root_latest(&self) -> Word {
230        self.latest.root()
231    }
232
233    /// Returns the root hash at a specific historical block.
234    ///
235    /// Returns `None` if the block is in the future or too old (pruned).
236    pub fn root_at(&self, block_number: BlockNumber) -> Option<Word> {
237        match self.historical_selector(block_number) {
238            HistoricalSelector::Latest => Some(self.latest.root()),
239            HistoricalSelector::At(block_number) => {
240                let overlay = self.overlays.get(&block_number)?;
241                debug_assert_eq!(overlay.block_number, block_number);
242                Some(overlay.root)
243            },
244            HistoricalSelector::Future | HistoricalSelector::TooAncient => None,
245        }
246    }
247
248    /// Returns the number of accounts in the latest state.
249    pub fn num_accounts_latest(&self) -> usize {
250        self.latest.num_accounts()
251    }
252
253    /// Returns the number of historical blocks currently stored.
254    pub fn history_len(&self) -> usize {
255        self.overlays.len()
256    }
257
258    /// Opens an account at the latest block, returning its witness.
259    pub fn open_latest(&self, account_id: AccountId) -> AccountWitness {
260        self.latest.open(account_id)
261    }
262
263    /// Opens an account at a historical block, returning its witness.
264    ///
265    /// This method reconstructs the account witness at the given historical block by:
266    /// 1. Starting with the latest account state
267    /// 2. Applying reversion mutations from the overlays to walk back in time
268    /// 3. Reconstructing the Merkle path with the historical node values
269    ///
270    /// Returns `None` if the block is in the future or too old (pruned).
271    pub fn open_at(
272        &self,
273        account_id: AccountId,
274        block_number: BlockNumber,
275    ) -> Option<AccountWitness> {
276        match self.historical_selector(block_number) {
277            HistoricalSelector::Latest => Some(self.latest.open(account_id)),
278            HistoricalSelector::At(block_number) => {
279                // Ensure overlay exists before reconstruction
280                self.overlays.get(&block_number)?;
281                Self::reconstruct_historical_witness(self, account_id, block_number)
282            },
283            HistoricalSelector::Future | HistoricalSelector::TooAncient => None,
284        }
285    }
286
287    /// Gets the account state commitment at the latest block.
288    pub fn get_latest_commitment(&self, account_id: AccountId) -> Word {
289        self.latest.get(account_id)
290    }
291
292    /// Checks if the tree contains an account with the given prefix.
293    pub fn contains_account_id_prefix_in_latest(&self, prefix: AccountIdPrefix) -> bool {
294        self.latest.contains_account_id_prefix(prefix)
295    }
296
297    // PRIVATE HELPERS - HISTORICAL RECONSTRUCTION
298    // --------------------------------------------------------------------------------------------
299
300    /// Determines the historical state selector of a requested block number.
301    fn historical_selector(&self, desired_block_number: BlockNumber) -> HistoricalSelector {
302        if desired_block_number == self.block_number {
303            return HistoricalSelector::Latest;
304        }
305
306        // Check if block is in the future
307        if self.block_number.checked_sub(desired_block_number.as_u32()).is_none() {
308            return HistoricalSelector::Future;
309        }
310
311        // Check if block exists in overlays
312        if !self.overlays.contains_key(&desired_block_number) {
313            return HistoricalSelector::TooAncient;
314        }
315
316        HistoricalSelector::At(desired_block_number)
317    }
318
319    /// Reconstructs a historical account witness by applying reversion overlays.
320    fn reconstruct_historical_witness(
321        &self,
322        account_id: AccountId,
323        block_target: BlockNumber,
324    ) -> Option<AccountWitness> {
325        // Start with the latest witness
326        let latest_witness = self.latest.open(account_id);
327        let (latest_path, leaf) = latest_witness.into_proof().into_parts();
328        let path_nodes = Self::initialize_path_nodes(&latest_path);
329
330        let leaf_index = NodeIndex::from(leaf.index());
331
332        // Apply reversion overlays to reconstruct historical state.
333        // We reverse the overlay iteration (newest to oldest) to walk backwards in time
334        // from the latest state to the target block.
335        let (path, leaf) = Self::apply_reversion_overlays(
336            self.overlays.range(block_target..).rev().map(|(_, overlay)| overlay),
337            path_nodes,
338            leaf_index,
339            leaf,
340        )?;
341
342        // Extract commitment from leaf
343        let commitment = match leaf {
344            SmtLeaf::Empty(_) => EMPTY_WORD,
345            SmtLeaf::Single((_, value)) => value,
346            SmtLeaf::Multiple(_) => unreachable!("AccountTree uses prefix-free IDs"),
347        };
348
349        AccountWitness::new(account_id, commitment, path).ok()
350    }
351
352    /// Initializes the path nodes array from the latest state.
353    ///
354    /// Converts the sparse path to a dense path and reverses it for indexing by depth from leaf.
355    fn initialize_path_nodes(path: &SparseMerklePath) -> [Word; SMT_DEPTH as usize] {
356        let mut path_nodes: [Word; SMT_DEPTH as usize] = MerklePath::from(path.clone())
357            .to_vec()
358            .try_into()
359            .expect("MerklePath should have exactly SMT_DEPTH nodes");
360        path_nodes.reverse();
361        path_nodes
362    }
363
364    /// Applies reversion overlays to reconstruct the historical state.
365    ///
366    /// Iterates through overlays from newest to oldest (walking backwards in time),
367    /// updating both the path nodes and the leaf value based on reversion mutations.
368    fn apply_reversion_overlays<'a>(
369        overlays: impl IntoIterator<Item = &'a HistoricalOverlay>,
370        mut path_nodes: [Word; SMT_DEPTH as usize],
371        leaf_index: NodeIndex,
372        mut leaf: SmtLeaf,
373    ) -> Option<(SparseMerklePath, SmtLeaf)> {
374        // Iterate through overlays
375        for overlay in overlays {
376            // Update path sibling nodes that changed in this overlay
377            for sibling in leaf_index.proof_indices() {
378                let height = sibling
379                    .depth()
380                    .checked_sub(1) // -1: Convert from 1-indexed to 0-indexed
381                    .expect("proof_indices should not include root")
382                    as usize;
383
384                // Apply reversion mutation if this node was modified.
385                // It's sound since `proof_indices()`` returns siblings on the path from leaf to
386                // root, hence the height is always less than `SMT_DEPTH`, the leaf
387                // and root are not included.
388                if let Some(hash) = overlay.node_mutations.get(&sibling) {
389                    path_nodes[height] = *hash;
390                }
391            }
392
393            // Update leaf if it was modified in this overlay
394            if let Some(&(key, value)) = overlay.account_updates.get(&leaf.index()) {
395                leaf = if value == EMPTY_WORD {
396                    SmtLeaf::new_empty(leaf.index())
397                } else {
398                    SmtLeaf::new_single(key, value)
399                };
400            }
401        }
402
403        // Build the Merkle path directly from the reconstructed nodes
404        // No need for build_dense_path since all nodes have actual values (not sentinels)
405        let dense: Vec<Word> = path_nodes.iter().rev().copied().collect();
406        let path = MerklePath::new(dense);
407        let path = SparseMerklePath::try_from(path).ok()?;
408        Some((path, leaf))
409    }
410
411    // PUBLIC MUTATORS
412    // --------------------------------------------------------------------------------------------
413
414    /// Computes and applies mutations in one operation.
415    ///
416    /// This is a convenience method primarily for testing.
417    pub fn compute_and_apply_mutations(
418        &mut self,
419        account_commitments: impl IntoIterator<Item = (AccountId, Word)>,
420    ) -> Result<(), HistoricalError> {
421        let mutations = self.compute_mutations(account_commitments)?;
422        self.apply_mutations(mutations)
423    }
424
425    /// Computes mutations relative to the latest state.
426    pub fn compute_mutations(
427        &self,
428        account_commitments: impl IntoIterator<Item = (AccountId, Word)>,
429    ) -> Result<AccountMutationSet, HistoricalError> {
430        Ok(self.latest.compute_mutations(account_commitments)?)
431    }
432
433    /// Applies mutations and advances to the next block.
434    ///
435    /// This method:
436    /// 1. Applies the mutations to the latest tree, getting back reversion data
437    /// 2. Stores the reversion data as a historical overlay
438    /// 3. Advances the block number
439    /// 4. Prunes old overlays if exceeding `MAX_HISTORY`
440    pub fn apply_mutations(
441        &mut self,
442        mutations: AccountMutationSet,
443    ) -> Result<(), HistoricalError> {
444        // Apply mutations and get reversion data
445        let rev = self.latest.apply_mutations_with_reversion(mutations)?;
446
447        // Store reversion data for current block before advancing
448        let block_num = self.block_number;
449        let overlay = HistoricalOverlay::new(block_num, rev);
450        self.overlays.insert(block_num, overlay);
451
452        // Advance to next block
453        self.block_number = block_num.child();
454
455        // Prune old history if needed
456        Self::drain_excess(&mut self.overlays);
457
458        Ok(())
459    }
460}