Skip to content

Espresso 3a: Fallback batcher#448

Draft
QuentinI wants to merge 19 commits into
celo-org:celo-rebase-17from
EspressoSystems:ag/batcher-fallback
Draft

Espresso 3a: Fallback batcher#448
QuentinI wants to merge 19 commits into
celo-org:celo-rebase-17from
EspressoSystems:ag/batcher-fallback

Conversation

@QuentinI
Copy link
Copy Markdown

@QuentinI QuentinI commented May 27, 2026

Relatively small change based on #445 - makes fallback batcher authenticate its transactions. Relevant commits are c4c8b51 and 4af8d1a.

QuentinI and others added 19 commits May 8, 2026 02:50
Adds the Espresso-introduced contracts and the minimum supporting changes
required for them to compile, test, and pass the contract checks.

New contracts and scripts:

- src/L1/BatchAuthenticator.sol and interfaces/L1/IBatchAuthenticator.sol
  (upgradeable contract that authenticates batch transactions, with switching
  between Espresso and fallback batchers)
- scripts/deploy/DeployBatchAuthenticator.s.sol and
  scripts/deploy/DeployEspresso.s.sol
- test/L1/BatchAuthenticator.t.sol and test/mocks/MockEspressoTEEVerifiers.sol
- snapshots/{abi,storageLayout}/BatchAuthenticator.json
- snapshots/semver-lock.json entry for BatchAuthenticator

New submodules:

- lib/espresso-tee-contracts (interfaces required by BatchAuthenticator)
- lib/openzeppelin-contracts-upgradeable-v5 (OZ v5 used by BatchAuthenticator
  via OwnableUpgradeable)

Supporting changes (Espresso-driven):

- foundry.toml: remappings for OZ v5 and espresso-tee-contracts; ignored
  warning codes for vendored libs; OOM-safe jobs settings; via-ir profile.
- justfile: fix-proxy-artifact recipe to handle OZ v5 shadowing Proxy/ProxyAdmin
  artifacts; build/coverage hooks.
- src/universal/Proxy.sol, src/universal/ProxyAdmin.sol: pin pragma to exact
  0.8.15 so they stay in their own compilation group and never emit PUSH0.
- src/universal/ReinitializableBase.sol: loosen pragma to ^0.8.15 so
  BatchAuthenticator (compiled with OZ v5) can import it.
