diff --git a/go/batch-submitter/batch_submitter.go b/go/batch-submitter/batch_submitter.go index 4731a98151e6a..1c03e4f1e77f1 100644 --- a/go/batch-submitter/batch_submitter.go +++ b/go/batch-submitter/batch_submitter.go @@ -4,7 +4,6 @@ import ( "context" "crypto/ecdsa" "fmt" - "math/big" "net/http" "os" "strconv" @@ -13,6 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/go/batch-submitter/drivers/proposer" "github.com/ethereum-optimism/optimism/go/batch-submitter/drivers/sequencer" "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" + "github.com/ethereum-optimism/optimism/go/batch-submitter/utils" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -159,9 +159,9 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) { } txManagerConfig := txmgr.Config{ - MinGasPrice: gasPriceFromGwei(1), - MaxGasPrice: gasPriceFromGwei(cfg.MaxGasPriceInGwei), - GasRetryIncrement: gasPriceFromGwei(cfg.GasRetryIncrement), + MinGasPrice: utils.GasPriceFromGwei(1), + MaxGasPrice: utils.GasPriceFromGwei(cfg.MaxGasPriceInGwei), + GasRetryIncrement: utils.GasPriceFromGwei(cfg.GasRetryIncrement), ResubmissionTimeout: cfg.ResubmissionTimeout, ReceiptQueryInterval: time.Second, } @@ -186,6 +186,7 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) { Context: ctx, Driver: batchTxDriver, PollInterval: cfg.PollInterval, + ClearPendingTx: cfg.ClearPendingTxs, L1Client: l1Client, TxManagerConfig: txManagerConfig, }) @@ -212,6 +213,7 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) { Context: ctx, Driver: batchStateDriver, PollInterval: cfg.PollInterval, + ClearPendingTx: cfg.ClearPendingTxs, L1Client: l1Client, TxManagerConfig: txManagerConfig, }) @@ -333,7 +335,3 @@ func traceRateToFloat64(rate time.Duration) float64 { } return rate64 } - -func gasPriceFromGwei(gasPriceInGwei uint64) *big.Int { - return new(big.Int).SetUint64(gasPriceInGwei * 1e9) -} diff --git a/go/batch-submitter/drivers/clear_pending_tx.go b/go/batch-submitter/drivers/clear_pending_tx.go new file mode 100644 index 0000000000000..b6cf18d0290ec --- /dev/null +++ b/go/batch-submitter/drivers/clear_pending_tx.go @@ -0,0 +1,173 @@ +package drivers + +import ( + "context" + "crypto/ecdsa" + "errors" + "math/big" + "strings" + + "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// ErrClearPendingRetry signals that a transaction from a previous running +// instance confirmed rather than our clearing transaction on startup. In this +// case the caller should retry. +var ErrClearPendingRetry = errors.New("retry clear pending txn") + +// ClearPendingTx publishes a NOOP transaction at the wallet's next unused +// nonce. This is used on restarts in order to clear the mempool of any prior +// publications and ensure the batch submitter starts submitting from a clean +// slate. +func ClearPendingTx( + name string, + ctx context.Context, + txMgr txmgr.TxManager, + l1Client L1Client, + walletAddr common.Address, + privKey *ecdsa.PrivateKey, + chainID *big.Int, +) error { + + // Query for the submitter's current nonce. + nonce, err := l1Client.NonceAt(ctx, walletAddr, nil) + if err != nil { + log.Error(name+" unable to get current nonce", + "err", err) + return err + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Construct the clearing transaction submission clousure that will attempt + // to send the a clearing transaction transaction at the given nonce and gas + // price. + sendTx := func( + ctx context.Context, + gasPrice *big.Int, + ) (*types.Transaction, error) { + log.Info(name+" clearing pending tx", "nonce", nonce, + "gasPrice", gasPrice) + + signedTx, err := SignClearingTx( + ctx, walletAddr, nonce, gasPrice, l1Client, privKey, chainID, + ) + if err != nil { + log.Error(name+" unable to sign clearing tx", "nonce", nonce, + "gasPrice", gasPrice, "err", err) + return nil, err + } + txHash := signedTx.Hash() + + err = l1Client.SendTransaction(ctx, signedTx) + switch { + + // Clearing transaction successfully confirmed. + case err == nil: + log.Info(name+" submitted clearing tx", "nonce", nonce, + "gasPrice", gasPrice, "txHash", txHash) + + return signedTx, nil + + // Getting a nonce too low error implies that a previous transaction in + // the mempool has confirmed and we should abort trying to publish at + // this nonce. + case strings.Contains(err.Error(), core.ErrNonceTooLow.Error()): + log.Info(name + " transaction from previous restart confirmed, " + + "aborting mempool clearing") + cancel() + return nil, context.Canceled + + // An unexpected error occurred. This also handles the case where the + // clearing transaction has not yet bested the gas price a prior + // transaction in the mempool at this nonce. In such a case we will + // continue until our ratchetting strategy overtakes the old + // transaction, or abort if the old one confirms. + default: + log.Error(name+" unable to submit clearing tx", + "nonce", nonce, "gasPrice", gasPrice, "txHash", txHash, + "err", err) + return nil, err + } + } + + receipt, err := txMgr.Send(ctx, sendTx) + switch { + + // If the current context is canceled, a prior transaction in the mempool + // confirmed. The caller should retry, which will use the next nonce, before + // proceeding. + case err == context.Canceled: + log.Info(name + " transaction from previous restart confirmed, " + + "proceeding to startup") + return ErrClearPendingRetry + + // Otherwise we were unable to confirm our transaction, this method should + // be retried by the caller. + case err != nil: + log.Warn(name+" unable to send clearing tx", "nonce", nonce, + "err", err) + return err + + // We succeeded in confirming a clearing transaction. Proceed to startup as + // normal. + default: + log.Info(name+" cleared pending tx", "nonce", nonce, + "txHash", receipt.TxHash) + return nil + } +} + +// SignClearingTx creates a signed clearing tranaction which sends 0 ETH back to +// the sender's address. EstimateGas is used to set an appropriate gas limit. +func SignClearingTx( + ctx context.Context, + walletAddr common.Address, + nonce uint64, + gasPrice *big.Int, + l1Client L1Client, + privKey *ecdsa.PrivateKey, + chainID *big.Int, +) (*types.Transaction, error) { + + gasLimit, err := l1Client.EstimateGas(ctx, ethereum.CallMsg{ + To: &walletAddr, + GasPrice: gasPrice, + Value: nil, + Data: nil, + }) + if err != nil { + return nil, err + } + + tx := CraftClearingTx(walletAddr, nonce, gasPrice, gasLimit) + + return types.SignTx( + tx, types.LatestSignerForChainID(chainID), privKey, + ) +} + +// CraftClearingTx creates an unsigned clearing transaction which sends 0 ETH +// back to the sender's address. +func CraftClearingTx( + walletAddr common.Address, + nonce uint64, + gasPrice *big.Int, + gasLimit uint64, +) *types.Transaction { + + return types.NewTx(&types.LegacyTx{ + To: &walletAddr, + Nonce: nonce, + GasPrice: gasPrice, + Gas: gasLimit, + Value: nil, + Data: nil, + }) +} diff --git a/go/batch-submitter/drivers/clear_pending_tx_test.go b/go/batch-submitter/drivers/clear_pending_tx_test.go new file mode 100644 index 0000000000000..b2e70fe2ea159 --- /dev/null +++ b/go/batch-submitter/drivers/clear_pending_tx_test.go @@ -0,0 +1,192 @@ +package drivers_test + +import ( + "context" + "crypto/ecdsa" + "errors" + "math/big" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/go/batch-submitter/drivers" + "github.com/ethereum-optimism/optimism/go/batch-submitter/mock" + "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" + "github.com/ethereum-optimism/optimism/go/batch-submitter/utils" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +func init() { + privKey, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + testPrivKey = privKey + testWalletAddr = crypto.PubkeyToAddress(privKey.PublicKey) + testChainID = new(big.Int).SetUint64(1) + testGasPrice = new(big.Int).SetUint64(3) +} + +var ( + testPrivKey *ecdsa.PrivateKey + testWalletAddr common.Address + testChainID *big.Int // 1 + testNonce = uint64(2) + testGasPrice *big.Int // 3 + testGasLimit = uint64(4) +) + +// TestCraftClearingTx asserts that CraftClearingTx produces the expected +// unsigned clearing transaction. +func TestCraftClearingTx(t *testing.T) { + tx := drivers.CraftClearingTx( + testWalletAddr, testNonce, testGasPrice, testGasLimit, + ) + require.Equal(t, &testWalletAddr, tx.To()) + require.Equal(t, testNonce, tx.Nonce()) + require.Equal(t, testGasPrice, tx.GasPrice()) + require.Equal(t, testGasLimit, tx.Gas()) + require.Equal(t, new(big.Int), tx.Value()) + require.Nil(t, tx.Data()) +} + +// TestSignClearingTxSuccess asserts that we will sign a properly formed +// clearing transaction when the call to EstimateGas succeeds. +func TestSignClearingTxEstimateGasSuccess(t *testing.T) { + l1Client := mock.NewL1Client(mock.L1ClientConfig{ + EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) { + return testGasLimit, nil + }, + }) + + tx, err := drivers.SignClearingTx( + context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client, + testPrivKey, testChainID, + ) + require.Nil(t, err) + require.NotNil(t, tx) + require.Equal(t, &testWalletAddr, tx.To()) + require.Equal(t, testNonce, tx.Nonce()) + require.Equal(t, testGasPrice, tx.GasPrice()) + require.Equal(t, testGasLimit, tx.Gas()) + require.Equal(t, new(big.Int), tx.Value()) + require.Nil(t, tx.Data()) + + // Finally, ensure the sender is correct. + sender, err := types.Sender(types.LatestSignerForChainID(testChainID), tx) + require.Nil(t, err) + require.Equal(t, testWalletAddr, sender) +} + +// TestSignClearingTxEstimateGasFail asserts that signing a clearing transaction +// will fail if the underlying call to EstimateGas fails. +func TestSignClearingTxEstimateGasFail(t *testing.T) { + errEstimateGas := errors.New("estimate gas") + + l1Client := mock.NewL1Client(mock.L1ClientConfig{ + EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) { + return 0, errEstimateGas + }, + }) + + tx, err := drivers.SignClearingTx( + context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client, + testPrivKey, testChainID, + ) + require.Equal(t, errEstimateGas, err) + require.Nil(t, tx) +} + +type clearPendingTxHarness struct { + l1Client drivers.L1Client + txMgr txmgr.TxManager +} + +func newClearPendingTxHarness(l1ClientConfig mock.L1ClientConfig) *clearPendingTxHarness { + if l1ClientConfig.NonceAt == nil { + l1ClientConfig.NonceAt = func(_ context.Context, _ common.Address, _ *big.Int) (uint64, error) { + return testNonce, nil + } + } + if l1ClientConfig.EstimateGas == nil { + l1ClientConfig.EstimateGas = func(_ context.Context, _ ethereum.CallMsg) (uint64, error) { + return testGasLimit, nil + } + } + + l1Client := mock.NewL1Client(l1ClientConfig) + txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{ + MinGasPrice: utils.GasPriceFromGwei(1), + MaxGasPrice: utils.GasPriceFromGwei(100), + GasRetryIncrement: utils.GasPriceFromGwei(5), + ResubmissionTimeout: time.Second, + ReceiptQueryInterval: 50 * time.Millisecond, + }, l1Client) + + return &clearPendingTxHarness{ + l1Client: l1Client, + txMgr: txMgr, + } +} + +// TestClearPendingTxClearingTxÇonfirms asserts the happy path where our +// clearing transactions confirms unobstructed. +func TestClearPendingTxClearingTxConfirms(t *testing.T) { + h := newClearPendingTxHarness(mock.L1ClientConfig{ + SendTransaction: func(_ context.Context, _ *types.Transaction) error { + return nil + }, + TransactionReceipt: func(_ context.Context, txHash common.Hash) (*types.Receipt, error) { + return &types.Receipt{ + TxHash: txHash, + }, nil + }, + }) + + err := drivers.ClearPendingTx( + "test", context.Background(), h.txMgr, h.l1Client, testWalletAddr, + testPrivKey, testChainID, + ) + require.Nil(t, err) +} + +// TestClearPendingTx∏reviousTxConfirms asserts that if the mempool starts +// rejecting our transactions because the nonce is too low that ClearPendingTx +// will abort continuing to publish a clearing transaction. +func TestClearPendingTxPreviousTxConfirms(t *testing.T) { + h := newClearPendingTxHarness(mock.L1ClientConfig{ + SendTransaction: func(_ context.Context, _ *types.Transaction) error { + return core.ErrNonceTooLow + }, + }) + + err := drivers.ClearPendingTx( + "test", context.Background(), h.txMgr, h.l1Client, testWalletAddr, + testPrivKey, testChainID, + ) + require.Equal(t, drivers.ErrClearPendingRetry, err) +} + +// TestClearPendingTxTimeout asserts that ClearPendingTx returns an +// ErrPublishTimeout if the clearing transaction fails to confirm in a timely +// manner and no prior transaction confirms. +func TestClearPendingTxTimeout(t *testing.T) { + h := newClearPendingTxHarness(mock.L1ClientConfig{ + SendTransaction: func(_ context.Context, _ *types.Transaction) error { + return nil + }, + TransactionReceipt: func(_ context.Context, txHash common.Hash) (*types.Receipt, error) { + return nil, nil + }, + }) + + err := drivers.ClearPendingTx( + "test", context.Background(), h.txMgr, h.l1Client, testWalletAddr, + testPrivKey, testChainID, + ) + require.Equal(t, txmgr.ErrPublishTimeout, err) +} diff --git a/go/batch-submitter/drivers/interface.go b/go/batch-submitter/drivers/interface.go new file mode 100644 index 0000000000000..99c2f1f55b21f --- /dev/null +++ b/go/batch-submitter/drivers/interface.go @@ -0,0 +1,36 @@ +package drivers + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// L1Client is an abstraction over an L1 Ethereum client functionality required +// by the batch submitter. +type L1Client interface { + // EstimateGas tries to estimate the gas needed to execute a specific + // transaction based on the current pending state of the backend blockchain. + // There is no guarantee that this is the true gas limit requirement as + // other transactions may be added or removed by miners, but it should + // provide a basis for setting a reasonable default. + EstimateGas(context.Context, ethereum.CallMsg) (uint64, error) + + // NonceAt returns the account nonce of the given account. The block number + // can be nil, in which case the nonce is taken from the latest known block. + NonceAt(context.Context, common.Address, *big.Int) (uint64, error) + + // SendTransaction injects a signed transaction into the pending pool for + // execution. + // + // If the transaction was a contract creation use the TransactionReceipt + // method to get the contract address after the transaction has been mined. + SendTransaction(context.Context, *types.Transaction) error + + // TransactionReceipt returns the receipt of a transaction by transaction + // hash. Note that the receipt is not available for pending transactions. + TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error) +} diff --git a/go/batch-submitter/drivers/proposer/driver.go b/go/batch-submitter/drivers/proposer/driver.go index 20c65088a17c5..1c8ab1cddd22a 100644 --- a/go/batch-submitter/drivers/proposer/driver.go +++ b/go/batch-submitter/drivers/proposer/driver.go @@ -9,7 +9,9 @@ import ( "github.com/ethereum-optimism/optimism/go/batch-submitter/bindings/ctc" "github.com/ethereum-optimism/optimism/go/batch-submitter/bindings/scc" + "github.com/ethereum-optimism/optimism/go/batch-submitter/drivers" "github.com/ethereum-optimism/optimism/go/batch-submitter/metrics" + "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient" "github.com/ethereum-optimism/optimism/l2geth/log" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -85,6 +87,21 @@ func (d *Driver) Metrics() *metrics.Metrics { return d.metrics } +// ClearPendingTx a publishes a transaction at the next available nonce in order +// to clear any transactions in the mempool left over from a prior running +// instance of the batch submitter. +func (d *Driver) ClearPendingTx( + ctx context.Context, + txMgr txmgr.TxManager, + l1Client *ethclient.Client, +) error { + + return drivers.ClearPendingTx( + d.cfg.Name, ctx, txMgr, l1Client, d.walletAddr, d.cfg.PrivKey, + d.cfg.ChainID, + ) +} + // GetBatchBlockRange returns the start and end L2 block heights that need to be // processed. Note that the end value is *exclusive*, therefore if the returned // values are identical nothing needs to be processed. diff --git a/go/batch-submitter/drivers/sequencer/driver.go b/go/batch-submitter/drivers/sequencer/driver.go index 16381f2a81c53..a954da9ffbde0 100644 --- a/go/batch-submitter/drivers/sequencer/driver.go +++ b/go/batch-submitter/drivers/sequencer/driver.go @@ -9,7 +9,9 @@ import ( "time" "github.com/ethereum-optimism/optimism/go/batch-submitter/bindings/ctc" + "github.com/ethereum-optimism/optimism/go/batch-submitter/drivers" "github.com/ethereum-optimism/optimism/go/batch-submitter/metrics" + "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -98,6 +100,21 @@ func (d *Driver) Metrics() *metrics.Metrics { return d.metrics } +// ClearPendingTx a publishes a transaction at the next available nonce in order +// to clear any transactions in the mempool left over from a prior running +// instance of the batch submitter. +func (d *Driver) ClearPendingTx( + ctx context.Context, + txMgr txmgr.TxManager, + l1Client *ethclient.Client, +) error { + + return drivers.ClearPendingTx( + d.cfg.Name, ctx, txMgr, l1Client, d.walletAddr, d.cfg.PrivKey, + d.cfg.ChainID, + ) +} + // GetBatchBlockRange returns the start and end L2 block heights that need to be // processed. Note that the end value is *exclusive*, therefore if the returned // values are identical nothing needs to be processed. diff --git a/go/batch-submitter/mock/l1client.go b/go/batch-submitter/mock/l1client.go new file mode 100644 index 0000000000000..c0f46102065b9 --- /dev/null +++ b/go/batch-submitter/mock/l1client.go @@ -0,0 +1,123 @@ +package mock + +import ( + "context" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// L1ClientConfig houses the internal methods that are executed by the mock +// L1Client. Any members left as nil will panic on execution. +type L1ClientConfig struct { + // EstimateGas tries to estimate the gas needed to execute a specific + // transaction based on the current pending state of the backend blockchain. + // There is no guarantee that this is the true gas limit requirement as + // other transactions may be added or removed by miners, but it should + // provide a basis for setting a reasonable default. + EstimateGas func(context.Context, ethereum.CallMsg) (uint64, error) + + // NonceAt returns the account nonce of the given account. The block number + // can be nil, in which case the nonce is taken from the latest known block. + NonceAt func(context.Context, common.Address, *big.Int) (uint64, error) + + // SendTransaction injects a signed transaction into the pending pool for + // execution. + // + // If the transaction was a contract creation use the TransactionReceipt + // method to get the contract address after the transaction has been mined. + SendTransaction func(context.Context, *types.Transaction) error + + // TransactionReceipt returns the receipt of a transaction by transaction + // hash. Note that the receipt is not available for pending transactions. + TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error) +} + +// L1Client represents a mock L1Client. +type L1Client struct { + cfg L1ClientConfig + mu sync.RWMutex +} + +// NewL1Client returns a new L1Client using the mocked methods in the +// L1ClientConfig. +func NewL1Client(cfg L1ClientConfig) *L1Client { + return &L1Client{ + cfg: cfg, + } +} + +// EstimateGas executes the mock EstimateGas method. +func (c *L1Client) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.cfg.EstimateGas(ctx, call) +} + +// NonceAt executes the mock NonceAt method. +func (c *L1Client) NonceAt(ctx context.Context, addr common.Address, blockNumber *big.Int) (uint64, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.cfg.NonceAt(ctx, addr, blockNumber) +} + +// SendTransaction executes the mock SendTransaction method. +func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) error { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.cfg.SendTransaction(ctx, tx) +} + +// TransactionReceipt executes the mock TransactionReceipt method. +func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.cfg.TransactionReceipt(ctx, txHash) +} + +// SetEstimateGasFunc overrwrites the mock EstimateGas method. +func (c *L1Client) SetEstimateGasFunc( + f func(context.Context, ethereum.CallMsg) (uint64, error)) { + + c.mu.Lock() + defer c.mu.Unlock() + + c.cfg.EstimateGas = f +} + +// SetNonceAtFunc overrwrites the mock NonceAt method. +func (c *L1Client) SetNonceAtFunc( + f func(context.Context, common.Address, *big.Int) (uint64, error)) { + + c.mu.Lock() + defer c.mu.Unlock() + + c.cfg.NonceAt = f +} + +// SetSendTransactionFunc overrwrites the mock SendTransaction method. +func (c *L1Client) SetSendTransactionFunc( + f func(context.Context, *types.Transaction) error) { + + c.mu.Lock() + defer c.mu.Unlock() + + c.cfg.SendTransaction = f +} + +// SetTransactionReceiptFunc overwrites the mock TransactionReceipt method. +func (c *L1Client) SetTransactionReceiptFunc( + f func(context.Context, common.Hash) (*types.Receipt, error)) { + + c.mu.Lock() + defer c.mu.Unlock() + + c.cfg.TransactionReceipt = f +} diff --git a/go/batch-submitter/service.go b/go/batch-submitter/service.go index dcb7f4c412b2a..0523612c172f5 100644 --- a/go/batch-submitter/service.go +++ b/go/batch-submitter/service.go @@ -32,6 +32,11 @@ type Driver interface { // Metrics returns the subservice telemetry object. Metrics() *metrics.Metrics + // ClearPendingTx a publishes a transaction at the next available nonce in + // order to clear any transactions in the mempool left over from a prior + // running instance of the batch submitter. + ClearPendingTx(context.Context, txmgr.TxManager, *ethclient.Client) error + // GetBatchBlockRange returns the start and end L2 block heights that // need to be processed. Note that the end value is *exclusive*, // therefore if the returned values are identical nothing needs to be @@ -51,6 +56,7 @@ type ServiceConfig struct { Context context.Context Driver Driver PollInterval time.Duration + ClearPendingTx bool L1Client *ethclient.Client TxManagerConfig txmgr.Config } @@ -99,6 +105,19 @@ func (s *Service) eventLoop() { name := s.cfg.Driver.Name() + if s.cfg.ClearPendingTx { + const maxClearRetries = 3 + for i := 0; i < maxClearRetries; i++ { + err := s.cfg.Driver.ClearPendingTx(s.ctx, s.txMgr, s.cfg.L1Client) + if err == nil { + break + } else if i < maxClearRetries-1 { + continue + } + log.Crit("Unable to confirm a clearing transaction", "err", err) + } + } + for { select { case <-time.After(s.cfg.PollInterval): diff --git a/go/batch-submitter/utils/gas_price.go b/go/batch-submitter/utils/gas_price.go new file mode 100644 index 0000000000000..7a97c572f40e4 --- /dev/null +++ b/go/batch-submitter/utils/gas_price.go @@ -0,0 +1,12 @@ +package utils + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/params" +) + +// GasPriceFromGwei converts an uint64 gas price in gwei to a big.Int in wei. +func GasPriceFromGwei(gasPriceInGwei uint64) *big.Int { + return new(big.Int).SetUint64(gasPriceInGwei * params.GWei) +} diff --git a/go/batch-submitter/utils/gas_price_test.go b/go/batch-submitter/utils/gas_price_test.go new file mode 100644 index 0000000000000..284fdc511ca47 --- /dev/null +++ b/go/batch-submitter/utils/gas_price_test.go @@ -0,0 +1,18 @@ +package utils_test + +import ( + "math/big" + "testing" + + "github.com/ethereum-optimism/optimism/go/batch-submitter/utils" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestGasPriceFromGwei asserts that the integer value is scaled properly by +// 10^9. +func TestGasPriceFromGwei(t *testing.T) { + require.Equal(t, utils.GasPriceFromGwei(0), new(big.Int)) + require.Equal(t, utils.GasPriceFromGwei(1), big.NewInt(params.GWei)) + require.Equal(t, utils.GasPriceFromGwei(100), big.NewInt(100*params.GWei)) +}