Skip to content

espresso: enforce calldata-only DA for Espresso - #482

Merged
palango merged 13 commits into
celo-rebase-18from
espresso-enforce-calldata-only-da
Aug 11, 2026
Merged

espresso: enforce calldata-only DA for Espresso#482
palango merged 13 commits into
celo-rebase-18from
espresso-enforce-calldata-only-da

Conversation

@philippecamacho

@philippecamacho philippecamacho commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Context

Least Authority audit Suggestion 4: the Celo fault-proof host (celo-kona) does not implement the L1Blob preimage hint, yet authenticated blob batches were still accepted by the derivation pipeline. A blob batch landing in the inbox would be accepted by full nodes but stall fault-proof execution at its L1 block. The spec settled on calldata-only DA, but only as a documented operational restriction — nothing in the code enforced it.

What this PR does

Enforces the calldata-only restriction at two layers:

Derivation pipeline (consensus-level, op-node/rollup/derive)

  • From Espresso activation onward (IsEspresso(ref.Time), including the auth-enforcement grace window), dataAndHashesFromTxs drops blob-carrying inbox transactions before any authorization check — authenticated or not.
  • Post-Espresso derivation therefore never requests blob preimages, so fault-proof execution can no longer be stalled by a blob batch. An authenticated blob batch now simply contributes no data. Celo's proof path is celo-kona under op-succinct, so the mirrored change there (celo-kona#289) is what makes this hold for proofs; op-program also compiles this package but is not deployed for any Celo chain.
  • The guarantee is per-L1-block: deriving an L1 block at or after espresso_time never needs blob preimages. It does not extend to the pre-fork blocks a proof walks back through — a proof starts channel_timeout L1 blocks before the agreed head — and those are covered by the batcher guard below rather than by consensus.

Batcher startup guard (operational, op-batcher)

  • checkEspressoDataAvailability refuses to start the batcher with --data-availability-type=blobs|auto when EspressoTime is scheduled. Without this, a misconfigured (fallback) batcher would have every blob batch ignored by verifiers after activation: the safe head stalls for one sequence window, then verifiers force empty batches and reorg away the unsafe chain, discarding the transactions in it.
  • The gate is deliberately broader than derivation's — it fires as soon as espresso_time is set, not once it is active. That asymmetry is load-bearing: it is the only thing keeping blob batches out of the pre-fork window a proof walks back through, so it should not be "cleaned up" to IsEspresso.
  • This makes the blob-pair headroom check in checkFallbackAuthConfirmations and the serialized blob fallback-auth path unreachable in practice. Both are kept as defence in depth and annotated as unreachable in the code.

Effect on existing chains

Chaos is the only chain with espresso_time set: 1782910800 (2026-07-01 13:00 UTC). Mainnet, Celo Sepolia, Alfajores and Baklava carry no espresso_time, so nothing changes for them.

Because the gate keys on the scanned L1 block's own time, the rule is retroactive on Chaos: re-syncing a node applies it to six weeks of existing history. That is safe here but worth stating plainly rather than claiming the change activates only at a future boundary. Verified on 2026-08-11: 3545 transactions have reached the Chaos inbox since activation, from two senders, none of them blob-type, and Blobscan reports zero blob transactions to that address over the window. The only behavioural delta was confined to the 20-minute pre-enforcement window on 2026-07-01, and cancel transactions skip authentication anyway, so post-enforcement derivation drops them with or without this PR.

Tests

  • authenticated blob tx rejected: blob DA unsupported post-fork — a fully event-authenticated blob batch is dropped and no blob preimages are requested.
  • mixed calldata+blob block: only the calldata batch accepted.
  • Fork-boundary walk: pre-fork blob batches keep upstream sender-auth acceptance; from activation onward (incl. grace window) they are dropped, with the empty L1 mock asserting no receipt scanning pre-enforcement.
  • TestOpenDataDropsBlobsPostEspresso — drives NewDataSourceFactoryOpenDataNext with Espresso enforced and a blob inbox tx, asserting no blob fetch is issued. The blobs fetcher has no expectations, so any GetBlobsByHash fails the test.
  • TestCheckEspressoDataAvailability — batcher guard matrix (unscheduled / scheduled-and-active / scheduled-but-not-yet-active × calldata/blobs/auto).

Not covered: nothing drives the startup guard's call site. Deleting the checkEspressoDataAvailability call block from initFromCLIConfig leaves the batcher package green, because no test exercises initFromCLIConfig at all — TestCheckEspressoDataAvailability calls the method on a hand-built BatcherService. Wiring a real test needs mock L1 and rollup RPC, which is disproportionate for a three-line call block; checkFallbackAuthConfirmations was wired the same way in #458.

go test ./op-node/rollup/derive/ ./op-batcher/batcher/ passes; op-node and op-batcher build.

Cross-repo status and release ordering

celo-kona needs the mirrored change in its Rust data source: that is celo-kona#289, open and mergeable. It gates on BatchAuthConfig::is_active (fork-active), not is_enforced (fork plus grace), placed after the batcher-address filter and before is_batch_authorized — the same slot as the Go gate. Had it used is_enforced, every blob batch inside the grace window would have been a Go/Rust consensus split.

