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