Skip to content

Commit 2a2a89f

Browse files
committed
rollup: test-friendliness flags for shadow-fork submit-only mode
- l2_config.disable_l2_watcher: skip the L2 missing-blocks fetch loop. In shadow mode the DB is populated by import/poll-sync; an empty l2_block table previously made the watcher crawl from genesis (shadow Trap 26). Default false keeps production behavior. - sender_config.chain_nonce_only: initialize the sender nonce from the chain pending nonce only, ignoring pending_transaction rows, which are not chain/fork-scoped and poisoned nonces on fresh forks (shadow Traps 7/27). Default false keeps max(db+1, chain) behavior. - estimategas: set an explicit 30M gas cap in the estimation CallMsg instead of 0; Anvil rejects fee-capped eth_estimateGas with Gas=0 (shadow Trap 9). The cap only bounds the binary search, estimates are unchanged. - l2_relayer: move full-calldata dumps on commit/finalize send errors from Error to Debug (was flooding logs every retry tick), and log a loud Error when a bundle is marked RollupFinalizeFailed — status 7 is never retried by ProcessPendingBundles and needs manual reset.
1 parent 6ecc982 commit 2a2a89f

6 files changed

Lines changed: 61 additions & 16 deletions

File tree

rollup/cmd/rollup_relayer/app/app.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,19 @@ func action(ctx *cli.Context) error {
148148
}
149149

150150
// Watcher loop to fetch missing blocks
151-
go utils.LoopWithContext(subCtx, 2*time.Second, func(ctx context.Context) {
152-
number, loopErr := rutils.GetLatestConfirmedBlockNumber(ctx, l2ethClient, cfg.L2Config.Confirmations)
153-
if loopErr != nil {
154-
log.Error("failed to get block number", "err", loopErr)
155-
return
156-
}
157-
// errors are logged in the try method as well
158-
_ = l2watcher.TryFetchRunningMissingBlocks(number)
159-
})
151+
if cfg.L2Config.DisableL2Watcher {
152+
log.Info("L2 watcher is disabled (disable_l2_watcher=true), skipping missing-blocks fetch loop")
153+
} else {
154+
go utils.LoopWithContext(subCtx, 2*time.Second, func(ctx context.Context) {
155+
number, loopErr := rutils.GetLatestConfirmedBlockNumber(ctx, l2ethClient, cfg.L2Config.Confirmations)
156+
if loopErr != nil {
157+
log.Error("failed to get block number", "err", loopErr)
158+
return
159+
}
160+
// errors are logged in the try method as well
161+
_ = l2watcher.TryFetchRunningMissingBlocks(number)
162+
})
163+
}
160164

161165
go utils.Loop(subCtx, time.Duration(cfg.L2Config.ChunkProposerConfig.ProposeIntervalMilliseconds)*time.Millisecond, chunkProposer.TryProposeChunk)
162166

rollup/internal/config/l2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ type L2Config struct {
1616
L2MessageQueueAddress common.Address `json:"l2_message_queue_address"`
1717
// The WithdrawTrieRootSlot in L2MessageQueue contract.
1818
WithdrawTrieRootSlot common.Hash `json:"withdraw_trie_root_slot,omitempty"`
19+
// DisableL2Watcher disables the L2 watcher loop that fetches missing blocks.
20+
// Useful for shadow-fork testing where the l2_block table is imported/empty and a
21+
// genesis crawl is undesirable. Defaults to false (watcher enabled).
22+
DisableL2Watcher bool `json:"disable_l2_watcher"`
1923
// The relayer config
2024
RelayerConfig *RelayerConfig `json:"relayer_config"`
2125
// The chunk_proposer config

rollup/internal/config/relayer.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ type SenderConfig struct {
3737
MaxPendingBlobTxs int64 `json:"max_pending_blob_txs"`
3838
// The timestamp of the Ethereum Fusaka upgrade in seconds since epoch.
3939
FusakaTimestamp uint64 `json:"fusaka_timestamp"`
40+
// ChainNonceOnly initializes the sender nonce from the chain pending nonce only,
41+
// ignoring stale pending_transaction rows in the database.
42+
// Useful for shadow-fork testing against a DB imported from production.
43+
// Defaults to false (nonce = max(db nonce + 1, chain pending nonce)).
44+
ChainNonceOnly bool `json:"chain_nonce_only"`
4045
}
4146

4247
type BatchSubmission struct {

rollup/internal/controller/relayer/l2_relayer.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,11 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
536536
"end hash", lastBatch.Hash,
537537
"RollupContractAddress", r.cfg.RollupContractAddress,
538538
"err", err,
539+
)
540+
log.Debug(
541+
"Failed to send commitBatch tx to layer1, calldata dump",
542+
"start index", firstBatch.Index,
543+
"end index", lastBatch.Index,
539544
"calldata", common.Bytes2Hex(calldata),
540545
)
541546
return
@@ -768,7 +773,8 @@ func (r *Layer2Relayer) finalizeBundle(bundle *orm.Bundle, withProof bool) error
768773
if err != nil {
769774
log.Error("finalizeBundle in layer1 failed", "with proof", withProof, "index", bundle.Index,
770775
"start batch index", bundle.StartBatchIndex, "end batch index", bundle.EndBatchIndex,
771-
"RollupContractAddress", r.cfg.RollupContractAddress, "err", err, "calldata", common.Bytes2Hex(calldata))
776+
"RollupContractAddress", r.cfg.RollupContractAddress, "err", err)
777+
log.Debug("finalizeBundle in layer1 failed, calldata dump", "index", bundle.Index, "calldata", common.Bytes2Hex(calldata))
772778
return err
773779
}
774780

