Skip to content

store: SyncAccountVault / SyncAccountStorageMaps return silently incomplete updates when block_to is older than the account history retention window #2624

Description

@Bruce039

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions