Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
7dd0a79
contracts-bedrock: add Espresso BatchAuthenticator and supporting infra
QuentinI May 7, 2026
5e47299
Update packages/contracts-bedrock/src/L1/BatchAuthenticator.sol
QuentinI May 13, 2026
e88cbe8
Remove stray path
QuentinI May 13, 2026
2704403
Remove hardcoded Sepolia URL
QuentinI May 13, 2026
b99bcb9
Use OZ v5 in mock verifier
QuentinI May 13, 2026
dcff468
Fix Codex's suggestion
QuentinI May 20, 2026
c102ced
Lower pragma
QuentinI May 20, 2026
011c7a7
Remove unrelated foundry.toml changes
QuentinI May 20, 2026
d769ea2
scripts/checks: clean up exclude lists per PR review
QuentinI May 20, 2026
5651e2a
Fix re-initialization
QuentinI May 21, 2026
ab161a1
Add batcher address history
QuentinI May 25, 2026
400f1f5
contracts-bedrock: drop OZ TUP from espresso deploy
palango May 20, 2026
e898f5c
Remove pause from BatchAuthenticator
QuentinI May 25, 2026
0f4d001
Add a defensive check
QuentinI May 25, 2026
ea1f014
test: add end-to-end dual-batcher switch test
QuentinI May 25, 2026
c993c2a
Fix tests
QuentinI May 28, 2026
09533af
switchBatcher from toggle to a setter
QuentinI May 29, 2026
2d634fd
Use OZ Checkpoints for batcher history
QuentinI May 29, 2026
e6d65e0
contracts-bedrock: deploy espresso impls via vm.getCode, drop suppres…
QuentinI Jun 1, 2026
a99e175
Check batcher in Espresso mode
shenkeyao May 28, 2026
483e8e4
regenerate snapshots for UnauthorizedEspressoBatcher error
QuentinI Jun 2, 2026
2e5cc7d
contracts-bedrock: wire espresso proxies to shared OP Stack ProxyAdmin
QuentinI Jun 4, 2026
59f5d38
add caller to BatchInfoAuthenticated event
QuentinI Jun 4, 2026
5461fba
forge fmt
piersy Jun 9, 2026
96dfd24
Remove unused imports
piersy Jun 10, 2026
f132cbf
Rename tests to fit test name convention
piersy Jun 10, 2026
2354b89
op-chain-ops/script: resolve directory-qualified getCode artifact names
QuentinI Jun 11, 2026
70291e3
contracts-bedrock: fix semgrep checks-fast findings
QuentinI Jun 11, 2026
14210d4
contracts-bedrock: prevent same-block espressoBatcher history corruption
QuentinI Jun 15, 2026
68784e3
Fix formatting
piersy Jun 17, 2026
27f75a7
Fix recompile tests
QuentinI Jun 17, 2026
c87f184
op-node: add event-based batch authentication
QuentinI May 25, 2026
d797a1b
op-node: rename EspressoEnforcementTime to EspressoTime
QuentinI May 25, 2026
1f3150f
op-node: require BatchAuthenticatorAddress when Espresso is enabled
QuentinI Jun 2, 2026
9638afd
op-node: fix EspressoTime fork-activation doc comments
QuentinI Jun 2, 2026
dd772e3
op-node: bind authenticated batches to the authenticating caller
QuentinI Jun 8, 2026
7a4a401
op-node: replace global batch auth caches with dependency-injected in…
piersy Jun 2, 2026
9b14f40
Use rollup.Config in DataSourceConfig
piersy Jun 3, 2026
3a76b2d
op-node: fix up batch-auth cache DI and rollupCfg cherry-picks
QuentinI Jun 8, 2026
182d84e
Hardcode BatchAuthLookbackWindow
QuentinI Jun 8, 2026
5aff9d1
Tighten assertions for batch auth tests
piersy Jun 11, 2026
7979391
op-node: test Espresso fork boundary and per-commitment batch auth
piersy Jun 11, 2026
8917fef
op-node: isolate Espresso derivation tests into espresso_-prefixed files
piersy Jun 11, 2026
05cb149
Update op-node/rollup/derive/espresso_blob_data_source_test.go
piersy Jun 15, 2026
25b8fa9
Update op-node/rollup/derive/espresso_blob_data_source_test.go
piersy Jun 15, 2026
2782976
Expand LRU size comment
QuentinI Jun 18, 2026
93fa0dc
espresso/bindings: add regenerated BatchAuthenticator Go bindings
QuentinI May 27, 2026
1267e87
op-batcher: integrate fallback batcher authentication
QuentinI May 27, 2026
25a6c63
op-batcher: route fallback auth through ordered tx queue
QuentinI Jun 18, 2026
42382b6
Merge branch 'celo-rebase-18' into espresso/batcher-fallback
jcortejoso Jun 22, 2026
49c4321
Include espresso folder in docker build
jcortejoso Jun 22, 2026
76b729b
Merge branch 'celo-rebase-18' into espresso/batcher-fallback
lukeiannucci Jul 3, 2026
8632b97
address m1
lukeiannucci Jul 3, 2026
02831d8
m4
lukeiannucci Jul 3, 2026
9c9ba69
op-batcher: drop unused bind.ContractBackend from L1Client
palango Jun 24, 2026
4671fec
op-batcher: compute batch commitment via shared derive helpers
palango Jun 24, 2026
98dbe45
op-batcher: fix inaccurate fallback-auth comments
palango Jun 24, 2026
cbb6942
op-batcher: use sync.WaitGroup for fallback-auth watchers
palango Jun 24, 2026
4c437a2
op-batcher: extract newTxRef helper
palango Jun 24, 2026
25c5086
address comments
lukeiannucci Jul 6, 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
2,277 changes: 2,277 additions & 0 deletions espresso/bindings/batch_authenticator.go

