diff --git a/crates/rust-client/src/note/note_update_tracker.rs b/crates/rust-client/src/note/note_update_tracker.rs index 84adc3413..915b62f2a 100644 --- a/crates/rust-client/src/note/note_update_tracker.rs +++ b/crates/rust-client/src/note/note_update_tracker.rs @@ -370,6 +370,19 @@ impl NoteUpdateTracker { input_note_unspent_nullifiers.chain(output_note_unspent_nullifiers) } + /// Returns the block numbers containing input notes that remain unspent after this update. + pub(crate) fn unspent_input_note_block_numbers( + &self, + ) -> impl Iterator + '_ { + self.input_notes + .values() + .chain(self.expected_input_notes.values()) + .filter(|update| !update.inner().is_consumed()) + .filter_map(|update| { + update.inner().inclusion_proof().map(|proof| proof.location().block_num()) + }) + } + /// Appends nullifiers to the per-account ordered nullifier list. /// /// Nullifiers from the same account must be in execution order; ordering across different diff --git a/crates/rust-client/src/pswap/discovery.rs b/crates/rust-client/src/pswap/discovery.rs index 42dd002bc..a66620613 100644 --- a/crates/rust-client/src/pswap/discovery.rs +++ b/crates/rust-client/src/pswap/discovery.rs @@ -37,7 +37,7 @@ pub(crate) async fn discover_pswap_rounds( chain_note_updates: &[ObservedPswapNote], ) -> Result, PswapLineageError> { let consumed_note_ids: BTreeSet = - state_sync_update.note_updates.consumed_note_ids().collect(); + state_sync_update.note_updates().consumed_note_ids().collect(); if consumed_note_ids.is_empty() && chain_note_updates.is_empty() { return Ok(Vec::new()); @@ -54,7 +54,7 @@ pub(crate) async fn discover_pswap_rounds( // Commit-block note roots for inserting reconstructed notes as `Committed`. let block_headers: BTreeMap = state_sync_update - .partial_blockchain_updates + .partial_blockchain_updates() .block_headers() .map(|(header, _)| (header.block_num(), header.clone())) .collect(); diff --git a/crates/rust-client/src/pswap/observer.rs b/crates/rust-client/src/pswap/observer.rs index 064af7fca..e4b1ae008 100644 --- a/crates/rust-client/src/pswap/observer.rs +++ b/crates/rust-client/src/pswap/observer.rs @@ -86,7 +86,7 @@ impl NoteObserver for PswapChainObserver { // Nothing observed AND nothing consumed — correlator has no work. if chain_note_updates.is_empty() - && sync_update.note_updates.consumed_note_ids().next().is_none() + && sync_update.note_updates().consumed_note_ids().next().is_none() { return Ok(()); } diff --git a/crates/rust-client/src/sync/mod.rs b/crates/rust-client/src/sync/mod.rs index cce42a14b..f2e9577d6 100644 --- a/crates/rust-client/src/sync/mod.rs +++ b/crates/rust-client/src/sync/mod.rs @@ -84,6 +84,7 @@ mod state_sync; pub use state_sync::{NoteUpdateAction, OnNoteReceived, StateSync, StateSyncInput}; mod state_sync_update; +use state_sync_update::untrack_blocks; pub use state_sync_update::{ AccountUpdates, PartialBlockchainUpdates, @@ -289,10 +290,7 @@ where // Rebuild the PartialMmr and untrack each block to collect the authentication node // indices that are no longer needed by any remaining tracked leaf. let mut partial_mmr = self.get_current_partial_mmr().await?; - for &block_pos in &to_untrack { - nodes_to_remove - .extend(partial_mmr.untrack(block_pos).into_iter().map(|(idx, _)| idx)); - } + nodes_to_remove = untrack_blocks(&mut partial_mmr, to_untrack.iter().copied()); blocks_to_untrack = to_untrack .iter() diff --git a/crates/rust-client/src/sync/state_sync.rs b/crates/rust-client/src/sync/state_sync.rs index f0bfae326..48f357db8 100644 --- a/crates/rust-client/src/sync/state_sync.rs +++ b/crates/rust-client/src/sync/state_sync.rs @@ -259,26 +259,29 @@ impl StateSync { let note_tags = Arc::new(note_tags); let account_ids: Vec = accounts.iter().map(AccountHeader::id).collect(); - let mut state_sync_update = StateSyncUpdate { - block_num, - note_updates: NoteUpdateTracker::new(input_notes, output_notes), - transaction_updates: TransactionUpdateTracker::new(uncommitted_transactions), - ..Default::default() - }; - let Some(sync_data) = self - .fetch_sync_data(state_sync_update.block_num, &account_ids, ¬e_tags) - .await? + let mut note_updates = NoteUpdateTracker::new(input_notes, output_notes); + let mut transaction_updates = TransactionUpdateTracker::new(uncommitted_transactions); + let mut partial_blockchain_updates = PartialBlockchainUpdates::default(); + let mut account_updates = AccountUpdates::default(); + + let Some(sync_data) = self.fetch_sync_data(block_num, &account_ids, ¬e_tags).await? else { // No progress — already at the tip. - return Ok(state_sync_update); + return Ok(StateSyncUpdate::from_parts( + block_num, + partial_blockchain_updates, + note_updates, + transaction_updates, + account_updates, + )); }; - state_sync_update.block_num = sync_data.chain_tip_header.block_num(); + let chain_tip = sync_data.chain_tip_header.block_num(); let new_commitments = derive_account_commitments(&sync_data.transactions); let superseded_states = self .account_state_sync( - &mut state_sync_update.account_updates, + &mut account_updates, &accounts, &new_commitments, block_num, @@ -288,20 +291,43 @@ impl StateSync { // Discard the local transactions whose result lost a same-nonce race against the network. for superseded_state in superseded_states { - state_sync_update - .transaction_updates - .apply_superseded_account_state(superseded_state); + transaction_updates.apply_superseded_account_state(superseded_state); } // Apply local changes: update the MMR, screen notes, and apply state transitions. - self.apply_sync_result(sync_data, &mut state_sync_update, current_partial_mmr) - .await?; + self.apply_sync_result( + sync_data, + &mut partial_blockchain_updates, + &mut note_updates, + &mut transaction_updates, + current_partial_mmr, + ) + .await?; if self.sync_nullifiers { - self.nullifiers_state_sync(&mut state_sync_update, block_num).await?; + self.nullifiers_state_sync( + &mut note_updates, + &mut transaction_updates, + chain_tip, + block_num, + ) + .await?; } - Ok(state_sync_update) + // Drop newly-synced block data that no unspent input note needs: blocks whose notes were + // all consumed within this same sync don't have to be tracked or persisted. + let live_blocks: BTreeSet = + note_updates.unspent_input_note_block_numbers().collect(); + partial_blockchain_updates + .untrack_irrelevant_note_blocks(&live_blocks, current_partial_mmr); + + Ok(StateSyncUpdate::from_parts( + chain_tip, + partial_blockchain_updates, + note_updates, + transaction_updates, + account_updates, + )) } /// Fetches the sync data from the node by calling the following endpoints: @@ -400,7 +426,9 @@ impl StateSync { async fn apply_sync_result( &self, sync_data: FetchedSyncData, - state_sync_update: &mut StateSyncUpdate, + partial_blockchain_updates: &mut PartialBlockchainUpdates, + note_updates: &mut NoteUpdateTracker, + transaction_updates: &mut TransactionUpdateTracker, current_partial_mmr: &mut PartialMmr, ) -> Result<(), ClientError> { let FetchedSyncData { @@ -419,16 +447,23 @@ impl StateSync { mmr_delta, &chain_tip_header, &mut working_mmr, - &mut state_sync_update.partial_blockchain_updates, + partial_blockchain_updates, )?; - self.screen_note_blocks(note_blocks, synced_notes, state_sync_update, &mut working_mmr) - .await?; + self.screen_note_blocks( + note_blocks, + synced_notes, + note_updates, + partial_blockchain_updates, + &mut working_mmr, + ) + .await?; self.apply_transactions_and_nullifiers( &chain_tip_header, &transactions, - state_sync_update, + note_updates, + transaction_updates, )?; // Commit the working MMR back to the caller once all checks pass. @@ -556,7 +591,8 @@ impl StateSync { &self, note_blocks: Vec, synced_notes: BTreeMap, - state_sync_update: &mut StateSyncUpdate, + note_updates: &mut NoteUpdateTracker, + partial_blockchain_updates: &mut PartialBlockchainUpdates, current_partial_mmr: &mut PartialMmr, ) -> Result<(), ClientError> { // Attachment content for private notes, keyed by note ID. Joined to each committed note @@ -573,7 +609,7 @@ impl StateSync { for block in note_blocks { let found_relevant_note = self .note_state_sync( - &mut state_sync_update.note_updates, + note_updates, block.notes, &block.block_header, &public_note_records, @@ -603,11 +639,7 @@ impl StateSync { .map(|(k, v)| (*k, *v)) .collect(); - state_sync_update.partial_blockchain_updates.insert( - block.block_header, - true, - track_auth_nodes, - ); + partial_blockchain_updates.insert(block.block_header, true, track_auth_nodes); } } @@ -621,31 +653,26 @@ impl StateSync { &self, chain_tip_header: &BlockHeader, transactions: &[RpcTransactionRecord], - state_sync_update: &mut StateSyncUpdate, + note_updates: &mut NoteUpdateTracker, + transaction_updates: &mut TransactionUpdateTracker, ) -> Result<(), ClientError> { - state_sync_update - .note_updates - .extend_nullifiers(compute_ordered_nullifiers(transactions)); + note_updates.extend_nullifiers(compute_ordered_nullifiers(transactions)); for record in transactions { - state_sync_update - .transaction_updates + transaction_updates .apply_transaction_inclusion(record, u64::from(chain_tip_header.timestamp())); //TODO: Change timestamps from u64 to u32 } - state_sync_update - .transaction_updates + transaction_updates .apply_sync_height_update(chain_tip_header.block_num(), self.tx_discard_delta); for transaction in transactions { // Transition tracked output notes to Committed using inclusion proofs from the // transaction sync response. This covers output notes regardless of whether their // tags were tracked in the note sync. - state_sync_update - .note_updates - .apply_output_note_inclusion_proofs(&transaction.output_notes)?; + note_updates.apply_output_note_inclusion_proofs(&transaction.output_notes)?; // Detect output notes erased by same-batch note erasure. - Self::mark_erased_notes_as_consumed(state_sync_update, transaction); + Self::mark_erased_notes_as_consumed(note_updates, transaction); } Ok(()) @@ -657,14 +684,12 @@ impl StateSync { /// the block body. The node reports these as erased output notes in the transaction /// record (note ID only, no inclusion proof). We mark them as consumed. fn mark_erased_notes_as_consumed( - state_sync_update: &mut StateSyncUpdate, + note_updates: &mut NoteUpdateTracker, transaction: &RpcTransactionRecord, ) { for note_header in &transaction.erased_output_notes { // Best-effort: ignore errors for notes not tracked by this client. - let _ = state_sync_update - .note_updates - .mark_erased_note_as_consumed(note_header, transaction.block_num); + let _ = note_updates.mark_erased_note_as_consumed(note_header, transaction.block_num); } } @@ -1071,38 +1096,37 @@ impl StateSync { /// the `sync_nullifiers` endpoint to check if there are new nullifiers for these /// notes. It then processes the nullifiers to apply the state transitions on the note updates. /// - /// The `state_sync_update` parameter will be updated to track the new discarded transactions. + /// The `transaction_updates` parameter will be updated to track the new discarded + /// transactions. async fn nullifiers_state_sync( &self, - state_sync_update: &mut StateSyncUpdate, + note_updates: &mut NoteUpdateTracker, + transaction_updates: &mut TransactionUpdateTracker, + chain_tip: BlockNumber, current_block_num: BlockNumber, ) -> Result<(), ClientError> { // To receive information about added nullifiers, we reduce them to the higher 16 bits // Note that besides filtering by nullifier prefixes, the node also filters by block number - // (it only returns nullifiers from current_block_num + 1 until state_sync_update.block_num) + // (it only returns nullifiers from current_block_num + 1 until chain_tip) // Check for new nullifiers for input notes that were updated - let nullifiers_tags: Vec = state_sync_update - .note_updates - .unspent_nullifiers() - .map(|nullifier| nullifier.prefix()) - .collect(); + let nullifiers_tags: Vec = + note_updates.unspent_nullifiers().map(|nullifier| nullifier.prefix()).collect(); let mut new_nullifiers = self .rpc_api - .sync_nullifiers(&nullifiers_tags, current_block_num + 1, state_sync_update.block_num) + .sync_nullifiers(&nullifiers_tags, current_block_num + 1, chain_tip) .await?; // Discard nullifiers that are newer than the current block (this might happen if the block // changes between the sync_state and the check_nullifier calls) - new_nullifiers.retain(|update| update.block_num <= state_sync_update.block_num); + new_nullifiers.retain(|update| update.block_num <= chain_tip); // Match each nullifier update with the externally-tracked consumer account. let consumptions: Vec = new_nullifiers .into_iter() .map(|update| NoteConsumption { - external_consumer: state_sync_update - .transaction_updates + external_consumer: transaction_updates .external_nullifier_account(&update.nullifier), nullifier: update.nullifier, block_num: update.block_num, @@ -1110,17 +1134,15 @@ impl StateSync { .collect(); for consumption in consumptions { - state_sync_update.note_updates.apply_note_consumption( + note_updates.apply_note_consumption( &consumption, - state_sync_update.transaction_updates.committed_transactions(), + transaction_updates.committed_transactions(), )?; // Process nullifiers and track the updates of local tracked transactions that were // discarded because the notes that they were processing were nullified by an // another transaction. - state_sync_update - .transaction_updates - .apply_input_note_nullified(consumption.nullifier); + transaction_updates.apply_input_note_nullified(consumption.nullifier); } Ok(()) @@ -1935,7 +1957,7 @@ mod tests { let update = state_sync.sync_state(&mut partial_mmr, sync_input).await.unwrap(); - let updated_notes: Vec<_> = update.note_updates.updated_input_notes().collect(); + let updated_notes: Vec<_> = update.note_updates().updated_input_notes().collect(); let find_order = |details_commitment| -> Option { updated_notes @@ -1979,7 +2001,7 @@ mod tests { // First sync let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap(); - assert_eq!(update.block_num, chain_tip_1); + assert_eq!(update.block_num(), chain_tip_1); let forest_1 = partial_mmr.forest(); // The MMR should contain one leaf per block (genesis + the new blocks). assert_eq!(forest_1.num_leaves(), chain_tip_1.as_u32() as usize + 1); @@ -1990,7 +2012,7 @@ mod tests { let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap(); - assert_eq!(update.block_num, chain_tip_2); + assert_eq!(update.block_num(), chain_tip_2); let forest_2 = partial_mmr.forest(); assert!(forest_2 > forest_1); assert_eq!(forest_2.num_leaves(), chain_tip_2.as_u32() as usize + 1); @@ -1998,7 +2020,7 @@ mod tests { // Third sync (no new blocks) let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap(); - assert_eq!(update.block_num, chain_tip_2); + assert_eq!(update.block_num(), chain_tip_2); assert_eq!(partial_mmr.forest(), forest_2); } @@ -2370,7 +2392,7 @@ mod tests { // The output note record should transition to consumed. let updated_output = update - .note_updates + .note_updates() .updated_output_notes() .find(|n| n.id() == erased_note_id) .expect("output note should be in the update"); @@ -2382,7 +2404,7 @@ mod tests { // A new input note record should be created with the network account as consumer. let input_note_update = update - .note_updates + .note_updates() .updated_input_notes() .find(|n| n.id() == Some(erased_note_id)) .expect("input note should be created from the erased output note"); diff --git a/crates/rust-client/src/sync/state_sync_update.rs b/crates/rust-client/src/sync/state_sync_update.rs index 4adc570fb..4f3027003 100644 --- a/crates/rust-client/src/sync/state_sync_update.rs +++ b/crates/rust-client/src/sync/state_sync_update.rs @@ -14,7 +14,7 @@ use miden_protocol::account::{ }; use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey}; use miden_protocol::block::{BlockHeader, BlockNumber}; -use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks}; +use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks, PartialMmr}; use miden_protocol::errors::{AccountDeltaError, AccountError}; use miden_protocol::note::{NoteId, Nullifier}; use miden_protocol::transaction::TransactionId; @@ -31,18 +31,89 @@ use crate::transaction::{DiscardCause, TransactionRecord, TransactionStatus}; // ================================================================================================ /// Contains all information needed to apply the update in the store after syncing with the node. -#[derive(Default)] +/// +/// Immutable once built: [`StateSync::sync_state`](super::StateSync::sync_state) assembles the +/// individual trackers and seals them into this type at the end of the sync pass. Use +/// [`Self::from_parts`] to build one directly. pub struct StateSyncUpdate { /// The block number of the last block that was synced. - pub block_num: BlockNumber, + block_num: BlockNumber, /// New blocks, authentication nodes and MMR peaks. - pub partial_blockchain_updates: PartialBlockchainUpdates, + partial_blockchain_updates: PartialBlockchainUpdates, /// New and updated notes to be upserted in the store. - pub note_updates: NoteUpdateTracker, + note_updates: NoteUpdateTracker, /// Committed and discarded transactions after the sync. - pub transaction_updates: TransactionUpdateTracker, + transaction_updates: TransactionUpdateTracker, /// Public account updates and mismatched private accounts after the sync. - pub account_updates: AccountUpdates, + account_updates: AccountUpdates, +} + +impl StateSyncUpdate { + /// Assembles an update from its constituent parts, mirroring [`Self::into_parts`]. + /// + /// The parts are stored as given: no validation or minimization is applied. In particular, + /// blockchain updates for blocks whose notes are all spent are kept as-is — + /// [`StateSync::sync_state`](super::StateSync::sync_state) strips those before assembling + /// the update it returns. + pub fn from_parts( + block_num: BlockNumber, + partial_blockchain_updates: PartialBlockchainUpdates, + note_updates: NoteUpdateTracker, + transaction_updates: TransactionUpdateTracker, + account_updates: AccountUpdates, + ) -> Self { + Self { + block_num, + partial_blockchain_updates, + note_updates, + transaction_updates, + account_updates, + } + } + + /// Returns the block number of the last synced block. + pub fn block_num(&self) -> BlockNumber { + self.block_num + } + + /// Returns the partial blockchain updates. + pub fn partial_blockchain_updates(&self) -> &PartialBlockchainUpdates { + &self.partial_blockchain_updates + } + + /// Returns the note updates. + pub fn note_updates(&self) -> &NoteUpdateTracker { + &self.note_updates + } + + /// Returns the transaction updates. + pub fn transaction_updates(&self) -> &TransactionUpdateTracker { + &self.transaction_updates + } + + /// Returns the account updates. + pub fn account_updates(&self) -> &AccountUpdates { + &self.account_updates + } + + /// Decomposes this update into its constituent parts. + pub fn into_parts( + self, + ) -> ( + BlockNumber, + PartialBlockchainUpdates, + NoteUpdateTracker, + TransactionUpdateTracker, + AccountUpdates, + ) { + ( + self.block_num, + self.partial_blockchain_updates, + self.note_updates, + self.transaction_updates, + self.account_updates, + ) + } } impl From<&StateSyncUpdate> for SyncSummary { @@ -156,11 +227,68 @@ impl PartialBlockchainUpdates { self.block_headers.values() } + /// Returns block headers that need to be persisted for this update. + pub fn block_headers_to_store( + &self, + sync_height: BlockNumber, + ) -> impl Iterator { + self.block_headers.values().filter(move |(header, has_client_notes)| { + *has_client_notes + || header.block_num() == BlockNumber::GENESIS + || header.block_num() == sync_height + }) + } + /// Returns the new authentication nodes that are meant to be stored in order to authenticate /// block headers. pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] { &self.new_authentication_nodes } + + /// Untracks note blocks that are not in `live_blocks`: clears their `has_client_notes` flag, + /// untracks their leaves from `partial_mmr`, and drops the authentication nodes that no + /// remaining tracked leaf needs. + pub(super) fn untrack_irrelevant_note_blocks( + &mut self, + live_blocks: &BTreeSet, + partial_mmr: &mut PartialMmr, + ) { + let blocks_to_untrack: Vec = self + .block_headers + .iter_mut() + .filter_map(|(block_num, (_, has_client_notes))| { + if *has_client_notes && !live_blocks.contains(block_num) { + *has_client_notes = false; + Some(*block_num) + } else { + None + } + }) + .collect(); + + let removed_nodes: BTreeSet = + untrack_blocks(partial_mmr, blocks_to_untrack.into_iter().map(|b| b.as_usize())) + .into_iter() + .collect(); + self.new_authentication_nodes + .retain(|(index, _)| !removed_nodes.contains(index)); + } +} + +/// Untracks the given block leaves from `partial_mmr`, returning the authentication-node indices +/// that are no longer needed by any remaining tracked leaf. +/// +/// Untracking a leaf frees an inner node only once no other tracked leaf still needs it, so the +/// returned indices are exactly the nodes that became removable. +pub(crate) fn untrack_blocks( + partial_mmr: &mut PartialMmr, + block_positions: impl IntoIterator, +) -> Vec { + block_positions + .into_iter() + .flat_map(|block_pos| partial_mmr.untrack(block_pos)) + .map(|(index, _)| index) + .collect() } /// Contains transaction changes to apply to the store. diff --git a/crates/sqlite-store/src/sync.rs b/crates/sqlite-store/src/sync.rs index 6a9d946ac..55f464d3a 100644 --- a/crates/sqlite-store/src/sync.rs +++ b/crates/sqlite-store/src/sync.rs @@ -108,13 +108,13 @@ impl SqliteStore { smt_forest: &Arc>, state_sync_update: StateSyncUpdate, ) -> Result<(), StoreError> { - let StateSyncUpdate { + let ( block_num, partial_blockchain_updates, note_updates, transaction_updates, account_updates, - } = state_sync_update; + ) = state_sync_update.into_parts(); with_forest_snapshot(conn, smt_forest, |tx, smt_forest| { // Update blockchain checkpoint (block number and peaks) only if moving forward. @@ -131,7 +131,7 @@ impl SqliteStore { .into_store_error()?; for (block_header, block_has_relevant_notes) in - partial_blockchain_updates.block_headers() + partial_blockchain_updates.block_headers_to_store(block_num) { Self::insert_block_header_tx(tx, block_header, *block_has_relevant_notes)?; } diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 029745133..e4c6a38ec 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -433,8 +433,9 @@ async fn sync_state() { #[tokio::test] async fn sync_state_mmr() { - // generate test client with a random store name - let (mut client, rpc_api, keystore) = Box::pin(create_test_client()).await; + let (builder, rpc_api, keystore) = Box::pin(create_test_client_builder()).await; + let mut client = builder.irrelevant_block_prune_interval(None).build().await.unwrap(); + client.ensure_genesis_in_place().await.unwrap(); // Import note and create wallet so that synced notes do not get discarded (due to being // irrelevant) insert_new_wallet(&mut client, AccountType::Private, &keystore).await.unwrap(); @@ -485,12 +486,11 @@ async fn sync_state_mmr() { // Try reconstructing the partial_mmr from what's in the database let partial_mmr = client.test_store().get_current_partial_mmr().await.unwrap(); assert!(partial_mmr.forest().num_leaves() >= 6); - assert!(partial_mmr.open(0).unwrap().is_none()); // Block 1 holds the only unspent public note, so its leaf stays tracked. assert!(partial_mmr.open(1).unwrap().is_some()); assert!(partial_mmr.open(2).unwrap().is_none()); assert!(partial_mmr.open(3).unwrap().is_none()); - // Block 4's notes are all consumed externally, so pruning untracks its leaf. + // Block 4's notes are all consumed externally, so its leaf is never persisted as tracked. assert!(partial_mmr.open(4).unwrap().is_none()); assert!(partial_mmr.open(5).unwrap().is_none()); @@ -499,9 +499,24 @@ async fn sync_state_mmr() { let (block_1, _) = rpc_api.get_block_header_by_number(Some(1.into()), false).await.unwrap(); partial_mmr.peaks().verify(block_1.commitment(), mmr_proof).unwrap(); - // Only block 1 remains tracked after pruning; block 4 was untracked because all its - // notes are already consumed externally. - assert_eq!(client.test_store().get_tracked_block_headers().await.unwrap().len(), 1); + // Automatic pruning is disabled: block 4 is absent because the sync update did not persist it. + assert!( + client + .test_store() + .get_tracked_block_headers() + .await + .unwrap() + .iter() + .all(|header| header.block_num() != BlockNumber::from(4u32)) + ); + assert!( + client + .test_store() + .get_block_headers(&[BlockNumber::from(4u32)].into_iter().collect()) + .await + .unwrap() + .is_empty() + ); } #[tokio::test] @@ -659,12 +674,12 @@ async fn sync_persists_auth_nodes_for_skipped_blocks() { // Blocks 1 and 4 had matching note tags but the screener discarded them, // so `include_block` was false for those steps. assert_eq!( - state_sync_update.partial_blockchain_updates.block_headers().count(), + state_sync_update.partial_blockchain_updates().block_headers().count(), 1, "expected only the chain tip block header to be stored" ); let (tip_header, ..) = - state_sync_update.partial_blockchain_updates.block_headers().next().unwrap(); + state_sync_update.partial_blockchain_updates().block_headers().next().unwrap(); assert_eq!(tip_header.block_num(), rpc_api.get_chain_tip_block_num()); // Authentication nodes must be non-empty: they include nodes produced by applying @@ -672,7 +687,7 @@ async fn sync_persists_auth_nodes_for_skipped_blocks() { // tracked genesis leaf's Merkle proof path, which changes as the tree grows. assert!( !state_sync_update - .partial_blockchain_updates + .partial_blockchain_updates() .new_authentication_nodes() .is_empty(), "expected authentication nodes from intermediate (skipped) blocks to be persisted" @@ -744,7 +759,7 @@ async fn sync_state_no_redundant_get_account_calls() { // Only 1 updated public account entry, not N duplicates assert_eq!( - state_sync_update.account_updates.updated_public_accounts().len(), + state_sync_update.account_updates().updated_public_accounts().len(), 1, "expected exactly 1 updated public account" );