espresso: enforce calldata-only DA for Espresso - #482
Conversation
…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
left a comment
There was a problem hiding this comment.
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.
- 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. - 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. - 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. - 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.
- 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.
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
left a comment
There was a problem hiding this comment.
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.
…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>
…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>
Context
Least Authority audit Suggestion 4: the Celo fault-proof host (celo-kona) does not implement the
L1Blobpreimage 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)IsEspresso(ref.Time), including the auth-enforcement grace window),dataAndHashesFromTxsdrops blob-carrying inbox transactions before any authorization check — authenticated or not.op-programalso compiles this package but is not deployed for any Celo chain.espresso_timenever needs blob preimages. It does not extend to the pre-fork blocks a proof walks back through — a proof startschannel_timeoutL1 blocks before the agreed head — and those are covered by the batcher guard below rather than by consensus.Batcher startup guard (operational,
op-batcher)checkEspressoDataAvailabilityrefuses to start the batcher with--data-availability-type=blobs|autowhenEspressoTimeis 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.espresso_timeis 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" toIsEspresso.checkFallbackAuthConfirmationsand 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_timeset:1782910800(2026-07-01 13:00 UTC). Mainnet, Celo Sepolia, Alfajores and Baklava carry noespresso_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.TestOpenDataDropsBlobsPostEspresso— drivesNewDataSourceFactory→OpenData→Nextwith Espresso enforced and a blob inbox tx, asserting no blob fetch is issued. The blobs fetcher has no expectations, so anyGetBlobsByHashfails 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
checkEspressoDataAvailabilitycall block frominitFromCLIConfigleaves the batcher package green, because no test exercisesinitFromCLIConfigat all —TestCheckEspressoDataAvailabilitycalls the method on a hand-builtBatcherService. Wiring a real test needs mock L1 and rollup RPC, which is disproportionate for a three-line call block;checkFallbackAuthConfirmationswas wired the same way in #458.go test ./op-node/rollup/derive/ ./op-batcher/batcher/passes;op-nodeandop-batcherbuild.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), notis_enforced(fork plus grace), placed after the batcher-address filter and beforeis_batch_authorized— the same slot as the Go gate. Had it usedis_enforced, every blob batch inside the grace window would have been a Go/Rust consensus split.Suggested ordering:
espresso_timeset (done for Chaos, above). If one ever had, this would need a coordinated restart rather than a rolling upgrade.celo-derivecompiles into the SP1 range ELF, so the range vkey rotates, andAGGREGATION_VKEY,RANGE_VKEY_COMMITMENTandROLLUP_CONFIG_HASHare constructor immutables inOPSuccinctFaultDisputeGame.sol.Steps 4 and 5 do not block this PR: the deployed prover currently pins celo-kona
celo/v1.1.1, whose host bails onL1Blobunconditionally, 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.