Suggested ordering:

  1. Confirm no blob-type tx has reached the inbox of any chain with espresso_time set (done for Chaos, above). If one ever had, this would need a coordinated restart rather than a rolling upgrade.
  2. Roll the new op-batcher first — it closes the source of new blob batches before any node changes its derivation. Note the guard rides on the flag default, since neither Chaos manifest sets a DA type.
  3. Roll op-node and full nodes.
  4. Merge celo-kona#289.
  5. Bump the op-succinct pin on the branch that builds the deployed image, rebuild the range and aggregation ELFs, and redeploy the game implementation with the new vkeys. celo-derive compiles into the SP1 range ELF, so the range vkey rotates, and AGGREGATION_VKEY, RANGE_VKEY_COMMITMENT and ROLLUP_CONFIG_HASH are constructor immutables in OPSuccinctFaultDisputeGame.sol.

Steps 4 and 5 do not block this PR: the deployed prover currently pins celo-kona celo/v1.1.1, whose host bails on L1Blob unconditionally, so a post-Espresso blob batch is unprovable with or without this change. The drop is a pure function of L1 data plus config, so nothing becomes permanently unprovable — the range proves once the new ELF ships.

The spec (the-book) can then be updated to note the restriction is code-enforced.

…ion 4)

The Celo fault-proof host (celo-kona) does not implement the L1Blob
preimage hint, so an authenticated blob batch — while accepted by full
nodes — would stall fault-proof execution at its L1 block. The spec
declares blob DA unsupported (DEC-op-026/n-026), but until now that was
an operational restriction only.

Enforce it in code, at two layers:

- Derivation pipeline (consensus): from Espresso activation onward
  (including the auth-enforcement grace window), dataAndHashesFromTxs
  drops blob-carrying inbox transactions before any authorization
  check, so post-Espresso derivation never requires blob preimages.
  This covers op-node and the Go op-program alike, since they share the
  derive package. Pre-fork semantics are unchanged (upstream behavior).

- Batcher startup (operational): refuse to start with a blob or auto
  data-availability type when EspressoTime is scheduled, instead of
  letting verifiers silently drop every blob batch after activation and
  stalling the safe head. This makes the existing blob-pair headroom
  check in checkFallbackAuthConfirmations unreachable in practice; it
  is kept as defense in depth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@palango palango 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.

Reviewed at f7e587d. go test ./op-node/rollup/derive/... ./op-batcher/... passes, go vet clean on both.

Premise holds up. celo-kona's host bails on HintType::L1Blob outright (bin/host/src/single/handler.rs:244), and types.go:401 already forces espresso_time >= ecotone_time, so everything
post-Espresso goes through BlobDataSource. No way around the new gate via the calldata source. Dropping on IsEspresso rather than isEspressoAuthEnforced is right too, since proofs run from
activation and the grace window has to be blob-free.

Approach is good. Five small things.

  1. blob_data_source.go:122-124 is stale now. It still says that before enforcement "this runs upstream Optimism semantics: filter by batch inbox + sender == batcher". Not true in the grace window
    anymore, and the paragraph you added just below says the opposite.
  2. ComputeBlobBatchHash is dead, but doesn't look dead. Blob txs get dropped at :164 whenever IsEspresso(ref.Time), so the batchHash = ComputeBlobBatchHash(...) at :173 and the blob-append block
    under it only run pre-Espresso, and pre-Espresso isBatchTxAuthorized takes the sender path and never touches batchHash. Reads like blob commitments still get authenticated after the fork. A comment
    would do.
  3. Same story in the batcher. checkEspressoDataAvailability runs at service.go:174, ahead of checkFallbackAuthConfirmations at :177, and rejects every non-calldata config when EspressoTime != nil,
    so checkFallbackAuthConfirmations can't ever fire and its test covers unreachable code. Keeping both as belt and braces is fine by me. I'd just rather that live in the code than only in the PR
    description.
  4. config.rollupCfg.IsEspresso(ref.Time) runs per tx, while isEspressoAuthEnforced(...) right above at :143 is hoisted out of the loop. Might as well do both in one place.
  5. The Warn fires before the auth check, so anyone can buy themselves a log line per blob tx sent to the inbox. Upstream warns on unauthorized senders anyway, so it's consistent. Only raising it in
    case volume matters.

Tests look good. Using an empty MockL1Source to assert no receipt scanning is a real check, not decoration, and upstream's blob_data_source_test.go passing untouched is decent evidence for the
pre-fork claim.

One you could add: the fork-boundary walk stops at espressoTime + BatchAuthEnforcementDelaySecs - 1 for blobs, so the post-enforcement blob case only gets covered sideways by
TestDataAndHashesFromTxsEventAuth (espressoTime = 0, ref.Time = delay). Three lines would make it direct.

