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::transaction::TransactionId;
23use miden_protocol::{Felt, ONE, Word};
24
25use super::SyncSummary;
26use crate::note::{NoteUpdateTracker, NoteUpdateType};
27use crate::rpc::domain::transaction::TransactionRecord as RpcTransactionRecord;
28use crate::transaction::{DiscardCause, TransactionRecord, TransactionStatus};
29
30pub struct StateSyncUpdate {
39 block_num: BlockNumber,
41 partial_blockchain_updates: PartialBlockchainUpdates,
43 note_updates: NoteUpdateTracker,
45 transaction_updates: TransactionUpdateTracker,
47 account_updates: AccountUpdates,
49}
50
51impl StateSyncUpdate {
52 pub fn from_parts(
56 block_num: BlockNumber,
57 partial_blockchain_updates: PartialBlockchainUpdates,
58 note_updates: NoteUpdateTracker,
59 transaction_updates: TransactionUpdateTracker,
60 account_updates: AccountUpdates,
61 ) -> Self {
62 Self {
63 block_num,
64 partial_blockchain_updates,
65 note_updates,
66 transaction_updates,
67 account_updates,
68 }
69 }
70
71 pub fn block_num(&self) -> BlockNumber {
73 self.block_num
74 }
75
76 pub fn partial_blockchain_updates(&self) -> &PartialBlockchainUpdates {
78 &self.partial_blockchain_updates
79 }
80
81 pub fn note_updates(&self) -> &NoteUpdateTracker {
83 &self.note_updates
84 }
85
86 pub fn transaction_updates(&self) -> &TransactionUpdateTracker {
88 &self.transaction_updates
89 }
90
91 pub fn account_updates(&self) -> &AccountUpdates {
93 &self.account_updates
94 }
95
96 pub fn into_parts(
98 self,
99 ) -> (
100 BlockNumber,
101 PartialBlockchainUpdates,
102 NoteUpdateTracker,
103 TransactionUpdateTracker,
104 AccountUpdates,
105 ) {
106 (
107 self.block_num,
108 self.partial_blockchain_updates,
109 self.note_updates,
110 self.transaction_updates,
111 self.account_updates,
112 )
113 }
114}
115
116impl From<&StateSyncUpdate> for SyncSummary {
117 fn from(value: &StateSyncUpdate) -> Self {
118 let new_public_note_ids = value
119 .note_updates
120 .updated_input_notes()
121 .filter_map(|note_update| {
122 let note = note_update.inner();
123 if let NoteUpdateType::Insert = note_update.update_type() {
124 note.id()
125 } else {
126 None
127 }
128 })
129 .collect();
130
131 let committed_note_ids: BTreeSet<NoteId> = value
132 .note_updates
133 .updated_input_notes()
134 .filter_map(|note_update| {
135 let note = note_update.inner();
136 if matches!(
140 note_update.update_type(),
141 NoteUpdateType::Update | NoteUpdateType::InsertCommitted
142 ) && note.is_committed()
143 {
144 note.id()
145 } else {
146 None
147 }
148 })
149 .chain(value.note_updates.updated_output_notes().filter_map(|note_update| {
150 let note = note_update.inner();
151 if let NoteUpdateType::Update = note_update.update_type() {
152 note.is_committed().then_some(note.id())
153 } else {
154 None
155 }
156 }))
157 .collect();
158
159 let consumed_note_ids: BTreeSet<NoteId> =
160 value.note_updates.consumed_input_note_ids().collect();
161
162 SyncSummary::new(
163 value.block_num,
164 new_public_note_ids,
165 Vec::new(),
167 committed_note_ids.into_iter().collect(),
168 consumed_note_ids.into_iter().collect(),
169 value
170 .account_updates
171 .updated_public_accounts()
172 .iter()
173 .map(PublicAccountUpdate::id)
174 .collect(),
175 value
176 .account_updates
177 .mismatched_private_accounts()
178 .iter()
179 .map(|(id, _)| *id)
180 .collect(),
181 value.transaction_updates.committed_transactions().map(|t| t.id).collect(),
182 )
183 }
184}
185
186#[derive(Debug, Clone, Default)]
191pub struct PartialBlockchainUpdates {
192 block_headers: BTreeMap<BlockNumber, (BlockHeader, bool)>,
195 new_authentication_nodes: Vec<(InOrderIndex, Word)>,
197 pub new_peaks: MmrPeaks,
199}
200
201impl PartialBlockchainUpdates {
202 pub fn insert(&mut self, block_header: BlockHeader, is_relevant: bool) {
208 self.block_headers
209 .entry(block_header.block_num())
210 .and_modify(|(_, existing_is_relevant)| {
211 *existing_is_relevant |= is_relevant;
212 })
213 .or_insert((block_header, is_relevant));
214 }
215
216 pub fn extend_authentication_nodes(
221 &mut self,
222 nodes: impl IntoIterator<Item = (InOrderIndex, Word)>,
223 ) {
224 self.new_authentication_nodes.extend(nodes);
225 }
226
227 pub fn block_headers(&self) -> impl Iterator<Item = &(BlockHeader, bool)> {
230 self.block_headers.values()
231 }
232
233 pub fn block_headers_to_store(
235 &self,
236 sync_height: BlockNumber,
237 ) -> impl Iterator<Item = &(BlockHeader, bool)> {
238 self.block_headers.values().filter(move |(header, is_relevant)| {
239 *is_relevant
240 || header.block_num() == BlockNumber::GENESIS
241 || header.block_num() == sync_height
242 })
243 }
244
245 pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
248 &self.new_authentication_nodes
249 }
250}
251
252#[derive(Default)]
254pub struct TransactionUpdateTracker {
255 transactions: BTreeMap<TransactionId, TransactionRecord>,
257 external_nullifier_accounts: BTreeMap<Nullifier, AccountId>,
259}
260
261impl TransactionUpdateTracker {
262 pub fn new(transactions: Vec<TransactionRecord>) -> Self {
264 let transactions =
265 transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
266
267 Self {
268 transactions,
269 external_nullifier_accounts: BTreeMap::new(),
270 }
271 }
272
273 pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
275 self.transactions
276 .values()
277 .filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
278 }
279
280 pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
282 self.transactions
283 .values()
284 .filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
285 }
286
287 fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
289 self.transactions
290 .values_mut()
291 .filter(|tx| matches!(tx.status, TransactionStatus::Pending))
292 }
293
294 pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
296 self.committed_transactions()
297 .chain(self.discarded_transactions())
298 .map(|tx| tx.id)
299 }
300
301 pub fn external_nullifier_account(&self, nullifier: &Nullifier) -> Option<AccountId> {
304 self.external_nullifier_accounts.get(nullifier).copied()
305 }
306
307 pub fn apply_transaction_inclusion(&mut self, record: &RpcTransactionRecord, timestamp: u64) {
310 let header = &record.transaction_header;
311 let account_id = header.account_id();
312
313 if let Some(transaction) = self.transactions.get_mut(&header.id()) {
314 transaction.commit_transaction(record.block_num, timestamp);
315 return;
316 }
317
318 if let Some(transaction) = self.transactions.values_mut().find(|tx| {
322 tx.details.account_id == account_id
323 && tx.details.init_account_state == header.initial_state_commitment()
324 }) {
325 transaction.commit_transaction(record.block_num, timestamp);
326 return;
327 }
328
329 for commitment in header.input_notes().iter() {
333 self.external_nullifier_accounts.insert(commitment.nullifier(), account_id);
334 }
335 }
336
337 pub fn apply_sync_height_update(
340 &mut self,
341 new_sync_height: BlockNumber,
342 tx_discard_delta: Option<u32>,
343 ) {
344 if let Some(tx_discard_delta) = tx_discard_delta {
345 self.discard_transaction_with_predicate(
346 |transaction| {
347 transaction.details.submission_height
348 < new_sync_height.checked_sub(tx_discard_delta).unwrap_or_default()
349 },
350 DiscardCause::Stale,
351 );
352 }
353
354 self.discard_transaction_with_predicate(
357 |transaction| transaction.details.expiration_block_num <= new_sync_height,
358 DiscardCause::Expired,
359 );
360 }
361
362 pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
366 self.discard_transaction_with_predicate(
367 |transaction| {
368 transaction
371 .details
372 .input_note_nullifiers
373 .contains(&input_note_nullifier.as_word())
374 },
375 DiscardCause::InputConsumed,
376 );
377 }
378
379 pub fn apply_superseded_account_state(&mut self, superseded_account_state: Word) {
381 self.discard_transaction_with_predicate(
382 |transaction| transaction.details.final_account_state == superseded_account_state,
383 DiscardCause::Superseded,
384 );
385 }
386
387 pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Word) {
389 self.discard_transaction_with_predicate(
390 |transaction| transaction.details.init_account_state == invalid_account_state,
391 DiscardCause::DiscardedInitialState,
392 );
393 }
394
395 fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
398 where
399 F: Fn(&TransactionRecord) -> bool,
400 {
401 let mut new_invalid_account_states = vec![];
402
403 for transaction in self.mutable_pending_transactions() {
404 if predicate(transaction) && transaction.discard_transaction(discard_cause) {
410 new_invalid_account_states.push(transaction.details.final_account_state);
411 }
412 }
413
414 for state in new_invalid_account_states {
415 self.apply_invalid_initial_account_state(state);
416 }
417 }
418}
419
420#[derive(Debug, Clone)]
436pub enum PublicAccountUpdate {
437 Full(Account),
439 Patch {
442 new_header: AccountHeader,
444 patch: AccountPatch,
446 },
447}
448
449impl PublicAccountUpdate {
450 pub fn id(&self) -> AccountId {
452 match self {
453 Self::Full(account) => account.id(),
454 Self::Patch { new_header, .. } => new_header.id(),
455 }
456 }
457
458 pub fn nonce(&self) -> Felt {
460 match self {
461 Self::Full(account) => account.nonce(),
462 Self::Patch { new_header, .. } => new_header.nonce(),
463 }
464 }
465}
466
467pub(crate) fn build_account_patch(
480 new_header: &AccountHeader,
481 value_slot_updates: Vec<(StorageSlotName, Word)>,
482 map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries>,
483 vault_patch: AccountVaultPatch,
484 code: AccountCode,
485) -> Result<AccountPatch, AccountPatchError> {
486 let is_full_state = new_header.nonce() == ONE;
487
488 let value_entries = value_slot_updates.into_iter().map(|(slot_name, new_value)| {
489 let value_patch = if is_full_state {
490 StorageValuePatch::Create { value: new_value }
491 } else {
492 StorageValuePatch::Update { value: new_value }
493 };
494 (slot_name, StorageSlotPatch::Value(value_patch))
495 });
496
497 let map_entries = map_entries.into_iter().map(|(slot_name, entries)| {
498 let map_patch = if is_full_state {
499 StorageMapPatch::Create { entries }
500 } else {
501 StorageMapPatch::Update { entries }
502 };
503 (slot_name, StorageSlotPatch::Map(map_patch))
504 });
505
506 let storage = AccountStoragePatch::from_entries(value_entries.chain(map_entries))?;
507
508 let code = is_full_state.then_some(code);
509
510 AccountPatch::new(new_header.id(), storage, vault_patch, code, Some(new_header.nonce()))
511}
512
513#[derive(Debug, Clone, Default)]
518#[allow(clippy::struct_field_names)]
519pub struct AccountUpdates {
520 updated_public_accounts: Vec<PublicAccountUpdate>,
522 mismatched_private_accounts: Vec<(AccountId, Word)>,
529}
530
531impl AccountUpdates {
532 pub fn new(
534 updated_public_accounts: Vec<PublicAccountUpdate>,
535 mismatched_private_accounts: Vec<(AccountId, Word)>,
536 ) -> Self {
537 Self {
538 updated_public_accounts,
539 mismatched_private_accounts,
540 }
541 }
542
543 pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
545 &self.updated_public_accounts
546 }
547
548 pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
550 &self.mismatched_private_accounts
551 }
552
553 pub fn extend(&mut self, other: AccountUpdates) {
554 self.updated_public_accounts.extend(other.updated_public_accounts);
555 self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
556 }
557}
558
559#[cfg(test)]
563mod tests {
564 use alloc::collections::BTreeMap;
565 use alloc::vec;
566
567 use miden_protocol::account::{AccountCode, StorageMapKey, StorageMapPatchEntries};
568 use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
569
570 use super::*;
571
572 fn account_id() -> AccountId {
573 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap()
574 }
575
576 fn slot_name(name: &str) -> StorageSlotName {
577 StorageSlotName::new(name).unwrap()
578 }
579
580 fn word(n: u64) -> Word {
581 Word::from([
582 Felt::new_unchecked(n),
583 Felt::new_unchecked(0),
584 Felt::new_unchecked(0),
585 Felt::new_unchecked(0),
586 ])
587 }
588
589 fn header_with_nonce(nonce: u64) -> AccountHeader {
590 AccountHeader::new(
591 account_id(),
592 Felt::new(nonce).expect("test nonce must be a valid Felt"),
593 Word::default(),
594 Word::default(),
595 Word::default(),
596 )
597 }
598
599 fn build_patch(
600 new_nonce: u64,
601 value_slot_updates: Vec<(StorageSlotName, Word)>,
602 map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries>,
603 ) -> Result<AccountPatch, AccountPatchError> {
604 build_account_patch(
605 &header_with_nonce(new_nonce),
606 value_slot_updates,
607 map_entries,
608 AccountVaultPatch::default(),
609 AccountCode::mock(),
610 )
611 }
612
613 #[test]
614 fn build_patch_empty_payload_carries_only_nonce() {
615 let patch = build_patch(4, vec![], BTreeMap::new()).unwrap();
616
617 assert_eq!(patch.final_nonce(), Some(Felt::new_unchecked(4)));
618 assert!(patch.storage().is_empty());
619 assert!(patch.vault().is_empty());
620 assert!(!patch.is_full_state());
621 }
622
623 #[test]
624 fn build_patch_sets_value_slot_absolutely() {
625 let value_slot = slot_name("miden::test::value");
626 let patch = build_patch(2, vec![(value_slot.clone(), word(2))], BTreeMap::new()).unwrap();
627
628 assert_eq!(patch.storage().updated_value(&value_slot), Some(word(2)));
629 }
630
631 #[test]
632 fn build_patch_wraps_merged_map_entries() {
633 let map_slot = slot_name("miden::test::map");
634 let key = StorageMapKey::from_raw(word(42));
635 let mut entries = StorageMapPatchEntries::new();
636 entries.insert(key, word(300));
637 let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
638
639 let patch = build_patch(2, vec![], map_entries).unwrap();
640
641 let entries =
642 patch.storage().updated_map(&map_slot).expect("patch should contain map slot");
643 assert_eq!(entries.as_map().len(), 1);
644 assert_eq!(*entries.as_map().values().next().unwrap(), word(300));
645 }
646
647 #[test]
648 fn build_patch_rejects_zero_nonce() {
649 let result = build_patch(0, vec![], BTreeMap::new());
650 assert!(result.is_err());
651 }
652
653 #[test]
656 fn build_patch_for_new_account_is_full_state() {
657 let value_slot = slot_name("miden::test::value");
658 let patch = build_patch(1, vec![(value_slot, word(1))], BTreeMap::new()).unwrap();
659
660 assert!(patch.is_full_state());
661 assert_eq!(patch.final_nonce(), Some(ONE));
662 }
663
664 #[test]
667 fn build_patch_emits_map_create_for_new_account() {
668 let map_slot = slot_name("miden::test::map");
669 let mut entries = StorageMapPatchEntries::new();
670 entries.insert(StorageMapKey::from_raw(word(1)), word(100));
671 let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
672
673 let patch = build_patch(1, vec![], map_entries).unwrap();
674
675 assert!(patch.storage().created_map(&map_slot).is_some());
676 }
677
678 #[test]
681 fn build_patch_emits_map_update_for_existing_account() {
682 let map_slot = slot_name("miden::test::map");
683 let mut entries = StorageMapPatchEntries::new();
684 entries.insert(StorageMapKey::from_raw(word(1)), word(100));
685 let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
686
687 let patch = build_patch(2, vec![], map_entries).unwrap();
688
689 assert!(patch.storage().updated_map(&map_slot).is_some());
690 }
691}