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
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 Patch {
356 new_header: AccountHeader,
358 patch: AccountPatch,
360 },
361}
362
363impl PublicAccountUpdate {
364 pub fn id(&self) -> AccountId {
366 match self {
367 Self::Full(account) => account.id(),
368 Self::Patch { new_header, .. } => new_header.id(),
369 }
370 }
371
372 pub fn nonce(&self) -> Felt {
374 match self {
375 Self::Full(account) => account.nonce(),
376 Self::Patch { new_header, .. } => new_header.nonce(),
377 }
378 }
379}
380
381pub(crate) fn build_account_patch(
394 new_header: &AccountHeader,
395 value_slot_updates: Vec<(StorageSlotName, Word)>,
396 map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries>,
397 vault_patch: AccountVaultPatch,
398 code: AccountCode,
399) -> Result<AccountPatch, AccountPatchError> {
400 let is_full_state = new_header.nonce() == ONE;
401
402 let value_entries = value_slot_updates.into_iter().map(|(slot_name, new_value)| {
403 let value_patch = if is_full_state {
404 StorageValuePatch::Create { value: new_value }
405 } else {
406 StorageValuePatch::Update { value: new_value }
407 };
408 (slot_name, StorageSlotPatch::Value(value_patch))
409 });
410
411 let map_entries = map_entries.into_iter().map(|(slot_name, entries)| {
412 let map_patch = if is_full_state {
413 StorageMapPatch::Create { entries }
414 } else {
415 StorageMapPatch::Update { entries }
416 };
417 (slot_name, StorageSlotPatch::Map(map_patch))
418 });
419
420 let storage = AccountStoragePatch::from_entries(value_entries.chain(map_entries))?;
421
422 let code = is_full_state.then_some(code);
423
424 AccountPatch::new(new_header.id(), storage, vault_patch, code, Some(new_header.nonce()))
425}
426
427#[derive(Debug, Clone, Default)]
432#[allow(clippy::struct_field_names)]
433pub struct AccountUpdates {
434 updated_public_accounts: Vec<PublicAccountUpdate>,
436 mismatched_private_accounts: Vec<(AccountId, Word)>,
443}
444
445impl AccountUpdates {
446 pub fn new(
448 updated_public_accounts: Vec<PublicAccountUpdate>,
449 mismatched_private_accounts: Vec<(AccountId, Word)>,
450 ) -> Self {
451 Self {
452 updated_public_accounts,
453 mismatched_private_accounts,
454 }
455 }
456
457 pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
459 &self.updated_public_accounts
460 }
461
462 pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
464 &self.mismatched_private_accounts
465 }
466
467 pub fn extend(&mut self, other: AccountUpdates) {
468 self.updated_public_accounts.extend(other.updated_public_accounts);
469 self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
470 }
471}
472
473#[cfg(test)]
477mod tests {
478 use alloc::collections::BTreeMap;
479 use alloc::vec;
480
481 use miden_protocol::account::{AccountCode, StorageMapKey, StorageMapPatchEntries};
482 use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
483
484 use super::*;
485
486 fn account_id() -> AccountId {
487 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap()
488 }
489
490 fn slot_name(name: &str) -> StorageSlotName {
491 StorageSlotName::new(name).unwrap()
492 }
493
494 fn word(n: u64) -> Word {
495 Word::from([
496 Felt::new_unchecked(n),
497 Felt::new_unchecked(0),
498 Felt::new_unchecked(0),
499 Felt::new_unchecked(0),
500 ])
501 }
502
503 fn header_with_nonce(nonce: u64) -> AccountHeader {
504 AccountHeader::new(
505 account_id(),
506 Felt::new(nonce).expect("test nonce must be a valid Felt"),
507 Word::default(),
508 Word::default(),
509 Word::default(),
510 )
511 }
512
513 fn build_patch(
514 new_nonce: u64,
515 value_slot_updates: Vec<(StorageSlotName, Word)>,
516 map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries>,
517 ) -> Result<AccountPatch, AccountPatchError> {
518 build_account_patch(
519 &header_with_nonce(new_nonce),
520 value_slot_updates,
521 map_entries,
522 AccountVaultPatch::default(),
523 AccountCode::mock(),
524 )
525 }
526
527 #[test]
528 fn build_patch_empty_payload_carries_only_nonce() {
529 let patch = build_patch(4, vec![], BTreeMap::new()).unwrap();
530
531 assert_eq!(patch.final_nonce(), Some(Felt::new_unchecked(4)));
532 assert!(patch.storage().is_empty());
533 assert!(patch.vault().is_empty());
534 assert!(!patch.is_full_state());
535 }
536
537 #[test]
538 fn build_patch_sets_value_slot_absolutely() {
539 let value_slot = slot_name("miden::test::value");
540 let patch = build_patch(2, vec![(value_slot.clone(), word(2))], BTreeMap::new()).unwrap();
541
542 assert_eq!(patch.storage().updated_value(&value_slot), Some(word(2)));
543 }
544
545 #[test]
546 fn build_patch_wraps_merged_map_entries() {
547 let map_slot = slot_name("miden::test::map");
548 let key = StorageMapKey::from_raw(word(42));
549 let mut entries = StorageMapPatchEntries::new();
550 entries.insert(key, word(300));
551 let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
552
553 let patch = build_patch(2, vec![], map_entries).unwrap();
554
555 let entries =
556 patch.storage().updated_map(&map_slot).expect("patch should contain map slot");
557 assert_eq!(entries.as_map().len(), 1);
558 assert_eq!(*entries.as_map().values().next().unwrap(), word(300));
559 }
560
561 #[test]
562 fn build_patch_rejects_zero_nonce() {
563 let result = build_patch(0, vec![], BTreeMap::new());
564 assert!(result.is_err());
565 }
566
567 #[test]
570 fn build_patch_for_new_account_is_full_state() {
571 let value_slot = slot_name("miden::test::value");
572 let patch = build_patch(1, vec![(value_slot, word(1))], BTreeMap::new()).unwrap();
573
574 assert!(patch.is_full_state());
575 assert_eq!(patch.final_nonce(), Some(ONE));
576 }
577
578 #[test]
581 fn build_patch_emits_map_create_for_new_account() {
582 let map_slot = slot_name("miden::test::map");
583 let mut entries = StorageMapPatchEntries::new();
584 entries.insert(StorageMapKey::from_raw(word(1)), word(100));
585 let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
586
587 let patch = build_patch(1, vec![], map_entries).unwrap();
588
589 assert!(patch.storage().created_map(&map_slot).is_some());
590 }
591
592 #[test]
595 fn build_patch_emits_map_update_for_existing_account() {
596 let map_slot = slot_name("miden::test::map");
597 let mut entries = StorageMapPatchEntries::new();
598 entries.insert(StorageMapKey::from_raw(word(1)), word(100));
599 let map_entries = BTreeMap::from([(map_slot.clone(), entries)]);
600
601 let patch = build_patch(2, vec![], map_entries).unwrap();
602
603 assert!(patch.storage().updated_map(&map_slot).is_some());
604 }
605}