Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7559170
refactor(rust-client): split both syncs into fetch and apply phases
ricomateo Aug 26, 2026
db355b2
refactor(rust-client): keep flush_relay_outbox as a single function
ricomateo Aug 26, 2026
b12aff4
refactor(rust-client): group the expected-note import fns in one impl…
ricomateo Aug 26, 2026
1fca608
refactor(rust-client): make ChainSyncData plain data and move its met…
ricomateo Aug 26, 2026
ce332b8
refactor(rust-client): store note blocks as they are fetched
ricomateo Aug 27, 2026
0ff8e1a
refactor(rust-client): rename the expected-note update types and fields
ricomateo Aug 27, 2026
8e886b0
refactor(rust-client): carry a single merged ExpectedNoteUpdates per …
ricomateo Aug 27, 2026
393f6b2
refactor(rust-client): cache and prune inside apply_chain_updates
ricomateo Aug 27, 2026
7888dcf
refactor(rust-client): fetch note blocks in the fetch phase, store th…
ricomateo Aug 27, 2026
47c4bd5
docs(rust-client): correct and trim the sync phase doc comments
ricomateo Aug 27, 2026
692f01b
fix(rust-client): screen the chain sync's notes after the transport w…
ricomateo Aug 28, 2026
1c6d625
fix(rust-client): check delivered notes for spends below the sync height
ricomateo Aug 28, 2026
ddd92d9
chore: update changelog
ricomateo Aug 28, 2026
a072502
refactor(rust-client): name the sync data types and bindings after wh…
ricomateo Aug 28, 2026
f6642f3
refactor(rust-client): name the transport fetch fns after what they r…
ricomateo Aug 28, 2026
f1d251c
refactor(rust-client): pair the transport fetch and apply fns by the …
ricomateo Aug 28, 2026
2669f29
chore: simplify doc comments
ricomateo Aug 28, 2026
c79d60a
chore: improve doc comments
ricomateo Aug 28, 2026
4cb2afa
refactor(rust-client): key the awaiting-block notes on their record s…
ricomateo Aug 29, 2026
f442817
docs(rust-client): correct the note import docs for the single note list
ricomateo Aug 29, 2026
603e7c7
refactor(rust-client): import notes by details through the transport …
ricomateo Aug 29, 2026
daf6dc8
refactor(rust-client): match the transport note fetch interface to th…
ricomateo Aug 29, 2026
6e4fcb5
refactor(rust-client): return the by-details import records for impor…
ricomateo Aug 29, 2026
e843309
refactor(rust-client): simplify the sync_state phase boundaries
ricomateo Aug 30, 2026
bffcc4d
refactor(rust-client): rename screen_fetched_notes to derive_note_and…
ricomateo Aug 30, 2026
b442c60
docs(rust-client): explain the NTL note merge and nullifier coverage …
ricomateo Aug 30, 2026
7c11dc3
docs(rust-client): simplify the nullifier fetch comment in sync_state
ricomateo Aug 30, 2026
9f9b365
refactor(rust-client): take note blocks from the sync response instea…
ricomateo Aug 31, 2026
13975b5
chore: improve doc comments
ricomateo Aug 31, 2026
5a0d45c
fix(rust-client): return imported notes only when the fetched informa…
ricomateo Aug 31, 2026
5504940
chore: update changelog
ricomateo Aug 31, 2026
4f6bd78
Merge branch 'next' into ricomateo-concurrent-ntl-chain-sync
ricomateo Aug 31, 2026
dcbe5bb
feat(rust-client): screen NTL-fetched notes against tracked accounts
ricomateo Aug 31, 2026
eabf980
chore: update changelog
ricomateo Aug 31, 2026
9e33f77
fix(rust-client): screen only notes associated to account-derived tags
ricomateo Aug 31, 2026
526f2ca
refactor(rust-client): screen only NTL notes matching a tracked accou…
ricomateo Sep 1, 2026
66c3825
Merge branch 'next' into ricomateo-concurrent-ntl-chain-sync
ricomateo Sep 1, 2026
8650e9b
fix: clippy warnings
ricomateo Sep 1, 2026
20bf02c
chore: improve tests legibility
ricomateo Sep 1, 2026
90ce893
chore: move apply_superseded_account_state call to derive_state_updates
ricomateo Sep 1, 2026
c987407
chore: add type alias for the import_note_records_by_details parameter
ricomateo Sep 1, 2026
0e8ba3c
Merge branch 'next' into ricomateo-concurrent-ntl-chain-sync
ricomateo Sep 4, 2026
d24f669
chore: replace assertion with an error
ricomateo Sep 4, 2026
22efe56
chore: capitalize id in doc comment
ricomateo Sep 4, 2026
8e562c9
test: check the note is committed before checking the sync_state result
ricomateo Sep 4, 2026
2fdf309
test: collapse the tests into a single one
ricomateo Sep 4, 2026
552f9a2
Merge branch 'next' into ricomateo-concurrent-ntl-chain-sync
ricomateo Sep 9, 2026
c5980b5
refactor(rust-client): make only the note transport fetch concurrent …
ricomateo Sep 9, 2026
d309701
fix(rust-client): keep the consumer account of a note spent above the…
ricomateo Sep 10, 2026
3e5ce6f
fix: replace try_join with join
ricomateo Sep 10, 2026
4a1b54b
chore: rename NoteImportRequest type alias to NoteImportByDetailsRequest
ricomateo Sep 10, 2026
cb04134
chore: rename functions and structs
ricomateo Sep 10, 2026
6edabf9
chore: update changelog
ricomateo Sep 10, 2026
ff45838
Merge branch 'next' into ricomateo-concurrent-ntl-chain-sync
ricomateo Sep 10, 2026
d2f2b91
refactor(rust-client): drop import_note_records and read the imported…
ricomateo Sep 10, 2026
f41fff8
chore: simplify sync_expected_notes and import_note_records_by_details
ricomateo Sep 10, 2026
33e242d
Merge branch 'ricomateo-concurrent-ntl-chain-sync' into ricomateo-scr…
ricomateo Sep 10, 2026
820c969
Merge branch 'next' into ricomateo-screen-ntl-notes
igamigo Sep 11, 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 @@ -4,6 +4,7 @@

