Skip to content

Commit f8480f8

Browse files
QuentinIOpenCode
andcommitted
op-batcher: integrate fallback batcher authentication
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 #445. The TEE batcher is a separate PR stacked on top. Co-authored-by: OpenCode <noreply@opencode.ai>
1 parent edda805 commit f8480f8

8 files changed

Lines changed: 299 additions & 0 deletions

File tree

op-batcher/batcher/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ type CLIConfig struct {
152152
PprofConfig oppprof.CLIConfig
153153
RPC oprpc.CLIConfig
154154
AltDA altda.CLIConfig
155+
156+
// FallbackAuthLeadTime is the lead time for the fallback batcher's
157+
// authentication gate. See BatcherConfig.FallbackAuthLeadTime in
158+
// service.go and isFallbackAuthRequired in espresso_active.go.
159+
FallbackAuthLeadTime time.Duration
155160
}
156161

157162
func (c *CLIConfig) Check() error {
@@ -248,6 +253,7 @@ func NewConfig(ctx *cli.Context) *CLIConfig {
248253
PprofConfig: oppprof.ReadCLIConfig(ctx),
249254
RPC: oprpc.ReadCLIConfig(ctx),
250255
AltDA: altda.ReadCLIConfig(ctx),
256+
FallbackAuthLeadTime: ctx.Duration(flags.FallbackAuthLeadTimeFlag.Name),
251257
ThrottleConfig: ThrottleConfig{
252258
AdditionalEndpoints: ctx.StringSlice(flags.AdditionalThrottlingEndpointsFlag.Name),
253259
TxSizeLowerLimit: ctx.Uint64(flags.ThrottleTxSizeLowerLimitFlag.Name),

op-batcher/batcher/driver.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"golang.org/x/sync/errgroup"
1414

15+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1516
"github.com/ethereum/go-ethereum/common"
1617
"github.com/ethereum/go-ethereum/common/hexutil"
1718
"github.com/ethereum/go-ethereum/core"
@@ -73,6 +74,7 @@ func (r txRef) string(txIDStringer func(txID) string) string {
7374
type L1Client interface {
7475
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
7576
NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error)
77+
bind.ContractBackend
7678
}
7779

7880
type L2Client interface {
@@ -124,6 +126,12 @@ type BatchSubmitter struct {
124126
throttleController *throttler.ThrottleController
125127

126128
publishSignal chan pubInfo
129+
130+
// authGroup serializes in-flight BatchAuthenticator submissions issued by
131+
// the fallback batcher's authentication path so the publishing loop can
132+
// drain them on shutdown. Bounded to fallbackAuthGroupLimit; see
133+
// espresso_driver.go.
134+
authGroup errgroup.Group
127135
}
128136

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

154+
batcher.initAuthGroup()
155+
146156
return batcher
147157
}
148158

@@ -516,6 +526,12 @@ func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup,
516526
}
517527
}
518528

529+
// Wait for all in-flight fallback-auth submissions to complete to prevent
530+
// new transactions being queued. No-op when the rollup is not configured
531+
// with a BatchAuthenticator or when the EspressoTime hardfork has not
532+
// activated.
533+
l.waitForAuthGroup()
534+
519535
// We _must_ wait for all senders on receiptsCh to finish before we can close it.
520536
if err := txQueue.Wait(); err != nil {
521537
if !errors.Is(err, context.Canceled) {
@@ -1035,6 +1051,13 @@ func (l *BatchSubmitter) sendTx(txdata txData, isCancel bool, candidate *txmgr.T
10351051
candidate.GasLimit = floorDataGas
10361052
}
10371053

1054+
// Route through the fallback-auth path when a BatchAuthenticator is
1055+
// configured and the EspressoTime hardfork is active. Falls through to
1056+
// the upstream queue.Send path otherwise.
1057+
if l.dispatchAuthenticatedSendTx(txdata, isCancel, candidate, queue, receiptsCh) {
1058+
return
1059+
}
1060+
10381061
queue.Send(txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}, *candidate, receiptsCh)
10391062
}
10401063

op-batcher/batcher/driver_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"testing"
1515
"time"
1616

17+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
18+
1719
altda "github.com/ethereum-optimism/optimism/op-alt-da"
1820
"github.com/ethereum-optimism/optimism/op-batcher/compressor"
1921
"github.com/ethereum-optimism/optimism/op-batcher/config"
@@ -481,6 +483,10 @@ func TestBatchSubmitter_CriticalError(t *testing.T) {
481483

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

486492
func (f *fakeL1Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package batcher
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
)
10+
11+
// hasBatchAuthenticator returns true if the rollup config has a non-zero
12+
// BatchAuthenticatorAddress, indicating that the BatchAuthenticator-based
13+
// authentication path is in use.
14+
func (l *BatchSubmitter) hasBatchAuthenticator() bool {
15+
return l.RollupConfig.BatchAuthenticatorAddress != (common.Address{})
16+
}
17+
18+
// isFallbackAuthRequired reports whether the fallback (non-TEE) batcher must
19+
// route its batch txs through BatchAuthenticator.authenticateBatchInfo before
20+
// posting to the BatchInbox.
21+
//
22+
// This decision must align with the verifier's per-L1-block fork gate
23+
// (DataSourceConfig.isEspressoEnforcement, which evaluates the hardfork
24+
// activation predicate against the *containing* L1 block's timestamp). Since
25+
// the tx is not yet mined at decision time, its eventual containing block
26+
// has a strictly greater timestamp than the L1 tip the batcher observes:
27+
//
28+
// l1Tip.Time (batcher's view) < l1OriginTime (block containing the tx)
29+
//
30+
// Without compensation, in the window [forkTime − maxL1InclusionDelay, forkTime)
31+
// the batcher would skip authenticateBatchInfo while the verifier — once the
32+
// tx lands in a post-fork block — would require the resulting
33+
// BatchInfoAuthenticated event, silently dropping the batch.
34+
//
35+
// To prevent this, we add Config.FallbackAuthLeadTime to the L1 tip's
36+
// timestamp before evaluating the fork predicate. This makes the batcher
37+
// start authenticating slightly before the verifier requires it. The reverse
38+
// asymmetry (authenticated tx lands pre-fork) is harmless: pre-fork the
39+
// verifier uses sender-based authorization and the auth event is just an
40+
// unrelated L1 tx that does not affect derivation.
41+
func (l *BatchSubmitter) isFallbackAuthRequired(ctx context.Context) (bool, error) {
42+
tip, err := l.l1Tip(ctx)
43+
if err != nil {
44+
return false, fmt.Errorf("failed to fetch L1 tip for fallback-auth gate: %w", err)
45+
}
46+
leadSec := uint64(l.Config.FallbackAuthLeadTime / time.Second)
47+
return l.RollupConfig.IsEspresso(tip.Time + leadSec), nil
48+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package batcher
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/ethereum-optimism/optimism/op-service/txmgr"
9+
)
10+
11+
// authGroup serializes in-flight fallback-auth submissions so the
12+
// publishingLoop can drain them on shutdown. Initialized in
13+
// NewBatchSubmitter and lifted in waitForAuthGroup. The TEE batcher follow-up
14+
// PR reuses the same group.
15+
//
16+
// Bounded to a fixed concurrency limit to cap the number of BatchInbox
17+
// transactions simultaneously waiting on an authenticateBatchInfo
18+
// transaction to be confirmed.
19+
const fallbackAuthGroupLimit = 128
20+
21+
// initAuthGroup applies the concurrency limit. Called from NewBatchSubmitter.
22+
func (l *BatchSubmitter) initAuthGroup() {
23+
l.authGroup.SetLimit(fallbackAuthGroupLimit)
24+
}
25+
26+
// waitForAuthGroup blocks until all in-flight fallback-auth submissions have
27+
// completed. Called from publishingLoop's tail; blocks until killCtx is
28+
// cancelled if any auth retries are still in flight.
29+
func (l *BatchSubmitter) waitForAuthGroup() {
30+
if err := l.authGroup.Wait(); err != nil {
31+
if !errors.Is(err, context.Canceled) {
32+
l.Log.Error("error waiting for fallback-auth transactions to complete", "err", err)
33+
}
34+
}
35+
}
36+
37+
// dispatchAuthenticatedSendTx routes sendTx through the fallback-batcher
38+
// post-fork auth path, returning true when the tx has been handed off to
39+
// authGroup. Returns false to mean "fall through to the upstream queue.Send
40+
// path" — pre-fork operation and any cancel tx.
41+
//
42+
// The fallback batcher consults isFallbackAuthRequired to gate authentication
43+
// behind the EspressoTime hardfork: pre-fork the verifier accepts plain
44+
// sender-authenticated batches, and the BatchAuthenticator contract is
45+
// irrelevant; calling authenticateBatchInfo pre-fork would also revert against
46+
// the default activeIsEspresso=true contract state.
47+
func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) bool {
48+
if isCancel {
49+
return false
50+
}
51+
if !l.hasBatchAuthenticator() {
52+
return false
53+
}
54+
fallbackAuthRequired, err := l.isFallbackAuthRequired(l.killCtx)
55+
if err != nil {
56+
receiptsCh <- txmgr.TxReceipt[txRef]{
57+
ID: txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()},
58+
Err: fmt.Errorf("failed to evaluate fallback-auth gate: %w", err),
59+
}
60+
return true
61+
}
62+
if !fallbackAuthRequired {
63+
return false
64+
}
65+
l.authGroup.Go(
66+
func() error {
67+
l.sendTxWithFallbackAuth(txdata, isCancel, candidate, queue, receiptsCh)
68+
return nil
69+
},
70+
)
71+
return true
72+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package batcher
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/common/hexutil"
8+
"github.com/ethereum/go-ethereum/crypto"
9+
10+
"github.com/ethereum-optimism/optimism/espresso/bindings"
11+
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
12+
"github.com/ethereum-optimism/optimism/op-service/eth"
13+
"github.com/ethereum-optimism/optimism/op-service/txmgr"
14+
)
15+
16+
// computeCommitment computes the batch commitment hash from a transaction candidate.
17+
// For calldata transactions, it returns keccak256(calldata).
18+
// For blob transactions, it returns keccak256(concat(blobVersionedHashes)).
19+
func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) {
20+
if len(candidate.Blobs) == 0 {
21+
return crypto.Keccak256Hash(candidate.TxData), nil
22+
}
23+
24+
concatenatedBlobHashes := make([]byte, 0)
25+
for _, blob := range candidate.Blobs {
26+
blobCommitment, err := blob.ComputeKZGCommitment()
27+
if err != nil {
28+
return [32]byte{}, fmt.Errorf("failed to compute KZG commitment for blob: %w", err)
29+
}
30+
blobHash := eth.KZGToVersionedHash(blobCommitment)
31+
concatenatedBlobHashes = append(concatenatedBlobHashes, blobHash.Bytes()...)
32+
}
33+
return crypto.Keccak256Hash(concatenatedBlobHashes), nil
34+
}
35+
36+
// sendTxWithFallbackAuth authenticates a batch transaction via the BatchAuthenticator contract
37+
// using the fallback batcher's sender identity (msg.sender check on-chain), then sends the
38+
// batch data to the BatchInbox address.
39+
//
40+
// The contract's fallback path checks msg.sender against systemConfig.batcherHash(), so no
41+
// separate signature is needed — the L1 transaction is already signed by the TxManager's key.
42+
func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) {
43+
transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}
44+
l.Log.Debug("Sending fallback-authenticated L1 transaction", "txRef", transactionReference)
45+
46+
commitment, err := computeCommitment(candidate)
47+
if err != nil {
48+
receiptsCh <- txmgr.TxReceipt[txRef]{
49+
ID: transactionReference,
50+
Err: fmt.Errorf("failed to compute commitment: %w", err),
51+
}
52+
return
53+
}
54+
l.Log.Debug("Computed fallback batch commitment", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:]))
55+
56+
batchAuthenticatorAbi, err := bindings.BatchAuthenticatorMetaData.GetAbi()
57+
if err != nil {
58+
receiptsCh <- txmgr.TxReceipt[txRef]{
59+
ID: transactionReference,
60+
Err: fmt.Errorf("failed to get batch authenticator ABI: %w", err),
61+
}
62+
return
63+
}
64+
65+
// Pass an empty signature — the contract checks msg.sender for the fallback path.
66+
authenticateBatchCalldata, err := batchAuthenticatorAbi.Pack("authenticateBatchInfo", commitment, []byte{})
67+
if err != nil {
68+
receiptsCh <- txmgr.TxReceipt[txRef]{
69+
ID: transactionReference,
70+
Err: fmt.Errorf("failed to pack authenticateBatchInfo calldata: %w", err),
71+
}
72+
return
73+
}
74+
75+
verifyCandidate := txmgr.TxCandidate{
76+
TxData: authenticateBatchCalldata,
77+
To: &l.RollupConfig.BatchAuthenticatorAddress,
78+
}
79+
80+
l.Log.Debug(
81+
"Sending fallback authenticateBatchInfo transaction",
82+
"txRef", transactionReference,
83+
"commitment", hexutil.Encode(commitment[:]),
84+
"address", l.RollupConfig.BatchAuthenticatorAddress.String(),
85+
)
86+
verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate)
87+
if err != nil {
88+
l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", err)
89+
receiptsCh <- txmgr.TxReceipt[txRef]{
90+
ID: transactionReference,
91+
Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", err),
92+
}
93+
return
94+
}
95+
96+
receipt, err := l.Txmgr.Send(l.killCtx, *candidate)
97+
if err != nil {
98+
l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err)
99+
receiptsCh <- txmgr.TxReceipt[txRef]{
100+
ID: transactionReference,
101+
Err: fmt.Errorf("failed to send batch inbox transaction: %w", err),
102+
}
103+
return
104+
}
105+
106+
distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.BlockNumber)
107+
lookbackWindow := new(big.Int).SetUint64(derive.BatchAuthLookbackWindow)
108+
if distance.Sign() < 0 || distance.Cmp(lookbackWindow) >= 0 {
109+
l.Log.Error("authenticateBatchInfo transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance)
110+
receiptsCh <- txmgr.TxReceipt[txRef]{
111+
ID: transactionReference,
112+
Err: fmt.Errorf("authenticateBatchInfo transaction too far from batch inbox transaction: %s", distance),
113+
}
114+
return
115+
}
116+
117+
receiptsCh <- txmgr.TxReceipt[txRef]{
118+
ID: transactionReference,
119+
Receipt: receipt,
120+
Err: nil,
121+
}
122+
}

