Skip to content

fix(prover): apply the anchor window to forced-inclusion sources - #55

Open
davidtaikocha wants to merge 3 commits into
mainfrom
fix/forced-inclusion-anchor-window
Open

davidtaikocha wants to merge 3 commits into
mainfrom
fix/forced-inclusion-anchor-window

Conversation

@davidtaikocha

Copy link
Copy Markdown
Member

Problem

validateManifestAnchorNumbers short-circuits for forced-inclusion sources:

if isForcedInclusion {
    for _, anchor := range anchors {
        if anchor != parentAnchorBlockNumber { return false }
    }
    return true          // returns before the window check
}

It never reaches validateManifestAnchorProgression, the only place the
[origin-MAX_ANCHOR_OFFSET, origin] window is enforced (512 blocks on mainnet, 128 elsewhere).

Canonical derivation applies the monotonicity and window rules to every block of every source
and exempts forced inclusions only from the strict-advance rule
(taiko-client-rs crates/driver/src/derivation/pipeline/shasta/validation.rs,
validate_anchor_numbers: the range checks sit in the per-block loop with no is_forced_inclusion
guard; only the trailing !is_forced_inclusion && highest_anchor <= parent_anchor rule is gated).

applyInheritedManifestMetadata pins a forced source's anchor to the parent's and leaves
Transactions untouched. So whenever the parent anchor has stalled more than MAX_ANCHOR_OFFSET
behind the proposal origin, the inherited anchor is outside the window and the two implementations
disagree:

forced source with a stalled anchor
canonical source replaced by the default manifest → anchor-only block
gaiko2 (before) source kept → block still carries its transactions

Same proposal, two different block hashes and state roots — so gaiko2 would attest to a checkpoint
over a block canonical node software does not derive. git log -L 509,545:internal/prover/manifest_validate.go
shows the window check used to apply here and was replaced by this early return in 3d4b50c.

Fix

Run validateManifestAnchorProgression for every source, and skip only the advance rule for forced
inclusions — a direct mirror of the canonical function.

The per-source anchor == parentAnchor check is dropped as redundant, not relaxed:
applyInheritedManifestMetadata already pins forced anchors to the parent value before validation,
and validateSourceAwareManifestAnchors re-checks the same equality across sources. Keeping it
would have made gaiko2 stricter than canonical, which is its own divergence risk.

Note this is consistent with Derivation.md, which exempts forced inclusions from being defaulted
"even when the anchor number does not advance" — the advance rule is exactly what this change still
skips for them.

Tests

Five new tests, each watched failing before the fix:

  • forced source below the window → rejected
  • forced source above the origin → rejected
  • forced source stalled but inside the window → still accepted (the advance exemption is preserved)
  • mainnet 512-block edge: accepted at the boundary, rejected one block past it
  • validateSourceManifest composition: an otherwise well-formed forced source with transactions and
    a stalled anchor is invalidated, so prepareSourceManifest replaces it with the transaction-free
    default manifest

go test ./..., go vet ./... and gofmt -l are clean.

Sequencing

Please land this before any fix to the parent CheckpointStore storage-slot derivation in
shastaCheckpointStorageSlots. That slot derivation currently fails closed for every proposal
containing a forced-inclusion source, which is the only reason the divergence above is not reachable
today; correcting the slot first would make it reachable. With this merged, that ordering constraint
goes away.

🤖 Generated with Claude Code

davidtaikocha and others added 3 commits July 26, 2026 10:20
validateManifestAnchorNumbers short-circuited for forced-inclusion sources: it
checked only that each anchor equalled the parent anchor and returned before
validateManifestAnchorProgression, the only place the
[origin-MAX_ANCHOR_OFFSET, origin] window is enforced.

Canonical derivation (taiko-client-rs validate_anchor_numbers) applies the
monotonicity and window rules to every block of every source, and exempts
forced inclusions only from the strict-advance rule. Because inherited metadata
pins a forced source's anchor to the parent's, a parent anchor that had stalled
more than MAX_ANCHOR_OFFSET behind the proposal origin left the window:
canonical derivation replaces such a source with the transaction-free default
manifest, while gaiko2 kept the source and derived a block still carrying its
transactions. Same proposal, two different block hashes and state roots.

Run the progression check for every source and skip only the advance rule for
forced inclusions. The per-source equality check is dropped as redundant:
applyInheritedManifestMetadata already pins forced anchors to the parent value
before validation, and validateSourceAwareManifestAnchors re-checks the same
equality across sources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous test documented prepareSourceManifest's fallback but only called
validateSourceManifest and asserted false, so it never executed the replacement
at manifest_validate.go:331-334 and never checked the outcome that actually
matters: that the source's transactions are gone.

Drive prepareSourceManifest with a blob-backed forced-inclusion source carrying
a transaction and a stalled parent anchor, and assert the default manifest it
returns has exactly one block, zero transactions, and the inherited timestamp,
coinbase, anchor and gas limit. Against the pre-fix source this now fails with
"got 1 transactions" rather than passing vacuously.

The predicate-level test is kept alongside it, driven from the same fixture, so
the composition it isolates (timestamps and gas limit in range, anchor window
the only rejecting rule) stays covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
decodeBlobBackedSourceManifest degrades to the default manifest — one block,
zero transactions — on any codec failure, and that default satisfies every
assertion both stalled-anchor tests make. A blob-codec or fixture regression
would therefore leave them green while covering nothing; corrupting the fixture
blob confirmed both still passed.

The fixture now decodes its own blob and asserts exactly one block carrying one
transaction, so callers assert against what the blob really contains. The
predicate test additionally pins which rule rejects: timestamps and gas limit
must pass and the anchor window must be what fails, so a fixture drift that
pushed either out of range cannot make the test pass for the wrong reason.

Verified three ways: green with the fix, red against the pre-fix production code
("got 1 transactions"), and red under a forced decode fallback ("fixture must
decode to exactly one transaction, got 0").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@davidtaikocha
davidtaikocha requested a review from smtmfft July 26, 2026 03:05
@davidtaikocha davidtaikocha added the enhancement New feature or request label Jul 26, 2026
@davidtaikocha
davidtaikocha requested a review from johntaiko July 26, 2026 04:09
@smtmfft

smtmfft commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Let’s hold off on merging this for now, since this is a permissionless feature that will only be enabled in about 3–6 months.
We also need to update the bootstrap script accordingly. Raiko2 SGX will need this feature as well.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants