fix(world-state): treat historical block 0 queries as historical, not latest#22679
Merged
spalladino merged 5 commits intomerge-train/spartanfrom Apr 21, 2026
Merged
Conversation
… latest `WorldStateRevision.blockNumber == 0` was overloaded on the C++ side as a sentinel meaning "use latest committed state" (via `if (revision.blockNumber)` checks), rather than pinning to block 0. Combined with a short-circuit in `AztecNodeService.getWorldState` that mapped initial-header queries directly to `getSnapshot(BlockNumber.ZERO)` and bypassed the archive-root double-check, this caused witnesses returned for genesis-anchored queries to silently use the current tip once the node advanced past genesis. PXEs with `syncChainTip: 'checkpointed'` stay pinned to the initial header until the first checkpoint commits; any private-kernel proof built in that window would fail with `Proving public value inclusion failed` because the public-data-tree witness was taken against the advanced tip while the circuit validated it against the initial header's root. Fix: - Add an explicit `WorldStateRevision::LATEST` sentinel (max uint32) on both the C++ and TS sides. Use `revision.is_historical()` at the C++ call sites so `blockNumber == 0` now means "pin to block 0". Adjust `getCommitted()` and `fork()` to pass `LATEST` where the previous "0 means latest" semantics were relied on. - Route the initial-header case in `getWorldState` through the normal snapshot + archive-root double-check path rather than returning early, so any future tree-state mismatch at block 0 is caught. - Defensively capture the anchor header once per `proveTx`/`simulateTx`/ `profileTx` and pass it through to both `#executePrivate` and `#prove`, rather than re-reading `anchorBlockStore` independently in each. This cannot drift today (both sit inside the same job-queue slot), but makes the invariant explicit and type-checked going forward.
The header file has template methods (get_leaf, get_indexed_leaf, find_leaf_sibling_paths, find_leaves_indexes) that were still using the old if (rev.blockNumber) check. With the new LATEST sentinel, that check always evaluates true and routed committed/latest reads through the historical path with an invalid block number, causing WorldStateTest.GetInitialTreeInfoForAllTrees to get nullopt from get_leaf. Switch all remaining sites to is_historical().
Contributor
|
@spalladino it would be good to port at least the PXE side of this change to v4-next |
PhilWindle
approved these changes
Apr 21, 2026
Contributor
Author
|
/claudebox create a PR to backport ONLY the pxe changes from this PR to branch v4-next |
Collaborator
|
⏳ Run #1 — Session completed (4m) You've hit your limit · resets Apr 23, 8pm (UTC) |
After introducing LATEST as the "not historical" sentinel, the internal revisions still set `.blockNumber = 0` which is now interpreted as a real historical block query and fails with "Unable to get meta data for block 0". Remove the explicit `.blockNumber = 0` so the default LATEST sentinel is used instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Include the block hash actually stored in world state and the genesis header hash in the "Block hash not found in world state" error, to help diagnose whether the mismatch is against the initial header specifically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…is hash Block 0 has no historical snapshot in world state — the genesis header state only lives in the committed/uncommitted view (via the tree's initial values). A historical snapshot at block 0 fails with "Unable to get leaf at block 0" in the native tree, which surfaces as "Block hash <genesis> not found in world state at block number 0" when the PXE queries with the genesis hash as anchor. Since the anchor hash matching the known genesis hash means there is no reorg risk, short-circuit and return the committed db directly. Updates the existing unit test, which already reproduced the failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
/claudebox Try again to create a PR to backport ONLY the pxe changes from this PR to branch v4-next |
Collaborator
|
⏳ Run #1 — Session completed (6m) Backported PXE-only portion of #22679 to v4-next. Patch applied cleanly to |
This was referenced Apr 21, 2026
AztecBot
added a commit
that referenced
this pull request
Apr 24, 2026
nchamo
added a commit
that referenced
this pull request
Apr 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WorldStateRevision.blockNumber == 0as "use latest committed state" viaif (revision.blockNumber)checks, rather than pinning to block 0. This silently returned the current tip instead of the genesis tree for any genesis-anchored query, once the node advanced past genesis.AztecNodeService.getWorldStatehad a short-circuit that mapped initial-header queries directly togetSnapshot(BlockNumber.ZERO)and bypassed the archive-root double-check that would otherwise catch the mismatch.syncChainTip: 'checkpointed'before the first checkpoint commits) produced private-kernel proofs that failed withProving public value inclusion failed: the public-data-tree witness came from the node's advanced tip while the circuit validated it against the initial header's root.How PXE ends up querying block zero
BlockSynchronizer.doSync(pxe/src/block_synchronizer/block_synchronizer.ts:178-181) sees no stored anchor and callsnode.getBlockHeader(BlockNumber.ZERO). It stores the resulting header — whose hash is the initial-header hash and whose tree roots are the genesis roots.syncChainTip: 'checkpointed'keeps it pinned.handleBlockStreamEvent(block_synchronizer.ts:60-74) only advances the anchor onchain-checkpointedevents;blocks-addedis ignored. Until a checkpoint commits on L1 — which takes seconds or longer after sequencers start — the PXE's anchor stays at the initial header.proveTxhands that anchor to the kernel oracle. After the sync at the top ofproveTx, the PXE reads the anchor and passes its hash intonew PrivateKernelOracle(..., anchorBlockHash). Every oracle call (getPublicDataWitness,getPublicStorageAt, etc.) uses that hash.PrivateKernelOracle.getUpdatedClassIdHints(pxe/src/private_kernel/private_kernel_oracle.ts:121-150) issuesnode.getPublicDataWitness(initialHeaderHash, hashLeafSlot)plus a matchinggetPublicStorageAtread, both pinned to the same hash.AztecNodeService.getWorldState(aztec-node/src/aztec-node/server.ts:1714-1719, pre-fix) recognised the initial-header hash and returnedworldStateSynchronizer.getSnapshot(BlockNumber.ZERO)directly, skipping the archive-tree reorg check below.getSnapshot(0)builds a revision withblockNumber = 0.NativeWorldState.getSnapshot(world-state/src/native/native_world_state.ts:157-163) constructednew WorldStateRevision(forkId=0, blockNumber=0, includeUncommitted=false)and handed it to theMerkleTreesFacade, which forwards it unchanged on every native call.blockNumber == 0as "latest". Inbarretenberg/cpp/src/barretenberg/world_state/world_state.cppevery tree op (get_meta_data,get_sibling_path,find_low_leaf, etc.) checkedif (revision.blockNumber)/if (revision.blockNumber != 0U)— zero is falsy, so the code fell into the "no block pin, use latest committed" branch. The returned sibling path, low-leaf preimage, next-index and next-slot were all taken against the current tip.noir-protocol-circuits/crates/types/src/data/storage_read.nr:41viadelayed_public_mutable/with_hash.nr), the membership hash is compared againsthistorical_header.state.partial.public_data_tree.root— the genesis root from the PXE's anchor. The oracle-supplied witness was computed against the advanced tip's root. Pre-sequencer the tip happens to equal genesis so it "works"; once block 1 lands they diverge andassert(is_leaf_in_tree, ...)fires.Fix
WorldStateRevision::LATESTsentinel (std::numeric_limits<uint32_t>::max()in C++; mirrored in TS) and anis_historical()helper. Replace everyif (revision.blockNumber)call site inworld_state.cppwithif (revision.is_historical()). Zero now correctly means "pin to block 0".WorldStateRevision.empty()andNativeWorldState.fork()to passLATESTwhere the old "0 means latest" semantics were relied on.getSnapshot(blockNumber)passes the number through unchanged, sogetSnapshot(0)now genuinely pins to block 0.AztecNodeService.getWorldState, replace the initial-header early return with a block-number resolution toBlockNumber.ZEROthat falls through to the standard snapshot + archive-root double-check. The archive tree at index 0 stores the initial-header hash (per the assertion innative_world_state.ts:143), so the check works uniformly for block 0 too.proveTx/simulateTx/profileTxinpxe/src/pxe.tsand thread it through to both#executePrivateand#prove, rather than re-readinganchorBlockStoreindependently in each call. This cannot drift today (both live inside the same job-queue slot), but makes the invariant explicit and type-checked going forward.Test plan
yarn buildfromyarn-project.syncChainTip: 'checkpointed'.e2e_epochs/epochs_mbps_redistributionsecond test stops emittingProving public value inclusion failederrors once the full stack (including rebuiltbb) is deployed.