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