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
6 changes: 5 additions & 1 deletion op-batcher/batcher/fallback_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca
)

if len(candidate.Blobs) > 0 {
// SendPair doesn't support blobs.
// SendPair doesn't support blobs. Unreachable while calldata-only DA is
// enforced: checkEspressoDataAvailability refuses to start a blob- or
// auto-configured batcher on a chain with EspressoTime set.
l.sendFallbackAuthSerialized(transactionReference, authReference, verifyCandidate, candidate, queue, receiptsCh)
return
}
Expand All @@ -119,6 +121,8 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca
// 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.
// Its only caller is the blob branch of sendTxWithFallbackAuth, so it cannot run while
// calldata-only DA is enforced.
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)
Expand Down
6 changes: 5 additions & 1 deletion op-batcher/batcher/fallback_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ func testFallbackTxData() txData {
}

// testBlobCandidate returns a tx candidate carrying one (zero) blob, which
// routes sendTxWithFallbackAuth onto the serialized blob path.
// routes sendTxWithFallbackAuth onto the serialized blob path. That path is
// unreachable in production while calldata-only DA is enforced; its tests are kept
// so it stays covered if the restriction is lifted.
func testBlobCandidate() *txmgr.TxCandidate {
return &txmgr.TxCandidate{Blobs: []*eth.Blob{{}}}
}
Expand Down Expand Up @@ -410,6 +412,8 @@ func TestComputeCommitment_Parity(t *testing.T) {
}
})