- scripts/* and test/*: disambiguate Proxy artifact lookups to
  src/universal/Proxy.sol:Proxy (avoids OZ v5 proxy/Proxy.sol shadow).
- scripts/checks: bypass interface checks for artifacts originating from lib/;
  add Espresso-related contract names to exclude lists; pragma exclusions for
  Proxy/ProxyAdmin/BatchAuthenticator.
- test/vendor/Initializable.t.sol: exclude BatchAuthenticator (deployed by a
  separate Espresso script).

Co-authored-by: OpenCode <noreply@opencode.ai>
Co-authored-by: piersy <pierspowlesland@gmail.com>
- strict-pragma: remove unneeded exclusions for src/universal/Proxy.sol
  and src/universal/ProxyAdmin.sol — both already use strict
  'pragma solidity 0.8.15;', so the entries (and their misleading
  comment claiming '^') were dead.
- interfaces: move the Espresso excludeContracts block out of the
  upstream-shared area and down next to the Celo block, with one
  entry per line to match the surrounding style. Localizes future
  rebase deltas.

Co-authored-by: OpenCode <noreply@opencode.ai>
Inline the EspressoTEEVerifier deployment in DeployEspresso.s.sol so it
no longer imports lib/espresso-tee-contracts/scripts/DeployTEEVerifier.s.sol
or DeployNitroTEEVerifier.s.sol. The upstream scripts pulled OZ v5's
TransparentUpgradeableProxy (and its auto-deployed ProxyAdmin) into the
OP artifact tree, shadowing src/universal/ProxyAdmin.sol and forcing a
~90-line fix-proxy-artifact justfile recipe.

The TEEVerifier is now deployed behind src/universal/Proxy.sol +
src/universal/ProxyAdmin.sol, matching how BatchAuthenticator is
deployed in the same script. ERC-1967 slots are unchanged, so external
callers see no difference.

The raw vm.getCode("ProxyAdmin") lookups in the deploy scripts and
BatchAuthenticator tests are switched to the explicit artifact path
vm.getCode("forge-artifacts/ProxyAdmin.sol/ProxyAdmin.json") to
deterministically resolve the default compilation profile's bytecode
(the dispute profile transitively compiles ProxyAdmin at optimizer_runs=5000,
creating a second artifact that broke unqualified lookups).

The fix-proxy-artifact recipe and its 5 callsites are removed.
Cherry-picked from piersy's commit 5d0a803 on PR ethereum-optimism#443.

Walks the dual-batcher state machine: Espresso path → switchBatcher →
fallback path → switchBatcher → Espresso path. Asserts every transition
emits the expected event, that signer registration survives the
round-trip, and that re-issuing the same call after a mode flip changes
the outcome (the previously-valid Espresso signature is no longer
consulted on the fallback path).

Co-authored-by: Piers Powlesland <pierspowlesland@gmail.com>
Co-authored-by: OpenCode <noreply@opencode.ai>
Adds derivation-pipeline support for the BatchAuthenticator contract
introduced in the previous PR. Stacks on the contracts PR.

Introduces an L2-timestamp hardfork (EspressoEnforcementTime) gating all
post-fork derivation semantics. Pre-fork, derivation behaves exactly as
upstream Optimism: batches are accepted based on the L1 transaction
sender matching the SystemConfig batcher address. Post-fork, batches are
authenticated via BatchInfoAuthenticated(bytes32) events emitted by the
BatchAuthenticator contract, and sender-based authorization is rejected.

Adds CollectAuthenticatedBatches which scans L1 receipts over a
configurable lookback window (default 100 blocks) to build the set of
authenticated batch commitment hashes for each L1 block being derived.
Results are cached in two reorg-safe (block-hash-keyed) LRU caches: one
for receipt-derived event sets, one for L1BlockRef resolution. For
consecutive L1 blocks the lookback windows overlap by ~99 blocks, so
only one new block's receipts need to be fetched on each call.

Adds rollup.Config fields: EspressoEnforcementTime *uint64,
BatchAuthenticatorAddress, BatchAuthLookbackWindow.

Adds unit tests for batch authentication across calldata, blob, and
altda data sources.

Co-authored-by: OpenCode <noreply@opencode.ai>
Matches upstream Optimism hardfork naming convention (RegolithTime,
EcotoneTime, IsthmusTime, ...). All hardforks enforce a new set of
rules, so the "Enforcement" qualifier was redundant.

Renames:
  EspressoEnforcementTime    -> EspressoTime    (rollup.Config field)
  IsEspressoEnforcement      -> IsEspresso      (rollup.Config method)
  espressoEnforcementTime    -> espressoTime    (DataSourceConfig field)
  isEspressoEnforcement      -> isEspresso      (DataSourceConfig method)
  espresso_enforcement_time  -> espresso_time   (JSON tag, forEachFork log key)
  "Espresso Enforcement"     -> "Espresso"      (forEachFork display name)

Also rewords prose docstrings: "EspressoEnforcement" -> "Espresso",
"Pre/Post-EspressoEnforcement" -> "Pre/Post-Espresso".

Addresses PR feedback: celo-org#445 (comment)

Co-authored-by: OpenCode <noreply@opencode.ai>
Regenerated against PR ethereum-optimism#443's BatchAuthenticator.sol via forge build +
abigen. Includes the new history-based API (espressoBatcherAt,
espressoBatcherAtBlock, espressoBatcherHistoryLength, setEspressoBatcher)
and the EspressoBatcherUpdated(address,address,uint64) event with the
fromBlock parameter; drops the removed paused() function.

Consumed by the fallback batcher (next commit) to read activeIsEspresso
and pack authenticateBatchInfo calldata. The TEE batcher in a follow-up
PR will use the same binding.

Co-authored-by: OpenCode <noreply@opencode.ai>
Add the fallback (non-TEE) batcher's BatchAuthenticator integration:

- op-batcher/batcher/fallback_auth.go: sendTxWithFallbackAuth path that
  posts authenticateBatchInfo before the batch tx, with a deadline check
  against the batch's L1 inclusion window. Computes the batch commitment
  hash from either calldata or concatenated blob versioned hashes.
- op-batcher/batcher/espresso_active.go: hasBatchAuthenticator (does
  this rollup use BatchAuthenticator at all?) and isFallbackAuthRequired
  (gates fallback authentication on Config.IsEspresso(tip.Time + lead)).
  The Espresso hardfork predicate is consulted with the configured
  FallbackAuthLeadTime added to the L1 tip, so the batcher starts
  authenticating slightly before the verifier requires it. This absorbs
  worst-case L1 inclusion delay between the batcher's decision time
  (L1 tip) and the verifier's evaluation time (containing L1 block).
- op-batcher/batcher/espresso_driver.go: the authGroup bookkeeping
  (initAuthGroup, waitForAuthGroup, fallbackAuthGroupLimit) and the
  dispatchAuthenticatedSendTx fan-out used by driver.go sendTx.

Small wiring edits to upstream files:

- op-batcher/flags/flags.go: register --espresso.fallback-auth-lead-time
  (default 5m).
- op-batcher/batcher/config.go: thread the FallbackAuthLeadTime through
  CLIConfig.
- op-batcher/batcher/service.go: BatcherConfig.FallbackAuthLeadTime
  field, propagated from CLIConfig in initFromCLIConfig.
- op-batcher/batcher/driver.go: extend L1Client to embed
  bind.ContractBackend (required by the BatchAuthenticator binding), add
  authGroup field to BatchSubmitter, call initAuthGroup in
  NewBatchSubmitter, call dispatchAuthenticatedSendTx in sendTx, call
  waitForAuthGroup in publishingLoop's shutdown drain.
- op-batcher/batcher/driver_test.go: embed bind.ContractBackend in
  fakeL1Client so the AltDA tests still satisfy L1Client.

The fallback batcher does nothing when the rollup config has no
BatchAuthenticator address, and it falls through to the upstream
queue.Send path pre-EspressoTime. Cancel transactions always take the
upstream path. No new external dependencies are added; the only third-
party Go modules needed are already in PR ethereum-optimism#445.

The TEE batcher is a separate PR stacked on top.

Co-authored-by: OpenCode <noreply@opencode.ai>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4af8d1a521

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +119 to +120
func (c DataSourceConfig) isEspresso(l1OriginTime uint64) bool {
return c.espressoTime != nil && l1OriginTime >= *c.espressoTime
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce Espresso auth using L2 timestamps

When an L1 block has ref.Time < EspressoTime but contains a batch whose L2 block timestamps are already >= EspressoTime (allowed around the boundary by sequencer drift), this predicate returns false and the data source keeps accepting sender-authorized inbox txs without any BatchInfoAuthenticated event. The rollup config documents Espresso as active by L2 block timestamp, so fork-boundary batches can bypass the new authentication until L1 time catches up.

Useful? React with 👍 / 👎.

Comment on lines +269 to +270
bytes memory initData =
abi.encodeCall(EspressoTEEVerifier.initialize, (proxyAdminOwner, IEspressoNitroTEEVerifier(address(0))));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep ownership with broadcaster until verifier wiring

When proxyAdminOwner is set to an address different from the broadcaster (for example a multisig), this initializes the TEE verifier with that address as owner, but the script later calls the owner-only setEspressoNitroTEEVerifier as msg.sender in the same deployment flow. That production deployment path will revert before outputs are written unless the final owner is also the deployer, so ownership should remain with the broadcaster until the Nitro verifier is wired or the call must be broadcast from the configured owner.

Useful? React with 👍 / 👎.

@QuentinI QuentinI marked this pull request as draft May 27, 2026 14:05
@QuentinI QuentinI changed the title Espresso 3a: Fallback batcher authentication Espresso 3a: Fallback batcher May 27, 2026
Comment thread op-batcher/flags/flags.go
"(based on L1 tip time) and the verifier's gate (based on the containing " +
"L1 block's time). Has no effect outside the boundary window around the " +
"EspressoTime hardfork.",
Value: 5 * time.Minute,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice idea, but we need to see how to set this value.

I think we either set this to something big and safe (1-2h) and accept that we're sending a couple of unused tx, or remove it at all and shutdown the batcher before the switch (causing more work for devops).

//
// The contract's fallback path checks msg.sender against systemConfig.batcherHash(), so no
// separate signature is needed — the L1 transaction is already signed by the TxManager's key.
func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted auth tx can be reported as success. sendTxWithFallbackAuth checks only err after l.Txmgr.Send and never checks verificationReceipt.Status in op-batcher/batcher/fallback_auth.go:85. txmgr.Send returns receipt, nil on receipt arrival in op-service/txmgr/txmgr.go:743, and derivation ignores failed auth receipts in op-node/rollup/derive/ batch_authenticator.go:85.

}
l.authGroup.Go(
func() error {
l.sendTxWithFallbackAuth(txdata, isCancel, candidate, queue, receiptsCh)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normal batch submission path sends every batch tx through txQueue.Send:

op-batcher/batcher/driver.go:1061

That queue is created with MaxPendingTransactions:

op-batcher/batcher/driver.go:515

and txmgr.Queue.Send explicitly assigns nonces synchronously so transactions
confirm in the order they are sent. This is important for Holocene, where
frames for a channel must arrive in order.

The fallback-auth path bypasses that queue. Once fallback auth is required,
dispatchAuthenticatedSendTx starts a goroutine in authGroup:

op-batcher/batcher/espresso_driver.go:65

and that goroutine calls l.Txmgr.Send directly for both the auth tx and the
batch inbox tx:

op-batcher/batcher/fallback_auth.go:85
op-batcher/batcher/fallback_auth.go:95

Txmgr.Send is concurrency-safe, but it only preserves the order in which
callers actually reach nonce assignment. With up to fallbackAuthGroupLimit = 128 goroutines racing, that order is no longer the publishing loop’s frame
order. As a result, batch inbox txs from the same channel can receive nonces
in a different order than the channel manager emitted them, and L1 inclusion
order follows those nonces.

That can violate Holocene strict frame ordering and cause derivation to drop
later/non-contiguous frames.

This is also a regression from the default config, where max-pending-tx
defaults to 1; operators who configured one-at- a-time submission no longer
get that behavior for fallback-authenticated batches.

The fix should preserve the original batch order across the whole auth+inbox
pair. Simply queueing inbox txs after concurrent auth confirmation is not
sufficient, because auth confirmations can complete out of order. The
fallback-auth path should either be serialized, or use an ordered mechanism
that keeps the original frame order while still ensuring each inbox tx is
posted only after its matching auth tx succeeds.

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