diff --git a/op-batcher/batcher/fallback_auth.go b/op-batcher/batcher/fallback_auth.go index 50f87b0f2a9..5fc279f74ac 100644 --- a/op-batcher/batcher/fallback_auth.go +++ b/op-batcher/batcher/fallback_auth.go @@ -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) @@ -88,13 +95,13 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca // Submit the auth tx and wait for its receipt, then send the batch tx. Each Send // blocks here when the queue is at its MaxPendingTransactions limit. authReceiptCh := make(chan txmgr.TxReceipt[txRef], 1) - queue.Send(transactionReference, verifyCandidate, authReceiptCh) + queue.Send(authReference, verifyCandidate, authReceiptCh) authResult := <-authReceiptCh if authResult.Err != nil { l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", authResult.Err) receiptsCh <- txmgr.TxReceipt[txRef]{ - ID: transactionReference, + ID: authReference, Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", authResult.Err), } return @@ -108,7 +115,7 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca 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, + ID: authReference, Err: fmt.Errorf("fallback authenticateBatchInfo transaction reverted: %s", authResult.Receipt.TxHash), } return diff --git a/op-batcher/batcher/fallback_auth_test.go b/op-batcher/batcher/fallback_auth_test.go index c50dd6a69cd..51a40f1ffab 100644 --- a/op-batcher/batcher/fallback_auth_test.go +++ b/op-batcher/batcher/fallback_auth_test.go @@ -117,6 +117,61 @@ func TestFallbackAuth_AuthFailureRetried(t *testing.T) { require.Len(t, queue.sends, 1) } +// TestFallbackAuth_AuthFailureTxRefType verifies that an auth-tx failure is +// reported under a calldata-typed txRef even when the batch txdata is blob. +// The auth tx is always calldata; if its ErrAlreadyReserved failure were +// labeled with the batch's blob type, cancelBlockingTx would send a calldata +// cancel against a blobpool reservation, which is rejected the same way, +// looping forever without ever displacing the stuck blob tx. +func TestFallbackAuth_AuthFailureTxRefType(t *testing.T) { + l := newFallbackAuthSubmitter(t) + txdata := testFallbackTxData(t) + txdata.daType = DaTypeBlob + candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} + + queue := &fakeTxSender{ + responses: []txmgr.TxReceipt[txRef]{ + {Err: errSendFailed}, // auth fails. No batch response, it must never be sent + }, + } + receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) + + l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + + got := <-receiptsCh + require.Error(t, got.Err) + require.Equal(t, txdata.ID().String(), got.ID.id.String()) + require.Len(t, queue.sends, 1) + require.False(t, got.ID.isBlob) + require.Equal(t, DaTypeCalldata, got.ID.daType) +} + +// TestFallbackAuth_BatchFailureTxRefType verifies the converse: a batch-tx +// failure keeps the batch txdata's own type on the forwarded receipt. +func TestFallbackAuth_BatchFailureTxRefType(t *testing.T) { + l := newFallbackAuthSubmitter(t) + txdata := testFallbackTxData(t) + txdata.daType = DaTypeBlob + candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} + + queue := &fakeTxSender{ + responses: []txmgr.TxReceipt[txRef]{ + {Receipt: receiptWithBlock(100)}, // auth lands + {Err: errSendFailed}, // batch fails + }, + } + receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) + + l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + + got := <-receiptsCh + require.Error(t, got.Err) + require.Equal(t, txdata.ID().String(), got.ID.id.String()) + require.Len(t, queue.sends, 2) + require.True(t, got.ID.isBlob) + require.Equal(t, DaTypeBlob, got.ID.daType) +} + func TestFallbackAuth_BatchFailureRetried(t *testing.T) { l := newFallbackAuthSubmitter(t) txdata := testFallbackTxData(t)