// Same caveat as testBlobCandidate: no live path computes a blob commitment while
// calldata-only DA is enforced.
t.Run("blobs", func(t *testing.T) {
for _, n := range []int{1, 3} {
blobs := make([]*eth.Blob, n)
Expand Down
39 changes: 36 additions & 3 deletions op-batcher/batcher/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, closeApp contex
if err := bs.initRollupConfig(ctx); err != nil {
return fmt.Errorf("failed to load rollup config: %w", err)
}
if err := bs.checkEspressoDataAvailability(cfg); err != nil {
return err
}
if err := bs.checkFallbackAuthConfirmations(cfg); err != nil {
return err
}
Expand Down Expand Up @@ -259,19 +262,49 @@ func (bs *BatcherService) initRollupConfig(ctx context.Context) error {
return nil
}

// checkEspressoDataAvailability enforces the calldata-only DA restriction of the
// Espresso integration: from Espresso activation the derivation
// pipeline drops blob batch transactions because the Celo fault-proof host cannot
// retrieve blob contents. A blob- or auto-configured batcher would have every blob
// batch silently ignored by verifiers once the fork activates: the safe head stalls
// for one sequence window, then verifiers force empty batches and reorg away the
// unsafe chain, discarding the transactions in it. Refuse to start instead.
//
// Deliberately broader than derivation's gate, which drops blobs only once Espresso
// is active: a proof walks back channel_timeout L1 blocks into pre-fork territory, and
// this check is the only thing keeping blob batches out of that window. Do not narrow
// it to IsEspresso.
func (bs *BatcherService) checkEspressoDataAvailability(cfg *CLIConfig) error {
if bs.RollupConfig.EspressoTime == nil {
return nil
}
if cfg.DataAvailabilityType != flags.CalldataType {
return fmt.Errorf("data availability type %q is not supported on chains with Espresso scheduled: "+
"batch data must be posted as calldata only (blob DA is dropped by post-Espresso derivation)",
cfg.DataAvailabilityType)
}
return nil
}

// checkFallbackAuthConfirmations validates that the configured number of L1
// confirmations leaves enough headroom inside BatchAuthLookbackWindow for the
// batch tx to land after its auth tx (see sendTxWithFallbackAuth). The bound
// only applies when the BatchAuthenticator is configured on the chain, which
// is only known once the rollup config is loaded
// is only known once the rollup config is loaded.
//
// While calldata-only DA is enforced this cannot return an error: it runs after
// checkEspressoDataAvailability, which rejects the one configuration the bound
// applies to, a scheduled EspressoTime with a non-calldata DA type. It is kept as a
// second line of defence and applies again if the restriction is ever lifted.
func (bs *BatcherService) checkFallbackAuthConfirmations(cfg *CLIConfig) error {
if bs.RollupConfig.BatchAuthenticatorAddress == (common.Address{}) {
return nil
}
// Fallback auth is gated behind the EspressoTime hardfork
// (dispatchAuthenticatedSendTx): with no activation scheduled no
// auth→batch pair can be emitted, so the bound does not apply. A future
// activation must still be checked — it switches the send path mid-run.
// auth→batch pair can be emitted, so the bound does not apply. A scheduled
// activation counts the same as an active one, since it switches the send
// path mid-run.
if bs.RollupConfig.EspressoTime == nil {
return nil
}
Expand Down
47 changes: 47 additions & 0 deletions op-batcher/batcher/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,56 @@ import (
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)

// TestCheckEspressoDataAvailability: chains with Espresso scheduled are
// calldata-only — post-Espresso derivation drops blob batches, so a
// blob/auto DA configuration must be rejected at startup.
func TestCheckEspressoDataAvailability(t *testing.T) {
espressoTime := uint64(0)
// Far enough out that the fork cannot have activated. The check keys on EspressoTime
// being set rather than on activation, deliberately: derivation only drops blobs from
// activation, so this is the only thing keeping blobs out of the pre-fork blocks a
// post-Espresso proof walks back through.
futureEspressoTime := uint64(1) << 40

tests := []struct {
name string
espressoTime *uint64
daType flags.DataAvailabilityType
wantErr bool
}{
{"espresso not scheduled: blobs allowed", nil, flags.BlobsType, false},
{"espresso not scheduled: auto allowed", nil, flags.AutoType, false},
{"espresso scheduled: calldata allowed", &espressoTime, flags.CalldataType, false},
{"espresso scheduled: blobs rejected", &espressoTime, flags.BlobsType, true},
{"espresso scheduled: auto rejected", &espressoTime, flags.AutoType, true},
{"espresso scheduled but not active: calldata allowed", &futureEspressoTime, flags.CalldataType, false},
{"espresso scheduled but not active: blobs rejected", &futureEspressoTime, flags.BlobsType, true},
{"espresso scheduled but not active: auto rejected", &futureEspressoTime, flags.AutoType, true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
bs := &BatcherService{RollupConfig: &rollup.Config{
EspressoTime: test.espressoTime,
}}
cfg := &CLIConfig{DataAvailabilityType: test.daType}
err := bs.checkEspressoDataAvailability(cfg)
if test.wantErr {
require.ErrorContains(t, err, "calldata only")
} else {
require.NoError(t, err)
}
})
}
}

// TestCheckFallbackAuthConfirmations: the NumConfirmations headroom bound only
// applies when a blob/auto DA batcher can actually emit auth→batch pairs — a
// BatchAuthenticator is configured AND the EspressoTime fork is scheduled.
//
// Cases pairing a set espressoTime with a non-calldata DA type describe batchers
// checkEspressoDataAvailability refuses to start. They only occur if the calldata-only
// restriction is lifted, and are kept so the bound stays covered if it is.
func TestCheckFallbackAuthConfirmations(t *testing.T) {
espressoTime := uint64(0)
authAddr := common.Address{0x01}
Expand Down
12 changes: 8 additions & 4 deletions op-node/rollup/derive/batch_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,18 @@ func NewBatchAuthCaches() *BatchAuthCaches {
return &BatchAuthCaches{AuthCache: authCache, RefCache: refCache}
}

// ComputeCalldataBatchHash computes keccak256(calldata), matching the BatchAuthenticator
// contract's calldata batch validation path.
// ComputeCalldataBatchHash computes keccak256(calldata), the commitment a calldata batch
// is authenticated under. BatchAuthenticator.authenticateBatchInfo takes the commitment as
// an opaque bytes32 and never inspects how it was derived, so this encoding is agreed
// off-chain between the batcher and derivation.
func ComputeCalldataBatchHash(data []byte) common.Hash {
return crypto.Keccak256Hash(data)
}

// ComputeBlobBatchHash computes keccak256(concat(blobHashes)), matching the BatchAuthenticator
// contract's blob batch validation path.
// ComputeBlobBatchHash computes keccak256(concat(blobHashes)), the same commitment for a
// blob batch, agreed off-chain as above. No live path consumes it while calldata-only DA
// is enforced: derivation drops blob batches before hashing them, and the batcher cannot
// start with blob DA once espresso_time is set.
func ComputeBlobBatchHash(blobHashes []common.Hash) common.Hash {
concatenated := make([]byte, 32*len(blobHashes))
for i, h := range blobHashes {
Expand Down
50 changes: 39 additions & 11 deletions op-node/rollup/derive/blob_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,32 @@ func (ds *BlobDataSource) open(ctx context.Context) ([]blobOrCalldata, error) {
// creates a placeholder blobOrCalldata element for each returned blob hash that must be populated
// by fillBlobPointers after blob bodies are retrieved.
//
// Before Espresso event-auth is enforced (Espresso inactive at the L1 origin time of
// `ref`, or within BatchAuthEnforcementDelaySecs of activation), this runs upstream
// Optimism semantics: filter by batch inbox + sender == batcher.
// Every transaction is filtered by the batch inbox address first. Two further rules then
// apply, both keyed on the L1 origin time of `ref`.
//
// Once enforced, it collects all authenticated batch hashes from a lookback
// window once and rejects any batch whose commitment hash is not in the
// authenticated set. For blob transactions, the batch hash is computed from
// the concatenated blob versioned hashes.
// From Espresso activation onward (including the enforcement grace window), batch
// data is calldata-only: blob-carrying inbox transactions are dropped entirely,
// authenticated or not. The Celo fault-proof host (celo-kona) does not implement
// the L1Blob preimage hint, so a blob batch accepted here would stall fault-proof
// execution at its L1 block.
//
// That only covers L1 blocks at or after espresso_time. A proof also walks back
// channel_timeout blocks into pre-fork territory; those are kept blob-free by the
// batcher (checkEspressoDataAvailability), not by consensus.
//
// The transactions that survive that rule are authorized by upstream Optimism semantics
// (sender == batcher) until Espresso event-auth is enforced, which happens once Espresso
// has been active for BatchAuthEnforcementDelaySecs. Once enforced, it collects all
// authenticated batch hashes from a lookback window once and rejects any batch whose
// commitment hash is not in the authenticated set.
func dataAndHashesFromTxs(ctx context.Context, txs types.Transactions, config *DataSourceConfig, batcherAddr common.Address, fetcher L1Fetcher, ref eth.L1BlockRef, logger log.Logger) ([]blobOrCalldata, []common.Hash, error) {
// Espresso activation and event-auth enforcement are both properties of the L1 origin
// time of the block we're scanning, so they hold for every transaction in it.
espressoActive := config.rollupCfg.IsEspresso(ref.Time)

// Only collect authenticated batch commitments once event-based authentication is
// enforced at the L1 origin time of the block we're scanning (Espresso active plus
// the enforcement grace period). Before that, the upstream sender-based
// authorization path is used and authenticatedHashes is unused.
// enforced (Espresso active plus the enforcement grace period). Before that, the
// upstream sender-based authorization path is used and authenticatedHashes is unused.
var authenticatedHashes map[common.Hash]common.Address
if isEspressoAuthEnforced(config.rollupCfg, ref.Time) {
var err error
Expand All @@ -151,7 +164,21 @@ func dataAndHashesFromTxs(ctx context.Context, txs types.Transactions, config *D
continue
}

// Compute batch hash depending on tx type
// Post-Espresso, blob DA is unsupported (calldata-only): drop blob
// batch transactions before any authorization check so derivation never
// requires blob preimages the Celo fault-proof host cannot supply.
if tx.Type() == types.BlobTxType && espressoActive {
logger.Warn("ignoring blob batch tx: blob DA is unsupported post-Espresso",
"txHash", tx.Hash())
continue
}

// Compute batch hash depending on tx type. The blob arm computes a value nothing
// reads: a blob tx only gets past the drop above pre-Espresso, and pre-Espresso
// isBatchTxAuthorized takes the sender-based path, which ignores batchHash. Keep it
// anyway. Folding it into the calldata arm would hash a blob tx over its
// usually-empty calldata, so if the drop above were ever narrowed, every blob batch
// would fail authentication for a reason the logs would not explain.
var batchHash common.Hash
if tx.Type() == types.BlobTxType {
batchHash = ComputeBlobBatchHash(tx.BlobHashes())
Expand All @@ -171,6 +198,7 @@ func dataAndHashesFromTxs(ctx context.Context, txs types.Transactions, config *D
continue
}
// handle blob batcher transactions by extracting their blob hashes, ignoring any calldata.
// Pre-Espresso only, for the reason given at the batch hash above.
if len(tx.Data()) > 0 {
log.Warn("blob tx has calldata, which will be ignored", "txhash", tx.Hash())
}
Expand Down
Loading
Loading