### Breaking Changes

* [BREAKING][behavior][rust] Notes fetched from the Note Transport Layer are screened when their tag matches a tracked account's tag, discarding the ones no tracked account can consume ([#2474](https://github.com/0xMiden/rust-sdk/pull/2474)).
* [BREAKING][behavior][rust] A Note Transport Layer failure no longer fails `Client::sync_state`. The error is logged and the chain sync still applies; the transport cursor is left where it was, so the next sync requests the same page again ([#2453](https://github.com/0xMiden/rust-sdk/pull/2453)).
* [BREAKING][type][rust] Added the `TransactionRequestError::SwapNoteWithZeroAsset` variant, so exhaustive matches on `TransactionRequestError` must handle it ([#2459](https://github.com/0xMiden/rust-sdk/pull/2459)).
* [BREAKING][removal][test] Loose helper functions in `miden_client::testing::common` are now methods on `TestClient`. `TestClient::keystore()` exposes the client's keystore, so `ClientConfig::into_client` and `into_unsynced_client` return just the `TestClient` instead of a client/keystore pair ([#2481](https://github.com/0xMiden/rust-sdk/pull/2481)).
Expand Down
42 changes: 42 additions & 0 deletions crates/rust-client/src/note_transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,43 @@ where
)))
}

/// Screens the transport-delivered notes carrying a tag derived from a tracked account,
/// discarding those that no tracked account can consume. Notes carrying any other tag are kept
/// as delivered.
async fn screen_transport_notes(
&self,
notes: &mut Vec<(Note, Option<BlockNumber>)>,
) -> Result<(), ClientError> {
let account_tags = self.tracked_account_tags().await?;

let notes_to_screen: Vec<Note> = notes
.iter()
.filter(|(note, _)| account_tags.contains(&note.metadata().tag()))
.map(|(note, _)| note.clone())
.collect();
let consumable = self.note_screener().get_batch_consumability(&notes_to_screen).await?;

// Discard the notes whose tag match the tracked accounts but are not consumable.
notes.retain(|(note, _)| {
!account_tags.contains(&note.metadata().tag()) || consumable.contains_key(&note.id())
});
Comment on lines +444 to +460

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.

We are not using the block number at all, we can just change the parameter to Vec<Note>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The thing is that the caller function fetch_note_transport_updates requires to have the note binded to the block number, since it uses it as a hint when fetching the notes from the node, so I think is simpler to keep it that way.


Ok(())
}

/// Returns the tracked tags that were registered for an account, i.e. derived from its ID.
async fn tracked_account_tags(&self) -> Result<BTreeSet<NoteTag>, ClientError> {
let tags = self
.store
.get_note_tags()
.await?
.into_iter()
.filter(|record| matches!(record.source, NoteTagSource::Account(_)))
.map(|record| record.tag)
.collect();
Ok(tags)
}

