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