Skip to main content

miden_node_store/accounts/
mod.rs

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