Large diffs are not rendered by default.

9 changes: 9 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

@piersy piersy Jun 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If I understand correctly this lead time is meant to account for the time difference between the block at which a batch is authenticated and the block at which the batch is submitted to the batcher address.

If so, there is already an upper bound for that inclusion delay, it's the 100 block lookback window. So can't this just be set to a constant and removed from the config?

@piersy piersy Jun 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Or simpler still, remove this entirely and have the derivation pipeline require espresso activation to be at least 100 blocks old before switching to espresso verification, thus allowing for a full lookback window. This keeps the 100 block window contained within derivation rather than leaking it here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Regarding this solution, i took a stab at it, but I think this changes some small code in the derivation pipeline, which is now being audited. So I am unsure if we want to make these changes now.

}

func (c *CLIConfig) Check() error {
Expand Down Expand Up @@ -191,6 +196,9 @@ func (c *CLIConfig) Check() error {
if !flags.ValidDataAvailabilityType(c.DataAvailabilityType) {
return fmt.Errorf("unknown data availability type: %q", c.DataAvailabilityType)
}
if c.FallbackAuthLeadTime <= 0 {
return fmt.Errorf("FallbackAuthLeadTime must be positive: %v", c.FallbackAuthLeadTime)
}
// Most chains' L1s still have only Cancun active, but we don't want to
// overcomplicate this check with a dynamic L1 query, so we just use maxBlobsPerBlock.
// We want to check for both, blobs and auto da-type.
Expand Down Expand Up @@ -248,6 +256,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
11 changes: 11 additions & 0 deletions op-batcher/batcher/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func validBatcherConfig() batcher.CLIConfig {
BatchType: 0,
DataAvailabilityType: flags.CalldataType,
TxMgrConfig: txmgr.NewCLIConfig("fake", txmgr.DefaultBatcherFlagValues),
FallbackAuthLeadTime: flags.FallbackAuthLeadTimeFlag.Value,
LogConfig: log.DefaultCLIConfig(),
MetricsConfig: metrics.DefaultCLIConfig(),
PprofConfig: oppprof.DefaultCLIConfig(),
Expand Down Expand Up @@ -106,6 +107,16 @@ func TestBatcherConfig(t *testing.T) {
override: func(c *batcher.CLIConfig) { c.DataAvailabilityType = "foo" },
errString: "unknown data availability type: \"foo\"",
},
{
name: "negative fallback auth lead time",
override: func(c *batcher.CLIConfig) { c.FallbackAuthLeadTime = -time.Second },
errString: "FallbackAuthLeadTime must be positive",
},
{
name: "zero fallback auth lead time",
override: func(c *batcher.CLIConfig) { c.FallbackAuthLeadTime = 0 },
errString: "FallbackAuthLeadTime must be positive",
},
{
name: "zero TargetNumFrames",
override: func(c *batcher.CLIConfig) { c.TargetNumFrames = 0 },
Expand Down
36 changes: 35 additions & 1 deletion op-batcher/batcher/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ type txRef struct {
size int
}

// newTxRef builds the txRef that identifies a batch submission across the txmgr
// queue and receipt handling.
func newTxRef(txdata txData, isCancel bool) txRef {
return txRef{
id: txdata.ID(),
isCancel: isCancel,
isBlob: txdata.daType == DaTypeBlob,
daType: txdata.daType,
size: txdata.Len(),
}
}

func (r txRef) String() string {
return r.string(func(id txID) string { return id.String() })
}
Expand Down Expand Up @@ -124,6 +136,15 @@ type BatchSubmitter struct {
throttleController *throttler.ThrottleController

publishSignal chan pubInfo

// authGroup tracks the fallback batcher's receipt-watcher goroutines (one
// per auth+batch pair) so the publishing loop can drain them via
// waitForAuthGroup before closing receiptsCh. New watchers are back-pressured
// (not hard-bounded) by the txmgr Queue: queue.Send blocks at
// MaxPendingTransactions, so watchers are created no faster than txs drain,
// though a slow receipts loop can briefly leave more than that parked on their
// final receiptsCh send.
authGroup sync.WaitGroup
}

// NewBatchSubmitter initializes the BatchSubmitter driver from a preconfigured DriverSetup
Expand Down Expand Up @@ -516,6 +537,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,7 +1062,14 @@ func (l *BatchSubmitter) sendTx(txdata txData, isCancel bool, candidate *txmgr.T
candidate.GasLimit = floorDataGas
}

queue.Send(txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}, *candidate, receiptsCh)
// 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(newTxRef(txdata, isCancel), *candidate, receiptsCh)
}

func (l *BatchSubmitter) blobTxCandidate(data txData) (*txmgr.TxCandidate, error) {
Expand Down
3 changes: 1 addition & 2 deletions op-batcher/batcher/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,7 @@ func TestBatchSubmitter_CriticalError(t *testing.T) {
// ======= ALTDA TESTS =======

// fakeL1Client is just a dummy struct. All fault injection is done via the fakeTxMgr (which doesn't interact with this fakeL1Client).
type fakeL1Client struct {
}
type fakeL1Client struct{}

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

import (
"context"
"fmt"
"time"

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

// isFallbackAuthRequired reports whether the fallback (non-TEE) batcher must
// route its batch txs through BatchAuthenticator.authenticateBatchInfo before
// posting to the BatchInbox. It returns false if the rollup config has a
// zero BatchAuthenticatorAddress, indicating that the BatchAuthenticator-based
// authentication path is not in use.
//
// This decision must align with the verifier's per-L1-block fork gate
// (rollupCfg.IsEspresso(l1OriginTime) in data_source.go, 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) {
if l.RollupConfig.BatchAuthenticatorAddress == (common.Address{}) {
return false, nil
}
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

M4 — FallbackAuthLeadTime is unvalidated; negative or zero is unsafe.

CLIConfig.Check() (op-batcher/batcher/config.go) never validates this flag. A negative duration makes uint64(... / time.Second) wrap to a huge value, so tip.Time + leadSec overflows and the gate misfires. A value of 0 removes the safety margin entirely and reintroduces the boundary-window silent drop the lead time exists to prevent: a tx decided pre-fork that lands in a post-fork block makes the verifier require a BatchInfoAuthenticated event the batcher never sent → batch silently dropped.

Recommend validating in Check(): reject negative, and reject (or at least warn) on 0; document that the lead time must exceed the worst-case L1 inclusion delay.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Added validation for anything <= 0. And updated the documentation. Let me know if this looks correct
02831d8

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually reading Piers comment below, i may look to remove this.

return l.RollupConfig.IsEspresso(tip.Time + leadSec), nil
}
45 changes: 45 additions & 0 deletions op-batcher/batcher/espresso_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package batcher

import (
"fmt"

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

// waitForAuthGroup blocks until all in-flight fallback-auth watcher goroutines
// have finished. publishingLoop calls it before closing receiptsCh: each watcher
// is a sender on receiptsCh, so the receipts loop must still be draining
// receiptsCh at this point or a watcher's final send would block forever. Each
// watcher always terminates because the txmgr Queue emits exactly one receipt per
// Send, even on context cancellation.
func (l *BatchSubmitter) waitForAuthGroup() {
l.authGroup.Wait()
}

// 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.
func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) bool {
if isCancel {
return false
}
fallbackAuthRequired, err := l.isFallbackAuthRequired(l.killCtx)
if err != nil {
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: newTxRef(txdata, isCancel),
Err: fmt.Errorf("failed to evaluate fallback-auth gate: %w", err),
}
return true
}
if !fallbackAuthRequired {
return false
}
l.sendTxWithFallbackAuth(txdata, isCancel, candidate, queue, receiptsCh)
return true
}
163 changes: 163 additions & 0 deletions op-batcher/batcher/fallback_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package batcher

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"

"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. It delegates to the same functions the verifier uses so the two
// provably agree on the bytes that get authenticated:
// - calldata transactions: keccak256(calldata).
// - blob transactions: keccak256(concat(blobVersionedHashes)).
func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) {
if len(candidate.Blobs) == 0 {
return derive.ComputeCalldataBatchHash(candidate.TxData), nil
}

blobHashes := make([]common.Hash, len(candidate.Blobs))
for i, 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)
}
blobHashes[i] = eth.KZGToVersionedHash(blobCommitment)
}
return derive.ComputeBlobBatchHash(blobHashes), 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
Collaborator Author

