Skip to content

fix(rpc): verify block window on range-scoped VerifyingRpcClient responses - #2503

Open
erhnysr wants to merge 3 commits into
0xMiden:nextfrom
erhnysr:fix/2454-verify-block-window-range
Open

fix(rpc): verify block window on range-scoped VerifyingRpcClient responses#2503
erhnysr wants to merge 3 commits into
0xMiden:nextfrom
erhnysr:fix/2454-verify-block-window-range

Conversation

@erhnysr

@erhnysr erhnysr commented Sep 5, 2026

Copy link
Copy Markdown

Summary

VerifyingRpcClient exists to reject responses that don't answer the request that was made. It does this thoroughly for one half of the request: returned note IDs, note tags, nullifier prefixes, account IDs and script roots are all checked against what was asked for.

The other half was missing. Five of its methods take a block_from/block_to window and none of them ever checked whether the response fell inside it — both parameters were forwarded to the inner client and then never mentioned again:

  • sync_notes — checked tags, not heights
  • sync_nullifiers — checked prefixes, not heights
  • sync_storage_maps — delegated, no check
  • sync_account_vault — delegated, no check
  • sync_transactions — checked account IDs, not heights

So a node could answer a query about blocks 100–200 with data stamped block 5000, and the wrapper whose whole purpose is catching that passed it straight through. The impact is concrete for sync_transactions: record.block_num flows unchecked into commit_transaction(record.block_num, …) (sync/state_sync_update.rs), committing a locally tracked transaction at whatever height the response carried, including one past the chain tip the client just read.

This is the same class as the recent VerifyingRpcClient fixes (#2419, #2381): a value the node controls, trusted without being checked.

The fix

One helper next to the existing verify_* checks:

fn verify_block_range(
    block_from: BlockNumber,
    block_to: BlockNumber,
    returned: impl IntoIterator<Item = BlockNumber>,
) -> Result<(), RpcError> { ... }

called from all five methods, rejecting out-of-window data with RpcError::InvalidResponse — consistent with every other check in the file. Notes and nullifiers carry a height per item; the storage-map and vault responses carry a single pagination cursor (block_number), so those pass one value.

Note on the storage-map / vault cursor

For sync_storage_maps / sync_account_vault the checked value is the pagination cursor, not per-item heights. The tonic client's BlockPagination already guarantees the cursor is >= block_from and respects block_to, so the inclusive [block_from, block_to] check does not falsely reject honest responses, while still catching an out-of-range cursor from a misbehaving node.

Verification

  • make lint — clean (cargo fix, cargo +nightly fmt, taplo fmt, cargo clippy --workspace --features "testing std" --all-targets -- -D warnings, cargo shear), no warnings
  • make test462 tests run: 462 passed, 2 skipped

Five new tests (sync_{notes,nullifiers,storage_maps,account_vault,transactions}_verifies_block_range) cover inside-window accept and out-of-window reject.

Closes #2454

erhnysr added a commit to erhnysr/rust-sdk that referenced this pull request Sep 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 053c756462

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rust-client/src/rpc/verifying_client/mod.rs
…onses

VerifyingRpcClient checked returned note IDs, tags, nullifier prefixes,
account IDs and script roots against the request, but none of the five
methods scoped to a block_from/block_to window ever checked that the
response fell inside it. The window was forwarded to the inner client and
never mentioned again, so a node could answer a query about one block range
with data stamped in another - a mis-stamped transaction record, for
instance, commits a locally tracked transaction at whatever height the
response carried.

Add a verify_block_range helper next to the existing verify_* checks and
call it from sync_notes, sync_nullifiers, sync_storage_maps,
sync_account_vault and sync_transactions, rejecting out-of-window data with
RpcError::InvalidResponse.

Closes 0xMiden#2454
… on build_patch_update

The VerifyingRpcClient wrapper range-checks only the pagination cursor of
the sync_storage_maps and sync_account_vault responses, not the height of
each individual update. Document that the store closes this gap by verifying
the applied patch against the authenticated header, and record the caller
obligation to authenticate details.header and pass its block as block_to.

Claude-Session: https://claude.ai/code/session_013wTu7UVnJ4fzkixZecXLkV
@erhnysr
erhnysr force-pushed the fix/2454-verify-block-window-range branch from 022e089 to 11ddd65 Compare September 14, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VerifyingRpcClient never checks that range-scoped responses fall inside the requested block window

1 participant