Skip to main content

miden_client/sync/
state_sync_update.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use miden_protocol::account::{
5    Account,
6    AccountCode,
7    AccountHeader,
8    AccountId,
9    AccountPatch,
10    AccountStoragePatch,
11    AccountVaultPatch,
12    StorageMapPatch,
13    StorageMapPatchEntries,
14    StorageSlotName,
15    StorageSlotPatch,
16    StorageValuePatch,
17};
18use miden_protocol::block::{BlockHeader, BlockNumber};
19use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks};
20use miden_protocol::errors::AccountPatchError;
21use miden_protocol::note::{NoteId, Nullifier};
22use miden_protocol::protocol_config::ProtocolConfig;
23use miden_protocol::transaction::TransactionId;
24use miden_protocol::{Felt, ONE, Word};
25
26use super::SyncSummary;
27use crate::note::{NoteUpdateTracker, NoteUpdateType};
28use crate::rpc::domain::transaction::TransactionRecord as RpcTransactionRecord;
29use crate::transaction::{DiscardCause, TransactionRecord, TransactionStatus};
30
31// STATE SYNC UPDATE
32// ================================================================================================
33
34/// Contains all information needed to apply the update in the store after syncing with the node.
35///
36/// Immutable once built: [`StateSync::sync_state`](super::StateSync::sync_state) assembles the
37/// individual trackers and seals them into this type at the end of the sync pass. Use
38/// [`Self::from_parts`] to build one directly.
39pub struct StateSyncUpdate {
40    /// The block number of the last block that was synced.
41    block_num: BlockNumber,
42    /// New blocks, authentication nodes and MMR peaks.
43    partial_blockchain_updates: PartialBlockchainUpdates,
44    /// New and updated notes to be upserted in the store.
45    note_updates: NoteUpdateTracker,
46    /// Committed and discarded transactions after the sync.
47    transaction_updates: TransactionUpdateTracker,
48    /// Public account updates and mismatched private accounts after the sync.
49    account_updates: AccountUpdates,
50    /// The protocol configuration active at `block_num`. The node sends it when the sync starts at
51    /// genesis, or when the starting block and `block_num` commit to different configurations.
52    protocol_config: Option<ProtocolConfig>,
53}
54
55impl StateSyncUpdate {
56    /// Assembles an update from its constituent parts, mirroring [`Self::into_parts`].
57    ///
58    /// The parts are stored as given: no validation or minimization is applied.
59    pub fn from_parts(
60        block_num: BlockNumber,
61        partial_blockchain_updates: PartialBlockchainUpdates,
62        note_updates: NoteUpdateTracker,
63        transaction_updates: TransactionUpdateTracker,
64        account_updates: AccountUpdates,
65        protocol_config: Option<ProtocolConfig>,
66    ) -> Self {
67        Self {
68            block_num,
69            partial_blockchain_updates,
70            note_updates,
71            transaction_updates,
72            account_updates,
73            protocol_config,
74        }
75    }
76
77    /// Returns the block number of the last synced block.
78    pub fn block_num(&self) -> BlockNumber {
79        self.block_num
80    }
81
82    /// Returns the partial blockchain updates.
83    pub fn partial_blockchain_updates(&self) -> &PartialBlockchainUpdates {
84        &self.partial_blockchain_updates
85    }
86
87    /// Returns the note updates.
88    pub fn note_updates(&self) -> &NoteUpdateTracker {
89        &self.note_updates
90    }
91
92    /// Returns the transaction updates.
93    pub fn transaction_updates(&self) -> &TransactionUpdateTracker {
94        &self.transaction_updates
95    }
96
97    /// Returns the account updates.
98    pub fn account_updates(&self) -> &AccountUpdates {
99        &self.account_updates
100    }
101
102    /// Returns the protocol configuration the node sent with this sync, if any.
103    pub fn protocol_config(&self) -> Option<&ProtocolConfig> {
104        self.protocol_config.as_ref()
105    }
106
107    /// Decomposes this update into its constituent parts.
108    pub fn into_parts(
109        self,
110    ) -> (
111        BlockNumber,
112        PartialBlockchainUpdates,
113        NoteUpdateTracker,
114        TransactionUpdateTracker,
115        AccountUpdates,
116        Option<ProtocolConfig>,
117    ) {
118        (
119            self.block_num,
120            self.partial_blockchain_updates,
121            self.note_updates,
122            self.transaction_updates,
123            self.account_updates,
124            self.protocol_config,
125        )
126    }
127}
128
129impl From<&StateSyncUpdate> for SyncSummary {
130    fn from(value: &StateSyncUpdate) -> Self {
131        let new_public_note_ids = value
132            .note_updates
133            .updated_input_notes()
134            .filter_map(|note_update| {
135                let note = note_update.inner();
136                if let NoteUpdateType::Insert = note_update.update_type() {
137                    note.id()
138                } else {
139                    None
140                }
141            })
142            .collect();
143
144        let committed_note_ids: BTreeSet<NoteId> = value
145            .note_updates
146            .updated_input_notes()
147            .filter_map(|note_update| {
148                let note = note_update.inner();
149                // `InsertCommitted` is a previously-tracked expected note that just committed, so
150                // it counts as committed (not as a newly-discovered note) even though it is
151                // persisted via a full-row insert.
152                if matches!(
153                    note_update.update_type(),
154                    NoteUpdateType::Update | NoteUpdateType::InsertCommitted
155                ) && note.is_committed()
156                {
157                    note.id()
158                } else {
159                    None
160                }
161            })
162            .chain(value.note_updates.updated_output_notes().filter_map(|note_update| {
163                let note = note_update.inner();
164                if let NoteUpdateType::Update = note_update.update_type() {
165                    note.is_committed().then_some(note.id())
166                } else {
167                    None
168                }
169            }))
170            .collect();
171
172        let consumed_note_ids: BTreeSet<NoteId> =
173            value.note_updates.consumed_input_note_ids().collect();
174
175        SyncSummary::new(
176            value.block_num,
177            new_public_note_ids,
178            // Populated by Client::sync_state from the Note Transport Layer fetch.
179            Vec::new(),
180            committed_note_ids.into_iter().collect(),
181            consumed_note_ids.into_iter().collect(),
182            value
183                .account_updates
184                .updated_public_accounts()
185                .iter()
186                .map(PublicAccountUpdate::id)
187                .collect(),
188            value
189                .account_updates
190                .mismatched_private_accounts()
191                .iter()
192                .map(|(id, _)| *id)
193                .collect(),
194            value.transaction_updates.committed_transactions().map(|t| t.id).collect(),
195        )
196    }
197}
198
199/// Contains all the partial blockchain information that needs to be added in the client's store
200/// after a sync: block headers, authentication nodes and the MMR peaks at the new sync height.
201///
202/// Insert-only: entries are staged once known to be worth keeping, never revised or removed.
203#[derive(Debug, Clone, Default)]
204pub struct PartialBlockchainUpdates {
205    /// New block headers to be stored, keyed by block number. The value contains the block header
206    /// and a flag indicating whether the block is relevant and should remain tracked.
207    block_headers: BTreeMap<BlockNumber, (BlockHeader, bool)>,
208    /// New authentication nodes that are meant to be stored in order to authenticate block headers.
209    new_authentication_nodes: Vec<(InOrderIndex, Word)>,
210    /// MMR peaks at the new sync height.
211    pub new_peaks: MmrPeaks,
212}
213
214impl PartialBlockchainUpdates {
215    /// Adds a block header to this [`PartialBlockchainUpdates`].
216    ///
217    /// On a repeated block number the `is_relevant` flag is OR-ed — the chain tip block may itself
218    /// be relevant — so it only ever moves from `false` to `true`, matching
219    /// [`Store::insert_block_header`](crate::store::Store::insert_block_header)'s one-way upgrade.
220    pub fn insert(&mut self, block_header: BlockHeader, is_relevant: bool) {
221        self.block_headers
222            .entry(block_header.block_num())
223            .and_modify(|(_, existing_is_relevant)| {
224                *existing_is_relevant |= is_relevant;
225            })
226            .or_insert((block_header, is_relevant));
227    }
228
229    /// Stages authentication nodes for storage.
230    ///
231    /// Kept as one flat set rather than per-header, since tracked blocks' paths share internal
232    /// nodes.
233    pub fn extend_authentication_nodes(
234        &mut self,
235        nodes: impl IntoIterator<Item = (InOrderIndex, Word)>,
236    ) {
237        self.new_authentication_nodes.extend(nodes);
238    }
239
240    /// Returns the new block headers to be stored, along with a flag indicating whether each block
241    /// is relevant and should remain tracked.
242    pub fn block_headers(&self) -> impl Iterator<Item = &(BlockHeader, bool)> {
243        self.block_headers.values()
244    }
245
246    /// Returns block headers that need to be persisted for this update.
247    pub fn block_headers_to_store(
248        &self,
249        sync_height: BlockNumber,
250    ) -> impl Iterator<Item = &(BlockHeader, bool)> {
251        self.block_headers.values().filter(move |(header, is_relevant)| {
252            *is_relevant
253                || header.block_num() == BlockNumber::GENESIS
254                || header.block_num() == sync_height
255        })
256    }
257
258    /// Returns the new authentication nodes that are meant to be stored in order to authenticate
259    /// block headers.
260    pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
261        &self.new_authentication_nodes
262    }
263}
264
265/// Contains transaction changes to apply to the store.
266#[derive(Default)]
267pub struct TransactionUpdateTracker {
268    /// Transactions that were committed in the block.
269    transactions: BTreeMap<TransactionId, TransactionRecord>,
270    /// Nullifier-to-account mappings from external transactions by tracked accounts.
271    external_nullifier_accounts: BTreeMap<Nullifier, AccountId>,
272}
273
274impl TransactionUpdateTracker {
275    /// Creates a new [`TransactionUpdateTracker`]
276    pub fn new(transactions: Vec<TransactionRecord>) -> Self {
277        let transactions =
278            transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
279
280        Self {
281            transactions,
282            external_nullifier_accounts: BTreeMap::new(),
283        }
284    }
285
286    /// Returns a reference to committed transactions.
287    pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
288        self.transactions
289            .values()
290            .filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
291    }
292
293    /// Returns a reference to discarded transactions.
294    pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
295        self.transactions
296            .values()
297            .filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
298    }
299
300    /// Returns a mutable reference to pending transactions in the tracker.
301    fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
302        self.transactions
303            .values_mut()
304            .filter(|tx| matches!(tx.status, TransactionStatus::Pending))
305    }
306
307    /// Returns transaction IDs of all transactions that have been updated.
308    pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
309        self.committed_transactions()
310            .chain(self.discarded_transactions())
311            .map(|tx| tx.id)
312    }
313
314    /// Returns the account ID that consumed the given nullifier in an external transaction, if
315    /// available.
316    pub fn external_nullifier_account(&self, nullifier: &Nullifier) -> Option<AccountId> {
317        self.external_nullifier_accounts.get(nullifier).copied()
318    }
319
320    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a
321    /// transaction is included in a block.
322    pub fn apply_transaction_inclusion(&mut self, record: &RpcTransactionRecord, timestamp: u64) {
323        let header = &record.transaction_header;
324        let account_id = header.account_id();
325
326        if let Some(transaction) = self.transactions.get_mut(&header.id()) {
327            transaction.commit_transaction(record.block_num, timestamp);
328            return;
329        }
330
331        // Fallback for transactions with unauthenticated input notes: the node authenticates these
332        // notes during processing, which changes the transaction ID. Match by account ID and
333        // pre-transaction state instead.
334        if let Some(transaction) = self.transactions.values_mut().find(|tx| {
335            tx.details.account_id == account_id
336                && tx.details.init_account_state == header.initial_state_commitment()
337        }) {
338            transaction.commit_transaction(record.block_num, timestamp);
339            return;
340        }
341
342        // No local transaction matched. This is an external transaction by a tracked account.
343        // Record the nullifier→account mappings so we can attribute note consumption to tracked
344        // accounts during nullifier processing.
345        for commitment in header.input_notes().iter() {
346            self.external_nullifier_accounts.insert(commitment.nullifier(), account_id);
347        }
348    }
349
350    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a the sync
351    /// height of the client is updated. This may result in stale or expired transactions.
352    pub fn apply_sync_height_update(
353        &mut self,
354        new_sync_height: BlockNumber,
355        tx_discard_delta: Option<u32>,
356    ) {
357        if let Some(tx_discard_delta) = tx_discard_delta {
358            self.discard_transaction_with_predicate(
359                |transaction| {
360                    transaction.details.submission_height
361                        < new_sync_height.checked_sub(tx_discard_delta).unwrap_or_default()
362                },
363                DiscardCause::Stale,
364            );
365        }
366
367        // NOTE: we check for <= new_sync height because at this point we would have committed the
368        // transaction otherwise
369        self.discard_transaction_with_predicate(
370            |transaction| transaction.details.expiration_block_num <= new_sync_height,
371            DiscardCause::Expired,
372        );
373    }
374
375    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a note is
376    /// nullified. this may result in transactions being discarded because they were processing the
377    /// nullified note.
378    pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
379        self.discard_transaction_with_predicate(
380            |transaction| {
381                // Check if the note was being processed by a local transaction that didn't end up
382                // being committed so it should be discarded
383                transaction
384                    .details
385                    .input_note_nullifiers
386                    .contains(&input_note_nullifier.as_word())
387            },
388            DiscardCause::InputConsumed,
389        );
390    }
391
392    /// Discards the local transaction that produced this now-superseded account state.
393    pub fn apply_superseded_account_state(&mut self, superseded_account_state: Word) {
394        self.discard_transaction_with_predicate(
395            |transaction| transaction.details.final_account_state == superseded_account_state,
396            DiscardCause::Superseded,
397        );
398    }
399
400    /// Discards transactions that have the same initial account state as the provided one.
401    pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Word) {
402        self.discard_transaction_with_predicate(
403            |transaction| transaction.details.init_account_state == invalid_account_state,
404            DiscardCause::DiscardedInitialState,
405        );
406    }
407
408    /// Discards transactions that match the predicate and also applies the new invalid account
409    /// states
410    fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
411    where
412        F: Fn(&TransactionRecord) -> bool,
413    {
414        let mut new_invalid_account_states = vec![];
415
416        for transaction in self.mutable_pending_transactions() {
417            // Discard transactions, and also push the invalid account state if the transaction got
418            // correctly discarded
419            // NOTE: previous updates in a chain of state syncs could have committed a transaction,
420            // so we need to check that `discard_transaction` returns `true` here (aka, it got
421            // discarded from a valid state)
422            if predicate(transaction) && transaction.discard_transaction(discard_cause) {
423                new_invalid_account_states.push(transaction.details.final_account_state);
424            }
425        }
426
427        for state in new_invalid_account_states {
428            self.apply_invalid_initial_account_state(state);
429        }
430    }
431}
432
433// PUBLIC ACCOUNT UPDATE
434// ================================================================================================
435
436/// Update to a single tracked public account.
437///
438/// `StateSync` emits one of two variants depending on whether the node could return the account's
439/// full state in a single response:
440///
441/// - [`PublicAccountUpdate::Full`] carries the new [`Account`] state directly (used when no storage
442///   map is oversized and the vault fits in the response). The store applies it by replacing the
443///   local state.
444/// - [`PublicAccountUpdate::Patch`] carries the new account header plus the absolute
445///   [`AccountPatch`] built from the node's incremental endpoints (`sync_storage_maps` and
446///   `sync_account_vault`, used when any part of the account is oversized). The header is included
447///   because the patch does not carry the final commitments.
448#[derive(Debug, Clone)]
449pub enum PublicAccountUpdate {
450    /// The account fits in a single proof response — the new full state is carried as-is.
451    Full(Account),
452    /// The account is oversized in some dimension. The new state is described by the absolute
453    /// patch, which advances the local state to `new_header`.
454    Patch {
455        /// The new account header after applying the patch.
456        new_header: AccountHeader,
457        /// The absolute patch to apply.
458        patch: AccountPatch,
459    },
460}
461
462impl PublicAccountUpdate {
463    /// Returns the account ID for this update.
464    pub fn id(&self) -> AccountId {
465        match self {
466            Self::Full(account) => account.id(),
467            Self::Patch { new_header, .. } => new_header.id(),
468        }
469    }
470
471    /// Returns the account nonce that this update advances the local state to.
472    pub fn nonce(&self) -> Felt {
473        match self {
474            Self::Full(account) => account.nonce(),
475            Self::Patch { new_header, .. } => new_header.nonce(),
476        }
477    }
478}
479
480/// Builds the absolute [`AccountPatch`] implied by the updates fetched from the node's incremental
481/// endpoints: the value-slot values, the absolute changed map entries per slot, and the absolute
482/// vault patch.
483///
484/// The carried updates are already merged to the new absolute value of each changed storage slot,
485/// map entry, and vault asset, so the patch is assembled directly from them with no need to load
486/// the prior account state.
487///
488/// An update of an existing account (final nonce > 1) yields a partial-state patch with no code. A
489/// newly created account (final nonce 1) cannot be represented as a partial-state patch, so the
490/// patch becomes a full-state patch carrying `code` (already validated against the on-chain code
491/// commitment by the caller).
492pub(crate) fn build_account_patch(
493    new_header: &AccountHeader,
494    value_slot_updates: Vec<(StorageSlotName, Word)>,
495    map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries>,
496    vault_patch: AccountVaultPatch,
497    code: AccountCode,
498) -> Result<AccountPatch, AccountPatchError> {
499    let is_full_state = new_header.nonce() == ONE;
500
501    let value_entries = value_slot_updates.into_iter().map(|(slot_name, new_value)| {
502        let value_patch = if is_full_state {
503            StorageValuePatch::Create { value: new_value }
504        } else {
505            StorageValuePatch::Update { value: new_value }
506        };
507        (slot_name, StorageSlotPatch::Value(value_patch))
508    });
509
510    let map_entries = map_entries.into_iter().map(|(slot_name, entries)| {
511        let map_patch = if is_full_state {
512            StorageMapPatch::Create { entries }
513        } else {
514            StorageMapPatch::Update { entries }
515        };
516        (slot_name, StorageSlotPatch::Map(map_patch))
517    });
518
519    let storage = AccountStoragePatch::from_entries(value_entries.chain(map_entries))?;
520
521    let code = is_full_state.then_some(code);
522
523    AccountPatch::new(new_header.id(), storage, vault_patch, code, Some(new_header.nonce()))
524}
525
526// ACCOUNT UPDATES
527// ================================================================================================
528
529/// Contains account changes to apply to the store after a sync request.
530#[derive(Debug, Clone, Default)]
531#[allow(clippy::struct_field_names)]
532pub struct AccountUpdates {
533    /// Updated public accounts, either as full state replacements or incremental patches.
534    updated_public_accounts: Vec<PublicAccountUpdate>,
535    /// Account commitments received from the network that don't match the currently locally-tracked
536    /// state of the private accounts.
537    ///
538    /// These updates may represent a stale account commitment (meaning that the latest local state
539    /// hasn't been committed). If this is not the case, the account may be locked until the state
540    /// is restored manually.
541    mismatched_private_accounts: Vec<(AccountId, Word)>,
542}
543
544impl AccountUpdates {
545    /// Creates a new instance of `AccountUpdates`.
546    pub fn new(
547        updated_public_accounts: Vec<PublicAccountUpdate>,
548        mismatched_private_accounts: Vec<(AccountId, Word)>,
549    ) -> Self {
550        Self {
551            updated_public_accounts,
552            mismatched_private_accounts,
553        }
554    }
555
556    /// Returns the updated public accounts.
557    pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
558        &self.updated_public_accounts
559    }
560
561    /// Returns the mismatched private accounts.
562    pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
563        &self.mismatched_private_accounts
564    }
565
566    pub fn extend(&mut self, other: AccountUpdates) {
567        self.updated_public_accounts.extend(other.updated_public_accounts);
568        self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
569    }
570}
571
572// TESTS
573// ================================================================================================
574
575#[cfg(test)]
576mod tests {
577    use alloc::collections::BTreeMap;
578    use alloc::vec;
579
580    use miden_protocol::account::{AccountCode, StorageMapKey, StorageMapPatchEntries};
581    use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
582
583    use super::*;
584
585    fn account_id() -> AccountId {
586        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap()
587    }
588
589    fn slot_name(name: &str) -> StorageSlotName {
590        StorageSlotName::new(name).unwrap()
591    }
592
593    fn word(n: u64) -> Word {
594        Word::from([
595            Felt::new_unchecked(n),
596            Felt::new_unchecked(0),
597            Felt::new_unchecked(0),
598            Felt::new_unchecked(0),
599        ])
600    }
601
602    fn header_with_nonce(nonce: u64) -> AccountHeader {
603        AccountHeader::new(
604            account_id(),
605            Felt::new(nonce).expect("test nonce must be a valid Felt"),
606            Word::default(),
607            Word::default(),
608            Word::default(),
609        )
610    }
611
612    fn build_patch(
613        new_nonce: u64,
614        value_slot_updates: Vec<(StorageSlotName, Word)>,
615        map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries>,
616    ) -> Result<AccountPatch, AccountPatchError> {
617        build_account_patch(
618            &header_with_nonce(new_nonce),
619            value_slot_updates,
620            map_entries,
621            AccountVaultPatch::default(),
622            AccountCode::mock(),
623        )
624    }
625
626    #[test]
627    fn build_patch_empty_payload_carries_only_nonce() {
628        let patch = build_patch(4, vec![], BTreeMap::new()).unwrap();
629
630        assert_eq!(patch.final_nonce(), Some(Felt::new_unchecked(4)));
631        assert!(patch.storage().is_empty());
632        assert!(patch.vault().is_empty());
633        assert!(!patch.is_full_state());
634    }
635
636    #[test]
637    fn build_patch_sets_value_slot_absolutely() {
638        let value_slot = slot_name("miden::test::value");
639        let patch = build_patch(2, vec![(value_slot.clone(), word(2))], BTreeMap::new()).unwrap();
640
641        assert_eq!(patch.storage().updated_value(&value_slot), Some(word(2)));
642    }
643
644    #[test]
645    fn build_patch_wraps_merged_map_entries() {
646        let map_slot = slot_name("miden::test::map");
647        let key = StorageMapKey::from_raw(word(42));
648        let mut entries = StorageMapPatchEntries::new();
649        entries.insert(key, word(300));
650        let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
651
652        let patch = build_patch(2, vec![], map_entries).unwrap();
653
654        let entries =
655            patch.storage().updated_map(&map_slot).expect("patch should contain map slot");
656        assert_eq!(entries.as_map().len(), 1);
657        assert_eq!(*entries.as_map().values().next().unwrap(), word(300));
658    }
659
660    #[test]
661    fn build_patch_rejects_zero_nonce() {
662        let result = build_patch(0, vec![], BTreeMap::new());
663        assert!(result.is_err());
664    }
665
666    /// A newly created account (final nonce 1) observed via the oversized sync path yields a
667    /// full-state patch carrying the supplied code, rather than failing to build.
668    #[test]
669    fn build_patch_for_new_account_is_full_state() {
670        let value_slot = slot_name("miden::test::value");
671        let patch = build_patch(1, vec![(value_slot, word(1))], BTreeMap::new()).unwrap();
672
673        assert!(patch.is_full_state());
674        assert_eq!(patch.final_nonce(), Some(ONE));
675    }
676
677    /// A newly created account (final nonce 1, full-state) emits each map slot as a `Create`, which
678    /// the store applies by starting the slot from an empty map.
679    #[test]
680    fn build_patch_emits_map_create_for_new_account() {
681        let map_slot = slot_name("miden::test::map");
682        let mut entries = StorageMapPatchEntries::new();
683        entries.insert(StorageMapKey::from_raw(word(1)), word(100));
684        let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
685
686        let patch = build_patch(1, vec![], map_entries).unwrap();
687
688        assert!(patch.storage().created_map(&map_slot).is_some());
689    }
690
691    /// An update to an existing account (final nonce > 1) emits map slots as `Update`, never
692    /// `Create`, so the sync path never asks the store to re-create a populated map.
693    #[test]
694    fn build_patch_emits_map_update_for_existing_account() {
695        let map_slot = slot_name("miden::test::map");
696        let mut entries = StorageMapPatchEntries::new();
697        entries.insert(StorageMapKey::from_raw(word(1)), word(100));
698        let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
699
700        let patch = build_patch(2, vec![], map_entries).unwrap();
701
702        assert!(patch.storage().updated_map(&map_slot).is_some());
703    }
704}