Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
fdfc7e5
contracts-bedrock: add Espresso BatchAuthenticator and supporting infra
QuentinI May 7, 2026
25f2234
Update packages/contracts-bedrock/src/L1/BatchAuthenticator.sol
QuentinI May 13, 2026
8b76632
Remove stray path
QuentinI May 13, 2026
97d4a34
Remove hardcoded Sepolia URL
QuentinI May 13, 2026
857717f
Use OZ v5 in mock verifier
QuentinI May 13, 2026
00d6510
Fix Codex's suggestion
QuentinI May 20, 2026
cabd2ee
Lower pragma
QuentinI May 20, 2026
7ea3c59
Remove unrelated foundry.toml changes
QuentinI May 20, 2026
5e4bcf8
scripts/checks: clean up exclude lists per PR review
QuentinI May 20, 2026
f84ac86
Fix re-initialization
QuentinI May 21, 2026
6b85fa1
Add batcher address history
QuentinI May 25, 2026
a882446
contracts-bedrock: drop OZ TUP from espresso deploy
palango May 20, 2026
c8db328
Remove pause from BatchAuthenticator
QuentinI May 25, 2026
8630695
Add a defensive check
QuentinI May 25, 2026
353252f
test: add end-to-end dual-batcher switch test
QuentinI May 25, 2026
de02776
Fix tests
QuentinI May 28, 2026
0ccf6ec
switchBatcher from toggle to a setter
QuentinI May 29, 2026
d9a4585
Use OZ Checkpoints for batcher history
QuentinI May 29, 2026
3b4618a
contracts-bedrock: deploy espresso impls via vm.getCode, drop suppres…
QuentinI Jun 1, 2026
4821ebe
Check batcher in Espresso mode
shenkeyao May 28, 2026
c67ab4d
regenerate snapshots for UnauthorizedEspressoBatcher error
QuentinI Jun 2, 2026
d2ed3bc
contracts-bedrock: wire espresso proxies to shared OP Stack ProxyAdmin
QuentinI Jun 4, 2026
12048f9
add caller to BatchInfoAuthenticated event
QuentinI Jun 4, 2026
9448a74
forge fmt
piersy Jun 9, 2026
05fd400
Remove unused imports
piersy Jun 10, 2026
eeed5c2
Rename tests to fit test name convention
piersy Jun 10, 2026
308f90f
op-chain-ops/script: resolve directory-qualified getCode artifact names
QuentinI Jun 11, 2026
6fd5401
contracts-bedrock: fix semgrep checks-fast findings
QuentinI Jun 11, 2026
74deff1
op-node: add event-based batch authentication
QuentinI May 25, 2026
c8bf60a
op-node: rename EspressoEnforcementTime to EspressoTime
QuentinI May 25, 2026
9b368a0
op-node: require BatchAuthenticatorAddress when Espresso is enabled
QuentinI Jun 2, 2026
2c8a2b2
op-node: fix EspressoTime fork-activation doc comments
QuentinI Jun 2, 2026
17fa0aa
op-node: bind authenticated batches to the authenticating caller
QuentinI Jun 8, 2026
e97ece8
op-node: replace global batch auth caches with dependency-injected in…
piersy Jun 2, 2026
7d05d6c
Use rollup.Config in DataSourceConfig
piersy Jun 3, 2026
9c017c3
op-node: fix up batch-auth cache DI and rollupCfg cherry-picks
QuentinI Jun 8, 2026
931a7dd
Hardcode BatchAuthLookbackWindow
QuentinI Jun 8, 2026
beb1b33
Tighten assertions for batch auth tests
piersy Jun 11, 2026
231bdbc
op-node: test Espresso fork boundary and per-commitment batch auth
piersy Jun 11, 2026
195a1df
op-node: isolate Espresso derivation tests into espresso_-prefixed files
piersy Jun 11, 2026
0787bc5
Update op-node/rollup/derive/espresso_blob_data_source_test.go
piersy Jun 15, 2026
f37b39e
Update op-node/rollup/derive/espresso_blob_data_source_test.go
piersy Jun 15, 2026
61b294c
espresso/bindings: add regenerated BatchAuthenticator Go bindings
QuentinI May 27, 2026
1119193
op-batcher: integrate fallback batcher authentication
QuentinI May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@
[submodule "packages/contracts-bedrock/lib/superchain-registry"]
path = packages/contracts-bedrock/lib/superchain-registry
url = https://github.com/ethereum-optimism/superchain-registry
[submodule "packages/contracts-bedrock/lib/espresso-tee-contracts"]
path = packages/contracts-bedrock/lib/espresso-tee-contracts
url = https://github.com/EspressoSystems/espresso-tee-contracts
[submodule "packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5"]
path = packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
2,277 changes: 2,277 additions & 0 deletions espresso/bindings/batch_authenticator.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions op-batcher/batcher/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ type CLIConfig struct {
PprofConfig oppprof.CLIConfig
RPC oprpc.CLIConfig
AltDA altda.CLIConfig

// FallbackAuthLeadTime is the lead time for the fallback batcher's
// authentication gate. See BatcherConfig.FallbackAuthLeadTime in
// service.go and isFallbackAuthRequired in espresso_active.go.
FallbackAuthLeadTime time.Duration
}