op-batcher/batcher/service.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ type BatcherConfig struct {
5050

5151
// For throttling DA. See CLIConfig in config.go for details on these parameters.
5252
ThrottleParams config.ThrottleParams
53+
54+
// FallbackAuthLeadTime is consulted by the fallback batcher's
55+
// authentication gate to advance the switch to authenticated batches
56+
// relative to the on-chain EspressoTime hardfork. It absorbs the
57+
// worst-case L1 inclusion delay between batcher decision time (L1 tip)
58+
// and verifier evaluation time (containing L1 block). See
59+
// isFallbackAuthRequired in espresso_active.go for details.
60+
FallbackAuthLeadTime time.Duration
5361
}
5462

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

113122
bs.ThrottleParams = config.ThrottleParams{
114123
LowerThreshold: cfg.ThrottleConfig.LowerThreshold,

op-batcher/flags/flags.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ var (
160160
Value: false,
161161
EnvVars: prefixEnvVars("WAIT_NODE_SYNC"),
162162
}
163+
FallbackAuthLeadTimeFlag = &cli.DurationFlag{
164+
Name: "espresso.fallback-auth-lead-time",
165+
Usage: "Lead time for the fallback batcher's Espresso authentication gate. " +
166+
"How far ahead of the on-chain EspressoTime the fallback batcher " +
167+
"starts routing batch txs through BatchAuthenticator.authenticateBatchInfo. " +
168+
"This absorbs worst-case L1 inclusion delay between the batcher's decision " +
169+
"(based on L1 tip time) and the verifier's gate (based on the containing " +
170+
"L1 block's time). Has no effect outside the boundary window around the " +
171+
"EspressoTime hardfork.",
172+
Value: 5 * time.Minute,
173+
EnvVars: prefixEnvVars("ESPRESSO_FALLBACK_AUTH_LEAD_TIME"),
174+
}
163175

164176
// Legacy Flags
165177
SequencerHDPathFlag = txmgr.SequencerHDPathFlag
@@ -189,6 +201,7 @@ var optionalFlags = []cli.Flag{
189201
DataAvailabilityTypeFlag,
190202
ActiveSequencerCheckDurationFlag,
191203
CompressionAlgoFlag,
204+
FallbackAuthLeadTimeFlag,
192205
}
193206

194207
func init() {

0 commit comments

Comments
 (0)