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
31pub struct StateSyncUpdate {
40 block_num: BlockNumber,
42 partial_blockchain_updates: PartialBlockchainUpdates,
44 note_updates: NoteUpdateTracker,
46 transaction_updates: TransactionUpdateTracker,
48 account_updates: AccountUpdates,
50 protocol_config: Option<ProtocolConfig>,
53}
54
55impl StateSyncUpdate {
56 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 pub fn block_num(&self) -> BlockNumber {
79 self.block_num
80 }
81
82 pub fn partial_blockchain_updates(&self) -> &PartialBlockchainUpdates {
84 &self.partial_blockchain_updates
85 }
86
87 pub fn note_updates(&self) -> &NoteUpdateTracker {
89 &self.note_updates
90 }
91
92 pub fn transaction_updates(&self) -> &TransactionUpdateTracker {
94 &self.transaction_updates
95 }
96
97 pub fn account_updates(&self) -> &AccountUpdates {
99 &self.account_updates
100 }
101
102 pub fn protocol_config(&self) -> Option<&ProtocolConfig> {
104 self.protocol_config.as_ref()
105 }
106
107 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 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 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#[derive(Debug, Clone, Default)]
204pub struct PartialBlockchainUpdates {
205 block_headers: BTreeMap<BlockNumber, (BlockHeader, bool)>,
208 new_authentication_nodes: Vec<(InOrderIndex, Word)>,
210 pub new_peaks: MmrPeaks,
212}
213
214impl PartialBlockchainUpdates {
215 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 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 pub fn block_headers(&self) -> impl Iterator<Item = &(BlockHeader, bool)> {
243 self.block_headers.values()
244 }
245
246 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 pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
261 &self.new_authentication_nodes
262 }
263}
264
265#[derive(Default)]
267pub struct TransactionUpdateTracker {
268 transactions: BTreeMap<TransactionId, TransactionRecord>,
270 external_nullifier_accounts: BTreeMap<Nullifier, AccountId>,
272}
273
274impl TransactionUpdateTracker {
275 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 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 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 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 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 pub fn external_nullifier_account(&self, nullifier: &Nullifier) -> Option<AccountId> {
317 self.external_nullifier_accounts.get(nullifier).copied()
318 }
319
320 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 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 for commitment in header.input_notes().iter() {
346 self.external_nullifier_accounts.insert(commitment.nullifier(), account_id);
347 }
348 }
349
350 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 self.discard_transaction_with_predicate(
370 |transaction| transaction.details.expiration_block_num <= new_sync_height,
371 DiscardCause::Expired,
372 );
373 }
374
375 pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
379 self.discard_transaction_with_predicate(
380 |transaction| {
381 transaction
384 .details
385 .input_note_nullifiers
386 .contains(&input_note_nullifier.as_word())
387 },
388 DiscardCause::InputConsumed,
389 );
390 }
391
392 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 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 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 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#[derive(Debug, Clone)]
449pub enum PublicAccountUpdate {
450 Full(Account),
452 Patch {
455 new_header: AccountHeader,
457 patch: AccountPatch,
459 },
460}
461
462impl PublicAccountUpdate {
463 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 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
480pub(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#[derive(Debug, Clone, Default)]
531#[allow(clippy::struct_field_names)]
532pub struct AccountUpdates {
533 updated_public_accounts: Vec<PublicAccountUpdate>,
535 mismatched_private_accounts: Vec<(AccountId, Word)>,
542}
543
544impl AccountUpdates {
545 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 pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
558 &self.updated_public_accounts
559 }
560
561 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#[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 #[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 #[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 #[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}