Skip to content
Merged
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
27 changes: 19 additions & 8 deletions op-batcher/batcher/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ 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 @@ -52,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 All @@ -74,7 +85,6 @@ 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 @@ -129,11 +139,12 @@ type BatchSubmitter struct {

// 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. It is intentionally unbounded:
// pending-tx throttling is enforced by the txmgr Queue (queue.Send blocks at
// MaxPendingTransactions), and the live watcher count is derived from the
// queue's in-flight tx count, so it inherits the same bound.
authGroup errgroup.Group
// 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 @@ -1058,7 +1069,7 @@ func (l *BatchSubmitter) sendTx(txdata txData, isCancel bool, candidate *txmgr.T
return
}

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

func (l *BatchSubmitter) blobTxCandidate(data txData) (*txmgr.TxCandidate, error) {
Expand Down
9 changes: 1 addition & 8 deletions op-batcher/batcher/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ 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 @@ -482,12 +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 {
// 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
}
type fakeL1Client struct{}

func (f *fakeL1Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
if number == nil {
Expand Down
4 changes: 2 additions & 2 deletions op-batcher/batcher/espresso_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func (l *BatchSubmitter) hasBatchAuthenticator() bool {
// 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
// (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:
//
Expand Down
22 changes: 9 additions & 13 deletions op-batcher/batcher/espresso_driver.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
package batcher

import (
"context"
"errors"
"fmt"

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

// 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.
// 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() {
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)
}
}
l.authGroup.Wait()
}

// dispatchAuthenticatedSendTx routes sendTx through the fallback-batcher
Expand All @@ -27,8 +24,7 @@ func (l *BatchSubmitter) waitForAuthGroup() {
// 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.
// 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
Expand All @@ -39,7 +35,7 @@ func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel boo
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()},
ID: newTxRef(txdata, isCancel),
Err: fmt.Errorf("failed to evaluate fallback-auth gate: %w", err),
}
return true
Expand Down
30 changes: 16 additions & 14 deletions op-batcher/batcher/fallback_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,35 @@ 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/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)).
// 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 crypto.Keccak256Hash(candidate.TxData), nil
return derive.ComputeCalldataBatchHash(candidate.TxData), nil
}

concatenatedBlobHashes := make([]byte, 0)
for _, blob := range candidate.Blobs {
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)
}
blobHash := eth.KZGToVersionedHash(blobCommitment)
concatenatedBlobHashes = append(concatenatedBlobHashes, blobHash.Bytes()...)
blobHashes[i] = eth.KZGToVersionedHash(blobCommitment)
}
return crypto.Keccak256Hash(concatenatedBlobHashes), nil
return derive.ComputeBlobBatchHash(blobHashes), nil
}

// sendTxWithFallbackAuth authenticates a batch transaction via the BatchAuthenticator contract
Expand All @@ -41,7 +42,7 @@ func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) {
// 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]) {
transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}
transactionReference := newTxRef(txdata, isCancel)
l.Log.Debug("Sending fallback-authenticated L1 transaction", "txRef", transactionReference)

commitment, err := computeCommitment(candidate)
Expand Down Expand Up @@ -96,10 +97,11 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca
queue.Send(transactionReference, verifyCandidate, authReceiptCh)
queue.Send(transactionReference, *candidate, batchReceiptCh)

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

// watchFallbackAuthReceipts collects the auth and batch receipts for a fallback-auth pair,
Expand Down
48 changes: 42 additions & 6 deletions op-batcher/batcher/fallback_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)
Expand Down Expand Up @@ -79,7 +80,7 @@ func TestFallbackAuth_OrderingAndSuccess(t *testing.T) {
receiptsCh := make(chan txmgr.TxReceipt[txRef], 1)

l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh)
require.NoError(t, l.authGroup.Wait())
l.authGroup.Wait()

require.Len(t, queue.sends, 2)
// First send must target the BatchAuthenticator (the auth tx), giving it the
Expand Down Expand Up @@ -109,7 +110,7 @@ func TestFallbackAuth_AuthFailureRetried(t *testing.T) {
receiptsCh := make(chan txmgr.TxReceipt[txRef], 1)

l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh)
require.NoError(t, l.authGroup.Wait())
l.authGroup.Wait()

got := <-receiptsCh
require.Error(t, got.Err)
Expand All @@ -130,7 +131,7 @@ func TestFallbackAuth_BatchFailureRetried(t *testing.T) {
receiptsCh := make(chan txmgr.TxReceipt[txRef], 1)

l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh)
require.NoError(t, l.authGroup.Wait())
l.authGroup.Wait()

got := <-receiptsCh
require.Error(t, got.Err)
Expand All @@ -154,7 +155,7 @@ func TestFallbackAuth_AuthRevertedRetried(t *testing.T) {
receiptsCh := make(chan txmgr.TxReceipt[txRef], 1)

l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh)
require.NoError(t, l.authGroup.Wait())
l.authGroup.Wait()

got := <-receiptsCh
require.Error(t, got.Err)
Expand All @@ -179,7 +180,7 @@ func TestFallbackAuth_WindowViolationRetried(t *testing.T) {
receiptsCh := make(chan txmgr.TxReceipt[txRef], 1)

l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh)
require.NoError(t, l.authGroup.Wait())
l.authGroup.Wait()

got := <-receiptsCh
require.Error(t, got.Err)
Expand All @@ -206,10 +207,45 @@ func TestFallbackAuth_WindowBoundaryAccepted(t *testing.T) {
receiptsCh := make(chan txmgr.TxReceipt[txRef], 1)

l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh)
require.NoError(t, l.authGroup.Wait())
l.authGroup.Wait()

got := <-receiptsCh
require.NoError(t, got.Err)
require.Equal(t, receiptWithBlock(boundary).BlockNumber, got.Receipt.BlockNumber)
require.Equal(t, txdata.ID().String(), got.ID.id.String())
}

// TestComputeCommitment_Parity locks the batcher's batch-commitment computation to
// the verifier's. The batcher must hash exactly what op-node derivation hashes, or
// post-fork batches fail the commitment match and are silently dropped. It checks
// both paths against derive.ComputeCalldataBatchHash / derive.ComputeBlobBatchHash;
// for blobs it uses the real versioned hashes the tx will carry (via MakeSidecar,
// the same hashes the verifier reads from tx.BlobHashes()).
func TestComputeCommitment_Parity(t *testing.T) {
t.Run("calldata", func(t *testing.T) {
for _, data := range [][]byte{[]byte("batch calldata payload"), {}, nil} {
got, err := computeCommitment(&txmgr.TxCandidate{TxData: data})
require.NoError(t, err)
require.Equal(t, derive.ComputeCalldataBatchHash(data), common.Hash(got))
}
})

t.Run("blobs", func(t *testing.T) {
for _, n := range []int{1, 3} {
blobs := make([]*eth.Blob, n)
for i := range blobs {
var blob eth.Blob
// Distinct first byte per blob so the versioned hashes differ and the
// concatenation order is actually exercised.
require.NoError(t, blob.FromData(eth.Data{byte(i), 0xab, 0xcd}))
blobs[i] = &blob
}
_, blobHashes, err := txmgr.MakeSidecar(blobs, false)
require.NoError(t, err)

got, err := computeCommitment(&txmgr.TxCandidate{Blobs: blobs})
require.NoError(t, err)
require.Equal(t, derive.ComputeBlobBatchHash(blobHashes), common.Hash(got))
}
})
}