Skip to main content

miden_client/sync/
mod.rs

1//! Provides the client APIs for synchronizing the client's local state with the Miden network. It
2//! 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 a
19//! summary of the new block number along with lists of received, committed, and consumed note IDs,
20//! 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};
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!("New private notes: {}", sync_summary.new_private_notes.len());
43//! println!("Committed notes: {}", sync_summary.committed_notes.len());
44//! println!("Consumed notes: {}", sync_summary.consumed_notes.len());
45//! println!("Updated accounts: {}", sync_summary.updated_accounts.len());
46//! println!("Locked accounts: {}", sync_summary.locked_accounts.len());
47//! println!("Committed transactions: {}", sync_summary.committed_transactions.len());
48//!
49//! Ok(())
50//! # }
51//! ```
52//!
53//! The `sync_state` method loops internally until the client is fully synced to the network tip.
54//!
55//! For more advanced usage, refer to the individual functions (such as `committed_note_updates` and
56//! `consumed_note_updates`) to understand how the sync data is processed and applied to the local
57//! store.
58
59use alloc::collections::BTreeSet;
60use alloc::format;
61use alloc::sync::Arc;
62use alloc::vec::Vec;
63use core::cmp::max;
64
65use futures::{StreamExt, TryStreamExt};
66use miden_protocol::account::AccountId;
67use miden_protocol::block::account_tree::AccountWitness;
68use miden_protocol::block::{BlockHeader, BlockNumber};
69use miden_protocol::crypto::merkle::mmr::{InOrderIndex, PartialMmr};
70use miden_protocol::note::NoteId;
71use miden_protocol::transaction::TransactionId;
72use miden_tx::auth::TransactionAuthenticator;
73use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
74use tracing::{info, warn};
75
76use crate::pswap::PswapChainObserver;
77use crate::rpc::AccountStateAt;
78use crate::rpc::domain::account::GetAccountRequest;
79use crate::store::{NoteFilter, TransactionFilter};
80use crate::{Client, ClientError};
81mod block_header;
82
83mod tag;
84pub use tag::{NoteTagRecord, NoteTagSource};
85
86mod note_observer;
87pub use note_observer::NoteObserver;
88
89mod state_sync;
90pub use state_sync::{ChainSyncData, NoteUpdateAction, OnNoteReceived, StateSync, StateSyncInput};
91pub(crate) use state_sync::{
92    MAX_CONCURRENT_ACCOUNT_FETCHES,
93    block_num_from_forest,
94    validate_account_witness,
95};
96
97mod state_sync_update;
98pub use state_sync_update::{
99    AccountUpdates,
100    PartialBlockchainUpdates,
101    PublicAccountUpdate,
102    StateSyncUpdate,
103    TransactionUpdateTracker,
104};
105
106/// Untracks the given block leaves from `partial_mmr`, returning the authentication-node indices
107/// that are no longer needed by any remaining tracked leaf.
108///
109/// Untracking a leaf frees an inner node only once no other tracked leaf still needs it, so the
110/// returned indices are exactly the nodes that became removable.
111fn untrack_blocks(
112    partial_mmr: &mut PartialMmr,
113    block_positions: impl IntoIterator<Item = usize>,
114) -> Vec<InOrderIndex> {
115    block_positions
116        .into_iter()
117        .flat_map(|block_pos| partial_mmr.untrack(block_pos))
118        .map(|(index, _)| index)
119        .collect()
120}
121
122/// Client synchronization methods.
123impl<AUTH> Client<AUTH>
124where
125    AUTH: TransactionAuthenticator + Sync + 'static,
126{
127    // SYNC STATE
128    // --------------------------------------------------------------------------------------------
129
130    /// Returns the block number of the last state sync block.
131    pub async fn get_sync_height(&self) -> Result<BlockNumber, ClientError> {
132        self.store.get_sync_height().await.map_err(Into::into)
133    }
134
135    /// Syncs the client's on-chain state with the current state of the Miden network and returns a
136    /// [`SyncSummary`] corresponding to the local state update.
137    ///
138    /// Does **not** fetch private notes from the Note Transport Layer. Use [`Client::sync_state`]
139    /// for the combined sync, or call [`Client::sync_note_transport`] separately.
140    ///
141    /// Fetches everything from the node first ([`Client::fetch_chain_updates`] and
142    /// [`StateSync::fetch_nullifiers`]), then applies the result with
143    /// [`Client::apply_chain_updates`], which also caches the partial MMR and prunes irrelevant
144    /// blocks according to the configured cadence.
145    pub async fn sync_chain(&mut self) -> Result<SyncSummary, ClientError> {
146        self.ensure_genesis_in_place().await?;
147        self.ensure_rpc_limits_in_place().await?;
148
149        let state_sync = self.state_sync().await?;
150        let mut chain_sync_data = self.fetch_chain_updates(&state_sync).await?;
151        state_sync.derive_state_updates(&mut chain_sync_data).await?;
152        state_sync.fetch_nullifiers(&mut chain_sync_data).await?;
153
154        self.apply_chain_updates(&state_sync, chain_sync_data).await
155    }
156
157    /// Fetches the node's view of everything that changed since the client's chain tip, without
158    /// storing anything or modifying the partial MMR.
159    ///
160    /// Builds the default sync input and runs [`StateSync::fetch_state`], then adds the account
161    /// witnesses the registered accounts still need. The state updates must be derived with
162    /// [`StateSync::derive_state_updates`]. The nullifier check is not part of this: run
163    /// [`StateSync::fetch_nullifiers`] on the result before applying it, so it can also cover
164    /// transport-delivered notes another sync path fetched in the same call.
165    pub async fn fetch_chain_updates(
166        &self,
167        state_sync: &StateSync,
168    ) -> Result<ChainSyncData, ClientError> {
169        let input = self.build_sync_input().await?;
170        let block_from = block_num_from_forest(&self.get_current_partial_mmr().await?)?;
171
172        let mut chain_sync_data = state_sync.fetch_state(block_from, input).await?;
173        self.collect_account_witnesses(&mut chain_sync_data).await?;
174
175        Ok(chain_sync_data)
176    }
177
178    /// Builds the [`StateSync`] driving one chain sync.
179    ///
180    /// Each `NoteObserver` owns its own per-sync state, so this must be called once per sync rather
181    /// than shared; `with_note_observer` just attaches it.
182    async fn state_sync(&self) -> Result<StateSync, ClientError> {
183        let validator_config = self.get_validator_config().await?;
184
185        Ok(StateSync::new(
186            self.rpc_api.clone(),
187            Arc::new(self.note_screener()),
188            self.tx_discard_delta,
189            validator_config,
190        )
191        .with_note_observer(Arc::new(PswapChainObserver::new(self.store.clone()))))
192    }
193
194    /// Verifies fetched chain data against the client's partial MMR and saves the resulting update
195    /// to the store.
196    ///
197    /// [`StateSync::derive_state_updates`] and [`StateSync::fetch_nullifiers`] must have run on the
198    /// data first. Also caches the partial MMR and prunes irrelevant blocks.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the client no longer starts where the data was fetched from, which means
203    /// another sync advanced the store in between and the data is stale.
204    pub async fn apply_chain_updates(
205        &mut self,
206        state_sync: &StateSync,
207        chain_sync_data: ChainSyncData,
208    ) -> Result<SyncSummary, ClientError> {
209        let mut partial_mmr = self.get_current_partial_mmr().await?;
210
211        let block_from = block_num_from_forest(&partial_mmr)?;
212        if block_from != chain_sync_data.block_from {
213            return Err(ClientError::ChainValidationError(format!(
214                "chain sync chain_sync_data starts at block {} but the client is at block {block_from}",
215                chain_sync_data.block_from
216            )));
217        }
218
219        let state_sync_update = StateSync::build_update(chain_sync_data, &mut partial_mmr)?;
220
221        let sync_summary: SyncSummary = (&state_sync_update).into();
222        // Post-sync observer hooks; run before persisting. Per-observer errors are logged, not
223        // propagated.
224        state_sync.run_apply_hooks(&state_sync_update).await?;
225
226        info!("Applying changes to the store.");
227
228        // Apply received and computed updates to the store
229        self.store
230            .apply_state_sync(state_sync_update)
231            .await
232            .map_err(ClientError::StoreError)?;
233        // Cache MMR so pruning can reuse in-memory MMR.
234        self.cache_partial_mmr(partial_mmr).await?;
235
236        self.maybe_untrack_and_prune_irrelevant_blocks().await?;
237
238        Ok(sync_summary)
239    }
240
241    /// Fetches private notes from the Note Transport Layer for the tracked note tags.
242    ///
243    /// Returns the IDs of notes imported in this call. No-op (returns an empty vec) if note
244    /// transport is disabled. A failed request returns an error after successful pages and their
245    /// cursors are saved.
246    pub async fn sync_note_transport(&mut self) -> Result<Vec<NoteId>, ClientError> {
247        if !self.is_note_transport_enabled() {
248            return Ok(Vec::new());
249        }
250        self.ensure_genesis_in_place().await?;
251
252        let mut note_transport_update = self.fetch_note_transport_updates().await?;
253        let fetch_error = note_transport_update.fetch_error.take();
254        let (imported_ids, _) = self.apply_note_transport_update(note_transport_update).await?;
255        if let Some(error) = fetch_error {
256            return Err(error);
257        }
258        Ok(imported_ids)
259    }
260
261    /// Runs the full client sync: private notes from the Note Transport Layer and the client's
262    /// on-chain state with the Miden node.
263    ///
264    /// The NTL and the node are fetched concurrently, and everything that writes runs sequentially
265    /// afterwards:
266    ///
267    /// 1. Concurrently: the note transport fetch and [`Client::fetch_chain_updates`]. Only node and
268    ///    NTL calls happen here, which is all that benefits from overlapping.
269    /// 2. The transport writes, when its fetch succeeded, whose records are then tracked in the
270    ///    chain sync's note updates.
271    /// 3. [`StateSync::derive_state_updates`], which screens the node's notes against the store —
272    ///    hence after step 2, so a transport-delivered note is recognised rather than discarded —
273    ///    and applies a commitment reported this sync to those records.
274    /// 4. [`StateSync::fetch_nullifiers`], covering the tracked notes *and* the transport-delivered
275    ///    ones, so a note delivered and consumed in the same window is reported as consumed by this
276    ///    call.
277    /// 5. The chain update, written last: a nullified transport-delivered note is saved as an
278    ///    update to the row step 2 inserts.
279    ///
280    /// A transport failure is logged and the chain sync continues. Successful transport pages are
281    /// imported and their cursors are saved. Failed requests keep their previous positions for
282    /// retry. The sync sends no notes to the transport.
283    pub async fn sync_state(&mut self) -> Result<SyncSummary, ClientError> {
284        // Both fetch phases need genesis in place, and connecting here means the two concurrent
285        // futures never race on the RPC client's lazy connect.
286        self.ensure_genesis_in_place().await?;
287        self.ensure_rpc_limits_in_place().await?;
288
289        let state_sync = self.state_sync().await?;
290        let (note_transport_update, chain_sync_data) = futures::join!(
291            self.fetch_note_transport_updates(),
292            self.fetch_chain_updates(&state_sync),
293        );
294
295        // An NTL failure does not end the sync
296        let (new_private_notes, imported_notes) = match note_transport_update {
297            Ok(note_transport_update) => {
298                self.apply_note_transport_update(note_transport_update).await?
299            },
300            Err(err) => {
301                warn!(?err, "note transport fetch failed; syncing the chain without it");
302                (Vec::new(), Vec::new())
303            },
304        };
305
306        let mut chain_sync_data = chain_sync_data?;
307
308        // The chain sync built its note updates from a store snapshot taken before the import, so
309        // the imported records are added here. Without them this sync has no record to apply its
310        // verdicts to, and a note committed within this sync's own block range stays expected.
311        let imported_notes =
312            self.get_input_notes(NoteFilter::DetailsCommitments(imported_notes)).await?;
313        chain_sync_data.note_updates.track_existing_input_notes(imported_notes);
314
315        state_sync.derive_state_updates(&mut chain_sync_data).await?;
316        state_sync.fetch_nullifiers(&mut chain_sync_data).await?;
317
318        let mut summary = self.apply_chain_updates(&state_sync, chain_sync_data).await?;
319        summary.new_private_notes = new_private_notes;
320        Ok(summary)
321    }
322
323    /// Builds a default [`StateSyncInput`] from the current client state.
324    ///
325    /// This includes all tracked account headers, all unique note tags, all unspent input and
326    /// output notes, and all uncommitted transactions.
327    pub async fn build_sync_input(&self) -> Result<StateSyncInput, ClientError> {
328        let accounts = self
329            .store
330            .get_account_headers()
331            .await?
332            .into_iter()
333            .map(|(header, _status)| header)
334            .collect();
335
336        let note_tags = self.store.get_unique_note_tags().await?;
337
338        let input_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
339        let output_notes = self.store.get_output_notes(NoteFilter::Unspent).await?;
340
341        let uncommitted_transactions =
342            self.store.get_transactions(TransactionFilter::Uncommitted).await?;
343
344        Ok(StateSyncInput {
345            accounts,
346            note_tags,
347            input_notes,
348            output_notes,
349            uncommitted_transactions,
350        })
351    }
352
353    /// Applies the state sync update to the store and prunes irrelevant blocks according to the
354    /// configured cadence.
355    ///
356    /// See [`crate::Store::apply_state_sync()`] for what the update implies.
357    pub async fn apply_state_sync(&mut self, update: StateSyncUpdate) -> Result<(), ClientError> {
358        self.store.apply_state_sync(update).await?;
359
360        self.maybe_untrack_and_prune_irrelevant_blocks().await?;
361
362        Ok(())
363    }
364
365    /// Prunes irrelevant blocks and their MMR authentication nodes according to the configured
366    /// cadence.
367    async fn maybe_untrack_and_prune_irrelevant_blocks(&mut self) -> Result<(), ClientError> {
368        let Some(interval) = self.irrelevant_block_prune_interval else {
369            return Ok(());
370        };
371
372        let sync_height = self.store.get_sync_height().await?;
373
374        if let Some(last_prune_height) = self.last_irrelevant_block_prune_sync_height
375            && sync_height < last_prune_height + interval
376        {
377            return Ok(());
378        }
379
380        self.untrack_and_prune_irrelevant_blocks().await?;
381        self.last_irrelevant_block_prune_sync_height = Some(sync_height);
382
383        Ok(())
384    }
385
386    /// Prunes irrelevant block data from the store.
387    ///
388    /// Identifies tracked blocks whose input notes have all been consumed, untracks them from the
389    /// `PartialMmr` to determine which authentication nodes are no longer needed, then delegates to
390    /// [`Store::untrack_and_prune_irrelevant_blocks`] to atomically remove the stale nodes, mark
391    /// the blocks as irrelevant, and delete irrelevant block headers. Any caller of this function
392    /// should've cached the `PartialMmr` beforehand.
393    async fn untrack_and_prune_irrelevant_blocks(&mut self) -> Result<(), ClientError> {
394        let tracked_blocks = self.store.get_tracked_block_header_numbers().await?;
395        let to_untrack: Vec<usize> = if tracked_blocks.is_empty() {
396            // Do not early-return: even without blocks to untrack, old irrelevant tip headers may
397            // need pruning.
398            Vec::new()
399        } else {
400            // Blocks that still have at least one unspent note need to stay tracked.
401            let unspent_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
402            let live_blocks: BTreeSet<usize> = unspent_notes
403                .iter()
404                .filter_map(|n| n.inclusion_proof().map(|p| p.location().block_num().as_usize()))
405                .collect();
406
407            tracked_blocks.difference(&live_blocks).copied().collect()
408        };
409
410        let mut blocks_to_untrack = Vec::new();
411        let mut nodes_to_remove = Vec::new();
412        let mut updated_partial_mmr = None;
413
414        if !to_untrack.is_empty() {
415            // Rebuild the PartialMmr and untrack each block to collect the authentication node
416            // indices that are no longer needed by any remaining tracked leaf.
417            let mut partial_mmr = self.get_current_partial_mmr().await?;
418            nodes_to_remove = untrack_blocks(&mut partial_mmr, to_untrack.iter().copied());
419
420            blocks_to_untrack = to_untrack
421                .iter()
422                .map(|&b| BlockNumber::from(u32::try_from(b).expect("block number fits in u32")))
423                .collect();
424            updated_partial_mmr = Some(partial_mmr);
425        }
426
427        // Store deletes stale auth nodes, marks blocks as irrelevant, and removes irrelevant block
428        // headers. Old irrelevant tip headers may still need pruning.
429        self.store
430            .untrack_and_prune_irrelevant_blocks(&blocks_to_untrack, &nodes_to_remove)
431            .await?;
432
433        if let Some(partial_mmr) = updated_partial_mmr {
434            self.cache_partial_mmr(partial_mmr).await?;
435        }
436
437        Ok(())
438    }
439
440    /// Ensures that the RPC limits are set in the RPC client. If not already cached, fetches them
441    /// from the node and persists them in the store.
442    pub async fn ensure_rpc_limits_in_place(&mut self) -> Result<(), ClientError> {
443        if self.rpc_api.has_rpc_limits().is_some() {
444            return Ok(());
445        }
446
447        let limits = self.rpc_api.get_rpc_limits().await?;
448        self.store.set_rpc_limits(limits).await?;
449        Ok(())
450    }
451
452    // ACCOUNT WITNESS PREFETCHING
453    // --------------------------------------------------------------------------------------------
454
455    /// Adds to `chain_sync_data` the account witness of every registered account the sync did not
456    /// already fetch one for, so that all of them are stored with the rest of the update.
457    ///
458    /// Every account needs a witness at the new chain tip, whether or not its own state changed,
459    /// since a witness breaks when any other account in the tree moves.
460    ///
461    /// # Errors
462    ///
463    /// Fails if any witness cannot be fetched or validated. A successful sync therefore leaves a
464    /// witness at the sync height for every registered account.
465    async fn collect_account_witnesses(
466        &self,
467        chain_sync_data: &mut ChainSyncData,
468    ) -> Result<(), ClientError> {
469        let account_ids = self.store.tracked_account_witnesses().await?;
470        if account_ids.is_empty() {
471            return Ok(());
472        }
473
474        // The header of the block the witnesses must open under. The sync only carries it when it
475        // advanced; otherwise the client is already at that block and the store holds its header.
476        let chain_tip_header = match chain_sync_data.chain_tip_header() {
477            Some(header) => header.clone(),
478            None => self.get_latest_block_header().await?,
479        };
480
481        let already_fetched: BTreeSet<AccountId> = chain_sync_data
482            .account_updates
483            .account_witnesses()
484            .iter()
485            .map(|(account_id, _)| *account_id)
486            .collect();
487
488        let mut to_fetch = Vec::new();
489        for account_id in account_ids {
490            if already_fetched.contains(&account_id) {
491                continue;
492            }
493            // The chain did not advance, so the client is already synced to the chain tip. A stored
494            // witness is therefore already the witness for this block.
495            if chain_sync_data.chain_tip_header().is_none()
496                && self.store.get_account_witness(account_id).await?.is_some()
497            {
498                continue;
499            }
500            to_fetch.push(account_id);
501        }
502
503        // Bounded fan-out, under the same limit the sync uses for its own `get_account` requests.
504        let header = &chain_tip_header;
505        let witnesses: Vec<(AccountId, AccountWitness)> = futures::stream::iter(to_fetch)
506            .map(|account_id| async move {
507                let witness = self.fetch_account_witness(account_id, header).await?;
508                Ok::<_, ClientError>((account_id, witness))
509            })
510            .buffered(MAX_CONCURRENT_ACCOUNT_FETCHES)
511            .try_collect()
512            .await?;
513
514        chain_sync_data
515            .account_updates
516            .extend(AccountUpdates::default().with_account_witnesses(witnesses));
517
518        Ok(())
519    }
520
521    /// Fetches a single account's witness at `chain_tip_header`'s block.
522    async fn fetch_account_witness(
523        &self,
524        account_id: AccountId,
525        chain_tip_header: &BlockHeader,
526    ) -> Result<AccountWitness, ClientError> {
527        let chain_tip = chain_tip_header.block_num();
528
529        // The minimal request: no vault, no storage map entries, only the witness is wanted.
530        let (proof_block_num, proof) = self
531            .rpc_api
532            .get_account(account_id, GetAccountRequest::new().at(AccountStateAt::Block(chain_tip)))
533            .await?;
534
535        if proof_block_num != chain_tip {
536            return Err(ClientError::ChainValidationError(format!(
537                "get_account returned a proof at block {proof_block_num}, expected {chain_tip}"
538            )));
539        }
540
541        let (witness, _) = proof.into_parts();
542        validate_account_witness(&witness, account_id, chain_tip_header)?;
543
544        Ok(witness)
545    }
546}
547
548// SYNC SUMMARY
549// ================================================================================================
550
551/// Contains stats about the sync operation.
552#[derive(Debug, PartialEq)]
553pub struct SyncSummary {
554    /// Block number up to which the client has been synced.
555    pub block_num: BlockNumber,
556    /// IDs of new public notes that the client has received.
557    pub new_public_notes: Vec<NoteId>,
558    /// IDs of private notes imported from the Note Transport Layer in this sync. They are still
559    /// `Expected` until observed on-chain.
560    ///
561    /// Only populated by [`Client::sync_state`]; [`Client::sync_chain`] always leaves this empty
562    /// because it does not touch the Note Transport Layer.
563    pub new_private_notes: Vec<NoteId>,
564    /// IDs of tracked notes that have been committed.
565    pub committed_notes: Vec<NoteId>,
566    /// IDs of notes that have been consumed.
567    pub consumed_notes: Vec<NoteId>,
568    /// IDs of on-chain accounts that have been updated.
569    pub updated_accounts: Vec<AccountId>,
570    /// IDs of private accounts that have been locked.
571    pub locked_accounts: Vec<AccountId>,
572    /// IDs of committed transactions.
573    pub committed_transactions: Vec<TransactionId>,
574}
575
576impl SyncSummary {
577    pub fn new(
578        block_num: BlockNumber,
579        new_public_notes: Vec<NoteId>,
580        new_private_notes: Vec<NoteId>,
581        committed_notes: Vec<NoteId>,
582        consumed_notes: Vec<NoteId>,
583        updated_accounts: Vec<AccountId>,
584        locked_accounts: Vec<AccountId>,
585        committed_transactions: Vec<TransactionId>,
586    ) -> Self {
587        Self {
588            block_num,
589            new_public_notes,
590            new_private_notes,
591            committed_notes,
592            consumed_notes,
593            updated_accounts,
594            locked_accounts,
595            committed_transactions,
596        }
597    }
598
599    pub fn new_empty(block_num: BlockNumber) -> Self {
600        Self {
601            block_num,
602            new_public_notes: vec![],
603            new_private_notes: vec![],
604            committed_notes: vec![],
605            consumed_notes: vec![],
606            updated_accounts: vec![],
607            locked_accounts: vec![],
608            committed_transactions: vec![],
609        }
610    }
611
612    pub fn is_empty(&self) -> bool {
613        self.new_public_notes.is_empty()
614            && self.new_private_notes.is_empty()
615            && self.committed_notes.is_empty()
616            && self.consumed_notes.is_empty()
617            && self.updated_accounts.is_empty()
618            && self.locked_accounts.is_empty()
619            && self.committed_transactions.is_empty()
620    }
621
622    pub fn combine_with(&mut self, mut other: Self) {
623        self.block_num = max(self.block_num, other.block_num);
624        self.new_public_notes.append(&mut other.new_public_notes);
625        self.new_private_notes.append(&mut other.new_private_notes);
626        self.committed_notes.append(&mut other.committed_notes);
627        self.consumed_notes.append(&mut other.consumed_notes);
628        self.updated_accounts.append(&mut other.updated_accounts);
629        self.locked_accounts.append(&mut other.locked_accounts);
630        self.committed_transactions.append(&mut other.committed_transactions);
631    }
632}
633
634impl Serializable for SyncSummary {
635    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
636        self.block_num.write_into(target);
637        self.new_public_notes.write_into(target);
638        self.new_private_notes.write_into(target);
639        self.committed_notes.write_into(target);
640        self.consumed_notes.write_into(target);
641        self.updated_accounts.write_into(target);
642        self.locked_accounts.write_into(target);
643        self.committed_transactions.write_into(target);
644    }
645}
646
647impl Deserializable for SyncSummary {
648    fn read_from<R: miden_tx::utils::serde::ByteReader>(
649        source: &mut R,
650    ) -> Result<Self, DeserializationError> {
651        let block_num = BlockNumber::read_from(source)?;
652        let new_public_notes = Vec::<NoteId>::read_from(source)?;
653        let new_private_notes = Vec::<NoteId>::read_from(source)?;
654        let committed_notes = Vec::<NoteId>::read_from(source)?;
655        let consumed_notes = Vec::<NoteId>::read_from(source)?;
656        let updated_accounts = Vec::<AccountId>::read_from(source)?;
657        let locked_accounts = Vec::<AccountId>::read_from(source)?;
658        let committed_transactions = Vec::<TransactionId>::read_from(source)?;
659
660        Ok(Self {
661            block_num,
662            new_public_notes,
663            new_private_notes,
664            committed_notes,
665            consumed_notes,
666            updated_accounts,
667            locked_accounts,
668            committed_transactions,
669        })
670    }
671}