func (c *CLIConfig) Check() error {
Expand Down Expand Up @@ -248,6 +253,7 @@ func NewConfig(ctx *cli.Context) *CLIConfig {
PprofConfig: oppprof.ReadCLIConfig(ctx),
RPC: oprpc.ReadCLIConfig(ctx),
AltDA: altda.ReadCLIConfig(ctx),
FallbackAuthLeadTime: ctx.Duration(flags.FallbackAuthLeadTimeFlag.Name),
ThrottleConfig: ThrottleConfig{
AdditionalEndpoints: ctx.StringSlice(flags.AdditionalThrottlingEndpointsFlag.Name),
TxSizeLowerLimit: ctx.Uint64(flags.ThrottleTxSizeLowerLimitFlag.Name),
Expand Down
23 changes: 23 additions & 0 deletions op-batcher/batcher/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"golang.org/x/sync/errgroup"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
Expand Down Expand Up @@ -73,6 +74,7 @@ func (r txRef) string(txIDStringer func(txID) string) string {
type L1Client interface {
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error)
bind.ContractBackend
}

type L2Client interface {
Expand Down Expand Up @@ -124,6 +126,12 @@ type BatchSubmitter struct {
throttleController *throttler.ThrottleController

publishSignal chan pubInfo

// authGroup serializes in-flight BatchAuthenticator submissions issued by
// the fallback batcher's authentication path so the publishing loop can
// drain them on shutdown. Bounded to fallbackAuthGroupLimit; see
// espresso_driver.go.
authGroup errgroup.Group
}

// NewBatchSubmitter initializes the BatchSubmitter driver from a preconfigured DriverSetup
Expand All @@ -143,6 +151,8 @@ func NewBatchSubmitter(setup DriverSetup) *BatchSubmitter {
panic(err)
}

batcher.initAuthGroup()

return batcher
}

Expand Down Expand Up @@ -516,6 +526,12 @@ func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup,
}
}

// Wait for all in-flight fallback-auth submissions to complete to prevent
// new transactions being queued. No-op when the rollup is not configured
// with a BatchAuthenticator or when the EspressoTime hardfork has not
// activated.
l.waitForAuthGroup()

// We _must_ wait for all senders on receiptsCh to finish before we can close it.
if err := txQueue.Wait(); err != nil {
if !errors.Is(err, context.Canceled) {
Expand Down Expand Up @@ -1035,6 +1051,13 @@ func (l *BatchSubmitter) sendTx(txdata txData, isCancel bool, candidate *txmgr.T
candidate.GasLimit = floorDataGas
}

// Route through the fallback-auth path when a BatchAuthenticator is
// configured and the EspressoTime hardfork is active. Falls through to
// the upstream queue.Send path otherwise.
if l.dispatchAuthenticatedSendTx(txdata, isCancel, candidate, queue, receiptsCh) {
return
}

queue.Send(txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}, *candidate, receiptsCh)
}

Expand Down
6 changes: 6 additions & 0 deletions op-batcher/batcher/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"testing"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"

altda "github.com/ethereum-optimism/optimism/op-alt-da"
"github.com/ethereum-optimism/optimism/op-batcher/compressor"
"github.com/ethereum-optimism/optimism/op-batcher/config"
Expand Down Expand Up @@ -481,6 +483,10 @@ func TestBatchSubmitter_CriticalError(t *testing.T) {

// fakeL1Client is just a dummy struct. All fault injection is done via the fakeTxMgr (which doesn't interact with this fakeL1Client).
type fakeL1Client struct {
// Embed bind.ContractBackend so the type satisfies the L1Client interface
// (which requires it for the BatchAuthenticator binding used by the
// fallback batcher). AltDA tests never exercise these methods.
bind.ContractBackend
}

func (f *fakeL1Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
Expand Down
48 changes: 48 additions & 0 deletions op-batcher/batcher/espresso_active.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package batcher

import (
"context"
"fmt"
"time"

"github.com/ethereum/go-ethereum/common"
)

// hasBatchAuthenticator returns true if the rollup config has a non-zero
// BatchAuthenticatorAddress, indicating that the BatchAuthenticator-based
// authentication path is in use.
func (l *BatchSubmitter) hasBatchAuthenticator() bool {
return l.RollupConfig.BatchAuthenticatorAddress != (common.Address{})
}

// isFallbackAuthRequired reports whether the fallback (non-TEE) batcher must
// route its batch txs through BatchAuthenticator.authenticateBatchInfo before
// posting to the BatchInbox.
//
// This decision must align with the verifier's per-L1-block fork gate
// (DataSourceConfig.isEspressoEnforcement, which evaluates the hardfork
// activation predicate against the *containing* L1 block's timestamp). Since
// the tx is not yet mined at decision time, its eventual containing block
// has a strictly greater timestamp than the L1 tip the batcher observes:
//
// l1Tip.Time (batcher's view) < l1OriginTime (block containing the tx)
//
// Without compensation, in the window [forkTime − maxL1InclusionDelay, forkTime)
// the batcher would skip authenticateBatchInfo while the verifier — once the
// tx lands in a post-fork block — would require the resulting
// BatchInfoAuthenticated event, silently dropping the batch.
//
// To prevent this, we add Config.FallbackAuthLeadTime to the L1 tip's
// timestamp before evaluating the fork predicate. This makes the batcher
// start authenticating slightly before the verifier requires it. The reverse
// asymmetry (authenticated tx lands pre-fork) is harmless: pre-fork the
// verifier uses sender-based authorization and the auth event is just an
// unrelated L1 tx that does not affect derivation.
func (l *BatchSubmitter) isFallbackAuthRequired(ctx context.Context) (bool, error) {
tip, err := l.l1Tip(ctx)
if err != nil {
return false, fmt.Errorf("failed to fetch L1 tip for fallback-auth gate: %w", err)
}
leadSec := uint64(l.Config.FallbackAuthLeadTime / time.Second)
return l.RollupConfig.IsEspresso(tip.Time + leadSec), nil
}
72 changes: 72 additions & 0 deletions op-batcher/batcher/espresso_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package batcher

import (
"context"
"errors"
"fmt"

"github.com/ethereum-optimism/optimism/op-service/txmgr"
)

// authGroup serializes in-flight fallback-auth submissions so the
// publishingLoop can drain them on shutdown. Initialized in
// NewBatchSubmitter and lifted in waitForAuthGroup. The TEE batcher follow-up
// PR reuses the same group.
//
// Bounded to a fixed concurrency limit to cap the number of BatchInbox
// transactions simultaneously waiting on an authenticateBatchInfo
// transaction to be confirmed.
const fallbackAuthGroupLimit = 128

// initAuthGroup applies the concurrency limit. Called from NewBatchSubmitter.
func (l *BatchSubmitter) initAuthGroup() {
l.authGroup.SetLimit(fallbackAuthGroupLimit)
}

// waitForAuthGroup blocks until all in-flight fallback-auth submissions have
// completed. Called from publishingLoop's tail; blocks until killCtx is
// cancelled if any auth retries are still in flight.
func (l *BatchSubmitter) waitForAuthGroup() {
if err := l.authGroup.Wait(); err != nil {
if !errors.Is(err, context.Canceled) {
l.Log.Error("error waiting for fallback-auth transactions to complete", "err", err)
}
}
}

// dispatchAuthenticatedSendTx routes sendTx through the fallback-batcher
// post-fork auth path, returning true when the tx has been handed off to
// authGroup. Returns false to mean "fall through to the upstream queue.Send
// path" — pre-fork operation and any cancel tx.
//
// The fallback batcher consults isFallbackAuthRequired to gate authentication
// behind the EspressoTime hardfork: pre-fork the verifier accepts plain
// sender-authenticated batches, and the BatchAuthenticator contract is
// irrelevant; calling authenticateBatchInfo pre-fork would also revert against
// the default activeIsEspresso=true contract state.
func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) bool {
if isCancel {
return false
}
if !l.hasBatchAuthenticator() {
return false
}
fallbackAuthRequired, err := l.isFallbackAuthRequired(l.killCtx)
if err != nil {
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()},
Err: fmt.Errorf("failed to evaluate fallback-auth gate: %w", err),
}
return true
}
if !fallbackAuthRequired {
return false
}
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.

return nil
},
)
return true
}
122 changes: 122 additions & 0 deletions op-batcher/batcher/fallback_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package batcher

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"

