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::{PartialBlockchain, TransactionId};
67use miden_tx::auth::TransactionAuthenticator;
68use miden_tx::utils::{Deserializable, DeserializationError, Serializable};
69
70use crate::note::NoteScreener;
71use crate::store::{NoteFilter, TransactionFilter};
72use crate::{Client, ClientError};
73mod block_header;
74
75mod tag;
76pub use tag::{NoteTagRecord, NoteTagSource};
77
78mod state_sync;
79pub use state_sync::{NoteUpdateAction, OnNoteReceived, StateSync};
80
81mod state_sync_update;
82pub use state_sync_update::{
83 AccountUpdates,
84 BlockUpdates,
85 StateSyncUpdate,
86 TransactionUpdateTracker,
87};
88
89/// Client synchronization methods.
90impl<AUTH> Client<AUTH>
91where
92 AUTH: TransactionAuthenticator + Sync + 'static,
93{
94 // SYNC STATE
95 // --------------------------------------------------------------------------------------------
96
97 /// Returns the block number of the last state sync block.
98 pub async fn get_sync_height(&self) -> Result<BlockNumber, ClientError> {
99 self.store.get_sync_height().await.map_err(Into::into)
100 }
101
102 /// Syncs the client's state with the current state of the Miden network and returns a
103 /// [`SyncSummary`] corresponding to the local state update.
104 ///
105 /// The sync process is done in multiple steps:
106 /// 1. A request is sent to the node to get the state updates. This request includes tracked
107 /// account IDs and the tags of notes that might have changed or that might be of interest to
108 /// the client.
109 /// 2. A response is received with the current state of the network. The response includes
110 /// information about new/committed/consumed notes, updated accounts, and committed
111 /// transactions.
112 /// 3. Tracked notes are updated with their new states.
113 /// 4. New notes are checked, and only relevant ones are stored. Relevant notes are those that
114 /// can be consumed by accounts the client is tracking (this is checked by the
115 /// [`crate::note::NoteScreener`])
116 /// 5. Transactions are updated with their new states.
117 /// 6. Tracked public accounts are updated and private accounts are validated against the node
118 /// state.
119 /// 7. The MMR is updated with the new peaks and authentication nodes.
120 /// 8. All updates are applied to the store to be persisted.
121 pub async fn sync_state(&mut self) -> Result<SyncSummary, ClientError> {
122 _ = self.ensure_genesis_in_place().await?;
123
124 let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
125 let state_sync =
126 StateSync::new(self.rpc_api.clone(), Arc::new(note_screener), self.tx_graceful_blocks);
127
128 // Get current state of the client
129 let accounts = self
130 .store
131 .get_account_headers()
132 .await?
133 .into_iter()
134 .map(|(acc_header, _)| acc_header)
135 .collect();
136
137 let note_tags: BTreeSet<NoteTag> = self.store.get_unique_note_tags().await?;
138
139 let unspent_input_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
140 let unspent_output_notes = self.store.get_output_notes(NoteFilter::Unspent).await?;
141
142 let uncommitted_transactions =
143 self.store.get_transactions(TransactionFilter::Uncommitted).await?;
144
145 // Build current partial MMR
146 let current_partial_mmr = self.build_current_partial_mmr().await?;
147
148 let all_block_numbers = (0..current_partial_mmr.forest().num_leaves())
149 .filter_map(|block_num| {
150 current_partial_mmr.is_tracked(block_num).then_some(BlockNumber::from(
151 u32::try_from(block_num).expect("block number should be less than u32::MAX"),
152 ))
153 })
154 .collect::<BTreeSet<_>>();
155
156 let block_headers = self
157 .store
158 .get_block_headers(&all_block_numbers)
159 .await?
160 .into_iter()
161 .map(|(header, _has_notes)| header);
162
163 // Get the sync update from the network
164 let state_sync_update = state_sync
165 .sync_state(
166 PartialBlockchain::new(current_partial_mmr, block_headers)?,
167 accounts,
168 note_tags,
169 unspent_input_notes,
170 unspent_output_notes,
171 uncommitted_transactions,
172 )
173 .await?;
174
175 let sync_summary: SyncSummary = (&state_sync_update).into();
176
177 // Apply received and computed updates to the store
178 self.store
179 .apply_state_sync(state_sync_update)
180 .await
181 .map_err(ClientError::StoreError)?;
182
183 // Remove irrelevant block headers
184 self.store.prune_irrelevant_blocks().await?;
185
186 Ok(sync_summary)
187 }
188}
189
190// SYNC SUMMARY
191// ================================================================================================
192
193/// Contains stats about the sync operation.
194#[derive(Debug, PartialEq)]
195pub struct SyncSummary {
196 /// Block number up to which the client has been synced.
197 pub block_num: BlockNumber,
198 /// IDs of new public notes that the client has received.
199 pub new_public_notes: Vec<NoteId>,
200 /// IDs of tracked notes that have been committed.
201 pub committed_notes: Vec<NoteId>,
202 /// IDs of notes that have been consumed.
203 pub consumed_notes: Vec<NoteId>,
204 /// IDs of on-chain accounts that have been updated.
205 pub updated_accounts: Vec<AccountId>,
206 /// IDs of private accounts that have been locked.
207 pub locked_accounts: Vec<AccountId>,
208 /// IDs of committed transactions.
209 pub committed_transactions: Vec<TransactionId>,
210}
211
212impl SyncSummary {
213 pub fn new(
214 block_num: BlockNumber,
215 new_public_notes: Vec<NoteId>,
216 committed_notes: Vec<NoteId>,
217 consumed_notes: Vec<NoteId>,
218 updated_accounts: Vec<AccountId>,
219 locked_accounts: Vec<AccountId>,
220 committed_transactions: Vec<TransactionId>,
221 ) -> Self {
222 Self {
223 block_num,
224 new_public_notes,
225 committed_notes,
226 consumed_notes,
227 updated_accounts,
228 locked_accounts,
229 committed_transactions,
230 }
231 }
232
233 pub fn new_empty(block_num: BlockNumber) -> Self {
234 Self {
235 block_num,
236 new_public_notes: vec![],
237 committed_notes: vec![],
238 consumed_notes: vec![],
239 updated_accounts: vec![],
240 locked_accounts: vec![],
241 committed_transactions: vec![],
242 }
243 }
244
245 pub fn is_empty(&self) -> bool {
246 self.new_public_notes.is_empty()
247 && self.committed_notes.is_empty()
248 && self.consumed_notes.is_empty()
249 && self.updated_accounts.is_empty()
250 && self.locked_accounts.is_empty()
251 && self.committed_transactions.is_empty()
252 }
253
254 pub fn combine_with(&mut self, mut other: Self) {
255 self.block_num = max(self.block_num, other.block_num);
256 self.new_public_notes.append(&mut other.new_public_notes);
257 self.committed_notes.append(&mut other.committed_notes);
258 self.consumed_notes.append(&mut other.consumed_notes);
259 self.updated_accounts.append(&mut other.updated_accounts);
260 self.locked_accounts.append(&mut other.locked_accounts);
261 self.committed_transactions.append(&mut other.committed_transactions);
262 }
263}
264
265impl Serializable for SyncSummary {
266 fn write_into<W: miden_tx::utils::ByteWriter>(&self, target: &mut W) {
267 self.block_num.write_into(target);
268 self.new_public_notes.write_into(target);
269 self.committed_notes.write_into(target);
270 self.consumed_notes.write_into(target);
271 self.updated_accounts.write_into(target);
272 self.locked_accounts.write_into(target);
273 self.committed_transactions.write_into(target);
274 }
275}
276
277impl Deserializable for SyncSummary {
278 fn read_from<R: miden_tx::utils::ByteReader>(
279 source: &mut R,
280 ) -> Result<Self, DeserializationError> {
281 let block_num = BlockNumber::read_from(source)?;
282 let new_public_notes = Vec::<NoteId>::read_from(source)?;
283 let committed_notes = Vec::<NoteId>::read_from(source)?;
284 let consumed_notes = Vec::<NoteId>::read_from(source)?;
285 let updated_accounts = Vec::<AccountId>::read_from(source)?;
286 let locked_accounts = Vec::<AccountId>::read_from(source)?;
287 let committed_transactions = Vec::<TransactionId>::read_from(source)?;
288
289 Ok(Self {
290 block_num,
291 new_public_notes,
292 committed_notes,
293 consumed_notes,
294 updated_accounts,
295 locked_accounts,
296 committed_transactions,
297 })
298 }
299}