Skip to main content

miden_client/sync/
state_sync_update.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::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// STATE SYNC UPDATE
18// ================================================================================================
19
20/// Contains all information needed to apply the update in the store after syncing with the node.
21#[derive(Default)]
22pub struct StateSyncUpdate {
23    /// The block number of the last block that was synced.
24    pub block_num: BlockNumber,
25    /// New blocks and authentication nodes.
26    pub block_updates: BlockUpdates,
27    /// New and updated notes to be upserted in the store.
28    pub note_updates: NoteUpdateTracker,
29    /// Committed and discarded transactions after the sync.
30    pub transaction_updates: TransactionUpdateTracker,
31    /// Public account updates and mismatched private accounts after the sync.
32    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(Account::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/// Contains all the block information that needs to be added in the client's store after a sync.
100#[derive(Debug, Clone, Default)]
101pub struct BlockUpdates {
102    /// New block headers to be stored, along with a flag indicating whether the block contains
103    /// notes that are relevant to the client and the MMR peaks for the block.
104    block_headers: Vec<(BlockHeader, bool, MmrPeaks)>,
105    /// New authentication nodes that are meant to be stored in order to authenticate block
106    /// headers.
107    new_authentication_nodes: Vec<(InOrderIndex, Word)>,
108}
109
110impl BlockUpdates {
111    /// Creates a new instance of [`BlockUpdates`].
112    pub fn new(
113        block_headers: Vec<(BlockHeader, bool, MmrPeaks)>,
114        new_authentication_nodes: Vec<(InOrderIndex, Word)>,
115    ) -> Self {
116        Self { block_headers, new_authentication_nodes }
117    }
118
119    /// Adds a new block header and its corresponding data to this [`BlockUpdates`].
120    ///
121    /// # Parameters
122    /// - `block_header`: The block header to add.
123    /// - `has_client_notes`: Whether this block contains input notes that the client could use.
124    /// - `peaks`: The MMR peaks after including `block_header`.
125    /// - `new_authentication_nodes`: Authentication (MMR) nodes produced while adding
126    ///   `block_header` to the client's MMR.
127    pub fn insert(
128        &mut self,
129        block_header: BlockHeader,
130        has_client_notes: bool,
131        peaks: MmrPeaks,
132        new_authentication_nodes: Vec<(InOrderIndex, Word)>,
133    ) {
134        self.block_headers.push((block_header, has_client_notes, peaks));
135
136        self.new_authentication_nodes.reserve(new_authentication_nodes.len());
137        self.new_authentication_nodes.extend(new_authentication_nodes);
138    }
139
140    /// Returns the new block headers to be stored, along with a flag indicating whether the block
141    /// contains notes that are relevant to the client and the MMR peaks for the block.
142    pub fn block_headers(&self) -> &[(BlockHeader, bool, MmrPeaks)] {
143        &self.block_headers
144    }
145
146    /// Adds authentication nodes without an associated block header.
147    ///
148    /// This is used when a synced block is not stored (no relevant notes and not the chain tip)
149    /// but the MMR authentication nodes it produced must still be persisted so that the on-disk
150    /// state stays consistent with the in-memory `PartialMmr`.
151    pub fn extend_authentication_nodes(&mut self, nodes: Vec<(InOrderIndex, Word)>) {
152        self.new_authentication_nodes.extend(nodes);
153    }
154
155    /// Returns the new authentication nodes that are meant to be stored in order to authenticate
156    /// block headers.
157    pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
158        &self.new_authentication_nodes
159    }
160}
161
162/// Contains transaction changes to apply to the store.
163#[derive(Default)]
164pub struct TransactionUpdateTracker {
165    transactions: BTreeMap<TransactionId, TransactionRecord>,
166}
167
168impl TransactionUpdateTracker {
169    /// Creates a new [`TransactionUpdateTracker`]
170    pub fn new(transactions: Vec<TransactionRecord>) -> Self {
171        let transactions =
172            transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
173
174        Self { transactions }
175    }
176
177    /// Returns a reference to committed transactions.
178    pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
179        self.transactions
180            .values()
181            .filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
182    }
183
184    /// Returns a reference to discarded transactions.
185    pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
186        self.transactions
187            .values()
188            .filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
189    }
190
191    /// Returns a mutable reference to pending transactions in the tracker.
192    fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
193        self.transactions
194            .values_mut()
195            .filter(|tx| matches!(tx.status, TransactionStatus::Pending))
196    }
197
198    /// Returns transaction IDs of all transactions that have been updated.
199    pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
200        self.committed_transactions()
201            .chain(self.discarded_transactions())
202            .map(|tx| tx.id)
203    }
204
205    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a
206    /// transaction is included in a block.
207    pub fn apply_transaction_inclusion(
208        &mut self,
209        transaction_inclusion: &TransactionInclusion,
210        timestamp: u64,
211    ) {
212        if let Some(transaction) = self.transactions.get_mut(&transaction_inclusion.transaction_id)
213        {
214            transaction.commit_transaction(transaction_inclusion.block_num, timestamp);
215        }
216    }
217
218    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a the sync
219    /// height of the client is updated. This may result in stale or expired transactions.
220    pub fn apply_sync_height_update(
221        &mut self,
222        new_sync_height: BlockNumber,
223        tx_graceful_blocks: Option<u32>,
224    ) {
225        if let Some(tx_graceful_blocks) = tx_graceful_blocks {
226            self.discard_transaction_with_predicate(
227                |transaction| {
228                    transaction.details.submission_height
229                        < new_sync_height.checked_sub(tx_graceful_blocks).unwrap_or_default()
230                },
231                DiscardCause::Stale,
232            );
233        }
234
235        // NOTE: we check for <= new_sync height because at this point we would have committed the
236        // transaction otherwise
237        self.discard_transaction_with_predicate(
238            |transaction| transaction.details.expiration_block_num <= new_sync_height,
239            DiscardCause::Expired,
240        );
241    }
242
243    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a note is
244    /// nullified. this may result in transactions being discarded because they were processing the
245    /// nullified note.
246    pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
247        self.discard_transaction_with_predicate(
248            |transaction| {
249                // Check if the note was being processed by a local transaction that didn't end up
250                // being committed so it should be discarded
251                transaction
252                    .details
253                    .input_note_nullifiers
254                    .contains(&input_note_nullifier.as_word())
255            },
256            DiscardCause::InputConsumed,
257        );
258    }
259
260    /// Discards transactions that have the same initial account state as the provided one.
261    pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Word) {
262        self.discard_transaction_with_predicate(
263            |transaction| transaction.details.init_account_state == invalid_account_state,
264            DiscardCause::DiscardedInitialState,
265        );
266    }
267
268    /// Discards transactions that match the predicate and also applies the new invalid account
269    /// states
270    fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
271    where
272        F: Fn(&TransactionRecord) -> bool,
273    {
274        let mut new_invalid_account_states = vec![];
275
276        for transaction in self.mutable_pending_transactions() {
277            // Discard transactions, and also push the invalid account state if the transaction
278            // got correctly discarded
279            // NOTE: previous updates in a chain of state syncs could have committed a transaction,
280            // so we need to check that `discard_transaction` returns `true` here (aka, it got
281            // discarded from a valid state)
282            if predicate(transaction) && transaction.discard_transaction(discard_cause) {
283                new_invalid_account_states.push(transaction.details.final_account_state);
284            }
285        }
286
287        for state in new_invalid_account_states {
288            self.apply_invalid_initial_account_state(state);
289        }
290    }
291}
292
293// ACCOUNT UPDATES
294// ================================================================================================
295
296/// Contains account changes to apply to the store after a sync request.
297#[derive(Debug, Clone, Default)]
298pub struct AccountUpdates {
299    /// Updated public accounts.
300    updated_public_accounts: Vec<Account>,
301    /// Account commitments received from the network that don't match the currently
302    /// locally-tracked state of the private accounts.
303    ///
304    /// These updates may represent a stale account commitment (meaning that the latest local state
305    /// hasn't been committed). If this is not the case, the account may be locked until the state
306    /// is restored manually.
307    mismatched_private_accounts: Vec<(AccountId, Word)>,
308}
309
310impl AccountUpdates {
311    /// Creates a new instance of `AccountUpdates`.
312    pub fn new(
313        updated_public_accounts: Vec<Account>,
314        mismatched_private_accounts: Vec<(AccountId, Word)>,
315    ) -> Self {
316        Self {
317            updated_public_accounts,
318            mismatched_private_accounts,
319        }
320    }
321
322    /// Returns the updated public accounts.
323    pub fn updated_public_accounts(&self) -> &[Account] {
324        &self.updated_public_accounts
325    }
326
327    /// Returns the mismatched private accounts.
328    pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
329        &self.mismatched_private_accounts
330    }
331
332    pub fn extend(&mut self, other: AccountUpdates) {
333        self.updated_public_accounts.extend(other.updated_public_accounts);
334        self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
335    }
336}