-
Notifications
You must be signed in to change notification settings - Fork 14
Espresso 3a: Fallback batcher #448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fdfc7e5
25f2234
8b76632
97d4a34
857717f
00d6510
cabd2ee
7ea3c59
5e4bcf8
f84ac86
6b85fa1
a882446
c8db328
8630695
353252f
de02776
0ccf6ec
d9a4585
3b4618a
4821ebe
c67ab4d
d2ed3bc
12048f9
9448a74
05fd400
eeed5c2
308f90f
6fd5401
74deff1
c8bf60a
9b368a0
2c8a2b2
17fa0aa
e97ece8
7d05d6c
9c017c3
931a7dd
beb1b33
231bdbc
195a1df
0787bc5
f37b39e
61b294c
1119193
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package batcher | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
| ) | ||
|
|
||
| // hasBatchAuthenticator returns true if the rollup config has a non-zero | ||
| // BatchAuthenticatorAddress, indicating that the BatchAuthenticator-based | ||
| // authentication path is in use. | ||
| func (l *BatchSubmitter) hasBatchAuthenticator() bool { | ||
| return l.RollupConfig.BatchAuthenticatorAddress != (common.Address{}) | ||
| } | ||
|
|
||
| // isFallbackAuthRequired reports whether the fallback (non-TEE) batcher must | ||
| // route its batch txs through BatchAuthenticator.authenticateBatchInfo before | ||
| // 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 | ||
| // 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) { | ||
| 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) | ||
| return l.RollupConfig.IsEspresso(tip.Time + leadSec), nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package batcher | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "github.com/ethereum-optimism/optimism/op-service/txmgr" | ||
| ) | ||
|
|
||
| // authGroup serializes in-flight fallback-auth submissions so the | ||
| // publishingLoop can drain them on shutdown. Initialized in | ||
| // NewBatchSubmitter and lifted in waitForAuthGroup. The TEE batcher follow-up | ||
| // PR reuses the same group. | ||
| // | ||
| // Bounded to a fixed concurrency limit to cap the number of BatchInbox | ||
| // transactions simultaneously waiting on an authenticateBatchInfo | ||
| // transaction to be confirmed. | ||
| const fallbackAuthGroupLimit = 128 | ||
|
|
||
| // initAuthGroup applies the concurrency limit. Called from NewBatchSubmitter. | ||
| func (l *BatchSubmitter) initAuthGroup() { | ||
| l.authGroup.SetLimit(fallbackAuthGroupLimit) | ||
| } | ||
|
|
||
| // 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. | ||
| 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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 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; calling authenticateBatchInfo pre-fork would also revert against | ||
| // the default activeIsEspresso=true contract state. | ||
| func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) bool { | ||
| if isCancel { | ||
| return false | ||
| } | ||
| if !l.hasBatchAuthenticator() { | ||
| return false | ||
| } | ||
| 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()}, | ||
| Err: fmt.Errorf("failed to evaluate fallback-auth gate: %w", err), | ||
| } | ||
| return true | ||
| } | ||
| if !fallbackAuthRequired { | ||
| return false | ||
| } | ||
| l.authGroup.Go( | ||
| func() error { | ||
| l.sendTxWithFallbackAuth(txdata, isCancel, candidate, queue, receiptsCh) | ||
| return nil | ||
| }, | ||
| ) | ||
| return true | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| package batcher | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "math/big" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common/hexutil" | ||
| "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)). | ||
| func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) { | ||
| if len(candidate.Blobs) == 0 { | ||
| return crypto.Keccak256Hash(candidate.TxData), nil | ||
| } | ||
|
|
||
| concatenatedBlobHashes := make([]byte, 0) | ||
| for _, 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()...) | ||
| } | ||
| return crypto.Keccak256Hash(concatenatedBlobHashes), 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]) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reverted auth tx can be reported as success. |
||
| transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()} | ||
| 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, | ||
| } | ||
|
|
||
| l.Log.Debug( | ||
| "Sending fallback authenticateBatchInfo transaction", | ||
| "txRef", transactionReference, | ||
| "commitment", hexutil.Encode(commitment[:]), | ||
| "address", l.RollupConfig.BatchAuthenticatorAddress.String(), | ||
| ) | ||
| verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate) | ||
| if err != nil { | ||
| l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", err) | ||
| receiptsCh <- txmgr.TxReceipt[txRef]{ | ||
| ID: transactionReference, | ||
| Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", err), | ||
| } | ||
| return | ||
| } | ||
|
|
||
| receipt, err := l.Txmgr.Send(l.killCtx, *candidate) | ||
| if err != nil { | ||
| l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err) | ||
| receiptsCh <- txmgr.TxReceipt[txRef]{ | ||
| ID: transactionReference, | ||
| Err: fmt.Errorf("failed to send batch inbox transaction: %w", err), | ||
| } | ||
| return | ||
| } | ||
|
|
||
| distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.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: receipt, | ||
| Err: nil, | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The normal batch submission path sends every batch tx through
txQueue.Send:op-batcher/batcher/driver.go:1061That queue is created with
MaxPendingTransactions:op-batcher/batcher/driver.go:515and
txmgr.Queue.Sendexplicitly assigns nonces synchronously so transactionsconfirm in the order they are sent. This is important for Holocene, where
frames for a channel must arrive in order.
The fallback-auth path bypasses that queue. Once fallback auth is required,
dispatchAuthenticatedSendTxstarts a goroutine inauthGroup:op-batcher/batcher/espresso_driver.go:65and that goroutine calls
l.Txmgr.Senddirectly for both the auth tx and thebatch inbox tx:
op-batcher/batcher/fallback_auth.go:85op-batcher/batcher/fallback_auth.go:95Txmgr.Sendis concurrency-safe, but it only preserves the order in whichcallers actually reach nonce assignment. With up to
fallbackAuthGroupLimit = 128goroutines racing, that order is no longer the publishing loop’s frameorder. As a result, batch inbox txs from the same channel can receive nonces
in a different order than the channel manager emitted them, and L1 inclusion
order follows those nonces.
That can violate Holocene strict frame ordering and cause derivation to drop
later/non-contiguous frames.
This is also a regression from the default config, where
max-pending-txdefaults to
1; operators who configured one-at- a-time submission no longerget that behavior for fallback-authenticated batches.
The fix should preserve the original batch order across the whole auth+inbox
pair. Simply queueing inbox txs after concurrent auth confirmation is not
sufficient, because auth confirmations can complete out of order. The
fallback-auth path should either be serialized, or use an ordered mechanism
that keeps the original frame order while still ensuring each inbox tx is
posted only after its matching auth tx succeeds.