1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use miden_protocol::account::{
5 Account,
6 AccountDelta,
7 AccountHeader,
8 AccountId,
9 AccountStorage,
10 AccountStorageDelta,
11 AccountVaultDelta,
12 StorageMapKey,
13 StorageSlotName,
14};
15use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey};
16use miden_protocol::block::{BlockHeader, BlockNumber};
17use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks};
18use miden_protocol::errors::{AccountDeltaError, AccountError};
19use miden_protocol::note::{NoteId, Nullifier};
20use miden_protocol::transaction::TransactionId;
21use miden_protocol::{Felt, Word};
22
23use super::SyncSummary;
24use crate::note::{NoteUpdateTracker, NoteUpdateType};
25use crate::rpc::domain::account_vault::AccountVaultUpdate;
26use crate::rpc::domain::storage_map::StorageMapUpdate;
27use crate::rpc::domain::transaction::TransactionRecord as RpcTransactionRecord;
28use crate::transaction::{DiscardCause, TransactionRecord, TransactionStatus};
29
30#[derive(Default)]
35pub struct StateSyncUpdate {
36 pub block_num: BlockNumber,
38 pub partial_blockchain_updates: PartialBlockchainUpdates,
40 pub note_updates: NoteUpdateTracker,
42 pub transaction_updates: TransactionUpdateTracker,
44 pub account_updates: AccountUpdates,
46}
47
48impl From<&StateSyncUpdate> for SyncSummary {
49 fn from(value: &StateSyncUpdate) -> Self {
50 let new_public_note_ids = value
51 .note_updates
52 .updated_input_notes()
53 .filter_map(|note_update| {
54 let note = note_update.inner();
55 if let NoteUpdateType::Insert = note_update.update_type() {
56 note.id()
57 } else {
58 None
59 }
60 })
61 .collect();
62
63 let committed_note_ids: BTreeSet<NoteId> = value
64 .note_updates
65 .updated_input_notes()
66 .filter_map(|note_update| {
67 let note = note_update.inner();
68 if matches!(
72 note_update.update_type(),
73 NoteUpdateType::Update | NoteUpdateType::InsertCommitted
74 ) && note.is_committed()
75 {
76 note.id()
77 } else {
78 None
79 }
80 })
81 .chain(value.note_updates.updated_output_notes().filter_map(|note_update| {
82 let note = note_update.inner();
83 if let NoteUpdateType::Update = note_update.update_type() {
84 note.is_committed().then_some(note.id())
85 } else {
86 None
87 }
88 }))
89 .collect();
90
91 let consumed_note_ids: BTreeSet<NoteId> =
92 value.note_updates.consumed_input_note_ids().collect();
93
94 SyncSummary::new(
95 value.block_num,
96 new_public_note_ids,
97 Vec::new(),
99 committed_note_ids.into_iter().collect(),
100 consumed_note_ids.into_iter().collect(),
101 value
102 .account_updates
103 .updated_public_accounts()
104 .iter()
105 .map(PublicAccountUpdate::id)
106 .collect(),
107 value
108 .account_updates
109 .mismatched_private_accounts()
110 .iter()
111 .map(|(id, _)| *id)
112 .collect(),
113 value.transaction_updates.committed_transactions().map(|t| t.id).collect(),
114 )
115 }
116}
117
118#[derive(Debug, Clone, Default)]
121pub struct PartialBlockchainUpdates {
122 block_headers: BTreeMap<BlockNumber, (BlockHeader, bool)>,
125 new_authentication_nodes: Vec<(InOrderIndex, Word)>,
128 pub new_peaks: MmrPeaks,
130}
131
132impl PartialBlockchainUpdates {
133 pub fn insert(
138 &mut self,
139 block_header: BlockHeader,
140 has_client_notes: bool,
141 new_authentication_nodes: Vec<(InOrderIndex, Word)>,
142 ) {
143 self.block_headers
144 .entry(block_header.block_num())
145 .and_modify(|(_, existing_has_notes)| {
146 *existing_has_notes |= has_client_notes;
147 })
148 .or_insert((block_header, has_client_notes));
149
150 self.new_authentication_nodes.extend(new_authentication_nodes);
151 }
152
153 pub fn block_headers(&self) -> impl Iterator<Item = &(BlockHeader, bool)> {
156 self.block_headers.values()
157 }
158
159 pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
162 &self.new_authentication_nodes
163 }
164}
165
166#[derive(Default)]
168pub struct TransactionUpdateTracker {
169 transactions: BTreeMap<TransactionId, TransactionRecord>,
171 external_nullifier_accounts: BTreeMap<Nullifier, AccountId>,
173}
174
175impl TransactionUpdateTracker {
176 pub fn new(transactions: Vec<TransactionRecord>) -> Self {
178 let transactions =
179 transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
180
181 Self {
182 transactions,
183 external_nullifier_accounts: BTreeMap::new(),
184 }
185 }
186
187 pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
189 self.transactions
190 .values()
191 .filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
192 }
193
194 pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
196 self.transactions
197 .values()
198 .filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
199 }
200
201 fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
203 self.transactions
204 .values_mut()
205 .filter(|tx| matches!(tx.status, TransactionStatus::Pending))
206 }
207
208 pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
210 self.committed_transactions()
211 .chain(self.discarded_transactions())
212 .map(|tx| tx.id)
213 }
214
215 pub fn external_nullifier_account(&self, nullifier: &Nullifier) -> Option<AccountId> {
218 self.external_nullifier_accounts.get(nullifier).copied()
219 }
220
221 pub fn apply_transaction_inclusion(&mut self, record: &RpcTransactionRecord, timestamp: u64) {
224 let header = &record.transaction_header;
225 let account_id = header.account_id();
226
227 if let Some(transaction) = self.transactions.get_mut(&header.id()) {
228 transaction.commit_transaction(record.block_num, timestamp);
229 return;
230 }
231
232 if let Some(transaction) = self.transactions.values_mut().find(|tx| {
236 tx.details.account_id == account_id
237 && tx.details.init_account_state == header.initial_state_commitment()
238 }) {
239 transaction.commit_transaction(record.block_num, timestamp);
240 return;
241 }
242
243 for commitment in header.input_notes().iter() {
247 self.external_nullifier_accounts.insert(commitment.nullifier(), account_id);
248 }
249 }
250
251 pub fn apply_sync_height_update(
254 &mut self,
255 new_sync_height: BlockNumber,
256 tx_discard_delta: Option<u32>,
257 ) {
258 if let Some(tx_discard_delta) = tx_discard_delta {
259 self.discard_transaction_with_predicate(
260 |transaction| {
261 transaction.details.submission_height
262 < new_sync_height.checked_sub(tx_discard_delta).unwrap_or_default()
263 },
264 DiscardCause::Stale,
265 );
266 }
267
268 self.discard_transaction_with_predicate(
271 |transaction| transaction.details.expiration_block_num <= new_sync_height,
272 DiscardCause::Expired,
273 );
274 }
275
276 pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
280 self.discard_transaction_with_predicate(
281 |transaction| {
282 transaction
285 .details
286 .input_note_nullifiers
287 .contains(&input_note_nullifier.as_word())
288 },
289 DiscardCause::InputConsumed,
290 );
291 }
292
293 pub fn apply_superseded_account_state(&mut self, superseded_account_state: Word) {
295 self.discard_transaction_with_predicate(
296 |transaction| transaction.details.final_account_state == superseded_account_state,
297 DiscardCause::Superseded,
298 );
299 }
300
301 pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Word) {
303 self.discard_transaction_with_predicate(
304 |transaction| transaction.details.init_account_state == invalid_account_state,
305 DiscardCause::DiscardedInitialState,
306 );
307 }
308
309 fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
312 where
313 F: Fn(&TransactionRecord) -> bool,
314 {
315 let mut new_invalid_account_states = vec![];
316
317 for transaction in self.mutable_pending_transactions() {
318 if predicate(transaction) && transaction.discard_transaction(discard_cause) {
324 new_invalid_account_states.push(transaction.details.final_account_state);
325 }
326 }
327
328 for state in new_invalid_account_states {
329 self.apply_invalid_initial_account_state(state);
330 }
331 }
332}
333
334#[derive(Debug, Clone)]
350pub enum PublicAccountUpdate {
351 Full(Account),
353 Delta(PublicAccountDelta),
356}
357
358impl PublicAccountUpdate {
359 pub fn id(&self) -> AccountId {
361 match self {
362 Self::Full(account) => account.id(),
363 Self::Delta(delta) => delta.id(),
364 }
365 }
366
367 pub fn nonce(&self) -> Felt {
369 match self {
370 Self::Full(account) => account.nonce(),
371 Self::Delta(delta) => delta.new_header().nonce(),
372 }
373 }
374}
375
376#[derive(Debug, Clone)]
383pub struct PublicAccountDelta {
384 new_header: AccountHeader,
386 block_from: BlockNumber,
388 block_to: BlockNumber,
390 value_slot_updates: Vec<(StorageSlotName, Word)>,
393 storage_map_updates: Vec<StorageMapUpdate>,
395 vault_updates: Vec<AccountVaultUpdate>,
397}
398
399impl PublicAccountDelta {
400 pub fn new(
402 new_header: AccountHeader,
403 block_from: BlockNumber,
404 block_to: BlockNumber,
405 value_slot_updates: Vec<(StorageSlotName, Word)>,
406 storage_map_updates: Vec<StorageMapUpdate>,
407 vault_updates: Vec<AccountVaultUpdate>,
408 ) -> Self {
409 Self {
410 new_header,
411 block_from,
412 block_to,
413 value_slot_updates,
414 storage_map_updates,
415 vault_updates,
416 }
417 }
418
419 pub fn id(&self) -> AccountId {
421 self.new_header.id()
422 }
423
424 pub fn new_header(&self) -> &AccountHeader {
426 &self.new_header
427 }
428
429 pub fn block_from(&self) -> BlockNumber {
431 self.block_from
432 }
433
434 pub fn value_slot_names(&self) -> Vec<StorageSlotName> {
437 self.value_slot_updates.iter().map(|(name, _)| name.clone()).collect()
438 }
439
440 pub fn block_to(&self) -> BlockNumber {
442 self.block_to
443 }
444
445 pub fn compute_account_delta(
450 &self,
451 local_header: &AccountHeader,
452 local_storage: &AccountStorage,
453 local_vault: &AssetVault,
454 ) -> Result<AccountDelta, AccountDeltaError> {
455 let old_nonce = local_header.nonce().as_canonical_u64();
456 let new_nonce = self.new_header.nonce().as_canonical_u64();
457 if new_nonce <= old_nonce {
458 return Err(AccountDeltaError::AccountDeltaApplicationFailed {
459 account_id: self.new_header.id(),
460 source: AccountError::other(format!(
461 "node returned non-monotonic account nonce: local {old_nonce} >= new {new_nonce}"
462 )),
463 });
464 }
465
466 let storage_delta = replay_storage_updates(
467 local_storage,
468 &self.value_slot_updates,
469 &self.storage_map_updates,
470 )?;
471 let vault_delta = replay_vault_updates(local_vault, &self.vault_updates)?;
472
473 let nonce_delta = Felt::new(new_nonce - old_nonce).expect(
474 "new_nonce was checked to be higher than old_nonce; should return a valid nonce",
475 );
476
477 AccountDelta::new(self.new_header.id(), storage_delta, vault_delta, nonce_delta)
478 }
479}
480
481fn replay_storage_updates(
486 local_storage: &AccountStorage,
487 value_slot_updates: &[(StorageSlotName, Word)],
488 storage_map_updates: &[StorageMapUpdate],
489) -> Result<AccountStorageDelta, AccountDeltaError> {
490 let mut storage_delta = AccountStorageDelta::new();
491
492 for (slot_name, new_value) in value_slot_updates {
494 let local_value = local_storage.get_item(slot_name).ok();
495 if local_value.as_ref() != Some(new_value) {
496 storage_delta.set_item(slot_name.clone(), *new_value)?;
497 }
498 }
499
500 let mut by_slot: BTreeMap<StorageSlotName, BTreeMap<StorageMapKey, Word>> = BTreeMap::new();
502 let mut sorted: Vec<&StorageMapUpdate> = storage_map_updates.iter().collect();
503 sorted.sort_by_key(|u| u.block_num);
504 for update in sorted {
505 by_slot
506 .entry(update.slot_name.clone())
507 .or_default()
508 .insert(update.key, update.value);
509 }
510 for (slot_name, entries) in by_slot {
511 for (key, value) in entries {
512 storage_delta.set_map_item(slot_name.clone(), key, value)?;
513 }
514 }
515
516 Ok(storage_delta)
517}
518
519fn replay_vault_updates(
521 local_vault: &AssetVault,
522 vault_updates: &[AccountVaultUpdate],
523) -> Result<AccountVaultDelta, AccountDeltaError> {
524 let mut vault_delta = AccountVaultDelta::default();
525
526 let mut final_vault: BTreeMap<AssetVaultKey, Asset> =
527 local_vault.assets().map(|asset| (asset.vault_key(), asset)).collect();
528
529 let mut sorted: Vec<&AccountVaultUpdate> = vault_updates.iter().collect();
530 sorted.sort_by_key(|u| u.block_num);
531 for update in sorted {
532 match update.asset {
533 Some(asset) => {
534 final_vault.insert(update.vault_key, asset);
535 },
536 None => {
537 final_vault.remove(&update.vault_key);
538 },
539 }
540 }
541
542 let local_assets: BTreeMap<AssetVaultKey, Asset> =
543 local_vault.assets().map(|a| (a.vault_key(), a)).collect();
544 for (key, final_asset) in &final_vault {
545 match local_assets.get(key) {
546 None => {
547 vault_delta.add_asset(*final_asset)?;
548 },
549 Some(local_asset) if local_asset != final_asset => {
550 vault_delta.remove_asset(*local_asset)?;
551 vault_delta.add_asset(*final_asset)?;
552 },
553 _ => {},
554 }
555 }
556 for (key, local_asset) in &local_assets {
557 if !final_vault.contains_key(key) {
558 vault_delta.remove_asset(*local_asset)?;
559 }
560 }
561
562 Ok(vault_delta)
563}
564
565#[derive(Debug, Clone, Default)]
570#[allow(clippy::struct_field_names)]
571pub struct AccountUpdates {
572 updated_public_accounts: Vec<PublicAccountUpdate>,
574 mismatched_private_accounts: Vec<(AccountId, Word)>,
581}
582
583impl AccountUpdates {
584 pub fn new(
586 updated_public_accounts: Vec<PublicAccountUpdate>,
587 mismatched_private_accounts: Vec<(AccountId, Word)>,
588 ) -> Self {
589 Self {
590 updated_public_accounts,
591 mismatched_private_accounts,
592 }
593 }
594
595 pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
597 &self.updated_public_accounts
598 }
599
600 pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
602 &self.mismatched_private_accounts
603 }
604
605 pub fn extend(&mut self, other: AccountUpdates) {
606 self.updated_public_accounts.extend(other.updated_public_accounts);
607 self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
608 }
609}
610
611#[cfg(test)]
615mod tests {
616 use alloc::vec;
617
618 use miden_protocol::account::{StorageMapKey, StorageSlot};
619 use miden_protocol::asset::{Asset, AssetVault, FungibleAsset};
620 use miden_protocol::testing::account_id::{
621 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
622 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
623 };
624
625 use super::*;
626
627 fn account_id() -> AccountId {
628 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap()
629 }
630
631 fn faucet_id() -> AccountId {
632 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap()
633 }
634
635 fn slot_name(name: &str) -> StorageSlotName {
636 StorageSlotName::new(name).unwrap()
637 }
638
639 fn map_key(n: u64) -> StorageMapKey {
640 StorageMapKey::from_raw(word(n))
641 }
642
643 fn word(n: u64) -> Word {
644 Word::from([
645 Felt::new_unchecked(n),
646 Felt::new_unchecked(0),
647 Felt::new_unchecked(0),
648 Felt::new_unchecked(0),
649 ])
650 }
651
652 fn fungible(amount: u64) -> Asset {
653 Asset::Fungible(FungibleAsset::new(faucet_id(), amount).unwrap())
654 }
655
656 fn header_with_nonce(nonce: u64) -> AccountHeader {
657 AccountHeader::new(
658 account_id(),
659 Felt::new(nonce).expect("test nonce must be a valid Felt"),
660 Word::default(),
661 Word::default(),
662 Word::default(),
663 )
664 }
665
666 fn empty_payload(new_header: AccountHeader) -> PublicAccountDelta {
667 PublicAccountDelta::new(
668 new_header,
669 BlockNumber::from(0u32),
670 BlockNumber::from(1u32),
671 vec![],
672 vec![],
673 vec![],
674 )
675 }
676
677 #[test]
681 fn replay_storage_empty_inputs_returns_empty_delta() {
682 let storage = AccountStorage::new(vec![]).unwrap();
683 let delta = replay_storage_updates(&storage, &[], &[]).unwrap();
684 assert!(delta.is_empty());
685 }
686
687 #[test]
688 fn replay_storage_value_slot_changed_emits_delta() {
689 let value_slot = slot_name("miden::test::value");
690 let storage =
691 AccountStorage::new(vec![StorageSlot::with_value(value_slot.clone(), word(1))])
692 .unwrap();
693
694 let delta =
695 replay_storage_updates(&storage, &[(value_slot.clone(), word(2))], &[]).unwrap();
696
697 let entry = delta.get(&value_slot).expect("delta should contain value slot");
698 assert_eq!(entry.clone().unwrap_value(), word(2));
699 }
700
701 #[test]
702 fn replay_storage_value_slot_unchanged_is_skipped() {
703 let value_slot = slot_name("miden::test::value");
704 let storage =
705 AccountStorage::new(vec![StorageSlot::with_value(value_slot.clone(), word(1))])
706 .unwrap();
707
708 let delta =
709 replay_storage_updates(&storage, &[(value_slot.clone(), word(1))], &[]).unwrap();
710
711 assert!(delta.is_empty());
712 }
713
714 #[test]
715 fn replay_storage_map_dedup_keeps_latest_block_per_key() {
716 let map_slot = slot_name("miden::test::map");
717 let storage =
718 AccountStorage::new(vec![StorageSlot::with_empty_map(map_slot.clone())]).unwrap();
719
720 let key = map_key(42);
721 let updates = vec![
722 StorageMapUpdate {
723 block_num: BlockNumber::from(1u32),
724 slot_name: map_slot.clone(),
725 key,
726 value: word(100),
727 },
728 StorageMapUpdate {
729 block_num: BlockNumber::from(3u32),
730 slot_name: map_slot.clone(),
731 key,
732 value: word(300),
733 },
734 StorageMapUpdate {
735 block_num: BlockNumber::from(2u32),
736 slot_name: map_slot.clone(),
737 key,
738 value: word(200),
739 },
740 ];
741
742 let delta = replay_storage_updates(&storage, &[], &updates).unwrap();
743
744 let map_delta = delta.get(&map_slot).expect("delta should contain map slot").clone();
745 let map = map_delta.unwrap_map();
746 assert_eq!(map.entries().len(), 1);
747 assert_eq!(*map.entries().values().next().unwrap(), word(300));
748 }
749
750 #[test]
751 fn replay_storage_map_multiple_keys_in_same_slot_all_kept() {
752 let map_slot = slot_name("miden::test::map");
753 let storage =
754 AccountStorage::new(vec![StorageSlot::with_empty_map(map_slot.clone())]).unwrap();
755
756 let updates = vec![
757 StorageMapUpdate {
758 block_num: BlockNumber::from(1u32),
759 slot_name: map_slot.clone(),
760 key: map_key(1),
761 value: word(100),
762 },
763 StorageMapUpdate {
764 block_num: BlockNumber::from(2u32),
765 slot_name: map_slot.clone(),
766 key: map_key(2),
767 value: word(200),
768 },
769 ];
770
771 let delta = replay_storage_updates(&storage, &[], &updates).unwrap();
772 let map = delta.get(&map_slot).unwrap().clone().unwrap_map();
773 assert_eq!(map.entries().len(), 2);
774 }
775
776 #[test]
780 fn replay_vault_empty_inputs_returns_empty_delta() {
781 let vault = AssetVault::new(&[]).unwrap();
782 let delta = replay_vault_updates(&vault, &[]).unwrap();
783 assert!(delta.is_empty());
784 }
785
786 #[test]
787 fn replay_vault_added_asset_emits_add() {
788 let vault = AssetVault::new(&[]).unwrap();
789 let asset = fungible(100);
790 let updates = vec![AccountVaultUpdate {
791 block_num: BlockNumber::from(1u32),
792 asset: Some(asset),
793 vault_key: asset.vault_key(),
794 }];
795
796 let delta = replay_vault_updates(&vault, &updates).unwrap();
797 let added: Vec<_> = delta.added_assets().collect();
798 assert_eq!(added, vec![asset]);
799 assert_eq!(delta.removed_assets().count(), 0);
800 }
801
802 #[test]
803 fn replay_vault_removed_asset_emits_remove() {
804 let asset = fungible(100);
805 let vault = AssetVault::new(&[asset]).unwrap();
806 let updates = vec![AccountVaultUpdate {
807 block_num: BlockNumber::from(1u32),
808 asset: None,
809 vault_key: asset.vault_key(),
810 }];
811
812 let delta = replay_vault_updates(&vault, &updates).unwrap();
813 let removed: Vec<_> = delta.removed_assets().collect();
814 assert_eq!(removed, vec![asset]);
815 assert_eq!(delta.added_assets().count(), 0);
816 }
817
818 #[test]
819 fn replay_vault_replace_asset_emits_net_diff() {
820 let asset_a = fungible(100);
821 let asset_b = fungible(150);
822 let vault = AssetVault::new(&[asset_a]).unwrap();
823 let updates = vec![AccountVaultUpdate {
824 block_num: BlockNumber::from(1u32),
825 asset: Some(asset_b),
826 vault_key: asset_b.vault_key(),
827 }];
828
829 let delta = replay_vault_updates(&vault, &updates).unwrap();
830 let added: Vec<_> = delta.added_assets().collect();
831 assert_eq!(added, vec![fungible(50)]);
832 assert_eq!(delta.removed_assets().count(), 0);
833 }
834
835 #[test]
836 fn replay_vault_dedup_keeps_latest_block_per_key() {
837 let vault = AssetVault::new(&[]).unwrap();
838 let asset_v1 = fungible(100);
839 let asset_v2 = fungible(200);
840 let asset_v3 = fungible(300);
841 let key = asset_v1.vault_key();
842
843 let updates = vec![
844 AccountVaultUpdate {
845 block_num: BlockNumber::from(1u32),
846 asset: Some(asset_v1),
847 vault_key: key,
848 },
849 AccountVaultUpdate {
850 block_num: BlockNumber::from(3u32),
851 asset: Some(asset_v3),
852 vault_key: key,
853 },
854 AccountVaultUpdate {
855 block_num: BlockNumber::from(2u32),
856 asset: Some(asset_v2),
857 vault_key: key,
858 },
859 ];
860
861 let delta = replay_vault_updates(&vault, &updates).unwrap();
862 let added: Vec<_> = delta.added_assets().collect();
863 assert_eq!(added, vec![asset_v3]);
864 }
865
866 #[test]
867 fn replay_vault_added_then_removed_is_noop() {
868 let vault = AssetVault::new(&[]).unwrap();
869 let asset = fungible(100);
870 let key = asset.vault_key();
871
872 let updates = vec![
873 AccountVaultUpdate {
874 block_num: BlockNumber::from(1u32),
875 asset: Some(asset),
876 vault_key: key,
877 },
878 AccountVaultUpdate {
879 block_num: BlockNumber::from(2u32),
880 asset: None,
881 vault_key: key,
882 },
883 ];
884
885 let delta = replay_vault_updates(&vault, &updates).unwrap();
886 assert!(delta.is_empty());
887 }
888
889 #[test]
893 fn compute_delta_happy_path_emits_nonce_delta() {
894 let local_header = header_with_nonce(1);
895 let local_storage = AccountStorage::new(vec![]).unwrap();
896 let local_vault = AssetVault::new(&[]).unwrap();
897 let payload = empty_payload(header_with_nonce(4));
898
899 let delta = payload
900 .compute_account_delta(&local_header, &local_storage, &local_vault)
901 .unwrap();
902
903 assert_eq!(delta.nonce_delta(), Felt::new_unchecked(3));
904 assert!(delta.storage().is_empty());
905 assert!(delta.vault().is_empty());
906 }
907
908 #[test]
909 fn compute_delta_rejects_equal_nonce() {
910 let local_header = header_with_nonce(5);
911 let local_storage = AccountStorage::new(vec![]).unwrap();
912 let local_vault = AssetVault::new(&[]).unwrap();
913 let payload = empty_payload(header_with_nonce(5));
914
915 let err = payload
916 .compute_account_delta(&local_header, &local_storage, &local_vault)
917 .unwrap_err();
918
919 assert!(matches!(
920 err,
921 AccountDeltaError::AccountDeltaApplicationFailed {
922 source: AccountError::Other { .. },
923 ..
924 }
925 ));
926 }
927
928 #[test]
929 fn compute_delta_rejects_decreasing_nonce() {
930 let local_header = header_with_nonce(10);
931 let local_storage = AccountStorage::new(vec![]).unwrap();
932 let local_vault = AssetVault::new(&[]).unwrap();
933 let payload = empty_payload(header_with_nonce(9));
934
935 let err = payload
936 .compute_account_delta(&local_header, &local_storage, &local_vault)
937 .unwrap_err();
938
939 assert!(matches!(
940 err,
941 AccountDeltaError::AccountDeltaApplicationFailed {
942 source: AccountError::Other { .. },
943 ..
944 }
945 ));
946 }
947}