Vector anchor - #841
Merged
Merged
Conversation
newHead is a uint256 in ProofOutputs but was narrowed with U256::to::<u64>(), which is uint_try_to().expect(..) and panics on overflow. The narrowing runs before Groth16Verifier::verify, so the value is still attacker-supplied at that point -- a dummy 4-byte proof reaches it. mock_fulfill narrows the same field with no proof verification at all. Only the Updater can call either, so this is not permissionless, but a runtime panic reachable from extrinsic input is a robustness bug: the extrinsic traps instead of returning an error, wasting the author's slot and making any block carrying it unimportable. Replace both conversions with try_into() and a SlotOutOfRange error.
The SP1 circuit derives prevHeader/prevHead from a store it reads straight from prover CBOR, so the proof alone does not tie an update to this pallet's state. Without an on-chain binding, Headers is a set of independent roots rather than a chain, and execute() authorizes bridge messages against those roots. Add the upstream SP1Helios.sol check to fulfill: Headers[prevHead] must equal the proof's prevHeader. This is anchor membership, not tip continuity -- a proof may anchor at any header the pallet has already accepted, which is what real prover output does. Verified against mainnet: the fixture's anchor matches Headers[14823232] exactly. That invariant has to be reachable from a standing start, and nothing outside fulfill wrote the state it needs. Genesis now seeds head, its header, and the head period's sync committee hash together, asserting they are consistent, so a fresh chain is either unconfigured or fully able to sync. Updater and the SP1 key stay root-settable: anchor state is history a chain cannot reconstruct, those two are rotatable policy. The fulfill benchmark sets Head without Headers, so it needs the same anchor seed to reach the code it measures.
enable_mock was root-gated but had no chain restriction, so mock could be armed on a chain bridging Ethereum mainnet, after which the Updater could write arbitrary headers and execution state roots with no proof at all. Refuse to arm it when SourceChainId is 1, and re-check in mock_fulfill so a flag set before this change, or surviving a source chain id change, still cannot be used. That guard is also what lets mock_fulfill keep working without the anchor binding fulfill has. The extrinsic already accepts an arbitrary header and state root unproven, so requiring a known anchor would buy no security there -- it would only stop mock from bootstrapping a test chain, which is its purpose. Recorded as a comment so the asymmetry with fulfill does not read as an oversight.
Three tests covering how a chain gets from empty to syncing, from both directions. The operator sequence: genesis supplies the anchor state no extrinsic can write, root extrinsics supply the rest, and a real SP1 proof is then accepted. No direct storage writes, so this exercises the actual public API rather than a shortcut. fulfill cannot self-start. Its values are proof-bound, so it cannot present the zero startSyncCommitteeHash an unseeded chain would match, and it fails at the committee check before the anchor binding is even reached -- which is why seeding head and header alone was not enough. mock_fulfill can start from literally nothing, because its values are unproven and can carry that zero hash. That asymmetry is the intended split: mock is the test-chain escape hatch, fulfill must never be able to conjure a bridge from thin air.
Genesis takes the anchor triple -- `head`, that head's `header`, and the sync committee hash for the head's period -- and writes them together, so a chain starting mid-history can accept its first `fulfill`. Require all three or none. `head` gates the branch, so a spec supplying `header` and `sync_committee_hash` while omitting `head` would silently drop both and boot a chain that looks configured while rejecting every update. Chain specs are JSON patches merged onto the default config, so an omitted field defaults to zero rather than failing to deserialize; nothing else catches this. Tests cover each missing leg of the triple, the inverse case, and the all-zero case that keeps the older testnet specs booting.
The variant now covers both the disabled flag and the mainnet source chain guard, so the old name reported `enable_mock` failures as "mock is not enabled" to a caller trying to enable it. The variant keeps its position, so on-chain error indices are unchanged. Also drops the append-here comment above the newer variants.
9f50561 added `head`, `header` and `syncCommitteeHash` to the vector genesis patch in mainnet.chain.spec.json. That edit could never take effect: the node embeds mainnet.chain.spec.raw.json, whose `genesis.raw` key/values are loaded directly, so `BuildGenesisConfig::build` never runs for mainnet. Regenerating the raw spec to pick the fields up is not an option either -- it would change the genesis hash and fork the live network. So the file only made the plain and raw specs disagree while implying mainnet gets seeded with an SP1 anchor when it does not. Restored to its previous contents byte for byte. Seeding the anchor on an already-running chain needs a migration or a root call, not genesis.
ToufeeqP
approved these changes
Aug 17, 2026
ToufeeqP
marked this pull request as ready for review
August 17, 2026 05:47
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.
No description provided.