Summary
SyncAccountVault and SyncAccountStorageMaps return silently incomplete results when block_to is older than the account history retention window (HISTORICAL_BLOCK_RETENTION = 50 blocks).
rpc.proto says for both endpoints that block_to "must be close to the chain tip … otherwise an error will be returned", and GetAccount does reject such blocks with BlockPruned. The two sync endpoints only run StateView::scope_range, which checks block_to <= tip, so nothing enforces the window.
Where
crates/store/src/state/view/sync.rs: StateView::sync_account_vault, sync_account_storage_maps.
crates/store/src/db/models/queries/accounts.rs: prune_history → prune_account_vault_assets / prune_account_storage_map_values delete versioned rows once valid_until <= tip - 50; select_account_vault_assets / select_account_storage_map_values_paged filter only on block_num BETWEEN from AND to.
What goes wrong
Take a key written at block b inside [from, to] and superseded at block v with to < v <= tip - 50. After pruning the row for b is gone (its valid_until is below the cutoff) while its successor at v is outside the requested range. The response carries no update for that key and reports last_block_included = to, which tells the client "nothing changed for this key up to to". That is false, and the client has no way to notice.
Reproduction
DB-level test in crates/store/src/db/tests.rs: set a vault asset at block 10, update it at block 60, prune at tip 130 (cutoff 80). Before pruning select_account_vault_assets(0..=50) returns one row; after pruning it returns zero rows with last_block_included == 50:
assertion failed: vault sync up to block 50 must not silently drop the update from block 10 (or must error because the range is below the retention cutoff)
Diff < left / right > : <0 >1
View-level test in crates/store/src/state/view/sync.rs: after writing enough empty blocks to move the tip past the window, sync_account_vault with block_to below tip - 50 returns Ok((BlockNumber(9), [])) instead of an error.
Impact
Any RPC caller whose block_to is more than 50 blocks behind the tip (a client resuming a sync against an older target header, or a flow anchored to an old block like the one in #2597) gets an account view with missing vault balances / storage-map entries and a pagination info that says the range is complete. With the SDK's patch-based public-account sync this ends up as a wrong AccountPatch.
Proposed fix
Add a scope_retained_range step after scope_range that rejects range.end() < tip.saturating_sub(HISTORICAL_BLOCK_RETENTION) with a RangeBelowRetention { oldest_retained, block_to } error (transparent DatabaseError variant next to RangeBeyondTip), use it in both sync endpoints, and map it to INVALID_ARGUMENT in the RPC layer like RangeBeyondTip. A range ending exactly at tip - 50 stays served; the check uses the snapshot tip, so it is never stricter than what the DB still holds.
I have this change with the two tests ready (miden-node-store lib tests: 185 passed; clippy clean for store and rpc) and can open a PR if this is assigned to me.
Summary
SyncAccountVaultandSyncAccountStorageMapsreturn silently incomplete results whenblock_tois older than the account history retention window (HISTORICAL_BLOCK_RETENTION= 50 blocks).rpc.protosays for both endpoints thatblock_to"must be close to the chain tip … otherwise an error will be returned", andGetAccountdoes reject such blocks withBlockPruned. The two sync endpoints only runStateView::scope_range, which checksblock_to <= tip, so nothing enforces the window.Where
crates/store/src/state/view/sync.rs:StateView::sync_account_vault,sync_account_storage_maps.crates/store/src/db/models/queries/accounts.rs:prune_history→prune_account_vault_assets/prune_account_storage_map_valuesdelete versioned rows oncevalid_until <= tip - 50;select_account_vault_assets/select_account_storage_map_values_pagedfilter only onblock_num BETWEEN from AND to.What goes wrong
Take a key written at block
binside[from, to]and superseded at blockvwithto < v <= tip - 50. After pruning the row forbis gone (itsvalid_untilis below the cutoff) while its successor atvis outside the requested range. The response carries no update for that key and reportslast_block_included = to, which tells the client "nothing changed for this key up toto". That is false, and the client has no way to notice.Reproduction
DB-level test in
crates/store/src/db/tests.rs: set a vault asset at block 10, update it at block 60, prune at tip 130 (cutoff 80). Before pruningselect_account_vault_assets(0..=50)returns one row; after pruning it returns zero rows withlast_block_included == 50:View-level test in
crates/store/src/state/view/sync.rs: after writing enough empty blocks to move the tip past the window,sync_account_vaultwithblock_tobelowtip - 50returnsOk((BlockNumber(9), []))instead of an error.Impact
Any RPC caller whose
block_tois more than 50 blocks behind the tip (a client resuming a sync against an older target header, or a flow anchored to an old block like the one in #2597) gets an account view with missing vault balances / storage-map entries and a pagination info that says the range is complete. With the SDK's patch-based public-account sync this ends up as a wrongAccountPatch.Proposed fix
Add a
scope_retained_rangestep afterscope_rangethat rejectsrange.end() < tip.saturating_sub(HISTORICAL_BLOCK_RETENTION)with aRangeBelowRetention { oldest_retained, block_to }error (transparentDatabaseErrorvariant next toRangeBeyondTip), use it in both sync endpoints, and map it toINVALID_ARGUMENTin the RPC layer likeRangeBeyondTip. A range ending exactly attip - 50stays served; the check uses the snapshot tip, so it is never stricter than what the DB still holds.I have this change with the two tests ready (
miden-node-storelib tests: 185 passed; clippy clean for store and rpc) and can open a PR if this is assigned to me.