miden_client/sync/
mod.rs

1//! Provides the client APIs for synchronizing the client's local state with the Miden
2//! network. It ensures that the client maintains a valid, up-to-date view of the chain.
3//!
4//! ## Overview
5//!
6//! This module handles the synchronization process between the local client and the Miden network.
7//! The sync operation involves:
8//!
9//! - Querying the Miden node for state updates using tracked account IDs, note tags, and nullifier
10//!   prefixes.
11//! - Processing the received data to update note inclusion proofs, reconcile note state (new,
12//!   committed, or consumed), and update account states.
13//! - Incorporating new block headers and updating the local Merkle Mountain Range (MMR) with new
14//!   peaks and authentication nodes.
15//! - Aggregating transaction updates to determine which transactions have been committed or
16//!   discarded.
17//!
18//! The result of the synchronization process is captured in a [`SyncSummary`], which provides
19//! a summary of the new block number along with lists of received, committed, and consumed note
20//! IDs, updated account IDs, locked accounts, and committed transaction IDs.
21//!
22//! Once the data is requested and retrieved, updates are persisted in the client's store.
23//!
24//! ## Examples
25//!
26//! The following example shows how to initiate a state sync and handle the resulting summary:
27//!
28//! ```rust
29//! # use miden_client::sync::SyncSummary;
30//! # use miden_client::{Client, ClientError};
31//! # use miden_objects::{block::BlockHeader, Felt, Word, StarkField};
32//! # use miden_objects::crypto::rand::FeltRng;
33//! # async fn run_sync(client: &mut Client) -> Result<(), ClientError> {
34//! // Attempt to synchronize the client's state with the Miden network.
35//! // The requested data is based on the client's state: it gets updates for accounts, relevant
36//! // notes, etc. For more information on the data that gets requested, see the doc comments for
37//! // `sync_state()`.
38//! let sync_summary: SyncSummary = client.sync_state().await?;
39//!
40//! println!("Synced up to block number: {}", sync_summary.block_num);
41//! println!("Committed notes: {}", sync_summary.committed_notes.len());
42//! println!("Consumed notes: {}", sync_summary.consumed_notes.len());
43//! println!("Updated accounts: {}", sync_summary.updated_accounts.len());
44//! println!("Locked accounts: {}", sync_summary.locked_accounts.len());
45//! println!("Committed transactions: {}", sync_summary.committed_transactions.len());
46//!
47//! Ok(())
48//! # }
49//! ```
50//!
51//! The `sync_state` method loops internally until the client is fully synced to the network tip.
52//!
53//! For more advanced usage, refer to the individual functions (such as
54//! `committed_note_updates` and `consumed_note_updates`) to understand how the sync data is
55//! processed and applied to the local store.
56
57use alloc::{boxed::Box, collections::BTreeSet, vec::Vec};
58use core::cmp::max;
59
60use miden_objects::{
61    account::AccountId,
62    block::BlockNumber,
63    note::{NoteId, NoteTag},
64    transaction::{PartialBlockchain, TransactionId},
65};
66use miden_tx::utils::{Deserializable, DeserializationError, Serializable};
67
68use crate::{
69    Client, ClientError,
70    note::NoteScreener,
71    store::{NoteFilter, TransactionFilter},
72};
73mod block_header;
74
75mod tag;
76pub use tag::{NoteTagRecord, NoteTagSource};
77
78mod state_sync;
79pub use state_sync::{OnNoteReceived, StateSync, on_note_received};
80
81mod state_sync_update;
82pub use state_sync_update::{
83    AccountUpdates, BlockUpdates, StateSyncUpdate, TransactionUpdateTracker,
84};
85
86/// Client synchronization methods.
87impl Client {
88    // SYNC STATE
89    // --------------------------------------------------------------------------------------------
90
91    /// Returns the block number of the last state sync block.
92    pub async fn get_sync_height(&self) -> Result<BlockNumber, ClientError> {
93        self.store.get_sync_height().await.map_err(Into::into)
94    }
95
96    /// Syncs the client's state with the current state of the Miden network and returns a
97    /// [`SyncSummary`] corresponding to the local state update.
98    ///
99    /// The sync process is done in multiple steps:
100    /// 1. A request is sent to the node to get the state updates. This request includes tracked
101    ///    account IDs and the tags of notes that might have changed or that might be of interest to
102    ///    the client.
103    /// 2. A response is received with the current state of the network. The response includes
104    ///    information about new/committed/consumed notes, updated accounts, and committed
105    ///    transactions.
106    /// 3. Tracked notes are updated with their new states.
107    /// 4. New notes are checked, and only relevant ones are stored. Relevant notes are those that
108    ///    can be consumed by accounts the client is tracking (this is checked by the
109    ///    [`crate::note::NoteScreener`])
110    /// 5. Transactions are updated with their new states.
111    /// 6. Tracked public accounts are updated and private accounts are validated against the node
112    ///    state.
113    /// 7. The MMR is updated with the new peaks and authentication nodes.
114    /// 8. All updates are applied to the store to be persisted.
115    pub async fn sync_state(&mut self) -> Result<SyncSummary, ClientError> {
116        _ = self.ensure_genesis_in_place().await?;
117
118        let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
119        let state_sync = StateSync::new(
120            self.rpc_api.clone(),
121            Box::new({
122                let store_clone = self.store.clone();
123                move |committed_note, public_note, note_screener, note_tags| {
124                    Box::pin(on_note_received(
125                        store_clone.clone(),
126                        committed_note,
127                        public_note,
128                        note_screener,
129                        note_tags,
130                    ))
131                }
132            }),
133            self.tx_graceful_blocks,
134            note_screener,
135        );
136
137        // Get current state of the client
138        let accounts = self
139            .store
140            .get_account_headers()
141            .await?
142            .into_iter()
143            .map(|(acc_header, _)| acc_header)
144            .collect();
145
146        let note_tags: BTreeSet<NoteTag> = self.store.get_unique_note_tags().await?;
147
148        let unspent_input_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
149        let unspent_output_notes = self.store.get_output_notes(NoteFilter::Unspent).await?;
150
151        let uncommitted_transactions =
152            self.store.get_transactions(TransactionFilter::Uncommitted).await?;
153
154        // Build current partial MMR
155        let current_partial_mmr = self.build_current_partial_mmr().await?;
156
157        let all_block_numbers = (0..current_partial_mmr.forest())
158            .filter_map(|block_num| {
159                current_partial_mmr.is_tracked(block_num).then_some(BlockNumber::from(
160                    u32::try_from(block_num).expect("block number should be less than u32::MAX"),
161                ))
162            })
163            .collect::<BTreeSet<_>>();
164
165        let block_headers = self
166            .store
167            .get_block_headers(&all_block_numbers)
168            .await?
169            .into_iter()
170            .map(|(header, _has_notes)| header);
171
172        // Get the sync update from the network
173        let state_sync_update = state_sync
174            .sync_state(
175                PartialBlockchain::new(current_partial_mmr, block_headers)?,
176                accounts,
177                note_tags,
178                unspent_input_notes,
179                unspent_output_notes,
180                uncommitted_transactions,
181            )
182            .await?;
183
184        let sync_summary: SyncSummary = (&state_sync_update).into();
185
186        // Apply received and computed updates to the store
187        self.store
188            .apply_state_sync(state_sync_update)
189            .await
190            .map_err(ClientError::StoreError)?;
191
192        // Remove irrelevant block headers
193        self.store.prune_irrelevant_blocks().await?;
194
195        Ok(sync_summary)
196    }
197}
198
199// SYNC SUMMARY
200// ================================================================================================
201
202/// Contains stats about the sync operation.
203#[derive(Debug, PartialEq)]
204pub struct SyncSummary {
205    /// Block number up to which the client has been synced.
206    pub block_num: BlockNumber,
207    /// IDs of new public notes that the client has received.
208    pub new_public_notes: Vec<NoteId>,
209    /// IDs of tracked notes that have been committed.
210    pub committed_notes: Vec<NoteId>,
211    /// IDs of notes that have been consumed.
212    pub consumed_notes: Vec<NoteId>,
213    /// IDs of on-chain accounts that have been updated.
214    pub updated_accounts: Vec<AccountId>,
215    /// IDs of private accounts that have been locked.
216    pub locked_accounts: Vec<AccountId>,
217    /// IDs of committed transactions.
218    pub committed_transactions: Vec<TransactionId>,
219}
220
221impl SyncSummary {
222    pub fn new(
223        block_num: BlockNumber,
224        new_public_notes: Vec<NoteId>,
225        committed_notes: Vec<NoteId>,
226        consumed_notes: Vec<NoteId>,
227        updated_accounts: Vec<AccountId>,
228        locked_accounts: Vec<AccountId>,
229        committed_transactions: Vec<TransactionId>,
230    ) -> Self {
231        Self {
232            block_num,
233            new_public_notes,
234            committed_notes,
235            consumed_notes,
236            updated_accounts,
237            locked_accounts,
238            committed_transactions,
239        }
240    }
241
242    pub fn new_empty(block_num: BlockNumber) -> Self {
243        Self {
244            block_num,
245            new_public_notes: vec![],
246            committed_notes: vec![],
247            consumed_notes: vec![],
248            updated_accounts: vec![],
249            locked_accounts: vec![],
250            committed_transactions: vec![],
251        }
252    }
253
254    pub fn is_empty(&self) -> bool {
255        self.new_public_notes.is_empty()
256            && self.committed_notes.is_empty()
257            && self.consumed_notes.is_empty()
258            && self.updated_accounts.is_empty()
259            && self.locked_accounts.is_empty()
260            && self.committed_transactions.is_empty()
261    }
262
263    pub fn combine_with(&mut self, mut other: Self) {
264        self.block_num = max(self.block_num, other.block_num);
265        self.new_public_notes.append(&mut other.new_public_notes);
266        self.committed_notes.append(&mut other.committed_notes);
267        self.consumed_notes.append(&mut other.consumed_notes);
268        self.updated_accounts.append(&mut other.updated_accounts);
269        self.locked_accounts.append(&mut other.locked_accounts);
270        self.committed_transactions.append(&mut other.committed_transactions);
271    }
272}
273
274impl Serializable for SyncSummary {
275    fn write_into<W: miden_tx::utils::ByteWriter>(&self, target: &mut W) {
276        self.block_num.write_into(target);
277        self.new_public_notes.write_into(target);
278        self.committed_notes.write_into(target);
279        self.consumed_notes.write_into(target);
280        self.updated_accounts.write_into(target);
281        self.locked_accounts.write_into(target);
282        self.committed_transactions.write_into(target);
283    }
284}
285
286impl Deserializable for SyncSummary {
287    fn read_from<R: miden_tx::utils::ByteReader>(
288        source: &mut R,
289    ) -> Result<Self, DeserializationError> {
290        let block_num = BlockNumber::read_from(source)?;
291        let new_public_notes = Vec::<NoteId>::read_from(source)?;
292        let committed_notes = Vec::<NoteId>::read_from(source)?;
293        let consumed_notes = Vec::<NoteId>::read_from(source)?;
294        let updated_accounts = Vec::<AccountId>::read_from(source)?;
295        let locked_accounts = Vec::<AccountId>::read_from(source)?;
296        let committed_transactions = Vec::<TransactionId>::read_from(source)?;
297
298        Ok(Self {
299            block_num,
300            new_public_notes,
301            committed_notes,
302            consumed_notes,
303            updated_accounts,
304            locked_accounts,
305            committed_transactions,
306        })
307    }
308}