/// Fetches and returns one batch of notes from the note transport layer for the provided tags
/// without applying any update to the store.
///
Expand Down Expand Up @@ -498,6 +535,11 @@ where
notes.push((note, note_info.block_hint));
}

// Screen the transport-delivered notes to discard the ones that are not relevant to the
// accounts tracked by the client. Boxed to avoid a `clippy::large_futures` warning, since
// the sync future is already close to the size limit.
Box::pin(self.screen_transport_notes(&mut notes)).await?;

self.drop_notes_processed_locally(&mut notes).await?;

let sync_height = self.get_sync_height().await?;
Expand Down
78 changes: 77 additions & 1 deletion crates/testing/miden-client-tests/src/tests/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ use miden_client::note::{
NoteSyncHint,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_client::note_transport::NoteTransportClient;
use miden_client::note_transport::{NoteTransportClient, NoteTransportCursor};
use miden_client::store::NoteFilter;
use miden_client::testing::common::{TestClient, create_test_store_path};
use miden_client::testing::mock::{MockClient, MockRpcApi};
Expand Down Expand Up @@ -1013,6 +1014,81 @@ async fn flush_relay_outbox_retries_failed_relay_without_full_sync() {
);
}

/// A note is routed by the tag in its own metadata, not by who can consume it, and an
/// account-target tag only covers the 14 most significant bits of the account id prefix. The
/// transport fetch screens what a tag match delivers, so a client that is offered a note paying
/// someone else discards it instead of storing it.
#[tokio::test]
async fn note_delivered_by_tag_match_is_only_kept_when_a_tracked_account_can_consume_it() {
let mock_node = Arc::new(RwLock::new(MockNoteTransportNode::new()));
let (mut sender, sender_account) = create_test_user_transport(mock_node.clone()).await;
let (mut client, account) = create_test_user_transport(mock_node.clone()).await;

// Any account that the client does not track serves as the note's target.
let unrelated_account: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();

// A P2ID note only the unrelated account can consume, but carrying the client's account tag.
let note: Note = P2idNote::builder()
.sender(sender_account.id())
.target(unrelated_account)
.asset(dummy_asset())
.note_type(NoteType::Private)
.generate_serial_number(sender.rng())
.build()
.unwrap()
.into();
// By default the P2ID note is tagged for the target account, so here we manually override the
// note with the client's account tag.
let (assets, _, recipient, attachments) = note.into_parts();
let account_tag = NoteTag::with_account_target(account.id());
let metadata =
PartialNoteMetadata::new(sender_account.id(), NoteType::Private).with_tag(account_tag);
let note = Note::with_attachments(assets, metadata, recipient, attachments);

// The address is not what routes the note: the relay keys off the tag in its header.
let address = Address::new(account.id())
.with_routing_parameters(RoutingParameters::new(AddressInterface::BasicWallet));
sender
.send_private_note_with_block_hint(note.clone(), &address, BlockNumber::from(0))
.await
.unwrap();

// Fetch the notes matching the `account_tag` and check the P2ID note was actually committed
let (notes_info, _) = mock_node.read().get_notes(&[account_tag], NoteTransportCursor::init());
let fetched_note_info = notes_info.first().unwrap();
assert_eq!(fetched_note_info.header, *note.header());

// During the sync, the client retrieves the note from the NTL (since the tag matches its
// account), but the note is discarded because its account cannot consume it.
client.sync_state().await.unwrap();
let notes = client.get_input_notes(NoteFilter::All).await.unwrap();
assert!(notes.is_empty(), "a note no tracked account can consume must not be stored");

// Now send a note consumable by the account and check the client tracks it
let note: Note = P2idNote::builder()
.sender(sender_account.id())
.target(account.id())
.asset(dummy_asset())
.note_type(NoteType::Private)
.generate_serial_number(sender.rng())
.build()
.unwrap()
.into();

let recipient_address = Address::new(account.id())
.with_routing_parameters(RoutingParameters::new(AddressInterface::BasicWallet));
sender
.send_private_note_with_block_hint(note, &recipient_address, BlockNumber::from(0))
.await
.unwrap();

// The client now will track the note during the sync because this time the note is consumable
// by the tracked account
client.sync_state().await.unwrap();
let notes = client.get_input_notes(NoteFilter::All).await.unwrap();
assert_eq!(notes.len(), 1, "a note the tracked account can consume must be stored");
}

/// A relay that keeps failing must not block `sync_state`. The outbox flush runs at the start of
/// the transport step; if its error propagated, a single undeliverable note would wedge every
/// subsequent sync. The entry must stay in the outbox for later retry while the sync itself
Expand Down
Loading