miden_client/sync/
state_sync_update.rs

1use alloc::{
2    collections::{BTreeMap, BTreeSet},
3    vec::Vec,
4};
5
6use miden_objects::{
7    Digest,
8    account::AccountId,
9    block::{BlockHeader, BlockNumber},
10    crypto::merkle::{InOrderIndex, MmrPeaks},
11    note::{NoteId, Nullifier},
12    transaction::TransactionId,
13};
14
15use super::SyncSummary;
16use crate::{
17    account::Account,
18    note::{NoteUpdateTracker, NoteUpdateType},
19    rpc::domain::transaction::TransactionInclusion,
20    transaction::{DiscardCause, TransactionRecord, TransactionStatus},
21};
22
23// STATE SYNC UPDATE
24// ================================================================================================
25
26/// Contains all information needed to apply the update in the store after syncing with the node.
27#[derive(Default)]
28pub struct StateSyncUpdate {
29    /// The block number of the last block that was synced.
30    pub block_num: BlockNumber,
31    /// New blocks and authentication nodes.
32    pub block_updates: BlockUpdates,
33    /// New and updated notes to be upserted in the store.
34    pub note_updates: NoteUpdateTracker,
35    /// Committed and discarded transactions after the sync.
36    pub transaction_updates: TransactionUpdateTracker,
37    /// Public account updates and mismatched private accounts after the sync.
38    pub account_updates: AccountUpdates,
39}
40
41impl From<&StateSyncUpdate> for SyncSummary {
42    fn from(value: &StateSyncUpdate) -> Self {
43        let new_public_note_ids = value
44            .note_updates
45            .updated_input_notes()
46            .filter_map(|note_update| {
47                let note = note_update.inner();
48                if let NoteUpdateType::Insert = note_update.update_type() {
49                    Some(note.id())
50                } else {
51                    None
52                }
53            })
54            .collect();
55
56        let committed_note_ids: BTreeSet<NoteId> = value
57            .note_updates
58            .updated_input_notes()
59            .filter_map(|note_update| {
60                let note = note_update.inner();
61                if let NoteUpdateType::Update = note_update.update_type() {
62                    note.is_committed().then_some(note.id())
63                } else {
64                    None
65                }
66            })
67            .chain(value.note_updates.updated_output_notes().filter_map(|note_update| {
68                let note = note_update.inner();
69                if let NoteUpdateType::Update = note_update.update_type() {
70                    note.is_committed().then_some(note.id())
71                } else {
72                    None
73                }
74            }))
75            .collect();
76
77        let consumed_note_ids: BTreeSet<NoteId> = value
78            .note_updates
79            .updated_input_notes()
80            .filter_map(|note| note.inner().is_consumed().then_some(note.inner().id()))
81            .collect();
82
83        SyncSummary::new(
84            value.block_num,
85            new_public_note_ids,
86            committed_note_ids.into_iter().collect(),
87            consumed_note_ids.into_iter().collect(),
88            value
89                .account_updates
90                .updated_public_accounts()
91                .iter()
92                .map(Account::id)
93                .collect(),
94            value
95                .account_updates
96                .mismatched_private_accounts()
97                .iter()
98                .map(|(id, _)| *id)
99                .collect(),
100            value.transaction_updates.committed_transactions().map(|t| t.id).collect(),
101        )
102    }
103}
104
105/// Contains all the block information that needs to be added in the client's store after a sync.
106#[derive(Debug, Clone, Default)]
107pub struct BlockUpdates {
108    /// New block headers to be stored, along with a flag indicating whether the block contains
109    /// notes that are relevant to the client and the MMR peaks for the block.
110    block_headers: Vec<(BlockHeader, bool, MmrPeaks)>,
111    /// New authentication nodes that are meant to be stored in order to authenticate block
112    /// headers.
113    new_authentication_nodes: Vec<(InOrderIndex, Digest)>,
114}
115
116impl BlockUpdates {
117    /// Creates a new instance of [`BlockUpdates`].
118    pub fn new(
119        block_headers: Vec<(BlockHeader, bool, MmrPeaks)>,
120        new_authentication_nodes: Vec<(InOrderIndex, Digest)>,
121    ) -> Self {
122        Self { block_headers, new_authentication_nodes }
123    }
124
125    /// Returns the new block headers to be stored, along with a flag indicating whether the block
126    /// contains notes that are relevant to the client and the MMR peaks for the block.
127    pub fn block_headers(&self) -> &[(BlockHeader, bool, MmrPeaks)] {
128        &self.block_headers
129    }
130
131    /// Returns the new authentication nodes that are meant to be stored in order to authenticate
132    /// block headers.
133    pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Digest)] {
134        &self.new_authentication_nodes
135    }
136
137    /// Extends the current [`BlockUpdates`] with the provided one.
138    pub(crate) fn extend(&mut self, other: BlockUpdates) {
139        self.block_headers.extend(other.block_headers);
140        self.new_authentication_nodes.extend(other.new_authentication_nodes);
141    }
142}
143
144/// Contains transaction changes to apply to the store.
145#[derive(Default)]
146pub struct TransactionUpdateTracker {
147    transactions: BTreeMap<TransactionId, TransactionRecord>,
148}
149
150impl TransactionUpdateTracker {
151    /// Creates a new [`TransactionUpdateTracker`]
152    pub fn new(transactions: Vec<TransactionRecord>) -> Self {
153        let transactions =
154            transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
155
156        Self { transactions }
157    }
158
159    /// Returns a reference to committed transactions.
160    pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
161        self.transactions
162            .values()
163            .filter(|tx| matches!(tx.status, TransactionStatus::Committed(_)))
164    }
165
166    /// Returns a reference to discarded transactions.
167    pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
168        self.transactions
169            .values()
170            .filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
171    }
172
173    /// Returns a mutable reference to pending transactions in the tracker.
174    fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
175        self.transactions
176            .values_mut()
177            .filter(|tx| matches!(tx.status, TransactionStatus::Pending))
178    }
179
180    /// Returns transaction IDs of all transactions that have been updated.
181    pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
182        self.committed_transactions()
183            .chain(self.discarded_transactions())
184            .map(|tx| tx.id)
185    }
186
187    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a
188    /// transaction is included in a block.
189    pub fn apply_transaction_inclusion(&mut self, transaction_inclusion: &TransactionInclusion) {
190        if let Some(transaction) = self.transactions.get_mut(&transaction_inclusion.transaction_id)
191        {
192            transaction.commit_transaction(transaction_inclusion.block_num.into());
193        }
194    }
195
196    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a the sync
197    /// height of the client is updated. This may result in stale or expired transactions.
198    pub fn apply_sync_height_update(
199        &mut self,
200        new_sync_height: BlockNumber,
201        tx_graceful_blocks: Option<u32>,
202    ) {
203        if let Some(tx_graceful_blocks) = tx_graceful_blocks {
204            self.discard_transaction_with_predicate(
205                |transaction| {
206                    transaction.details.submission_height
207                        < new_sync_height.checked_sub(tx_graceful_blocks).unwrap_or_default()
208                },
209                DiscardCause::Stale,
210            );
211        }
212
213        self.discard_transaction_with_predicate(
214            |transaction| transaction.details.expiration_block_num <= new_sync_height,
215            DiscardCause::Expired,
216        );
217    }
218
219    /// Applies the necessary state transitions to the [`TransactionUpdateTracker`] when a note is
220    /// nullified. this may result in transactions being discarded because they were processing the
221    /// nullified note.
222    pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
223        self.discard_transaction_with_predicate(
224            |transaction| {
225                // Check if the note was being processed by a local transaction that didn't end up
226                // being committed so it should be discarded
227                transaction
228                    .details
229                    .input_note_nullifiers
230                    .contains(&input_note_nullifier.inner())
231            },
232            DiscardCause::InputConsumed,
233        );
234    }
235
236    /// Discards transactions that have the same initial account state as the provided one.
237    pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Digest) {
238        self.discard_transaction_with_predicate(
239            |transaction| transaction.details.init_account_state == invalid_account_state,
240            DiscardCause::DiscardedInitialState,
241        );
242    }
243
244    /// Discards transactions that match the predicate and also applies the new invalid account
245    /// states
246    fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
247    where
248        F: Fn(&TransactionRecord) -> bool,
249    {
250        let mut new_invalid_account_states = vec![];
251
252        for transaction in self.mutable_pending_transactions() {
253            if predicate(transaction) {
254                transaction.discard_transaction(discard_cause);
255                new_invalid_account_states.push(transaction.details.final_account_state);
256            }
257        }
258
259        for state in new_invalid_account_states {
260            self.apply_invalid_initial_account_state(state);
261        }
262    }
263}
264
265// ACCOUNT UPDATES
266// ================================================================================================
267
268/// Contains account changes to apply to the store after a sync request.
269#[derive(Debug, Clone, Default)]
270pub struct AccountUpdates {
271    /// Updated public accounts.
272    updated_public_accounts: Vec<Account>,
273    /// Account commitments received from the network that don't match the currently
274    /// locally-tracked state of the private accounts.
275    ///
276    /// These updates may represent a stale account commitment (meaning that the latest local state
277    /// hasn't been committed). If this is not the case, the account may be locked until the state
278    /// is restored manually.
279    mismatched_private_accounts: Vec<(AccountId, Digest)>,
280}
281
282impl AccountUpdates {
283    /// Creates a new instance of `AccountUpdates`.
284    pub fn new(
285        updated_public_accounts: Vec<Account>,
286        mismatched_private_accounts: Vec<(AccountId, Digest)>,
287    ) -> Self {
288        Self {
289            updated_public_accounts,
290            mismatched_private_accounts,
291        }
292    }
293
294    /// Returns the updated public accounts.
295    pub fn updated_public_accounts(&self) -> &[Account] {
296        &self.updated_public_accounts
297    }
298
299    /// Returns the mismatched private accounts.
300    pub fn mismatched_private_accounts(&self) -> &[(AccountId, Digest)] {
301        &self.mismatched_private_accounts
302    }
303
304    pub fn extend(&mut self, other: AccountUpdates) {
305        self.updated_public_accounts.extend(other.updated_public_accounts);
306        self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
307    }
308}