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