1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::{AccountDelta, AccountHeader, AccountId};
6use miden_protocol::block::{BlockHeader, BlockNumber};
7use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks};
8use miden_protocol::note::{NoteId, Nullifier};
9use miden_protocol::transaction::TransactionId;
10
11use super::SyncSummary;
12use crate::account::Account;
13use crate::note::{NoteUpdateTracker, NoteUpdateType};
14use crate::rpc::domain::transaction::TransactionInclusion;
15use crate::transaction::{DiscardCause, TransactionRecord, TransactionStatus};
16
17#[derive(Default)]
22pub struct StateSyncUpdate {
23 pub block_num: BlockNumber,
25 pub block_updates: BlockUpdates,
27 pub note_updates: NoteUpdateTracker,
29 pub transaction_updates: TransactionUpdateTracker,
31 pub account_updates: AccountUpdates,
33}
34
35impl From<&StateSyncUpdate> for SyncSummary {
36 fn from(value: &StateSyncUpdate) -> Self {
37 let new_public_note_ids = value
38 .note_updates
39 .updated_input_notes()
40 .filter_map(|note_update| {
41 let note = note_update.inner();
42 if let NoteUpdateType::Insert = note_update.update_type() {
43 Some(note.id())
44 } else {
45 None
46 }
47 })
48 .collect();
49
50 let committed_note_ids: BTreeSet<NoteId> = value
51 .note_updates
52 .updated_input_notes()
53 .filter_map(|note_update| {
54 let note = note_update.inner();
55 if let NoteUpdateType::Update = note_update.update_type() {
56 note.is_committed().then_some(note.id())
57 } else {
58 None
59 }
60 })
61 .chain(value.note_updates.updated_output_notes().filter_map(|note_update| {
62 let note = note_update.inner();
63 if let NoteUpdateType::Update = note_update.update_type() {
64 note.is_committed().then_some(note.id())
65 } else {
66 None
67 }
68 }))
69 .collect();
70
71 let consumed_note_ids: BTreeSet<NoteId> = value
72 .note_updates
73 .updated_input_notes()
74 .filter_map(|note| note.inner().is_consumed().then_some(note.inner().id()))
75 .collect();
76
77 SyncSummary::new(
78 value.block_num,
79 new_public_note_ids,
80 committed_note_ids.into_iter().collect(),
81 consumed_note_ids.into_iter().collect(),
82 value
83 .account_updates
84 .updated_public_accounts()
85 .iter()
86 .map(PublicAccountUpdate::id)
87 .collect(),
88 value
89 .account_updates
90 .mismatched_private_accounts()
91 .iter()
92 .map(|(id, _)| *id)
93 .collect(),
94 value.transaction_updates.committed_transactions().map(|t| t.id).collect(),
95 )
96 }
97}
98
99#[derive(Debug, Clone, Default)]
101pub struct BlockUpdates {
102 block_headers: BTreeMap<BlockNumber, (BlockHeader, bool, MmrPeaks)>,
106 new_authentication_nodes: Vec<(InOrderIndex, Word)>,
109}
110
111impl BlockUpdates {
112 pub fn insert(
117 &mut self,
118 block_header: BlockHeader,
119 has_client_notes: bool,
120 peaks: MmrPeaks,
121 new_authentication_nodes: Vec<(InOrderIndex, Word)>,
122 ) {
123 debug_assert_eq!(
124 peaks.forest().num_leaves(),
125 block_header.block_num().as_usize(),
126 "MMR peaks stored for a block header must use that block number as the forest",
127 );
128
129 self.block_headers
130 .entry(block_header.block_num())
131 .and_modify(|(_, existing_has_notes, _)| {
132 *existing_has_notes |= has_client_notes;
133 })
134 .or_insert((block_header, has_client_notes, peaks));
135
136 self.new_authentication_nodes.extend(new_authentication_nodes);
137 }
138
139 pub fn block_headers(&self) -> impl Iterator<Item = &(BlockHeader, bool, MmrPeaks)> {
142 self.block_headers.values()
143 }
144
145 pub fn extend_authentication_nodes(&mut self, nodes: Vec<(InOrderIndex, Word)>) {
151 self.new_authentication_nodes.extend(nodes);
152 }
153
154 pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
157 &self.new_authentication_nodes
158 }
159}
160
161#[derive(Default)]
163pub struct TransactionUpdateTracker {
164 transactions: BTreeMap<TransactionId, TransactionRecord>,
166 external_nullifier_accounts: BTreeMap<Nullifier, AccountId>,
168}
169
170impl TransactionUpdateTracker {
171 pub fn new(transactions: Vec<TransactionRecord>) -> Self {
173 let transactions =
174 transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
175
176 Self {
177 transactions,
178 external_nullifier_accounts: BTreeMap::new(),
179 }
180 }
181
182 pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
184 self.transactions
185 .values()
186 .filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
187 }
188
189 pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
191 self.transactions
192 .values()
193 .filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
194 }
195
196 fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
198 self.transactions
199 .values_mut()
200 .filter(|tx| matches!(tx.status, TransactionStatus::Pending))
201 }
202
203 pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
205 self.committed_transactions()
206 .chain(self.discarded_transactions())
207 .map(|tx| tx.id)
208 }
209
210 pub fn external_nullifier_account(&self, nullifier: &Nullifier) -> Option<AccountId> {
213 self.external_nullifier_accounts.get(nullifier).copied()
214 }
215
216 pub fn apply_transaction_inclusion(
219 &mut self,
220 transaction_inclusion: &TransactionInclusion,
221 timestamp: u64,
222 ) {
223 if let Some(transaction) = self.transactions.get_mut(&transaction_inclusion.transaction_id)
224 {
225 transaction.commit_transaction(transaction_inclusion.block_num, timestamp);
226 return;
227 }
228
229 if let Some(transaction) = self.transactions.values_mut().find(|tx| {
233 tx.details.account_id == transaction_inclusion.account_id
234 && tx.details.init_account_state == transaction_inclusion.initial_state_commitment
235 }) {
236 transaction.commit_transaction(transaction_inclusion.block_num, timestamp);
237 return;
238 }
239
240 for nullifier in &transaction_inclusion.nullifiers {
244 self.external_nullifier_accounts
245 .insert(*nullifier, transaction_inclusion.account_id);
246 }
247 }
248
249 pub fn apply_sync_height_update(
252 &mut self,
253 new_sync_height: BlockNumber,
254 tx_discard_delta: Option<u32>,
255 ) {
256 if let Some(tx_discard_delta) = tx_discard_delta {
257 self.discard_transaction_with_predicate(
258 |transaction| {
259 transaction.details.submission_height
260 < new_sync_height.checked_sub(tx_discard_delta).unwrap_or_default()
261 },
262 DiscardCause::Stale,
263 );
264 }
265
266 self.discard_transaction_with_predicate(
269 |transaction| transaction.details.expiration_block_num <= new_sync_height,
270 DiscardCause::Expired,
271 );
272 }
273
274 pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
278 self.discard_transaction_with_predicate(
279 |transaction| {
280 transaction
283 .details
284 .input_note_nullifiers
285 .contains(&input_note_nullifier.as_word())
286 },
287 DiscardCause::InputConsumed,
288 );
289 }
290
291 pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Word) {
293 self.discard_transaction_with_predicate(
294 |transaction| transaction.details.init_account_state == invalid_account_state,
295 DiscardCause::DiscardedInitialState,
296 );
297 }
298
299 fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
302 where
303 F: Fn(&TransactionRecord) -> bool,
304 {
305 let mut new_invalid_account_states = vec![];
306
307 for transaction in self.mutable_pending_transactions() {
308 if predicate(transaction) && transaction.discard_transaction(discard_cause) {
314 new_invalid_account_states.push(transaction.details.final_account_state);
315 }
316 }
317
318 for state in new_invalid_account_states {
319 self.apply_invalid_initial_account_state(state);
320 }
321 }
322}
323
324#[derive(Debug, Clone)]
333pub enum PublicAccountUpdate {
334 Full(Account),
336 Delta {
338 new_header: AccountHeader,
340 delta: AccountDelta,
342 },
343}
344
345impl PublicAccountUpdate {
346 pub fn id(&self) -> AccountId {
348 match self {
349 Self::Full(account) => account.id(),
350 Self::Delta { new_header, .. } => new_header.id(),
351 }
352 }
353}
354
355#[derive(Debug, Clone, Default)]
360#[allow(clippy::struct_field_names)]
361pub struct AccountUpdates {
362 updated_public_accounts: Vec<PublicAccountUpdate>,
364 mismatched_private_accounts: Vec<(AccountId, Word)>,
371}
372
373impl AccountUpdates {
374 pub fn new(
376 updated_public_accounts: Vec<PublicAccountUpdate>,
377 mismatched_private_accounts: Vec<(AccountId, Word)>,
378 ) -> Self {
379 Self {
380 updated_public_accounts,
381 mismatched_private_accounts,
382 }
383 }
384
385 pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
387 &self.updated_public_accounts
388 }
389
390 pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
392 &self.mismatched_private_accounts
393 }
394
395 pub fn extend(&mut self, other: AccountUpdates) {
396 self.updated_public_accounts.extend(other.updated_public_accounts);
397 self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
398 }
399}