Choose a reason for hiding this comment

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

Mirrored from #448 (unresolved review thread) to keep tracking it here after the re-host. Original location: op-batcher/batcher/fallback_auth.go:42.

Original transcript:


@palango — 2026-06-02:

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed as part of the rework in 6f42f54

transactionReference := newTxRef(txdata, isCancel)
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,
}

// Private buffered channels: queue.Send forwards exactly one receipt to each, so the watcher
// reads exactly once per channel (even on context cancellation, the queue still emits a
// ctx-error receipt). These never reach handleReceipt; only the synthetic receipt below does.
authReceiptCh := make(chan txmgr.TxReceipt[txRef], 1)
batchReceiptCh := make(chan txmgr.TxReceipt[txRef], 1)
Comment on lines +85 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

M2 — max-pending-tx defaults to 1, and this cap-1 buffering is load-bearing.

With MaxPendingTransactions = 1 (the default, op-batcher/flags/flags.go:66), the second queue.Send (line 97) blocks in errgroup.Go until the auth tx mines — so batch submission stalls for a full auth-confirmation latency on every pair, and the batch tx isn't even crafted until the auth tx mines (~doubles per-batch latency).

It avoids deadlock only because these channels are buffered cap 1: the auth handler can write its receipt with no reader present (the watcher goroutine isn't started until after both Sends return, line 99). The comment should state that the buffering is what prevents the deadlock at the queue limit — switching these to unbuffered would hard-deadlock at the default config. Recommend documenting that MaxPendingTransactions >= 2 is required (or auto-bumping it) when fallback auth is active. This serialization is also untested — the fakeTxSender in the unit tests delivers receipts inline and doesn't model the blocking second Send.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With the changes made in this #467 addressing M3, i believe this would solve this concern as well. Let me know if you think otherwise


l.Log.Debug(
"Sending fallback authenticateBatchInfo transaction",
"txRef", transactionReference,
"commitment", hexutil.Encode(commitment[:]),
"address", l.RollupConfig.BatchAuthenticatorAddress.String(),
)
// Submit the auth tx then the batch tx, in order, on the publishing-loop goroutine so their
// nonces are assigned in submission order. Each Send blocks here when the queue is at its
// MaxPendingTransactions limit.
queue.Send(transactionReference, verifyCandidate, authReceiptCh)
queue.Send(transactionReference, *candidate, batchReceiptCh)

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 Serialize blob batches behind the auth transaction

When blob DA is used with --max-pending-tx set above the default 1 (or 0 for unlimited), this immediately submits a non-blob authenticateBatchInfo tx and then the blob batch tx from the same sender while the auth tx is still pending. The txmgr interface documents that callers mixing Blob and non-Blob transactions must handle ErrAlreadyReserved (op-service/txmgr/txmgr.go:81-83), so this path can repeatedly reject/requeue fallback-auth blob batches instead of posting them. The batch send should wait until the auth tx is mined, or otherwise avoid having both tx types pending from the same account.

Useful? React with 👍 / 👎.

Comment on lines +97 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

M3 — Nonce-gap hazard when the auth send fails asynchronously.

These two Sends craft consecutive nonces synchronously (auth = N, batch = N+1). If the auth tx is crafted but its async sendTx later fails, txmgr calls resetNonce() (op-service/txmgr/txmgr.go:372) while the batch tx at nonce N+1 is already in flight — leaving a permanent nonce gap that wedges the sender until the cancel/gap-fill path runs.

This is the same class as the upstream single-tx path, but amplified here because this path deliberately fires two ordered txs and a failure/revert on the first is now an expected case (e.g. the activeIsEspresso-still-true scenario). Worth a targeted integration test against the real txmgr.Queue; consider gating the batch Send on auth success rather than firing both unconditionally.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider the transactions for 2 subsequent batches of batches

A1 - Auth tx batch 1
B1 - Batch tx batch 1

A2 - ...
B2 - ...

Assuming A1 fails and causes the nonce to be reset B1s nonce will be too high and prevent it from getting mined.

However watchFallbackAuthReceipts doesn't block the next iteration of the publishingLoop so the loop will proceed to submit A2 and B2.

A2 should work because it would have a correct nonce, but the nonce of B2 would clash with B1.

Assuming that B2 succeeds B1 will now fail due to it's nonce being too low. Once B1 is dropped the synthetic failure receipt will be sent on receptsCh causing the channel to rewind to just before when A1 was sent.

If A & B were both in the same channel then they will just be submitted again and the previously submitted parts of A & B will be lost, but if they are separate channels and now B has arrived before A, then the batcher will ultimately end up having to rewind to the last safe head when it decides that the safe head is not progressing appropriately.

I think this can be pretty easily fixed by simply submitting and waiting for the auth tx before submitting and waiting for the batch tx and stopping and returning a synthetic error receipt at the first error encountered.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I believe I understand the issue, I will make a separate PR on top of this to address it so it is easier to review.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@piersy @palango let me know if I understood it correctly, and if this is on the right track
#467

Comment thread
lukeiannucci marked this conversation as resolved.

l.authGroup.Add(1)
go func() {
defer l.authGroup.Done()
l.watchFallbackAuthReceipts(transactionReference, authReceiptCh, batchReceiptCh, receiptsCh)
}()
}

