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::{debug, 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        debug!(sync_summary = ?sync_summary, "Sync summary computed");
223
224        // Post-sync observer hooks; run before persisting. Per-observer errors are logged, not
225        // propagated.
226        state_sync.run_apply_hooks(&state_sync_update).await?;
227
228        info!("Applying changes to the store.");
229
230        // Apply received and computed updates to the store
231        self.store
232            .apply_state_sync(state_sync_update)
233            .await
234            .map_err(ClientError::StoreError)?;
235
236        // Cache MMR so pruning can reuse in-memory MMR.
237        self.cache_partial_mmr(partial_mmr).await?;
238
239        self.maybe_untrack_and_prune_irrelevant_blocks().await?;
240
241        Ok(sync_summary)
242    }
243
244    /// Fetches private notes from the Note Transport Layer for the tracked note tags.
245    ///
246    /// Returns the IDs of notes imported in this call. No-op (returns an empty vec) if note
247    /// transport is disabled.
248    pub async fn sync_note_transport(&mut self) -> Result<Vec<NoteId>, ClientError> {
249        if !self.is_note_transport_enabled() {
250            return Ok(Vec::new());
251        }
252        self.ensure_genesis_in_place().await?;
253
254        let note_transport_update = self.fetch_note_transport_updates().await?;
255        let (imported_ids, _) = self.apply_note_transport_update(note_transport_update).await?;
256        Ok(imported_ids)
257    }
258
259    /// Runs the full client sync: private notes from the Note Transport Layer and the client's
260    /// on-chain state with the Miden node.
261    ///
262    /// The NTL and the node are fetched concurrently, and everything that writes runs sequentially
263    /// afterwards:
264    ///
265    /// 1. Concurrently: the note transport fetch and [`Client::fetch_chain_updates`]. Only node and
266    ///    NTL calls happen here, which is all that benefits from overlapping.
267    /// 2. The transport writes, when its fetch succeeded, whose records are then tracked in the
268    ///    chain sync's note updates.
269    /// 3. [`StateSync::derive_state_updates`], which screens the node's notes against the store —
270    ///    hence after step 2, so a transport-delivered note is recognised rather than discarded —
271    ///    and applies a commitment reported this sync to those records.
272    /// 4. [`StateSync::fetch_nullifiers`], covering the tracked notes *and* the transport-delivered
273    ///    ones, so a note delivered and consumed in the same window is reported as consumed by this
274    ///    call.
275    /// 5. The chain update, written last: a nullified transport-delivered note is saved as an
276    ///    update to the row step 2 inserts.
277    ///
278    /// A transport failure is logged and the chain sync continues without it, leaving the transport
279    /// cursor for the next call to retry. Before step 2 but the relay outbox, which
280    /// [`Client::flush_relay_outbox`] persists during the fetch and the next sync retries.
281    pub async fn sync_state(&mut self) -> Result<SyncSummary, ClientError> {
282        // Both fetch phases need genesis in place, and connecting here means the two concurrent
283        // futures never race on the RPC client's lazy connect.
284        self.ensure_genesis_in_place().await?;
285        self.ensure_rpc_limits_in_place().await?;
286
287        let state_sync = self.state_sync().await?;
288        let (note_transport_update, chain_sync_data) = futures::join!(
289            self.fetch_note_transport_updates(),
290            self.fetch_chain_updates(&state_sync),
291        );
292
293        // An NTL failure does not end the sync
294        let (new_private_notes, imported_notes) = match note_transport_update {
295            Ok(note_transport_update) => {
296                self.apply_note_transport_update(note_transport_update).await?
297            },
298            Err(err) => {
299                warn!(?err, "note transport fetch failed; syncing the chain without it");
300                (Vec::new(), Vec::new())
301            },
302        };
303
304        let mut chain_sync_data = chain_sync_data?;
305
306        // The chain sync built its note updates from a store snapshot taken before the import, so
307        // the imported records are added here. Without them this sync has no record to apply its
308        // verdicts to, and a note committed within this sync's own block range stays expected.
309        let imported_notes =
310            self.get_input_notes(NoteFilter::DetailsCommitments(imported_notes)).await?;
311        chain_sync_data.note_updates.track_existing_input_notes(imported_notes);
312
313        state_sync.derive_state_updates(&mut chain_sync_data).await?;
314        state_sync.fetch_nullifiers(&mut chain_sync_data).await?;
315
316        let mut summary = self.apply_chain_updates(&state_sync, chain_sync_data).await?;
317        summary.new_private_notes = new_private_notes;
318        Ok(summary)
319    }
320
321    /// Builds a default [`StateSyncInput`] from the current client state.
322    ///
323    /// This includes all tracked account headers, all unique note tags, all unspent input and
324    /// output notes, and all uncommitted transactions.
325    pub async fn build_sync_input(&self) -> Result<StateSyncInput, ClientError> {
326        let accounts = self
327            .store
328            .get_account_headers()
329            .await?
330            .into_iter()
331            .map(|(header, _status)| header)
332            .collect();
333
334        let note_tags = self.store.get_unique_note_tags().await?;
335
336        let input_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
337        let output_notes = self.store.get_output_notes(NoteFilter::Unspent).await?;
338
339        let uncommitted_transactions =
340            self.store.get_transactions(TransactionFilter::Uncommitted).await?;
341
342        Ok(StateSyncInput {
343            accounts,
344            note_tags,
345            input_notes,
346            output_notes,
347            uncommitted_transactions,
348        })
349    }
350
351    /// Applies the state sync update to the store and prunes irrelevant blocks according to the
352    /// configured cadence.
353    ///
354    /// See [`crate::Store::apply_state_sync()`] for what the update implies.
355    pub async fn apply_state_sync(&mut self, update: StateSyncUpdate) -> Result<(), ClientError> {
356        self.store.apply_state_sync(update).await?;
357
358        self.maybe_untrack_and_prune_irrelevant_blocks().await?;
359
360        Ok(())
361    }
362
363    /// Prunes irrelevant blocks and their MMR authentication nodes according to the configured
364    /// cadence.
365    async fn maybe_untrack_and_prune_irrelevant_blocks(&mut self) -> Result<(), ClientError> {
366        let Some(interval) = self.irrelevant_block_prune_interval else {
367            return Ok(());
368        };
369
370        let sync_height = self.store.get_sync_height().await?;
371
372        if let Some(last_prune_height) = self.last_irrelevant_block_prune_sync_height
373            && sync_height < last_prune_height + interval
374        {
375            return Ok(());
376        }
377
378        self.untrack_and_prune_irrelevant_blocks().await?;
379        self.last_irrelevant_block_prune_sync_height = Some(sync_height);
380
381        Ok(())
382    }
383
384    /// Prunes irrelevant block data from the store.
385    ///
386    /// Identifies tracked blocks whose input notes have all been consumed, untracks them from the
387    /// `PartialMmr` to determine which authentication nodes are no longer needed, then delegates to
388    /// [`Store::untrack_and_prune_irrelevant_blocks`] to atomically remove the stale nodes, mark
389    /// the blocks as irrelevant, and delete irrelevant block headers. Any caller of this function
390    /// should've cached the `PartialMmr` beforehand.
391    async fn untrack_and_prune_irrelevant_blocks(&mut self) -> Result<(), ClientError> {
392        let tracked_blocks = self.store.get_tracked_block_header_numbers().await?;
393        let to_untrack: Vec<usize> = if tracked_blocks.is_empty() {
394            // Do not early-return: even without blocks to untrack, old irrelevant tip headers may
395            // need pruning.
396            Vec::new()
397        } else {
398            // Blocks that still have at least one unspent note need to stay tracked.
399            let unspent_notes = self.store.get_input_notes(NoteFilter::Unspent).await?;
400            let live_blocks: BTreeSet<usize> = unspent_notes
401                .iter()
402                .filter_map(|n| n.inclusion_proof().map(|p| p.location().block_num().as_usize()))
403                .collect();
404
405            tracked_blocks.difference(&live_blocks).copied().collect()
406        };
407
408        let mut blocks_to_untrack = Vec::new();
409        let mut nodes_to_remove = Vec::new();
410        let mut updated_partial_mmr = None;
411
412        if !to_untrack.is_empty() {
413            // Rebuild the PartialMmr and untrack each block to collect the authentication node
414            // indices that are no longer needed by any remaining tracked leaf.
415            let mut partial_mmr = self.get_current_partial_mmr().await?;
416            nodes_to_remove = untrack_blocks(&mut partial_mmr, to_untrack.iter().copied());
417
418            blocks_to_untrack = to_untrack
419                .iter()
420                .map(|&b| BlockNumber::from(u32::try_from(b).expect("block number fits in u32")))
421                .collect();
422            updated_partial_mmr = Some(partial_mmr);
423        }
424
425        // Store deletes stale auth nodes, marks blocks as irrelevant, and removes irrelevant block
426        // headers. Old irrelevant tip headers may still need pruning.
427        self.store
428            .untrack_and_prune_irrelevant_blocks(&blocks_to_untrack, &nodes_to_remove)
429            .await?;
430
431        if let Some(partial_mmr) = updated_partial_mmr {
432            self.cache_partial_mmr(partial_mmr).await?;
433        }
434
435        Ok(())
436    }
437
438    /// Ensures that the RPC limits are set in the RPC client. If not already cached, fetches them
439    /// from the node and persists them in the store.
440    pub async fn ensure_rpc_limits_in_place(&mut self) -> Result<(), ClientError> {
441        if self.rpc_api.has_rpc_limits().is_some() {
442            return Ok(());
443        }
444
445        let limits = self.rpc_api.get_rpc_limits().await?;
446        self.store.set_rpc_limits(limits).await?;
447        Ok(())
448    }
449
450    // ACCOUNT WITNESS PREFETCHING
451    // --------------------------------------------------------------------------------------------
452
453    /// Adds to `chain_sync_data` the account witness of every registered account the sync did not
454    /// already fetch one for, so that all of them are stored with the rest of the update.
455    ///
456    /// Every account needs a witness at the new chain tip, whether or not its own state changed,
457    /// since a witness breaks when any other account in the tree moves.
458    ///
459    /// # Errors
460    ///
461    /// Fails if any witness cannot be fetched or validated. A successful sync therefore leaves a
462    /// witness at the sync height for every registered account.
463    async fn collect_account_witnesses(
464        &self,
465        chain_sync_data: &mut ChainSyncData,
466    ) -> Result<(), ClientError> {
467        let account_ids = self.store.tracked_account_witnesses().await?;
468        if account_ids.is_empty() {
469            return Ok(());
470        }
471
472        // The header of the block the witnesses must open under. The sync only carries it when it
473        // advanced; otherwise the client is already at that block and the store holds its header.
474        let chain_tip_header = match chain_sync_data.chain_tip_header() {
475            Some(header) => header.clone(),
476            None => self.get_latest_block_header().await?,
477        };
478
479        let already_fetched: BTreeSet<AccountId> = chain_sync_data
480            .account_updates
481            .account_witnesses()
482            .iter()
483            .map(|(account_id, _)| *account_id)
484            .collect();
485
486        let mut to_fetch = Vec::new();
487        for account_id in account_ids {
488            if already_fetched.contains(&account_id) {
489                continue;
490            }
491            // The chain did not advance, so the client is already synced to the chain tip. A stored
492            // witness is therefore already the witness for this block.
493            if chain_sync_data.chain_tip_header().is_none()
494                && self.store.get_account_witness(account_id).await?.is_some()
495            {
496                continue;
497            }
498            to_fetch.push(account_id);
499        }
500
501        // Bounded fan-out, under the same limit the sync uses for its own `get_account` requests.
502        let header = &chain_tip_header;
503        let witnesses: Vec<(AccountId, AccountWitness)> = futures::stream::iter(to_fetch)
504            .map(|account_id| async move {
505                let witness = self.fetch_account_witness(account_id, header).await?;
506                Ok::<_, ClientError>((account_id, witness))
507            })
508            .buffered(MAX_CONCURRENT_ACCOUNT_FETCHES)
509            .try_collect()
510            .await?;
511
512        chain_sync_data
513            .account_updates
514            .extend(AccountUpdates::default().with_account_witnesses(witnesses));
515
516        Ok(())
517    }
518
519    /// Fetches a single account's witness at `chain_tip_header`'s block.
520    async fn fetch_account_witness(
521        &self,
522        account_id: AccountId,
523        chain_tip_header: &BlockHeader,
524    ) -> Result<AccountWitness, ClientError> {
525        let chain_tip = chain_tip_header.block_num();
526
527        // The minimal request: no vault, no storage map entries, only the witness is wanted.
528        let (proof_block_num, proof) = self
529            .rpc_api
530            .get_account(account_id, GetAccountRequest::new().at(AccountStateAt::Block(chain_tip)))
531            .await?;
532
533        if proof_block_num != chain_tip {
534            return Err(ClientError::ChainValidationError(format!(
535                "get_account returned a proof at block {proof_block_num}, expected {chain_tip}"
536            )));
537        }
538
539        let (witness, _) = proof.into_parts();
540        validate_account_witness(&witness, account_id, chain_tip_header)?;
541
542        Ok(witness)
543    }
544}
545
546// SYNC SUMMARY
547// ================================================================================================
548
549/// Contains stats about the sync operation.
550#[derive(Debug, PartialEq)]
551pub struct SyncSummary {
552    /// Block number up to which the client has been synced.
553    pub block_num: BlockNumber,
554    /// IDs of new public notes that the client has received.
555    pub new_public_notes: Vec<NoteId>,
556    /// IDs of private notes imported from the Note Transport Layer in this sync. They are still
557    /// `Expected` until observed on-chain.
558    ///
559    /// Only populated by [`Client::sync_state`]; [`Client::sync_chain`] always leaves this empty
560    /// because it does not touch the Note Transport Layer.
561    pub new_private_notes: Vec<NoteId>,
562    /// IDs of tracked notes that have been committed.
563    pub committed_notes: Vec<NoteId>,
564    /// IDs of notes that have been consumed.
565    pub consumed_notes: Vec<NoteId>,
566    /// IDs of on-chain accounts that have been updated.
567    pub updated_accounts: Vec<AccountId>,
568    /// IDs of private accounts that have been locked.
569    pub locked_accounts: Vec<AccountId>,
570    /// IDs of committed transactions.
571    pub committed_transactions: Vec<TransactionId>,
572}
573
574impl SyncSummary {
575    pub fn new(
576        block_num: BlockNumber,
577        new_public_notes: Vec<NoteId>,
578        new_private_notes: Vec<NoteId>,
579        committed_notes: Vec<NoteId>,
580        consumed_notes: Vec<NoteId>,
581        updated_accounts: Vec<AccountId>,
582        locked_accounts: Vec<AccountId>,
583        committed_transactions: Vec<TransactionId>,
584    ) -> Self {
585        Self {
586            block_num,
587            new_public_notes,
588            new_private_notes,
589            committed_notes,
590            consumed_notes,
591            updated_accounts,
592            locked_accounts,
593            committed_transactions,
594        }
595    }
596
597    pub fn new_empty(block_num: BlockNumber) -> Self {
598        Self {
599            block_num,
600            new_public_notes: vec![],
601            new_private_notes: vec![],
602            committed_notes: vec![],
603            consumed_notes: vec![],
604            updated_accounts: vec![],
605            locked_accounts: vec![],
606            committed_transactions: vec![],
607        }
608    }
609
610    pub fn is_empty(&self) -> bool {
611        self.new_public_notes.is_empty()
612            && self.new_private_notes.is_empty()
613            && self.committed_notes.is_empty()
614            && self.consumed_notes.is_empty()
615            && self.updated_accounts.is_empty()
616            && self.locked_accounts.is_empty()
617            && self.committed_transactions.is_empty()
618    }
619
620    pub fn combine_with(&mut self, mut other: Self) {
621        self.block_num = max(self.block_num, other.block_num);
622        self.new_public_notes.append(&mut other.new_public_notes);
623        self.new_private_notes.append(&mut other.new_private_notes);
624        self.committed_notes.append(&mut other.committed_notes);
625        self.consumed_notes.append(&mut other.consumed_notes);
626        self.updated_accounts.append(&mut other.updated_accounts);
627        self.locked_accounts.append(&mut other.locked_accounts);
628        self.committed_transactions.append(&mut other.committed_transactions);
629    }
630}
631
632impl Serializable for SyncSummary {
633    fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
634        self.block_num.write_into(target);
635        self.new_public_notes.write_into(target);
636        self.new_private_notes.write_into(target);
637        self.committed_notes.write_into(target);
638        self.consumed_notes.write_into(target);
639        self.updated_accounts.write_into(target);
640        self.locked_accounts.write_into(target);
641        self.committed_transactions.write_into(target);
642    }
643}
644
645impl Deserializable for SyncSummary {
646    fn read_from<R: miden_tx::utils::serde::ByteReader>(
647        source: &mut R,
648    ) -> Result<Self, DeserializationError> {
649        let block_num = BlockNumber::read_from(source)?;
650        let new_public_notes = Vec::<NoteId>::read_from(source)?;
651        let new_private_notes = Vec::<NoteId>::read_from(source)?;
652        let committed_notes = Vec::<NoteId>::read_from(source)?;
653        let consumed_notes = Vec::<NoteId>::read_from(source)?;
654        let updated_accounts = Vec::<AccountId>::read_from(source)?;
655        let locked_accounts = Vec::<AccountId>::read_from(source)?;
656        let committed_transactions = Vec::<TransactionId>::read_from(source)?;
657
658        Ok(Self {
659            block_num,
660            new_public_notes,
661            new_private_notes,
662            committed_notes,
663            consumed_notes,
664            updated_accounts,
665            locked_accounts,
666            committed_transactions,
667        })
668    }
669}