palango and others added 11 commits August 11, 2026 15:31
Espresso activation is a property of the scanned L1 block's origin time, so
evaluate it once alongside the enforcement check rather than per transaction.
The pre-enforcement paragraph claimed upstream Optimism semantics for the whole
function, including the grace window, which the calldata-only rule below it
contradicts. Scope it to authorization, state the inbox filter once, and order
the rules as the code applies them.
Blob txs only get past the calldata-only drop pre-Espresso, where authorization
is sender-based and never reads the batch hash. Record why the arm stays rather
than folding into the calldata one.
checkEspressoDataAvailability runs first and rejects the only configuration
checkFallbackAuthConfirmations bounds, so the check cannot fire and the
serialized blob send path cannot run. Keep both, and say so in the code.
Derivation's drop only covers blocks at or after espresso_time; the pre-fork
blocks a proof walks back through are covered by the batcher gate, which keys on
espresso_time being set rather than active. Say why that asymmetry stays.
Not an indefinite safe-head stall: derivation forces empty batches once the
sequence window expires, and the verifier reorgs the unsafe chain away.
authenticateBatchInfo takes an opaque bytes32 and has no calldata or blob
validation path; the encoding is agreed off-chain. Note that the blob helper has
no live caller while calldata-only DA is enforced.
…start

Three TestCheckFallbackAuthConfirmations cases, the five tests built on
testBlobCandidate and the blob parity subtest all describe blob DA, which
checkEspressoDataAvailability rejects. Keep them, but say so.
…tcher

TestOpenDataDropsBlobsPostEspresso drives NewDataSourceFactory through OpenData
and Next with Espresso enforced, asserting a blob inbox tx triggers no blob
fetch; the bare MockBlobsFetcher fails the test if one is issued.

TestCheckEspressoDataAvailability gains a future espresso_time, the input that
distinguishes the batcher's scheduled-or-active gate from an activation check.
@palango palango changed the title Espresso: enforce calldata-only DA post-fork (Least Authority Suggestion 4) espresso: enforce calldata-only DA for Espresso Aug 11, 2026

@palango palango 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.

All five points from my earlier review are addressed in the commits now on this branch.

1-4 are fixed. 5 stays as it is: the calldata path already warns on unauthorized senders at data_source.go:156, so a blob tx to the inbox buys an attacker nothing new, and blob gas makes it the dearer way to get the same log line.

The post-enforcement blob case is covered by TestOpenDataDropsBlobsPostEspresso rather than by extending the fork-boundary walk. Three lines don't work there: adding a third refTime to that loop panics on an unexpected FetchReceipts, because the auth event scan is hoisted above the tx loop and runs as soon as enforcement is on. The new test drives NewDataSourceFactory through OpenData and Next with espresso_time = 1000 and the ref at espresso_time + BatchAuthEnforcementDelaySecs, and its blobs fetcher has no expectations set, so any GetBlobsByHash call fails the test.

Two things beyond the review. The DEC-op-026 citation is gone, since the token resolves nowhere in any celo-org repo and the rationale it pointed at is in the prose anyway. And I corrected the PR body: Chaos has had espresso_time set since 2026-07-01, so the rule is retroactive there rather than activating at a future boundary. Harmless in fact — 3545 inbox txs since activation, none blob-type — but the old wording claimed something that isn't true.

Still outstanding, neither blocking this PR: celo-kona#289 needs merging, and the deployed prover pins celo/v1.1.1, which predates batch authentication entirely.

With the calldata batch first, the sub-test passes whether the gate uses
continue or break: the blob tx is last, so a whole-block short-circuit costs
nothing observable. Putting the blob tx first makes break swallow the
calldata batch behind it and fail.

Matches the ordering celo-kona's mirror of this test uses.
@palango
palango merged commit 3357d43 into celo-rebase-18 Aug 11, 2026
41 checks passed
@palango
palango deleted the espresso-enforce-calldata-only-da branch August 11, 2026 15:45
palango pushed a commit to celo-org/celo-kona that referenced this pull request Aug 12, 2026
…stion 4)

The Celo fault-proof host does not implement the L1Blob preimage hint,
so an authenticated blob batch — while accepted by full nodes — would
stall fault-proof execution at its L1 block. The spec declares blob DA
unsupported (DEC-op-026/n-026), but nothing enforced it in code.

From Espresso activation onward (including the auth-enforcement grace
window), CeloBlobSource::extract_blob_data now drops blob-carrying
inbox transactions before any authorization check, so post-Espresso
derivation never requests blob preimages. Pre-fork semantics are
unchanged (upstream behavior).

Mirrors the op-node/op-program change in celo-org/optimism#482; the two
implementations must stay in lockstep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
palango pushed a commit to celo-org/celo-kona that referenced this pull request Aug 12, 2026
…stion 4)

The Celo fault-proof host does not implement the L1Blob preimage hint,
so an authenticated blob batch — while accepted by full nodes — would
stall fault-proof execution at its L1 block. The spec declares blob DA
unsupported (DEC-op-026/n-026), but nothing enforced it in code.

From Espresso activation onward (including the auth-enforcement grace
window), CeloBlobSource::extract_blob_data now drops blob-carrying
inbox transactions before any authorization check, so post-Espresso
derivation never requests blob preimages. Pre-fork semantics are
unchanged (upstream behavior).

Mirrors the op-node/op-program change in celo-org/optimism#482; the two
implementations must stay in lockstep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants