Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7900e83
surface NTL fetch errors via SyncSummary instead of failing sync_state
JereSalo Apr 21, 2026
e240a90
surface the inner NoteTransportError message in SyncSummary.ntl_error
JereSalo Apr 21, 2026
ee50832
tighten changelog entry and ntlError docstring
JereSalo Apr 21, 2026
2128f79
split sync_state into on-chain, NTL, and combined methods
JereSalo Apr 22, 2026
81afbaf
simplify sync lock with callback-based API
JereSalo Apr 23, 2026
066bf9e
update transport tests to call sync_note_transport after split
JereSalo Apr 23, 2026
eb087b6
Merge branch 'next' into jere/syncstate-ntl-decouple-1930
JereSalo Apr 23, 2026
7838054
add Node proxy aliases for syncNoteTransport and syncAll
JereSalo Apr 23, 2026
4987e2e
run NTL before chain sync in sync_all
JereSalo Apr 23, 2026
20c4688
allow syncNoteTransportImpl and syncAllImpl in method classification
JereSalo Apr 23, 2026
2d235d2
silence unhandled rejection from sync lock cleanup chain
JereSalo Apr 23, 2026
6561ac3
rename sync timeout error to reflect scope
JereSalo Apr 23, 2026
ac017f1
rename sync_state/sync_all to sync_chain/sync_state with combined def…
JereSalo Apr 23, 2026
6ab4564
restore transport tests to use sync_state
JereSalo Apr 23, 2026
268c5c6
remove timeout parameter from sync methods
JereSalo Apr 23, 2026
a1ead57
drop stale timeout option from MidenClient api types
JereSalo Apr 23, 2026
37c65e1
compress sync split changelog entries into one
JereSalo Apr 23, 2026
8ad4274
regenerate typedoc after removing timeout option
JereSalo Apr 24, 2026
e212878
document sync lock helpers
JereSalo Apr 28, 2026
b22e62d
Merge remote-tracking branch 'origin/next' into jere/syncstate-ntl-de…
JereSalo Apr 29, 2026
e34cc46
reframe sync split changelog entry as enhancement
JereSalo Apr 29, 2026
f0d1b94
trim redundant sync_state line from changelog entry
JereSalo Apr 29, 2026
b33b7f0
Merge branch 'next' into jere/syncstate-ntl-decouple-1930
JereSalo Apr 30, 2026
ab17474
expand sync state documentation
JereSalo May 5, 2026
b9ad816
Merge remote-tracking branch 'origin/next' into jere/syncstate-ntl-de…
JereSalo May 6, 2026
57ea066
Merge remote-tracking branch 'origin/jere/syncstate-ntl-decouple-1930…
JereSalo May 7, 2026
ccb3f4f
link sync_note_transport and sync_chain in sync_state docs
JereSalo May 7, 2026
2e0d1d9
address review feedback on sync docs and surface NTL notes in SyncSum…
JereSalo May 8, 2026
5fe0968
print new_private_notes count in CLI sync command
JereSalo May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>` 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)).
Expand Down
1 change: 1 addition & 0 deletions bin/miden-cli/src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit ambiguous: you can get private notes from the chain itself (or rather, we get the inclusion information for a private note that the client knew about), but I believe this just really displays the private notes retrieved from the NTL, right? If so, we'd need to make this clearer IMO.

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());
Expand Down
10 changes: 5 additions & 5 deletions crates/rust-client/src/note_transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<I>(
&mut self,
cursor: NoteTransportCursor,
tags: I,
) -> Result<(), ClientError>
) -> Result<Vec<NoteId>, ClientError>
where
I: IntoIterator<Item = NoteTag>,
{
Expand Down Expand Up @@ -154,12 +154,12 @@ where
};
note_requests.push(note_file);
}
self.import_notes(&note_requests).await?;
let imported_note_ids = self.import_notes(&note_requests).await?;

// Update cursor (pagination)
self.store.update_note_transport_cursor(rcursor).await?;

Ok(())
Ok(imported_note_ids)
}
}

Expand Down
81 changes: 54 additions & 27 deletions crates/rust-client/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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<SyncSummary, ClientError> {
/// 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<SyncSummary, ClientError> {
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(
Expand Down Expand Up @@ -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<Vec<NoteId>, 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<SyncSummary, ClientError> {
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)
Comment on lines +175 to +178

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We could add something like SyncSummary::from_ntl_sync(fetched_note_ids: Vec<NoteId>) and have SyncSummary::from_chain_sync(/* all other parameters */) and then combine them with SyncSummary::combine_with() here to avoid mutating the internal field, and also avoid having to initialize the SyncSummary with an empty list of private notes every time. I wonder if we could also make all fields private instead of pub by default.

}
Comment thread
JereSalo marked this conversation as resolved.

/// Builds a default [`StateSyncInput`] from the current client state.
///
/// This includes all tracked account headers, all unique note tags, all unspent input and
Expand Down Expand Up @@ -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<NoteId>,
/// 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<NoteId>,
Comment thread
JereSalo marked this conversation as resolved.
/// IDs of tracked notes that have been committed.
pub committed_notes: Vec<NoteId>,
/// IDs of notes that have been consumed.
Expand All @@ -323,6 +342,7 @@ impl SyncSummary {
pub fn new(
block_num: BlockNumber,
new_public_notes: Vec<NoteId>,
new_private_notes: Vec<NoteId>,
committed_notes: Vec<NoteId>,
consumed_notes: Vec<NoteId>,
updated_accounts: Vec<AccountId>,
Expand All @@ -332,6 +352,7 @@ impl SyncSummary {
Self {
block_num,
new_public_notes,
new_private_notes,
committed_notes,
consumed_notes,
updated_accounts,
Expand All @@ -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![],
Expand All @@ -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()
Expand All @@ -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);
Expand All @@ -376,6 +400,7 @@ impl Serializable for SyncSummary {
fn write_into<W: miden_tx::utils::serde::ByteWriter>(&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);
Expand All @@ -390,6 +415,7 @@ impl Deserializable for SyncSummary {
) -> Result<Self, DeserializationError> {
let block_num = BlockNumber::read_from(source)?;
let new_public_notes = Vec::<NoteId>::read_from(source)?;
let new_private_notes = Vec::<NoteId>::read_from(source)?;
let committed_notes = Vec::<NoteId>::read_from(source)?;
let consumed_notes = Vec::<NoteId>::read_from(source)?;
let updated_accounts = Vec::<AccountId>::read_from(source)?;
Expand All @@ -399,6 +425,7 @@ impl Deserializable for SyncSummary {
Ok(Self {
block_num,
new_public_notes,
new_private_notes,
committed_notes,
consumed_notes,
updated_accounts,
Expand Down
23 changes: 9 additions & 14 deletions crates/rust-client/src/sync/state_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions crates/rust-client/src/sync/state_sync_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion crates/testing/miden-client-tests/src/tests/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading