Skip to main content

miden_protocol/block/account_tree/
partial.rs

1use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf};
2
3use super::{AccountIdKey, AccountWitness};
4use crate::Word;
5use crate::account::AccountId;
6use crate::errors::AccountTreeError;
7
8/// The partial sparse merkle tree containing the state commitments of accounts in the chain.
9///
10/// This is the partial version of [`AccountTree`](crate::block::account_tree::AccountTree).
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct PartialAccountTree {
13    smt: PartialSmt,
14}
15
16impl PartialAccountTree {
17    // CONSTRUCTORS
18    // --------------------------------------------------------------------------------------------
19
20    /// Creates a new partial account tree with the provided root that does not track any account
21    /// IDs.
22    pub fn new(root: Word) -> Self {
23        PartialAccountTree { smt: PartialSmt::new(root) }
24    }
25
26    /// Returns a new [`PartialAccountTree`] instantiated with the provided entries.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if:
31    /// - the merkle paths of the witnesses do not result in the same tree root.
32    /// - there are multiple witnesses for the same ID _prefix_.
33    pub fn with_witnesses(
34        witnesses: impl IntoIterator<Item = AccountWitness>,
35    ) -> Result<Self, AccountTreeError> {
36        let mut witnesses = witnesses.into_iter();
37
38        let Some(first_witness) = witnesses.next() else {
39            return Ok(Self::default());
40        };
41
42        // Construct a partial account tree with the root of the first witness.
43        // SAFETY: This is guaranteed to _not_ result in a tree with more than one entry because
44        // the account witness type guarantees that it tracks zero or one entries.
45        let partial_smt = PartialSmt::from_proofs([first_witness.into_proof()])
46            .map_err(AccountTreeError::TreeRootConflict)?;
47        let mut tree = PartialAccountTree { smt: partial_smt };
48
49        // Add all remaining witnesses to the tree, which validates the invariants of the account
50        // tree.
51        for witness in witnesses {
52            tree.track_account(witness)?;
53        }
54
55        Ok(tree)
56    }
57
58    // PUBLIC ACCESSORS
59    // --------------------------------------------------------------------------------------------
60
61    /// Returns an opening of the leaf associated with the `account_id`. This is a proof of the
62    /// current state commitment of the given account ID.
63    ///
64    /// Conceptually, an opening is a Merkle path to the leaf, as well as the leaf itself.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if:
69    /// - the account ID is not tracked by this account tree.
70    pub fn open(&self, account_id: AccountId) -> Result<AccountWitness, AccountTreeError> {
71        let key = AccountIdKey::from(account_id).as_word();
72
73        self.smt
74            .open(&key)
75            .map(|proof| AccountWitness::from_smt_proof(account_id, proof))
76            .map_err(|source| AccountTreeError::UntrackedAccountId { id: account_id, source })
77    }
78
79    /// Returns the current state commitment of the given account ID.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if:
84    /// - the account ID is not tracked by this account tree.
85    pub fn get(&self, account_id: AccountId) -> Result<Word, AccountTreeError> {
86        let key = AccountIdKey::from(account_id).as_word();
87        self.smt
88            .get_value(&key)
89            .map_err(|source| AccountTreeError::UntrackedAccountId { id: account_id, source })
90    }
91
92    /// Returns the root of the tree.
93    pub fn root(&self) -> Word {
94        self.smt.root()
95    }
96
97    // PUBLIC MUTATORS
98    // --------------------------------------------------------------------------------------------
99
100    /// Adds the given account witness to the partial tree and tracks it. Once an account has
101    /// been added to the tree, it can be updated using [`Self::upsert_state_commitments`].
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if:
106    /// - after the witness' merkle path was added, the partial account tree has a different root
107    ///   than before it was added (except when the first witness is added).
108    /// - there exists a leaf in the tree whose account ID prefix matches the one in the provided
109    ///   witness.
110    pub fn track_account(&mut self, witness: AccountWitness) -> Result<(), AccountTreeError> {
111        let id_prefix = witness.id().prefix();
112        let id_key = AccountIdKey::from(witness.id()).as_word();
113
114        // If there exists a tracked leaf with a non-empty entry whose key differs from the one
115        // we're about to track, then two different account IDs share the same prefix, which is an
116        // error.
117        //
118        // Note that if the leaf is empty, that's fine: `PartialSmt::get_leaf` returns
119        // `Ok(SmtLeaf::Empty)` for any leaf position reachable through provably-empty subtrees,
120        // even if no proof was explicitly added for that position. In a sparse tree this covers
121        // most of the leaf space, so treating empty leaves as duplicates would reject nearly every
122        // second witness.
123        //
124        // Also note that the multiple variant cannot occur by construction of the account tree.
125        if let Ok(SmtLeaf::Single((existing_key, _))) = self.smt.get_leaf(&id_key)
126            && id_key != existing_key
127        {
128            return Err(AccountTreeError::DuplicateIdPrefix { duplicate_prefix: id_prefix });
129        }
130
131        self.smt
132            .add_proof(witness.into_proof())
133            .map_err(AccountTreeError::TreeRootConflict)?;
134
135        Ok(())
136    }
137
138    /// Inserts or updates the provided account ID -> state commitment updates into the partial tree
139    /// which results in a new tree root.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if:
144    /// - the prefix of the account ID already exists in the tree.
145    /// - the account_id is not tracked by this partial account tree.
146    pub fn upsert_state_commitments(
147        &mut self,
148        updates: impl IntoIterator<Item = (AccountId, Word)>,
149    ) -> Result<(), AccountTreeError> {
150        for (account_id, state_commitment) in updates {
151            self.insert(account_id, state_commitment)?;
152        }
153
154        Ok(())
155    }
156
157    /// Inserts the state commitment for the given account ID, returning the previous state
158    /// commitment associated with that ID.
159    ///
160    /// This also recomputes all hashes between the leaf (associated with the key) and the root,
161    /// updating the root itself.
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if:
166    /// - the prefix of the account ID already exists in the tree.
167    /// - the account_id is not tracked by this partial account tree.
168    fn insert(
169        &mut self,
170        account_id: AccountId,
171        state_commitment: Word,
172    ) -> Result<Word, AccountTreeError> {
173        let key = AccountIdKey::from(account_id).as_word();
174
175        // If there exists a tracked leaf whose key is _not_ the one we're about to overwrite, then
176        // we would insert the new commitment next to an existing account ID with the same prefix,
177        // which is an error.
178        // Note that if the leaf is empty, that's fine. It means it is tracked by the partial SMT,
179        // but no account ID is inserted yet.
180        // Also note that the multiple variant cannot occur by construction of the tree.
181        if let Ok(SmtLeaf::Single((existing_key, _))) = self.smt.get_leaf(&key)
182            && key != existing_key
183        {
184            return Err(AccountTreeError::DuplicateIdPrefix {
185                duplicate_prefix: account_id.prefix(),
186            });
187        }
188
189        self.smt
190            .insert(key, state_commitment)
191            .map_err(|source| AccountTreeError::UntrackedAccountId { id: account_id, source })
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use assert_matches::assert_matches;
198    use miden_crypto::merkle::smt::Smt;
199
200    use super::*;
201    use crate::block::account_tree::AccountTree;
202    use crate::block::account_tree::tests::setup_duplicate_prefix_ids;
203    use crate::testing::account_id::AccountIdBuilder;
204
205    #[test]
206    fn insert_fails_on_duplicate_prefix() -> anyhow::Result<()> {
207        let mut full_tree = AccountTree::<Smt>::default();
208
209        let [(id0, commitment0), (id1, commitment1)] = setup_duplicate_prefix_ids();
210
211        full_tree.insert(id0, commitment0).unwrap();
212        let witness = full_tree.open(id0);
213
214        let mut partial_tree = PartialAccountTree::with_witnesses([witness])?;
215
216        partial_tree.insert(id0, commitment0).unwrap();
217        assert_eq!(partial_tree.get(id0).unwrap(), commitment0);
218
219        let err = partial_tree.insert(id1, commitment1).unwrap_err();
220
221        assert_matches!(err, AccountTreeError::DuplicateIdPrefix {
222          duplicate_prefix
223        } if duplicate_prefix == id0.prefix());
224
225        partial_tree.upsert_state_commitments([(id1, commitment1)]).unwrap_err();
226
227        assert_matches!(err, AccountTreeError::DuplicateIdPrefix {
228          duplicate_prefix
229        } if duplicate_prefix == id0.prefix());
230
231        Ok(())
232    }
233
234    #[test]
235    fn insert_succeeds_on_multiple_updates() {
236        let mut full_tree = AccountTree::<Smt>::default();
237        let [(id0, commitment0), (_, commitment1)] = setup_duplicate_prefix_ids();
238
239        full_tree.insert(id0, commitment0).unwrap();
240        let witness = full_tree.open(id0);
241
242        let mut partial_tree = PartialAccountTree::new(full_tree.root());
243
244        partial_tree.track_account(witness.clone()).unwrap();
245        assert_eq!(
246            partial_tree.open(id0).unwrap(),
247            witness,
248            "full tree witness and partial tree witness should be the same"
249        );
250        assert_eq!(
251            partial_tree.root(),
252            full_tree.root(),
253            "full tree root and partial tree root should be the same"
254        );
255
256        partial_tree.insert(id0, commitment0).unwrap();
257        partial_tree.insert(id0, commitment1).unwrap();
258        assert_eq!(partial_tree.get(id0).unwrap(), commitment1);
259    }
260
261    /// Check that updating an account ID in the partial account tree fails if that ID is not
262    /// tracked.
263    #[test]
264    fn upsert_state_commitments_fails_on_untracked_key() -> anyhow::Result<()> {
265        let id0 = AccountIdBuilder::default().build_with_seed([5; 32]);
266        let id2 = AccountIdBuilder::default().build_with_seed([6; 32]);
267
268        let commitment0 = Word::from([1, 2, 3, 4u32]);
269        let commitment2 = Word::from([2, 3, 4, 5u32]);
270
271        let account_tree = AccountTree::with_entries([(id0, commitment0), (id2, commitment2)])?;
272        // Let the partial account tree only track id0, not id2.
273        let mut partial_tree = PartialAccountTree::with_witnesses([account_tree.open(id0)])?;
274
275        let err = partial_tree.upsert_state_commitments([(id2, commitment0)]).unwrap_err();
276        assert_matches!(err, AccountTreeError::UntrackedAccountId { id, .. }
277            if id == id2
278        );
279
280        Ok(())
281    }
282
283    /// Verifies that tracking multiple witnesses succeeds in a sparse tree, where most leaf
284    /// positions are reachable through provably-empty subtrees, including `SmtLeaf::Empty`
285    /// leaves that are provably empty but not actually occupied.
286    #[test]
287    fn track_succeeds_for_multiple_witnesses_in_sparse_tree() -> anyhow::Result<()> {
288        let id0 = AccountIdBuilder::default().build_with_seed([10; 32]);
289        let id1 = AccountIdBuilder::default().build_with_seed([11; 32]);
290        let id2 = AccountIdBuilder::default().build_with_seed([12; 32]);
291
292        let commitment0 = Word::from([1, 2, 3, 4u32]);
293        let commitment1 = Word::from([5, 6, 7, 8u32]);
294
295        // Create a tree with only one account (very sparse).
296        let account_tree = AccountTree::with_entries([(id0, commitment0)])?;
297
298        // Get witnesses for one existing and two new (empty) accounts.
299        let witness0 = account_tree.open(id0);
300        let witness1 = account_tree.open(id1);
301        let witness2 = account_tree.open(id2);
302
303        // Building a partial tree from all three witnesses should succeed:
304        // id1 and id2 have empty leaves that are provably empty via the sparse tree structure,
305        // but they are NOT duplicates of id0.
306        let mut partial_tree =
307            PartialAccountTree::with_witnesses([witness0, witness1.clone(), witness2])?;
308
309        // Adding the same witness again should also succeed.
310        partial_tree.track_account(witness1)?;
311
312        // Verify the existing account has its commitment.
313        assert_eq!(partial_tree.get(id0)?, commitment0);
314
315        // We should be able to insert new state commitments for the new accounts.
316        partial_tree.upsert_state_commitments([(id1, commitment1)])?;
317        assert_eq!(partial_tree.get(id1)?, commitment1);
318
319        Ok(())
320    }
321
322    #[test]
323    fn track_fails_on_duplicate_prefix() {
324        // Use a raw Smt since an account tree would not allow us to get the witnesses for two
325        // account IDs with the same prefix.
326        let full_tree = Smt::with_entries(
327            setup_duplicate_prefix_ids()
328                .map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment)),
329        )
330        .unwrap();
331
332        let [(id0, _), (id1, _)] = setup_duplicate_prefix_ids();
333
334        let key0 = AccountIdKey::from(id0).as_word();
335        let key1 = AccountIdKey::from(id1).as_word();
336        let proof0 = full_tree.open(&key0);
337        let proof1 = full_tree.open(&key1);
338        assert_eq!(proof0.leaf(), proof1.leaf());
339
340        let witness0 =
341            AccountWitness::new(id0, proof0.get(&key0).unwrap(), proof0.into_parts().0).unwrap();
342        let witness1 =
343            AccountWitness::new(id1, proof1.get(&key1).unwrap(), proof1.into_parts().0).unwrap();
344
345        let mut partial_tree = PartialAccountTree::with_witnesses([witness0]).unwrap();
346        let err = partial_tree.track_account(witness1).unwrap_err();
347
348        assert_matches!(err, AccountTreeError::DuplicateIdPrefix { duplicate_prefix, .. }
349          if duplicate_prefix == id1.prefix()
350        )
351    }
352}