@@ -906,6 +912,16 @@ func (r *Layer2Relayer) handleConfirmation(cfm *sender.Confirmation) {
906912
status = types.RollupFinalizeFailed
907913
r.metrics.rollupL2BundlesFinalizedConfirmedFailedTotal.Inc()
908914
log.Warn("FinalizeBundleTxType transaction confirmed but failed in layer1", "confirmation", cfm)
915+
// Status RollupFinalizeFailed (7) is NOT picked up again by ProcessPendingBundles
916+
// (GetFirstPendingBundle only queries rollup_status = RollupPending); the bundle is
917+
// stranded until rollup_status is manually reset to 1.
918+
bundleIndex := uint64(0)
919+
bundles, queryErr := r.bundleOrm.GetBundles(r.ctx, map[string]interface{}{"hash": bundleHash}, nil, 1)
920+
if queryErr == nil && len(bundles) > 0 {
921+
bundleIndex = bundles[0].Index
922+
}
923+
log.Error("Bundle is now STRANDED with rollup_status=RollupFinalizeFailed(7): it will NOT be retried by ProcessPendingBundles, manual intervention required (reset rollup_status to 1)",
924+
"bundle index", bundleIndex, "bundle hash", bundleHash, "tx hash", cfm.TxHash.String(), "query err", queryErr)
909925
}
910926

911927
err := r.db.Transaction(func(dbTX *gorm.DB) error {

rollup/internal/controller/sender/estimategas.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,19 @@ func (s *Sender) estimateBlobGas(to *common.Address, data []byte, sidecar *types
101101
return feeData, nil
102102
}
103103

104+
const (
105+
// estimateGasCap is an explicit non-zero gas limit for the eth_estimateGas CallMsg.
106+
// Some nodes (e.g. Anvil) reject estimation requests that carry fee caps but leave
107+
// Gas at the go-ethereum default of 0. go-ethereum's EstimateGas only uses this as
108+
// the upper bound of its binary search, so a generous cap does not affect the result.
109+
estimateGasCap = 30_000_000
110+
)
111+
104112
func (s *Sender) estimateGasLimit(to *common.Address, data []byte, sidecar *types.BlobTxSidecar, gasPrice, gasTipCap, gasFeeCap, blobGasFeeCap *big.Int) (uint64, *types.AccessList, error) {
105113
msg := ethereum.CallMsg{
106114
From: s.transactionSigner.GetAddr(),
107115
To: to,
116+
Gas: estimateGasCap,
108117
GasPrice: gasPrice,
109118
GasTipCap: gasTipCap,
110119
GasFeeCap: gasFeeCap,

rollup/internal/controller/sender/sender.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -450,19 +450,26 @@ func (s *Sender) createTx(feeData *FeeData, target *common.Address, data []byte,
450450
}
451451

452452
// initializeNonce initializes the nonce by taking the maximum of database nonce and pending nonce.
453+
// When ChainNonceOnly is enabled, the database is ignored and the chain pending nonce is used
454+
// directly (useful for shadow-fork testing with stale pending_transaction rows).
453455
func (s *Sender) initializeNonce() (uint64, error) {
454-
// Get maximum nonce from database
455-
dbNonce, err := s.pendingTransactionOrm.GetMaxNonceBySenderAddress(s.ctx, s.transactionSigner.GetAddr().Hex())
456-
if err != nil {
457-
return 0, fmt.Errorf("failed to get max nonce from database for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err)
458-
}
459-
460456
// Get pending nonce from the client
461457
pendingNonce, err := s.client.PendingNonceAt(s.ctx, s.transactionSigner.GetAddr())
462458
if err != nil {
463459
return 0, fmt.Errorf("failed to get pending nonce for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err)
464460
}
465461

462+
if s.config.ChainNonceOnly {
463+
log.Info("nonce initialization (chain_nonce_only mode, ignoring database pending transactions)", "address", s.transactionSigner.GetAddr().Hex(), "pendingNonce", pendingNonce, "finalNonce", pendingNonce)
464+
return pendingNonce, nil
465+
}
466+
467+
// Get maximum nonce from database
468+
dbNonce, err := s.pendingTransactionOrm.GetMaxNonceBySenderAddress(s.ctx, s.transactionSigner.GetAddr().Hex())
469+
if err != nil {
470+
return 0, fmt.Errorf("failed to get max nonce from database for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err)
471+
}
472+
466473
// Take the maximum of pending nonce and (db nonce + 1)
467474
// Database stores the used nonce, so the next available nonce should be dbNonce + 1
468475
// When dbNonce is -1 (no records), dbNonce + 1 = 0, which is correct

0 commit comments

Comments
 (0)