"github.com/ethereum-optimism/optimism/espresso/bindings"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)

// computeCommitment computes the batch commitment hash from a transaction candidate.
// For calldata transactions, it returns keccak256(calldata).
// For blob transactions, it returns keccak256(concat(blobVersionedHashes)).
func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) {
if len(candidate.Blobs) == 0 {
return crypto.Keccak256Hash(candidate.TxData), nil
}

concatenatedBlobHashes := make([]byte, 0)
for _, blob := range candidate.Blobs {
blobCommitment, err := blob.ComputeKZGCommitment()
if err != nil {
return [32]byte{}, fmt.Errorf("failed to compute KZG commitment for blob: %w", err)
}
blobHash := eth.KZGToVersionedHash(blobCommitment)
concatenatedBlobHashes = append(concatenatedBlobHashes, blobHash.Bytes()...)
}
return crypto.Keccak256Hash(concatenatedBlobHashes), nil
}

// sendTxWithFallbackAuth authenticates a batch transaction via the BatchAuthenticator contract
// using the fallback batcher's sender identity (msg.sender check on-chain), then sends the
// batch data to the BatchInbox address.
//
// 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.

transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}
l.Log.Debug("Sending fallback-authenticated L1 transaction", "txRef", transactionReference)

commitment, err := computeCommitment(candidate)
if err != nil {
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("failed to compute commitment: %w", err),
}
return
}
l.Log.Debug("Computed fallback batch commitment", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:]))

