diff --git a/CHANGELOG.md b/CHANGELOG.md index 47a4bff20..64993cd6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Enhancements +* [FEATURE][rust] Added `Client::sync_chain()` (on-chain sync only) and `Client::sync_note_transport()` (Note Transport Layer fetch only) for callers needing finer-grained control over sync. ([#2091](https://github.com/0xMiden/miden-client/pull/2091)) * [FEATURE][rust] Added `GrpcClient::with_bearer_auth(token)` to attach an `authorization: Bearer ` header to every outbound gRPC call, for use behind authenticating gateways. Tokens are validated at connection time and preserved across `set_genesis_commitment` updates ([#2101](https://github.com/0xMiden/miden-client/pull/2101)). * Made new-account construction use merged storage schema commitment (`build_with_schema_commitment`), re-exported `AccountBuilderSchemaCommitmentExt`, added WASM `buildWithoutSchemaCommitment()`, and fixed contract `accounts.create()` to require explicit `components` ([#1996](https://github.com/0xMiden/miden-client/pull/1996)). * Fixed the faucet token symbol display when showing account details ([#1985](https://github.com/0xMiden/miden-client/pull/1985)). diff --git a/bin/miden-cli/src/commands/sync.rs b/bin/miden-cli/src/commands/sync.rs index 599b0ed70..8dc80e4da 100644 --- a/bin/miden-cli/src/commands/sync.rs +++ b/bin/miden-cli/src/commands/sync.rs @@ -17,6 +17,7 @@ impl SyncCmd { println!("State synced to block {}", new_details.block_num); println!("New public notes: {}", new_details.new_public_notes.len()); + println!("New private notes: {}", new_details.new_private_notes.len()); println!("Committed notes: {}", new_details.committed_notes.len()); println!("Tracked notes consumed: {}", new_details.consumed_notes.len()); println!("Tracked accounts updated: {}", new_details.updated_accounts.len()); diff --git a/crates/rust-client/src/note_transport/mod.rs b/crates/rust-client/src/note_transport/mod.rs index e1616a8bd..8179dd818 100644 --- a/crates/rust-client/src/note_transport/mod.rs +++ b/crates/rust-client/src/note_transport/mod.rs @@ -10,7 +10,7 @@ use alloc::vec::Vec; use futures::Stream; use miden_protocol::address::Address; use miden_protocol::block::BlockNumber; -use miden_protocol::note::{Note, NoteDetails, NoteFile, NoteHeader, NoteTag}; +use miden_protocol::note::{Note, NoteDetails, NoteFile, NoteHeader, NoteId, NoteTag}; use miden_protocol::utils::serde::Serializable; use miden_tx::auth::TransactionAuthenticator; use miden_tx::utils::serde::{ @@ -110,12 +110,12 @@ where /// Fetch notes from the note transport network for provided note tags /// /// Pagination is employed, where only notes after the provided cursor are requested. - /// Downloaded notes are imported. + /// Downloaded notes are imported. Returns the IDs of the imported notes. pub(crate) async fn fetch_transport_notes( &mut self, cursor: NoteTransportCursor, tags: I, - ) -> Result<(), ClientError> + ) -> Result, ClientError> where I: IntoIterator, { @@ -154,12 +154,12 @@ where }; note_requests.push(note_file); } - self.import_notes(¬e_requests).await?; + let imported_note_ids = self.import_notes(¬e_requests).await?; // Update cursor (pagination) self.store.update_note_transport_cursor(rcursor).await?; - Ok(()) + Ok(imported_note_ids) } } diff --git a/crates/rust-client/src/sync/mod.rs b/crates/rust-client/src/sync/mod.rs index f162bf3e2..e53c9f5c8 100644 --- a/crates/rust-client/src/sync/mod.rs +++ b/crates/rust-client/src/sync/mod.rs @@ -39,6 +39,7 @@ //! let sync_summary: SyncSummary = client.sync_state().await?; //! //! println!("Synced up to block number: {}", sync_summary.block_num); +//! println!("New private notes: {}", sync_summary.new_private_notes.len()); //! println!("Committed notes: {}", sync_summary.committed_notes.len()); //! println!("Consumed notes: {}", sync_summary.consumed_notes.len()); //! println!("Updated accounts: {}", sync_summary.updated_accounts.len()); @@ -100,37 +101,20 @@ where self.store.get_sync_height().await.map_err(Into::into) } - /// Syncs the client's state with the current state of the Miden network and returns a - /// [`SyncSummary`] corresponding to the local state update. + /// Syncs the client's on-chain state with the current state of the Miden network and returns + /// a [`SyncSummary`] corresponding to the local state update. /// - /// The sync process is done in multiple steps: - /// 1. A request is sent to the node to get the state updates. This request includes tracked - /// account IDs and the tags of notes that might have changed or that might be of interest to - /// the client. - /// 2. A response is received with the current state of the network. The response includes - /// information about new/committed/consumed notes, updated accounts, and committed - /// transactions. - /// 3. Tracked notes are updated with their new states. - /// 4. New notes are checked, and only relevant ones are stored. Relevant notes are those that - /// can be consumed by accounts the client is tracking (this is checked by the - /// [`crate::note::NoteScreener`]) - /// 5. Transactions are updated with their new states. - /// 6. Tracked public accounts are updated and private accounts are validated against the node - /// state. - /// 7. The MMR is updated with the new peaks and authentication nodes. - /// 8. All updates are applied to the store to be persisted. - pub async fn sync_state(&mut self) -> Result { + /// Does **not** fetch private notes from the Note Transport Layer. Use + /// [`Client::sync_state`] for the combined sync, or call [`Client::sync_note_transport`] + /// separately. + /// + /// Builds the default sync input, runs [`StateSync::sync_state`] (see that method for the + /// detailed pipeline), applies the resulting update to the store, caches the partial MMR, and + /// prunes irrelevant blocks according to the configured cadence. + pub async fn sync_chain(&mut self) -> Result { self.ensure_genesis_in_place().await?; self.ensure_rpc_limits_in_place().await?; - // Note Transport update - // TODO We can run both sync_state, fetch_transport_notes futures in parallel - if self.is_note_transport_enabled() { - let cursor = self.store.get_note_transport_cursor().await?; - let note_tags = self.store.get_unique_note_tags().await?; - self.fetch_transport_notes(cursor, note_tags).await?; - } - // Build sync state components let note_screener = self.note_screener(); let state_sync = StateSync::new( @@ -164,6 +148,36 @@ where Ok(sync_summary) } + /// Fetches private notes from the Note Transport Layer for the tracked note tags. + /// + /// Returns the IDs of notes imported in this call. No-op (returns an empty vec) if note + /// transport is disabled. + pub async fn sync_note_transport(&mut self) -> Result, ClientError> { + if !self.is_note_transport_enabled() { + return Ok(Vec::new()); + } + + let cursor = self.store.get_note_transport_cursor().await?; + let note_tags = self.store.get_unique_note_tags().await?; + self.fetch_transport_notes(cursor, note_tags).await + } + + /// Runs the full client sync. + /// + /// First fetches private notes from the Note Transport Layer (see + /// [`Client::sync_note_transport`]), then syncs the client's on-chain state with the Miden + /// node (see [`Client::sync_chain`]). If note transport is disabled, this is equivalent to + /// [`Client::sync_chain`]. + /// + /// Fails fast on the first error. Private notes delivered via NTL are imported before the + /// chain sync reads its input set, so their nullifiers are checked in the same call. + pub async fn sync_state(&mut self) -> Result { + let new_private_notes = self.sync_note_transport().await?; + let mut summary = self.sync_chain().await?; + summary.new_private_notes = new_private_notes; + Ok(summary) + } + /// Builds a default [`StateSyncInput`] from the current client state. /// /// This includes all tracked account headers, all unique note tags, all unspent input and @@ -307,6 +321,11 @@ pub struct SyncSummary { pub block_num: BlockNumber, /// IDs of new public notes that the client has received. pub new_public_notes: Vec, + /// IDs of private notes imported from the Note Transport Layer in this sync. + /// + /// Only populated by [`Client::sync_state`]; [`Client::sync_chain`] always leaves this empty + /// because it does not touch the Note Transport Layer. + pub new_private_notes: Vec, /// IDs of tracked notes that have been committed. pub committed_notes: Vec, /// IDs of notes that have been consumed. @@ -323,6 +342,7 @@ impl SyncSummary { pub fn new( block_num: BlockNumber, new_public_notes: Vec, + new_private_notes: Vec, committed_notes: Vec, consumed_notes: Vec, updated_accounts: Vec, @@ -332,6 +352,7 @@ impl SyncSummary { Self { block_num, new_public_notes, + new_private_notes, committed_notes, consumed_notes, updated_accounts, @@ -344,6 +365,7 @@ impl SyncSummary { Self { block_num, new_public_notes: vec![], + new_private_notes: vec![], committed_notes: vec![], consumed_notes: vec![], updated_accounts: vec![], @@ -354,6 +376,7 @@ impl SyncSummary { pub fn is_empty(&self) -> bool { self.new_public_notes.is_empty() + && self.new_private_notes.is_empty() && self.committed_notes.is_empty() && self.consumed_notes.is_empty() && self.updated_accounts.is_empty() @@ -364,6 +387,7 @@ impl SyncSummary { pub fn combine_with(&mut self, mut other: Self) { self.block_num = max(self.block_num, other.block_num); self.new_public_notes.append(&mut other.new_public_notes); + self.new_private_notes.append(&mut other.new_private_notes); self.committed_notes.append(&mut other.committed_notes); self.consumed_notes.append(&mut other.consumed_notes); self.updated_accounts.append(&mut other.updated_accounts); @@ -376,6 +400,7 @@ impl Serializable for SyncSummary { fn write_into(&self, target: &mut W) { self.block_num.write_into(target); self.new_public_notes.write_into(target); + self.new_private_notes.write_into(target); self.committed_notes.write_into(target); self.consumed_notes.write_into(target); self.updated_accounts.write_into(target); @@ -390,6 +415,7 @@ impl Deserializable for SyncSummary { ) -> Result { let block_num = BlockNumber::read_from(source)?; let new_public_notes = Vec::::read_from(source)?; + let new_private_notes = Vec::::read_from(source)?; let committed_notes = Vec::::read_from(source)?; let consumed_notes = Vec::::read_from(source)?; let updated_accounts = Vec::::read_from(source)?; @@ -399,6 +425,7 @@ impl Deserializable for SyncSummary { Ok(Self { block_num, new_public_notes, + new_private_notes, committed_notes, consumed_notes, updated_accounts, diff --git a/crates/rust-client/src/sync/state_sync.rs b/crates/rust-client/src/sync/state_sync.rs index 2aaee506b..3165ce0ee 100644 --- a/crates/rust-client/src/sync/state_sync.rs +++ b/crates/rust-client/src/sync/state_sync.rs @@ -209,20 +209,15 @@ impl StateSync { /// mutable reference so callers can keep it in memory across syncs. /// /// During the sync process, the following steps are performed: - /// 1. A request is sent to the node to get the state updates. This request includes tracked - /// account IDs and the tags of notes that might have changed or that might be of interest to - /// the client. - /// 2. A response is received with the current state of the network. The response includes - /// information about new and committed notes, updated accounts, and committed transactions. - /// 3. Tracked public accounts are updated and private accounts are validated against the node - /// state. - /// 4. Tracked notes are updated with their new states. Notes might be committed or nullified - /// during the sync processing. - /// 5. New notes are checked, and only relevant ones are stored. Relevance is determined by the - /// [`OnNoteReceived`] callback. - /// 6. Transactions are updated with their new states. Transactions might be committed or - /// discarded. - /// 7. The MMR is updated with the new peaks and authentication nodes. + /// 1. Fetch sync data from the node (MMR delta, note inclusions, transactions). + /// 2. Update account states (fetch updated public accounts, flag mismatched private ones). + /// 3. Advance the partial MMR to the chain tip. + /// 4. Screen note inclusions via the configured [`OnNoteReceived`] callback and track relevant + /// blocks in the MMR. + /// 5. Process transaction inclusions (commit local txs, record external consumers, discard + /// stale/expired txs, commit output notes). + /// 6. Detect consumed notes via nullifier sync (optional, see + /// [`Self::disable_nullifier_sync`]). pub async fn sync_state( &self, current_partial_mmr: &mut PartialMmr, diff --git a/crates/rust-client/src/sync/state_sync_update.rs b/crates/rust-client/src/sync/state_sync_update.rs index d92500e14..7c6b3cb02 100644 --- a/crates/rust-client/src/sync/state_sync_update.rs +++ b/crates/rust-client/src/sync/state_sync_update.rs @@ -77,6 +77,8 @@ impl From<&StateSyncUpdate> for SyncSummary { SyncSummary::new( value.block_num, new_public_note_ids, + // Populated by Client::sync_state from the Note Transport Layer fetch. + Vec::new(), committed_note_ids.into_iter().collect(), consumed_note_ids.into_iter().collect(), value diff --git a/crates/testing/miden-client-tests/src/tests/transport.rs b/crates/testing/miden-client-tests/src/tests/transport.rs index 9561625c4..ff9b31bdd 100644 --- a/crates/testing/miden-client-tests/src/tests/transport.rs +++ b/crates/testing/miden-client-tests/src/tests/transport.rs @@ -267,7 +267,11 @@ async fn fetch_private_notes_finds_note_committed_at_sync_height() { // 6. Second sync_state: fetch_transport_notes imports the note, then chain sync runs. // Without the fix, after_block_num = sync_height, scan misses the note at block 1. // With the fix, lookback window catches it. - client.sync_state().await.unwrap(); + let summary = client.sync_state().await.unwrap(); + assert!( + summary.new_private_notes.contains(&private_note.id()), + "summary should report the NTL-imported note in new_private_notes" + ); // 7. The note should be Committed after the second sync. let committed_notes = client.get_input_notes(NoteFilter::Committed).await.unwrap();