Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions op-batcher/batcher/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ func (c *CLIConfig) Check() error {
if !flags.ValidDataAvailabilityType(c.DataAvailabilityType) {
return fmt.Errorf("unknown data availability type: %q", c.DataAvailabilityType)
}
// The auth→batch distance (num-confirmations + inclusion delay) must fit within
// BatchAuthLookbackWindow, so reserve room for the batch to land or the safe head stalls.
const fallbackAuthInclusionReserve = 75 // blocks (~15 min at 12s L1 slots)
if authLookback := derive.BatchAuthLookbackWindow; authLookback < c.TxMgrConfig.NumConfirmations+fallbackAuthInclusionReserve {

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 Gate fallback-auth confirmation limit to Espresso configs

When any deployment sets --num-confirmations above 25, this new check rejects the CLI config before the batcher has loaded the rollup config, so it also blocks chains that have no BatchAuthenticatorAddress or Espresso fallback-auth path at all. The lookback bound only matters for sendTxWithFallbackAuth; non-Espresso or pre-Espresso batchers can still safely wait for more confirmations, so this should be applied only when fallback auth is actually configured/active, or after rollup config is available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

return fmt.Errorf("NumConfirmations (%d) too high for BatchAuthLookbackWindow (%d): need %d blocks of inclusion headroom",
c.TxMgrConfig.NumConfirmations, authLookback, fallbackAuthInclusionReserve)
}
// 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
7 changes: 7 additions & 0 deletions op-batcher/batcher/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ func TestBatcherConfig(t *testing.T) {
override: func(c *batcher.CLIConfig) { c.DataAvailabilityType = "foo" },
errString: "unknown data availability type: \"foo\"",
},
{
name: "num-confirmations leaves too little batch auth lookback window",
override: func(c *batcher.CLIConfig) {
c.TxMgrConfig.NumConfirmations = derive.BatchAuthLookbackWindow
},
errString: "too high for BatchAuthLookbackWindow",
},
{
name: "zero TargetNumFrames",
override: func(c *batcher.CLIConfig) { c.TargetNumFrames = 0 },
Expand Down
3 changes: 3 additions & 0 deletions op-batcher/batcher/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,9 @@ func (l *BatchSubmitter) sendTransaction(txdata txData, queue *txmgr.Queue[txRef

type TxSender[T any] interface {
Send(id T, candidate txmgr.TxCandidate, receiptCh chan txmgr.TxReceipt[T])
// SendPair submits an ordered pair of transactions holding a single
// max-pending slot; see txmgr.Queue.SendPair.
SendPair(firstID T, first txmgr.TxCandidate, firstCh chan txmgr.TxReceipt[T], secondID T, second txmgr.TxCandidate, secondCh chan txmgr.TxReceipt[T])
}

// sendTx uses the txmgr queue to send the given transaction candidate after setting its
Expand Down
5 changes: 5 additions & 0 deletions op-batcher/batcher/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ func (q *MockTxQueue) Send(ref txRef, candidate txmgr.TxCandidate, receiptCh cha
q.m.Store(ref.id.String(), candidate)
}

func (q *MockTxQueue) SendPair(firstRef txRef, first txmgr.TxCandidate, firstCh chan txmgr.TxReceipt[txRef], secondRef txRef, second txmgr.TxCandidate, secondCh chan txmgr.TxReceipt[txRef]) {
q.m.Store(firstRef.id.String(), first)
q.m.Store(secondRef.id.String(), second)
}

func (q *MockTxQueue) Load(id string) txmgr.TxCandidate {
c, _ := q.m.Load(id)
return c.(txmgr.TxCandidate)
Expand Down
108 changes: 77 additions & 31 deletions op-batcher/batcher/fallback_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) {
// 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]) {
transactionReference := newTxRef(txdata, isCancel)
// The auth tx shares the batch txdata's identity (so a failure requeues the right frames)
// but is always a calldata tx. Auth failures must carry its real type: an ErrAlreadyReserved
// receipt labeled with the batch's blob type would make cancelBlockingTx cancel the wrong
// pool, leaving the reserving tx stuck.
authReference := transactionReference
authReference.isBlob = false
authReference.daType = DaTypeCalldata
l.Log.Debug("Sending fallback-authenticated L1 transaction", "txRef", transactionReference)

commitment, err := computeCommitment(candidate)
Expand Down Expand Up @@ -79,58 +86,96 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca
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)

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)

if len(candidate.Blobs) > 0 {
// SendPair doesn't support blobs.
l.sendFallbackAuthSerialized(transactionReference, authReference, verifyCandidate, candidate, queue, receiptsCh)
return
}

// Private buffered channels — Queue.SendPair forwards exactly one receipt to
// each, so the watcher reads exactly once per channel. These never reach
// handleReceipt; only the synthetic receipt from the watcher does.
authReceiptCh := make(chan txmgr.TxReceipt[txRef], 1)
batchReceiptCh := make(chan txmgr.TxReceipt[txRef], 1)

// Calldata pair: pipelined, auth leg first; see TxManager.SendPairAsync for the
// contract (ordering, slot accounting, cancellation of the batch leg on failure).
queue.SendPair(authReference, verifyCandidate, authReceiptCh, transactionReference, *candidate, batchReceiptCh)

l.authGroup.Add(1)
go func() {
defer l.authGroup.Done()
l.watchFallbackAuthReceipts(transactionReference, authReceiptCh, batchReceiptCh, receiptsCh)
l.watchFallbackAuthReceipts(transactionReference, authReference, 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]) {
// sendFallbackAuthSerialized submits an auth+batch pair serially: the auth tx is sent
// and confirmed first, then the batch tx. It runs on the publishing-loop goroutine and
// blocks it for the pair's full confirmation cycle, so consecutive pairs never overlap.
func (l *BatchSubmitter) sendFallbackAuthSerialized(transactionReference, authReference txRef, verifyCandidate txmgr.TxCandidate, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) {
authReceiptCh := make(chan txmgr.TxReceipt[txRef], 1)
queue.Send(authReference, verifyCandidate, authReceiptCh)
authResult := <-authReceiptCh
batchResult := <-batchReceiptCh

if authResult.Err != nil {
l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", authResult.Err)
if err := fallbackAuthResultError(authResult); err != nil {
l.Log.Error("Fallback authenticateBatchInfo transaction failed", "txRef", transactionReference, "err", err)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", authResult.Err),
ID: authReference,
Err: 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.
// The auth tx is confirmed on L1, releasing the account's reservation in the
// calldata pool. Now send the blob batch tx.
batchReceiptCh := make(chan txmgr.TxReceipt[txRef], 1)
queue.Send(transactionReference, *candidate, batchReceiptCh)
batchResult := <-batchReceiptCh
l.resolveFallbackAuthPair(transactionReference, authReference, authResult, batchResult, receiptsCh)
}

// watchFallbackAuthReceipts collects the auth and batch receipts of a pipelined fallback-auth
// pair and resolves them into a single synthetic receipt (see resolveFallbackAuthPair).
func (l *BatchSubmitter) watchFallbackAuthReceipts(transactionReference, authReference txRef, authReceiptCh, batchReceiptCh chan txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) {
authResult := <-authReceiptCh
batchResult := <-batchReceiptCh
l.resolveFallbackAuthPair(transactionReference, authReference, authResult, batchResult, receiptsCh)
}

// fallbackAuthResultError interprets the auth leg's receipt: a send failure or a
// mined-but-reverted auth tx both fail the pair, since a reverted authenticateBatchInfo call
// emits no BatchInfoAuthenticated event and the verifier would drop the batch (txmgr returns a
// receipt as soon as the tx is mined, regardless of execution status). The batch inbox tx
// needs no such status check: derivation reads its data by L1 inclusion, not by execution
// status.
func fallbackAuthResultError(authResult txmgr.TxReceipt[txRef]) error {
if authResult.Err != nil {
return fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", authResult.Err)
}
if authResult.Receipt.Status != types.ReceiptStatusSuccessful {
l.Log.Error("Fallback authenticateBatchInfo transaction reverted", "txRef", transactionReference, "txHash", authResult.Receipt.TxHash)
return fmt.Errorf("fallback authenticateBatchInfo transaction reverted: %s", authResult.Receipt.TxHash)
}
return nil
}

// resolveFallbackAuthPair validates the receipts of an auth+batch pair — auth success, batch
// success, and 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. Auth
// failures are reported under authReference (see its construction in sendTxWithFallbackAuth
// for why the calldata typing matters).
func (l *BatchSubmitter) resolveFallbackAuthPair(transactionReference, authReference txRef, authResult, batchResult txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) {
if err := fallbackAuthResultError(authResult); err != nil {
l.Log.Error("Fallback authenticateBatchInfo transaction failed", "txRef", transactionReference, "err", err)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Err: fmt.Errorf("fallback authenticateBatchInfo transaction reverted: %s", authResult.Receipt.TxHash),
ID: authReference,
Err: err,
}
return
}
Expand All @@ -147,6 +192,7 @@ func (l *BatchSubmitter) watchFallbackAuthReceipts(transactionReference txRef, a
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.Metr.RecordFallbackAuthWindowExceeded()
l.Log.Error("authenticateBatchInfo transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance)
receiptsCh <- txmgr.TxReceipt[txRef]{
ID: transactionReference,
Expand Down
Loading