// watchFallbackAuthReceipts collects the auth and batch receipts for a fallback-auth pair,
// validates that the batch tx landed within the lookback window of the auth tx, and forwards a
// single synthetic receipt keyed to the batch txData onto receiptsCh. Any failure produces an
// error receipt so the channel manager rewinds and resubmits the frame set.
func (l *BatchSubmitter) watchFallbackAuthReceipts(transactionReference txRef, authReceiptCh, batchReceiptCh chan txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) {
authResult := <-authReceiptCh
batchResult := <-batchReceiptCh

if authResult.Err != nil {
l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", authResult.Err)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,

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 Preserve the auth tx type when reporting auth send failures

When a fallback-auth blob batch hits ErrAlreadyReserved while sending the non-blob authenticateBatchInfo tx (for example with --max-pending-tx > 1 or an existing blob reservation), this synthetic receipt reports the blob batch's txRef instead of the auth tx's actual non-blob type. The receipts loop uses r.ID.isBlob to choose the cancellation transaction, so it will try a calldata cancellation after a non-blob auth send was rejected by a blob reservation, rather than sending a blob replacement to clear the reserved blob; the batcher can then keep requeueing without unblocking the txpool.

Useful? React with 👍 / 👎.

Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", authResult.Err),
}
return
}

// txmgr returns a receipt as soon as the tx is mined, regardless of execution status. A
// reverted authenticateBatchInfo call emits no BatchInfoAuthenticated event, so the verifier
// drops the batch and the safe head stalls; report failure so the frames are re-queued. The
// batch inbox tx needs no such check: derivation reads its data by L1 inclusion, not by
// execution status.
if authResult.Receipt.Status != types.ReceiptStatusSuccessful {
l.Log.Error("Fallback authenticateBatchInfo transaction reverted", "txRef", transactionReference, "txHash", authResult.Receipt.TxHash)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("fallback authenticateBatchInfo transaction reverted: %s", authResult.Receipt.TxHash),
}
return
}

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

distance := new(big.Int).Sub(batchResult.Receipt.BlockNumber, authResult.Receipt.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: batchResult.Receipt,
Err: nil,
}
}
Loading
Loading