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::auth::TransactionAuthenticator;
30//! # use miden_client::sync::SyncSummary;
31//! # use miden_client::{Client, ClientError};
32//! # use miden_objects::{block::BlockHeader, Felt, Word, StarkField};
33//! # use miden_objects::crypto::rand::FeltRng;
34//! # async fn run_sync<AUTH: TransactionAuthenticator + Sync + 'static>(client: &mut Client<AUTH>) -> Result<(), ClientError> {
35//! // Attempt to synchronize the client's state with the Miden network.
36//! // The requested data is based on the client's state: it gets updates for accounts, relevant
37//! // notes, etc. For more information on the data that gets requested, see the doc comments for
38//! // `sync_state()`.
39//! let sync_summary: SyncSummary = client.sync_state().await?;
40//!
41//! println!("Synced up to block number: {}", sync_summary.block_num);
42//! println!("Committed notes: {}", sync_summary.committed_notes.len());
43//! println!("Consumed notes: {}", sync_summary.consumed_notes.len());
44//! println!("Updated accounts: {}", sync_summary.updated_accounts.len());
45//! println!("Locked accounts: {}", sync_summary.locked_accounts.len());
46//! println!("Committed transactions: {}", sync_summary.committed_transactions.len());
47//!
48//! Ok(())
49//! # }
50//! ```
51//!
52//! The `sync_state` method loops internally until the client is fully synced to the network tip.
53//!
54//! For more advanced usage, refer to the individual functions (such as
55//! `committed_note_updates` and `consumed_note_updates`) to understand how the sync data is
56//! processed and applied to the local store.
57
58use alloc::collections::BTreeSet;
59use alloc::sync::Arc;
60use alloc::vec::Vec;
61use core::cmp::max;
62
63use miden_objects::account::AccountId;
64use miden_objects::block::BlockNumber;
65use miden_objects::note::{NoteId, NoteTag};
66use miden_objects::transaction::TransactionId;
67use miden_tx::auth::TransactionAuthenticator;
68use miden_tx::utils::{Deserializable, DeserializationError, Serializable};
69use tracing::{debug, info};
70
71use crate::note::NoteScreener;
72use crate::note_transport::NoteTransport;
73use crate::store::{NoteFilter, TransactionFilter};
74use crate::{Client, ClientError};
75mod block_header;
76
77mod tag;
78pub use tag::{NoteTagRecord, NoteTagSource};
79
80mod state_sync;
81pub use state_sync::{NoteUpdateAction, OnNoteReceived, StateSync};
82
83mod state_sync_update;
84pub use state_sync_update::{
85    AccountUpdates,
86    BlockUpdates,
87    StateSyncUpdate,
88    TransactionUpdateTracker,
89};
90
91/// Client synchronization methods.
92impl<AUTH> Client<AUTH>
93where
94    AUTH: TransactionAuthenticator + Sync + 'static,
95{
96    // SYNC STATE
97    // --------------------------------------------------------------------------------------------
98
99    /// Returns the block number of the last state sync block.
100    pub async fn get_sync_height(&self) -> Result<BlockNumber, ClientError> {
101        self.store.get_sync_height().await.map_err(Into::into)
102    }
103
104    /// Syncs the client's state with the current state of the Miden network and returns a
105    /// [`SyncSummary`] corresponding to the local state update.
106    ///
107    /// The sync process is done in multiple steps:
108    /// 1. A request is sent to the node to get the state updates. This request includes tracked
109    ///    account IDs and the tags of notes that might have changed or that might be of interest to
110    ///    the client.
111    /// 2. A response is received with the current state of the network. The response includes
112    ///    information about new/committed/consumed notes, updated accounts, and committed
113    ///    transactions.
114    /// 3. Tracked notes are updated with their new states.
115    /// 4. New notes are checked, and only relevant ones are stored. Relevant notes are those that
116    ///    can be consumed by accounts the client is tracking (this is checked by the
117    ///    [`crate::note::NoteScreener`])
118    /// 5. Transactions are updated with their new states.
119    /// 6. Tracked public accounts are updated and private accounts are validated against the node
120    ///    state.
121    /// 7. The MMR is updated with the new peaks and authentication nodes.
122    /// 8. All updates are applied to the store to be persisted.
123    pub async fn sync_state(&mut self) -> Result<SyncSummary, ClientError> {
124        _ = self.ensure_genesis_in_place().await?;
125
126        let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
127        let state_sync =
128            StateSync::new(self.rpc_api.clone(), Arc::new(note_screener), self.tx_graceful_blocks);
129
130        let note_transport =
131            self.note_transport_api.as_ref().map(|api| NoteTransport::new(api.clone()));
132
133        // Get current state of the client
134        let accounts = self
135            .store
136            .get_account_headers()
137            .await?
138            .into_iter()
139            .map(|(acc_header, _)| acc_header)
140            .collect();
141
142        let note_tags: BTreeSet<NoteTag> = self.store.get_unique_note_tags().await?;
143
144        let unspent_input_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
145        let unspent_output_notes = self.store.get_output_notes(NoteFilter::Unspent).await?;
146
147        let uncommitted_transactions =
148            self.store.get_transactions(TransactionFilter::Uncommitted).await?;
149
150        // Build current partial MMR
151        let current_partial_mmr = self.store.get_current_partial_mmr().await?;
152
153        // Get the sync update from the network
154        let state_sync_update: StateSyncUpdate = state_sync
155            .sync_state(
156                current_partial_mmr,
157                accounts,
158                note_tags.clone(),
159                unspent_input_notes,
160                unspent_output_notes,
161                uncommitted_transactions,
162            )
163            .await?;
164
165        // Note Transport update
166        // TODO We can run both sync_state, fetch_notes futures in parallel
167        let note_transport_update = if let Some(mut note_transport) = note_transport {
168            let cursor = self.store.get_note_transport_cursor().await?;
169            Some(note_transport.fetch_notes(cursor, note_tags).await?)
170        } else {
171            None
172        };
173
174        let sync_summary: SyncSummary = (&state_sync_update).into();
175        debug!(sync_summary = ?sync_summary, "Sync summary computed");
176        info!("Applying changes to the store.");
177
178        // Apply received and computed updates to the store
179        self.store
180            .apply_state_sync(state_sync_update)
181            .await
182            .map_err(ClientError::StoreError)?;
183
184        if let Some(note_transport_update) = note_transport_update {
185            self.store
186                .apply_note_transport_update(note_transport_update)
187                .await
188                .map_err(ClientError::StoreError)?;
189        }
190
191        // Remove irrelevant block headers
192        self.store.prune_irrelevant_blocks().await?;
193
194        Ok(sync_summary)
195    }
196
197    /// Applies the state sync update to the store.
198    ///
199    /// See [`crate::Store::apply_state_sync()`] for what the update implies.
200    pub async fn apply_state_sync(&mut self, update: StateSyncUpdate) -> Result<(), ClientError> {
201        self.store.apply_state_sync(update).await.map_err(ClientError::StoreError)?;
202
203        // Remove irrelevant block headers
204        self.store.prune_irrelevant_blocks().await.map_err(ClientError::StoreError)
205    }
206}
207
208// SYNC SUMMARY
209// ================================================================================================
210
211/// Contains stats about the sync operation.
212#[derive(Debug, PartialEq)]
213pub struct SyncSummary {
214    /// Block number up to which the client has been synced.
215    pub block_num: BlockNumber,
216    /// IDs of new public notes that the client has received.
217    pub new_public_notes: Vec<NoteId>,
218    /// IDs of tracked notes that have been committed.
219    pub committed_notes: Vec<NoteId>,
220    /// IDs of notes that have been consumed.
221    pub consumed_notes: Vec<NoteId>,
222    /// IDs of on-chain accounts that have been updated.
223    pub updated_accounts: Vec<AccountId>,
224    /// IDs of private accounts that have been locked.
225    pub locked_accounts: Vec<AccountId>,
226    /// IDs of committed transactions.
227    pub committed_transactions: Vec<TransactionId>,
228}
229
230impl SyncSummary {
231    pub fn new(
232        block_num: BlockNumber,
233        new_public_notes: Vec<NoteId>,
234        committed_notes: Vec<NoteId>,
235        consumed_notes: Vec<NoteId>,
236        updated_accounts: Vec<AccountId>,
237        locked_accounts: Vec<AccountId>,
238        committed_transactions: Vec<TransactionId>,
239    ) -> Self {
240        Self {
241            block_num,
242            new_public_notes,
243            committed_notes,
244            consumed_notes,
245            updated_accounts,
246            locked_accounts,
247            committed_transactions,
248        }
249    }
250
251    pub fn new_empty(block_num: BlockNumber) -> Self {
252        Self {
253            block_num,
254            new_public_notes: vec![],
255            committed_notes: vec![],
256            consumed_notes: vec![],
257            updated_accounts: vec![],
258            locked_accounts: vec![],
259            committed_transactions: vec![],
260        }
261    }
262
263    pub fn is_empty(&self) -> bool {
264        self.new_public_notes.is_empty()
265            && self.committed_notes.is_empty()
266            && self.consumed_notes.is_empty()
267            && self.updated_accounts.is_empty()
268            && self.locked_accounts.is_empty()
269            && self.committed_transactions.is_empty()
270    }
271
272    pub fn combine_with(&mut self, mut other: Self) {
273        self.block_num = max(self.block_num, other.block_num);
274        self.new_public_notes.append(&mut other.new_public_notes);
275        self.committed_notes.append(&mut other.committed_notes);
276        self.consumed_notes.append(&mut other.consumed_notes);
277        self.updated_accounts.append(&mut other.updated_accounts);
278        self.locked_accounts.append(&mut other.locked_accounts);
279        self.committed_transactions.append(&mut other.committed_transactions);
280    }
281}
282
283impl Serializable for SyncSummary {
284    fn write_into<W: miden_tx::utils::ByteWriter>(&self, target: &mut W) {
285        self.block_num.write_into(target);
286        self.new_public_notes.write_into(target);
287        self.committed_notes.write_into(target);
288        self.consumed_notes.write_into(target);
289        self.updated_accounts.write_into(target);
290        self.locked_accounts.write_into(target);
291        self.committed_transactions.write_into(target);
292    }
293}
294
295impl Deserializable for SyncSummary {
296    fn read_from<R: miden_tx::utils::ByteReader>(
297        source: &mut R,
298    ) -> Result<Self, DeserializationError> {
299        let block_num = BlockNumber::read_from(source)?;
300        let new_public_notes = Vec::<NoteId>::read_from(source)?;
301        let committed_notes = Vec::<NoteId>::read_from(source)?;
302        let consumed_notes = Vec::<NoteId>::read_from(source)?;
303        let updated_accounts = Vec::<AccountId>::read_from(source)?;
304        let locked_accounts = Vec::<AccountId>::read_from(source)?;
305        let committed_transactions = Vec::<TransactionId>::read_from(source)?;
306
307        Ok(Self {
308            block_num,
309            new_public_notes,
310            committed_notes,
311            consumed_notes,
312            updated_accounts,
313            locked_accounts,
314            committed_transactions,
315        })
316    }
317}