batchAuthenticatorAbi, err := bindings.BatchAuthenticatorMetaData.GetAbi()
if err != nil {
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("failed to get batch authenticator ABI: %w", err),
}
return
}

// Pass an empty signature — the contract checks msg.sender for the fallback path.
authenticateBatchCalldata, err := batchAuthenticatorAbi.Pack("authenticateBatchInfo", commitment, []byte{})
if err != nil {
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("failed to pack authenticateBatchInfo calldata: %w", err),
}
return
}

verifyCandidate := txmgr.TxCandidate{
TxData: authenticateBatchCalldata,
To: &l.RollupConfig.BatchAuthenticatorAddress,
}

l.Log.Debug(
"Sending fallback authenticateBatchInfo transaction",
"txRef", transactionReference,
"commitment", hexutil.Encode(commitment[:]),
"address", l.RollupConfig.BatchAuthenticatorAddress.String(),
)
verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate)
if err != nil {
l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", err)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", err),
}
return
}

receipt, err := l.Txmgr.Send(l.killCtx, *candidate)
if err != nil {
l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("failed to send batch inbox transaction: %w", err),
}
return
}

distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.BlockNumber)
lookbackWindow := new(big.Int).SetUint64(derive.BatchAuthLookbackWindow)
if distance.Sign() < 0 || distance.Cmp(lookbackWindow) >= 0 {
l.Log.Error("authenticateBatchInfo transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("authenticateBatchInfo transaction too far from batch inbox transaction: %s", distance),
}
return
}

receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Receipt: receipt,
Err: nil,
}
}
9 changes: 9 additions & 0 deletions op-batcher/batcher/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ type BatcherConfig struct {

// For throttling DA. See CLIConfig in config.go for details on these parameters.
ThrottleParams config.ThrottleParams

// FallbackAuthLeadTime is consulted by the fallback batcher's
// authentication gate to advance the switch to authenticated batches
// relative to the on-chain EspressoTime hardfork. It absorbs the
// worst-case L1 inclusion delay between batcher decision time (L1 tip)
// and verifier evaluation time (containing L1 block). See
// isFallbackAuthRequired in espresso_active.go for details.
FallbackAuthLeadTime time.Duration
}

// BatcherService represents a full batch-submitter instance and its resources,
Expand Down Expand Up @@ -109,6 +117,7 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, closeApp contex
bs.NetworkTimeout = cfg.TxMgrConfig.NetworkTimeout
bs.CheckRecentTxsDepth = cfg.CheckRecentTxsDepth
bs.WaitNodeSync = cfg.WaitNodeSync
bs.FallbackAuthLeadTime = cfg.FallbackAuthLeadTime

bs.ThrottleParams = config.ThrottleParams{
LowerThreshold: cfg.ThrottleConfig.LowerThreshold,
Expand Down
Loading
Loading