Commit dc1ed3c9 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2086 from cfromknecht/bss-eip-1559

feat: modify txmgr to send EIP-1559 txns
parents 36151fe8 3a7e7098
---
'@eth-optimism/batch-submitter-service': patch
---
use EIP-1559 txns for tx/state batches
...@@ -12,7 +12,6 @@ import ( ...@@ -12,7 +12,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/drivers/proposer" "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/drivers/sequencer"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" "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" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
...@@ -159,9 +158,6 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) { ...@@ -159,9 +158,6 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) {
} }
txManagerConfig := txmgr.Config{ txManagerConfig := txmgr.Config{
MinGasPrice: utils.GasPriceFromGwei(1),
MaxGasPrice: utils.GasPriceFromGwei(cfg.MaxGasPriceInGwei),
GasRetryIncrement: utils.GasPriceFromGwei(cfg.GasRetryIncrement),
ResubmissionTimeout: cfg.ResubmissionTimeout, ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second, ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations, NumConfirmations: cfg.NumConfirmations,
......
...@@ -133,14 +133,6 @@ type Config struct { ...@@ -133,14 +133,6 @@ type Config struct {
// blocks. // blocks.
BlockOffset uint64 BlockOffset uint64
// MaxGasPriceInGwei is the maximum gas price in gwei we will allow in order
// to confirm a transaction.
MaxGasPriceInGwei uint64
// GasRetryIncrement is the step size (in gwei) by which we will ratchet the
// gas price in order to get a transaction confirmed.
GasRetryIncrement uint64
// SequencerPrivateKey the private key of the wallet used to submit // SequencerPrivateKey the private key of the wallet used to submit
// transactions to the CTC contract. // transactions to the CTC contract.
SequencerPrivateKey string SequencerPrivateKey string
...@@ -199,8 +191,6 @@ func NewConfig(ctx *cli.Context) (Config, error) { ...@@ -199,8 +191,6 @@ func NewConfig(ctx *cli.Context) (Config, error) {
SentryDsn: ctx.GlobalString(flags.SentryDsnFlag.Name), SentryDsn: ctx.GlobalString(flags.SentryDsnFlag.Name),
SentryTraceRate: ctx.GlobalDuration(flags.SentryTraceRateFlag.Name), SentryTraceRate: ctx.GlobalDuration(flags.SentryTraceRateFlag.Name),
BlockOffset: ctx.GlobalUint64(flags.BlockOffsetFlag.Name), BlockOffset: ctx.GlobalUint64(flags.BlockOffsetFlag.Name),
MaxGasPriceInGwei: ctx.GlobalUint64(flags.MaxGasPriceInGweiFlag.Name),
GasRetryIncrement: ctx.GlobalUint64(flags.GasRetryIncrementFlag.Name),
SequencerPrivateKey: ctx.GlobalString(flags.SequencerPrivateKeyFlag.Name), SequencerPrivateKey: ctx.GlobalString(flags.SequencerPrivateKeyFlag.Name),
ProposerPrivateKey: ctx.GlobalString(flags.ProposerPrivateKeyFlag.Name), ProposerPrivateKey: ctx.GlobalString(flags.ProposerPrivateKeyFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name), Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"strings" "strings"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" "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/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -50,20 +49,20 @@ func ClearPendingTx( ...@@ -50,20 +49,20 @@ func ClearPendingTx(
// price. // price.
sendTx := func( sendTx := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
log.Info(name+" clearing pending tx", "nonce", nonce, log.Info(name+" clearing pending tx", "nonce", nonce)
"gasPrice", gasPrice)
signedTx, err := SignClearingTx( signedTx, err := SignClearingTx(
ctx, walletAddr, nonce, gasPrice, l1Client, privKey, chainID, name, ctx, walletAddr, nonce, l1Client, privKey, chainID,
) )
if err != nil { if err != nil {
log.Error(name+" unable to sign clearing tx", "nonce", nonce, log.Error(name+" unable to sign clearing tx", "nonce", nonce,
"gasPrice", gasPrice, "err", err) "err", err)
return nil, err return nil, err
} }
txHash := signedTx.Hash() txHash := signedTx.Hash()
gasTipCap := signedTx.GasTipCap()
gasFeeCap := signedTx.GasFeeCap()
err = l1Client.SendTransaction(ctx, signedTx) err = l1Client.SendTransaction(ctx, signedTx)
switch { switch {
...@@ -71,7 +70,8 @@ func ClearPendingTx( ...@@ -71,7 +70,8 @@ func ClearPendingTx(
// Clearing transaction successfully confirmed. // Clearing transaction successfully confirmed.
case err == nil: case err == nil:
log.Info(name+" submitted clearing tx", "nonce", nonce, log.Info(name+" submitted clearing tx", "nonce", nonce,
"gasPrice", gasPrice, "txHash", txHash) "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash)
return signedTx, nil return signedTx, nil
...@@ -91,8 +91,8 @@ func ClearPendingTx( ...@@ -91,8 +91,8 @@ func ClearPendingTx(
// transaction, or abort if the old one confirms. // transaction, or abort if the old one confirms.
default: default:
log.Error(name+" unable to submit clearing tx", log.Error(name+" unable to submit clearing tx",
"nonce", nonce, "gasPrice", gasPrice, "txHash", txHash, "nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"err", err) "txHash", txHash, "err", err)
return nil, err return nil, err
} }
} }
...@@ -127,26 +127,39 @@ func ClearPendingTx( ...@@ -127,26 +127,39 @@ func ClearPendingTx(
// SignClearingTx creates a signed clearing tranaction which sends 0 ETH back to // 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. // the sender's address. EstimateGas is used to set an appropriate gas limit.
func SignClearingTx( func SignClearingTx(
name string,
ctx context.Context, ctx context.Context,
walletAddr common.Address, walletAddr common.Address,
nonce uint64, nonce uint64,
gasPrice *big.Int,
l1Client L1Client, l1Client L1Client,
privKey *ecdsa.PrivateKey, privKey *ecdsa.PrivateKey,
chainID *big.Int, chainID *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
gasLimit, err := l1Client.EstimateGas(ctx, ethereum.CallMsg{ gasTipCap, err := l1Client.SuggestGasTipCap(ctx)
To: &walletAddr, if err != nil {
GasPrice: gasPrice, if !IsMaxPriorityFeePerGasNotFoundError(err) {
Value: nil, return nil, err
Data: nil, }
})
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this
// method, so in the event their API is unreachable we can fallback to a
// degraded mode of operation. This also applies to our test
// environments, as hardhat doesn't support the query either.
log.Warn(name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
gasTipCap = FallbackGasTipCap
}
head, err := l1Client.HeaderByNumber(ctx, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tx := CraftClearingTx(walletAddr, nonce, gasPrice, gasLimit) gasFeeCap := txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
tx := CraftClearingTx(walletAddr, nonce, gasFeeCap, gasTipCap)
return types.SignTx( return types.SignTx(
tx, types.LatestSignerForChainID(chainID), privKey, tx, types.LatestSignerForChainID(chainID), privKey,
...@@ -158,16 +171,16 @@ func SignClearingTx( ...@@ -158,16 +171,16 @@ func SignClearingTx(
func CraftClearingTx( func CraftClearingTx(
walletAddr common.Address, walletAddr common.Address,
nonce uint64, nonce uint64,
gasPrice *big.Int, gasFeeCap *big.Int,
gasLimit uint64, gasTipCap *big.Int,
) *types.Transaction { ) *types.Transaction {
return types.NewTx(&types.LegacyTx{ return types.NewTx(&types.DynamicFeeTx{
To: &walletAddr, To: &walletAddr,
Nonce: nonce, Nonce: nonce,
GasPrice: gasPrice, GasFeeCap: gasFeeCap,
Gas: gasLimit, GasTipCap: gasTipCap,
Value: nil, Value: nil,
Data: nil, Data: nil,
}) })
} }
...@@ -11,8 +11,6 @@ import ( ...@@ -11,8 +11,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/drivers" "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/mock"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" "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/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -27,8 +25,6 @@ func init() { ...@@ -27,8 +25,6 @@ func init() {
} }
testPrivKey = privKey testPrivKey = privKey
testWalletAddr = crypto.PubkeyToAddress(privKey.PublicKey) testWalletAddr = crypto.PubkeyToAddress(privKey.PublicKey)
testChainID = new(big.Int).SetUint64(1)
testGasPrice = new(big.Int).SetUint64(3)
} }
var ( var (
...@@ -36,21 +32,22 @@ var ( ...@@ -36,21 +32,22 @@ var (
testWalletAddr common.Address testWalletAddr common.Address
testChainID = big.NewInt(1) testChainID = big.NewInt(1)
testNonce = uint64(2) testNonce = uint64(2)
testGasPrice = big.NewInt(3) testGasFeeCap = big.NewInt(3)
testGasLimit = uint64(4) testGasTipCap = big.NewInt(4)
testBlockNumber = uint64(5) testBlockNumber = uint64(5)
testBaseFee = big.NewInt(6)
) )
// TestCraftClearingTx asserts that CraftClearingTx produces the expected // TestCraftClearingTx asserts that CraftClearingTx produces the expected
// unsigned clearing transaction. // unsigned clearing transaction.
func TestCraftClearingTx(t *testing.T) { func TestCraftClearingTx(t *testing.T) {
tx := drivers.CraftClearingTx( tx := drivers.CraftClearingTx(
testWalletAddr, testNonce, testGasPrice, testGasLimit, testWalletAddr, testNonce, testGasFeeCap, testGasTipCap,
) )
require.Equal(t, &testWalletAddr, tx.To()) require.Equal(t, &testWalletAddr, tx.To())
require.Equal(t, testNonce, tx.Nonce()) require.Equal(t, testNonce, tx.Nonce())
require.Equal(t, testGasPrice, tx.GasPrice()) require.Equal(t, testGasFeeCap, tx.GasFeeCap())
require.Equal(t, testGasLimit, tx.Gas()) require.Equal(t, testGasTipCap, tx.GasTipCap())
require.Equal(t, new(big.Int), tx.Value()) require.Equal(t, new(big.Int), tx.Value())
require.Nil(t, tx.Data()) require.Nil(t, tx.Data())
} }
...@@ -59,21 +56,31 @@ func TestCraftClearingTx(t *testing.T) { ...@@ -59,21 +56,31 @@ func TestCraftClearingTx(t *testing.T) {
// clearing transaction when the call to EstimateGas succeeds. // clearing transaction when the call to EstimateGas succeeds.
func TestSignClearingTxEstimateGasSuccess(t *testing.T) { func TestSignClearingTxEstimateGasSuccess(t *testing.T) {
l1Client := mock.NewL1Client(mock.L1ClientConfig{ l1Client := mock.NewL1Client(mock.L1ClientConfig{
EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) { HeaderByNumber: func(_ context.Context, _ *big.Int) (*types.Header, error) {
return testGasLimit, nil return &types.Header{
BaseFee: testBaseFee,
}, nil
},
SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
return testGasTipCap, nil
}, },
}) })
expGasFeeCap := new(big.Int).Add(
testGasTipCap,
new(big.Int).Mul(testBaseFee, big.NewInt(2)),
)
tx, err := drivers.SignClearingTx( tx, err := drivers.SignClearingTx(
context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client, "TEST", context.Background(), testWalletAddr, testNonce, l1Client,
testPrivKey, testChainID, testPrivKey, testChainID,
) )
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, tx) require.NotNil(t, tx)
require.Equal(t, &testWalletAddr, tx.To()) require.Equal(t, &testWalletAddr, tx.To())
require.Equal(t, testNonce, tx.Nonce()) require.Equal(t, testNonce, tx.Nonce())
require.Equal(t, testGasPrice, tx.GasPrice()) require.Equal(t, expGasFeeCap, tx.GasFeeCap())
require.Equal(t, testGasLimit, tx.Gas()) require.Equal(t, testGasTipCap, tx.GasTipCap())
require.Equal(t, new(big.Int), tx.Value()) require.Equal(t, new(big.Int), tx.Value())
require.Nil(t, tx.Data()) require.Nil(t, tx.Data())
...@@ -83,22 +90,44 @@ func TestSignClearingTxEstimateGasSuccess(t *testing.T) { ...@@ -83,22 +90,44 @@ func TestSignClearingTxEstimateGasSuccess(t *testing.T) {
require.Equal(t, testWalletAddr, sender) require.Equal(t, testWalletAddr, sender)
} }
// TestSignClearingTxEstimateGasFail asserts that signing a clearing transaction // TestSignClearingTxSuggestGasTipCapFail asserts that signing a clearing
// will fail if the underlying call to EstimateGas fails. // transaction will fail if the underlying call to SuggestGasTipCap fails.
func TestSignClearingTxEstimateGasFail(t *testing.T) { func TestSignClearingTxSuggestGasTipCapFail(t *testing.T) {
errEstimateGas := errors.New("estimate gas") errSuggestGasTipCap := errors.New("suggest gas tip cap")
l1Client := mock.NewL1Client(mock.L1ClientConfig{ l1Client := mock.NewL1Client(mock.L1ClientConfig{
EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) { SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
return 0, errEstimateGas return nil, errSuggestGasTipCap
}, },
}) })
tx, err := drivers.SignClearingTx( tx, err := drivers.SignClearingTx(
context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client, "TEST", context.Background(), testWalletAddr, testNonce, l1Client,
testPrivKey, testChainID, testPrivKey, testChainID,
) )
require.Equal(t, errEstimateGas, err) require.Equal(t, errSuggestGasTipCap, err)
require.Nil(t, tx)
}
// TestSignClearingTxHeaderByNumberFail asserts that signing a clearing
// transaction will fail if the underlying call to HeaderByNumber fails.
func TestSignClearingTxHeaderByNumberFail(t *testing.T) {
errHeaderByNumber := errors.New("header by number")
l1Client := mock.NewL1Client(mock.L1ClientConfig{
HeaderByNumber: func(_ context.Context, _ *big.Int) (*types.Header, error) {
return nil, errHeaderByNumber
},
SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
return testGasTipCap, nil
},
})
tx, err := drivers.SignClearingTx(
"TEST", context.Background(), testWalletAddr, testNonce, l1Client,
testPrivKey, testChainID,
)
require.Equal(t, errHeaderByNumber, err)
require.Nil(t, tx) require.Nil(t, tx)
} }
...@@ -117,22 +146,26 @@ func newClearPendingTxHarnessWithNumConfs( ...@@ -117,22 +146,26 @@ func newClearPendingTxHarnessWithNumConfs(
return testBlockNumber, nil return testBlockNumber, nil
} }
} }
if l1ClientConfig.HeaderByNumber == nil {
l1ClientConfig.HeaderByNumber = func(_ context.Context, _ *big.Int) (*types.Header, error) {
return &types.Header{
BaseFee: testBaseFee,
}, nil
}
}
if l1ClientConfig.NonceAt == nil { if l1ClientConfig.NonceAt == nil {
l1ClientConfig.NonceAt = func(_ context.Context, _ common.Address, _ *big.Int) (uint64, error) { l1ClientConfig.NonceAt = func(_ context.Context, _ common.Address, _ *big.Int) (uint64, error) {
return testNonce, nil return testNonce, nil
} }
} }
if l1ClientConfig.EstimateGas == nil { if l1ClientConfig.SuggestGasTipCap == nil {
l1ClientConfig.EstimateGas = func(_ context.Context, _ ethereum.CallMsg) (uint64, error) { l1ClientConfig.SuggestGasTipCap = func(_ context.Context) (*big.Int, error) {
return testGasLimit, nil return testGasTipCap, nil
} }
} }
l1Client := mock.NewL1Client(l1ClientConfig) l1Client := mock.NewL1Client(l1ClientConfig)
txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{ txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{
MinGasPrice: utils.GasPriceFromGwei(1),
MaxGasPrice: utils.GasPriceFromGwei(100),
GasRetryIncrement: utils.GasPriceFromGwei(5),
ResubmissionTimeout: time.Second, ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond, ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations, NumConfirmations: numConfirmations,
...@@ -200,11 +233,14 @@ func TestClearPendingTxTimeout(t *testing.T) { ...@@ -200,11 +233,14 @@ func TestClearPendingTxTimeout(t *testing.T) {
}, },
}) })
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := drivers.ClearPendingTx( err := drivers.ClearPendingTx(
"test", context.Background(), h.txMgr, h.l1Client, testWalletAddr, "test", ctx, h.txMgr, h.l1Client, testWalletAddr, testPrivKey,
testPrivKey, testChainID, testChainID,
) )
require.Equal(t, txmgr.ErrPublishTimeout, err) require.Equal(t, context.DeadlineExceeded, err)
} }
// TestClearPendingTxMultipleConfs tests we wait the appropriate number of // TestClearPendingTxMultipleConfs tests we wait the appropriate number of
...@@ -225,12 +261,15 @@ func TestClearPendingTxMultipleConfs(t *testing.T) { ...@@ -225,12 +261,15 @@ func TestClearPendingTxMultipleConfs(t *testing.T) {
}, },
}, numConfs) }, numConfs)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// The txmgr should timeout waiting for the txn to confirm. // The txmgr should timeout waiting for the txn to confirm.
err := drivers.ClearPendingTx( err := drivers.ClearPendingTx(
"test", context.Background(), h.txMgr, h.l1Client, testWalletAddr, "test", ctx, h.txMgr, h.l1Client, testWalletAddr, testPrivKey,
testPrivKey, testChainID, testChainID,
) )
require.Equal(t, txmgr.ErrPublishTimeout, err) require.Equal(t, context.DeadlineExceeded, err)
// Now set the chain height to the earliest the transaction will be // Now set the chain height to the earliest the transaction will be
// considered sufficiently confirmed. // considered sufficiently confirmed.
......
...@@ -4,7 +4,6 @@ import ( ...@@ -4,7 +4,6 @@ import (
"context" "context"
"math/big" "math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
) )
...@@ -12,12 +11,9 @@ import ( ...@@ -12,12 +11,9 @@ import (
// L1Client is an abstraction over an L1 Ethereum client functionality required // L1Client is an abstraction over an L1 Ethereum client functionality required
// by the batch submitter. // by the batch submitter.
type L1Client interface { type L1Client interface {
// EstimateGas tries to estimate the gas needed to execute a specific // HeaderByNumber returns a block header from the current canonical chain.
// transaction based on the current pending state of the backend blockchain. // If number is nil, the latest known header is returned.
// There is no guarantee that this is the true gas limit requirement as HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
// 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 // 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. // can be nil, in which case the nonce is taken from the latest known block.
...@@ -30,6 +26,10 @@ type L1Client interface { ...@@ -30,6 +26,10 @@ type L1Client interface {
// method to get the contract address after the transaction has been mined. // method to get the contract address after the transaction has been mined.
SendTransaction(context.Context, *types.Transaction) error SendTransaction(context.Context, *types.Transaction) error
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
// to allow a timely execution of a transaction.
SuggestGasTipCap(context.Context) (*big.Int, error)
// TransactionReceipt returns the receipt of a transaction by transaction // TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions. // hash. Note that the receipt is not available for pending transactions.
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error) TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
......
package drivers
import (
"errors"
"math/big"
"strings"
)
var (
errMaxPriorityFeePerGasNotFound = errors.New(
"Method eth_maxPriorityFeePerGas not found",
)
// FallbackGasTipCap is the default fallback gasTipCap used when we are
// unable to query an L1 backend for a suggested gasTipCap.
FallbackGasTipCap = big.NewInt(1500000000)
)
// IsMaxPriorityFeePerGasNotFoundError returns true if the provided error
// signals that the backend does not support the eth_maxPrirorityFeePerGas
// method. In this case, the caller should fallback to using the constant above.
func IsMaxPriorityFeePerGasNotFoundError(err error) bool {
return strings.Contains(
err.Error(), errMaxPriorityFeePerGasNotFound.Error(),
)
}
...@@ -14,7 +14,6 @@ import ( ...@@ -14,7 +14,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
"github.com/ethereum-optimism/optimism/l2geth/log" "github.com/ethereum-optimism/optimism/l2geth/log"
"github.com/ethereum-optimism/optimism/l2geth/params"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -197,22 +196,43 @@ func (d *Driver) CraftBatchTx( ...@@ -197,22 +196,43 @@ func (d *Driver) CraftBatchTx(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = nonce opts.Nonce = nonce
opts.GasPrice = big.NewInt(params.GWei) // dummy
opts.NoSend = true opts.NoSend = true
blockOffset := new(big.Int).SetUint64(d.cfg.BlockOffset) blockOffset := new(big.Int).SetUint64(d.cfg.BlockOffset)
offsetStartsAtIndex := new(big.Int).Sub(start, blockOffset) offsetStartsAtIndex := new(big.Int).Sub(start, blockOffset)
return d.sccContract.AppendStateBatch(opts, stateRoots, offsetStartsAtIndex) tx, err := d.sccContract.AppendStateBatch(
opts, stateRoots, offsetStartsAtIndex,
)
switch {
case err == nil:
return tx, nil
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this method,
// so in the event their API is unreachable we can fallback to a degraded
// mode of operation. This also applies to our test environments, as hardhat
// doesn't support the query either.
case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
opts.GasTipCap = drivers.FallbackGasTipCap
return d.sccContract.AppendStateBatch(
opts, stateRoots, offsetStartsAtIndex,
)
default:
return nil, err
}
} }
// SubmitBatchTx using the passed transaction as a template, signs and publishes // SubmitBatchTx using the passed transaction as a template, signs and
// an otherwise identical transaction after setting the provided gas price. The // publishes the transaction unmodified apart from sampling the current gas
// final transaction is returned to the caller. // price. The final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx( func (d *Driver) SubmitBatchTx(
ctx context.Context, ctx context.Context,
tx *types.Transaction, tx *types.Transaction,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID( opts, err := bind.NewKeyedTransactorWithChainID(
...@@ -223,7 +243,25 @@ func (d *Driver) SubmitBatchTx( ...@@ -223,7 +243,25 @@ func (d *Driver) SubmitBatchTx(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce()) opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.GasPrice = gasPrice
return d.rawSccContract.RawTransact(opts, tx.Data()) finalTx, err := d.rawSccContract.RawTransact(opts, tx.Data())
switch {
case err == nil:
return finalTx, nil
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this method,
// so in the event their API is unreachable we can fallback to a degraded
// mode of operation. This also applies to our test environments, as hardhat
// doesn't support the query either.
case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
opts.GasTipCap = drivers.FallbackGasTipCap
return d.rawSccContract.RawTransact(opts, tx.Data())
default:
return nil, err
}
} }
...@@ -12,7 +12,6 @@ import ( ...@@ -12,7 +12,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/metrics" "github.com/ethereum-optimism/optimism/go/batch-submitter/metrics"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr" "github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
"github.com/ethereum-optimism/optimism/l2geth/params"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -233,20 +232,37 @@ func (d *Driver) CraftBatchTx( ...@@ -233,20 +232,37 @@ func (d *Driver) CraftBatchTx(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = nonce opts.Nonce = nonce
opts.GasPrice = big.NewInt(params.GWei) // dummy
opts.NoSend = true opts.NoSend = true
return d.rawCtcContract.RawTransact(opts, batchCallData) tx, err := d.rawCtcContract.RawTransact(opts, batchCallData)
switch {
case err == nil:
return tx, nil
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this
// method, so in the event their API is unreachable we can fallback to a
// degraded mode of operation. This also applies to our test
// environments, as hardhat doesn't support the query either.
case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
opts.GasTipCap = drivers.FallbackGasTipCap
return d.rawCtcContract.RawTransact(opts, batchCallData)
default:
return nil, err
}
} }
} }
// SubmitBatchTx using the passed transaction as a template, signs and publishes // SubmitBatchTx using the passed transaction as a template, signs and publishes
// an otherwise identical transaction after setting the provided gas price. The // the transaction unmodified apart from sampling the current gas price. The
// final transaction is returned to the caller. // final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx( func (d *Driver) SubmitBatchTx(
ctx context.Context, ctx context.Context,
tx *types.Transaction, tx *types.Transaction,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID( opts, err := bind.NewKeyedTransactorWithChainID(
...@@ -257,7 +273,25 @@ func (d *Driver) SubmitBatchTx( ...@@ -257,7 +273,25 @@ func (d *Driver) SubmitBatchTx(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce()) opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.GasPrice = gasPrice
return d.rawCtcContract.RawTransact(opts, tx.Data()) finalTx, err := d.rawCtcContract.RawTransact(opts, tx.Data())
switch {
case err == nil:
return finalTx, nil
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this method,
// so in the event their API is unreachable we can fallback to a degraded
// mode of operation. This also applies to our test environments, as hardhat
// doesn't support the query either.
case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
opts.GasTipCap = drivers.FallbackGasTipCap
return d.rawCtcContract.RawTransact(opts, tx.Data())
default:
return nil, err
}
} }
...@@ -151,18 +151,6 @@ var ( ...@@ -151,18 +151,6 @@ var (
Value: 1, Value: 1,
EnvVar: prefixEnvVar("BLOCK_OFFSET"), EnvVar: prefixEnvVar("BLOCK_OFFSET"),
} }
MaxGasPriceInGweiFlag = cli.Uint64Flag{
Name: "max-gas-price-in-gwei",
Usage: "Maximum gas price the batch submitter can use for transactions",
Value: 100,
EnvVar: prefixEnvVar("MAX_GAS_PRICE_IN_GWEI"),
}
GasRetryIncrementFlag = cli.Uint64Flag{
Name: "gas-retry-increment",
Usage: "Default step by which to increment gas price bumps",
Value: 5,
EnvVar: prefixEnvVar("GAS_RETRY_INCREMENT_FLAG"),
}
SequencerPrivateKeyFlag = cli.StringFlag{ SequencerPrivateKeyFlag = cli.StringFlag{
Name: "sequencer-private-key", Name: "sequencer-private-key",
Usage: "The private key to use for sending to the sequencer contract", Usage: "The private key to use for sending to the sequencer contract",
...@@ -235,8 +223,6 @@ var optionalFlags = []cli.Flag{ ...@@ -235,8 +223,6 @@ var optionalFlags = []cli.Flag{
SentryDsnFlag, SentryDsnFlag,
SentryTraceRateFlag, SentryTraceRateFlag,
BlockOffsetFlag, BlockOffsetFlag,
MaxGasPriceInGweiFlag,
GasRetryIncrementFlag,
SequencerPrivateKeyFlag, SequencerPrivateKeyFlag,
ProposerPrivateKeyFlag, ProposerPrivateKeyFlag,
MnemonicFlag, MnemonicFlag,
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"math/big" "math/big"
"sync" "sync"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
) )
...@@ -16,12 +15,9 @@ type L1ClientConfig struct { ...@@ -16,12 +15,9 @@ type L1ClientConfig struct {
// BlockNumber returns the most recent block number. // BlockNumber returns the most recent block number.
BlockNumber func(context.Context) (uint64, error) BlockNumber func(context.Context) (uint64, error)
// EstimateGas tries to estimate the gas needed to execute a specific // HeaderByNumber returns a block header from the current canonical chain.
// transaction based on the current pending state of the backend blockchain. // If number is nil, the latest known header is returned.
// There is no guarantee that this is the true gas limit requirement as HeaderByNumber func(context.Context, *big.Int) (*types.Header, error)
// 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 // 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. // can be nil, in which case the nonce is taken from the latest known block.
...@@ -34,6 +30,10 @@ type L1ClientConfig struct { ...@@ -34,6 +30,10 @@ type L1ClientConfig struct {
// method to get the contract address after the transaction has been mined. // method to get the contract address after the transaction has been mined.
SendTransaction func(context.Context, *types.Transaction) error SendTransaction func(context.Context, *types.Transaction) error
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
// to allow a timely execution of a transaction.
SuggestGasTipCap func(context.Context) (*big.Int, error)
// TransactionReceipt returns the receipt of a transaction by transaction // TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions. // hash. Note that the receipt is not available for pending transactions.
TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error) TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error)
...@@ -61,12 +61,13 @@ func (c *L1Client) BlockNumber(ctx context.Context) (uint64, error) { ...@@ -61,12 +61,13 @@ func (c *L1Client) BlockNumber(ctx context.Context) (uint64, error) {
return c.cfg.BlockNumber(ctx) return c.cfg.BlockNumber(ctx)
} }
// EstimateGas executes the mock EstimateGas method. // HeaderByNumber returns a block header from the current canonical chain. If
func (c *L1Client) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { // number is nil, the latest known header is returned.
func (c *L1Client) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*types.Header, error) {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
return c.cfg.EstimateGas(ctx, call) return c.cfg.HeaderByNumber(ctx, blockNumber)
} }
// NonceAt executes the mock NonceAt method. // NonceAt executes the mock NonceAt method.
...@@ -85,6 +86,15 @@ func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) e ...@@ -85,6 +86,15 @@ func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) e
return c.cfg.SendTransaction(ctx, tx) return c.cfg.SendTransaction(ctx, tx)
} }
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559 to
// allow a timely execution of a transaction.
func (c *L1Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.SuggestGasTipCap(ctx)
}
// TransactionReceipt executes the mock TransactionReceipt method. // TransactionReceipt executes the mock TransactionReceipt method.
func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.mu.RLock() c.mu.RLock()
...@@ -103,17 +113,17 @@ func (c *L1Client) SetBlockNumberFunc( ...@@ -103,17 +113,17 @@ func (c *L1Client) SetBlockNumberFunc(
c.cfg.BlockNumber = f c.cfg.BlockNumber = f
} }
// SetEstimateGasFunc overrwrites the mock EstimateGas method. // SetHeaderByNumberFunc overwrites the mock HeaderByNumber method.
func (c *L1Client) SetEstimateGasFunc( func (c *L1Client) SetHeaderByNumberFunc(
f func(context.Context, ethereum.CallMsg) (uint64, error)) { f func(ctx context.Context, blockNumber *big.Int) (*types.Header, error)) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.cfg.EstimateGas = f c.cfg.HeaderByNumber = f
} }
// SetNonceAtFunc overrwrites the mock NonceAt method. // SetNonceAtFunc overwrites the mock NonceAt method.
func (c *L1Client) SetNonceAtFunc( func (c *L1Client) SetNonceAtFunc(
f func(context.Context, common.Address, *big.Int) (uint64, error)) { f func(context.Context, common.Address, *big.Int) (uint64, error)) {
...@@ -123,7 +133,7 @@ func (c *L1Client) SetNonceAtFunc( ...@@ -123,7 +133,7 @@ func (c *L1Client) SetNonceAtFunc(
c.cfg.NonceAt = f c.cfg.NonceAt = f
} }
// SetSendTransactionFunc overrwrites the mock SendTransaction method. // SetSendTransactionFunc overwrites the mock SendTransaction method.
func (c *L1Client) SetSendTransactionFunc( func (c *L1Client) SetSendTransactionFunc(
f func(context.Context, *types.Transaction) error) { f func(context.Context, *types.Transaction) error) {
...@@ -133,6 +143,16 @@ func (c *L1Client) SetSendTransactionFunc( ...@@ -133,6 +143,16 @@ func (c *L1Client) SetSendTransactionFunc(
c.cfg.SendTransaction = f c.cfg.SendTransaction = f
} }
// SetSuggestGasTipCapFunc overwrites themock SuggestGasTipCap method.
func (c *L1Client) SetSuggestGasTipCapFunc(
f func(context.Context) (*big.Int, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.SuggestGasTipCap = f
}
// SetTransactionReceiptFunc overwrites the mock TransactionReceipt method. // SetTransactionReceiptFunc overwrites the mock TransactionReceipt method.
func (c *L1Client) SetTransactionReceiptFunc( func (c *L1Client) SetTransactionReceiptFunc(
f func(context.Context, common.Hash) (*types.Receipt, error)) { f func(context.Context, common.Hash) (*types.Receipt, error)) {
......
...@@ -55,12 +55,11 @@ type Driver interface { ...@@ -55,12 +55,11 @@ type Driver interface {
) (*types.Transaction, error) ) (*types.Transaction, error)
// SubmitBatchTx using the passed transaction as a template, signs and // SubmitBatchTx using the passed transaction as a template, signs and
// publishes an otherwise identical transaction after setting the provided // publishes the transaction unmodified apart from sampling the current gas
// gas price. The final transaction is returned to the caller. // price. The final transaction is returned to the caller.
SubmitBatchTx( SubmitBatchTx(
ctx context.Context, ctx context.Context,
tx *types.Transaction, tx *types.Transaction,
gasPrice *big.Int,
) (*types.Transaction, error) ) (*types.Transaction, error)
} }
...@@ -194,15 +193,11 @@ func (s *Service) eventLoop() { ...@@ -194,15 +193,11 @@ func (s *Service) eventLoop() {
// Construct the transaction submission clousure that will attempt // Construct the transaction submission clousure that will attempt
// to send the next transaction at the given nonce and gas price. // to send the next transaction at the given nonce and gas price.
sendTx := func( sendTx := func(ctx context.Context) (*types.Transaction, error) {
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
log.Info(name+" attempting batch tx", "start", start, log.Info(name+" attempting batch tx", "start", start,
"end", end, "nonce", nonce, "end", end, "nonce", nonce)
"gasPrice", gasPrice)
tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx, gasPrice) tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -213,7 +208,6 @@ func (s *Service) eventLoop() { ...@@ -213,7 +208,6 @@ func (s *Service) eventLoop() {
"end", end, "end", end,
"nonce", nonce, "nonce", nonce,
"tx_hash", tx.Hash(), "tx_hash", tx.Hash(),
"gasPrice", gasPrice,
) )
return tx, nil return tx, nil
......
...@@ -2,46 +2,27 @@ package txmgr ...@@ -2,46 +2,27 @@ package txmgr
import ( import (
"context" "context"
"errors"
"math/big" "math/big"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// ErrPublishTimeout signals that the tx manager did not receive a confirmation
// for a given tx after publishing with the maximum gas price and waiting out a
// resubmission timeout.
var ErrPublishTimeout = errors.New("failed to publish tx with max gas price")
// SendTxFunc defines a function signature for publishing a desired tx with a // SendTxFunc defines a function signature for publishing a desired tx with a
// specific gas price. Implementations of this signature should also return // specific gas price. Implementations of this signature should also return
// promptly when the context is canceled. // promptly when the context is canceled.
type SendTxFunc = func( type SendTxFunc = func(ctx context.Context) (*types.Transaction, error)
ctx context.Context, gasPrice *big.Int) (*types.Transaction, error)
// Config houses parameters for altering the behavior of a SimpleTxManager. // Config houses parameters for altering the behavior of a SimpleTxManager.
type Config struct { type Config struct {
// Name the name of the driver to appear in log lines.
Name string Name string
// MinGasPrice is the minimum gas price (in gwei). This is used as the
// initial publication attempt.
MinGasPrice *big.Int
// MaxGasPrice is the maximum gas price (in gwei). This is used to clamp
// the upper end of the range that the TxManager will ever publish when
// attempting to confirm a transaction.
MaxGasPrice *big.Int
// GasRetryIncrement is the additive gas price (in gwei) that will be
// used to bump each successive tx after a ResubmissionTimeout has
// elapsed.
GasRetryIncrement *big.Int
// ResubmissionTimeout is the interval at which, if no previously // ResubmissionTimeout is the interval at which, if no previously
// published transaction has been mined, the new tx with a bumped gas // published transaction has been mined, the new tx with a bumped gas
// price will be published. Only one publication at MaxGasPrice will be // price will be published. Only one publication at MaxGasPrice will be
...@@ -135,25 +116,29 @@ func (m *SimpleTxManager) Send( ...@@ -135,25 +116,29 @@ func (m *SimpleTxManager) Send(
// background, returning the first successfully mined receipt back to // background, returning the first successfully mined receipt back to
// the main event loop via receiptChan. // the main event loop via receiptChan.
receiptChan := make(chan *types.Receipt, 1) receiptChan := make(chan *types.Receipt, 1)
sendTxAsync := func(gasPrice *big.Int) { sendTxAsync := func() {
defer wg.Done() defer wg.Done()
// Sign and publish transaction with current gas price. // Sign and publish transaction with current gas price.
tx, err := sendTx(ctxc, gasPrice) tx, err := sendTx(ctxc)
if err != nil { if err != nil {
if err == context.Canceled || if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") { strings.Contains(err.Error(), "context canceled") {
return return
} }
log.Error(name+" unable to publish transaction", log.Error(name+" unable to publish transaction", "err", err)
"gas_price", gasPrice, "err", err) if shouldAbortImmediately(err) {
cancel()
}
// TODO(conner): add retry? // TODO(conner): add retry?
return return
} }
txHash := tx.Hash() txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
log.Info(name+" transaction published successfully", "hash", txHash, log.Info(name+" transaction published successfully", "hash", txHash,
"gas_price", gasPrice) "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
// Wait for the transaction to be mined, reporting the receipt // Wait for the transaction to be mined, reporting the receipt
// back to the main event loop if found. // back to the main event loop if found.
...@@ -163,7 +148,7 @@ func (m *SimpleTxManager) Send( ...@@ -163,7 +148,7 @@ func (m *SimpleTxManager) Send(
) )
if err != nil { if err != nil {
log.Debug(name+" send tx failed", "hash", txHash, log.Debug(name+" send tx failed", "hash", txHash,
"gas_price", gasPrice, "err", err) "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap, "err", err)
} }
if receipt != nil { if receipt != nil {
// Use non-blocking select to ensure function can exit // Use non-blocking select to ensure function can exit
...@@ -171,20 +156,17 @@ func (m *SimpleTxManager) Send( ...@@ -171,20 +156,17 @@ func (m *SimpleTxManager) Send(
select { select {
case receiptChan <- receipt: case receiptChan <- receipt:
log.Trace(name+" send tx succeeded", "hash", txHash, log.Trace(name+" send tx succeeded", "hash", txHash,
"gas_price", gasPrice) "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
default: default:
} }
} }
} }
// Initialize our initial gas price to the configured minimum.
curGasPrice := new(big.Int).Set(m.cfg.MinGasPrice)
// Submit and wait for the receipt at our first gas price in the // Submit and wait for the receipt at our first gas price in the
// background, before entering the event loop and waiting out the // background, before entering the event loop and waiting out the
// resubmission timeout. // resubmission timeout.
wg.Add(1) wg.Add(1)
go sendTxAsync(curGasPrice) go sendTxAsync()
for { for {
select { select {
...@@ -192,24 +174,9 @@ func (m *SimpleTxManager) Send( ...@@ -192,24 +174,9 @@ func (m *SimpleTxManager) Send(
// Whenever a resubmission timeout has elapsed, bump the gas // Whenever a resubmission timeout has elapsed, bump the gas
// price and publish a new transaction. // price and publish a new transaction.
case <-time.After(m.cfg.ResubmissionTimeout): case <-time.After(m.cfg.ResubmissionTimeout):
// If our last attempt published at the max gas price,
// return an error as we are unlikely to succeed in
// publishing. This also indicates that the max gas
// price should likely be adjusted higher for the
// daemon.
if curGasPrice.Cmp(m.cfg.MaxGasPrice) >= 0 {
return nil, ErrPublishTimeout
}
// Bump the gas price using linear gas price increments.
curGasPrice = NextGasPrice(
curGasPrice, m.cfg.GasRetryIncrement,
m.cfg.MaxGasPrice,
)
// Submit and wait for the bumped traction to confirm. // Submit and wait for the bumped traction to confirm.
wg.Add(1) wg.Add(1)
go sendTxAsync(curGasPrice) go sendTxAsync()
// The passed context has been canceled, i.e. in the event of a // The passed context has been canceled, i.e. in the event of a
// shutdown. // shutdown.
...@@ -223,6 +190,13 @@ func (m *SimpleTxManager) Send( ...@@ -223,6 +190,13 @@ func (m *SimpleTxManager) Send(
} }
} }
// shouldAbortImmediately returns true if the txmgr should cancel all
// publication attempts and retry. For now, this only includes nonce errors, as
// that error indicates that none of the transactions will ever confirm.
func shouldAbortImmediately(err error) bool {
return strings.Contains(err.Error(), core.ErrNonceTooLow.Error())
}
// WaitMined blocks until the backend indicates confirmation of tx and returns // WaitMined blocks until the backend indicates confirmation of tx and returns
// the tx receipt. Queries are made every queryInterval, regardless of whether // the tx receipt. Queries are made every queryInterval, regardless of whether
// the backend returns an error. This method can be canceled using the passed // the backend returns an error. This method can be canceled using the passed
...@@ -289,17 +263,12 @@ func WaitMined( ...@@ -289,17 +263,12 @@ func WaitMined(
} }
} }
// NextGasPrice bumps the current gas price using an additive gasRetryIncrement, // CalcGasFeeCap deterministically computes the recommended gas fee cap given
// clamping the resulting value to maxGasPrice. // the base fee and gasTipCap. The resulting gasFeeCap is equal to:
// // gasTipCap + 2*baseFee.
// NOTE: This method does not mutate curGasPrice, but instead returns a copy. func CalcGasFeeCap(baseFee, gasTipCap *big.Int) *big.Int {
// This removes the possiblity of races occuring from goroutines sharing access return new(big.Int).Add(
// to the same underlying big.Int. gasTipCap,
func NextGasPrice(curGasPrice, gasRetryIncrement, maxGasPrice *big.Int) *big.Int { new(big.Int).Mul(baseFee, big.NewInt(2)),
nextGasPrice := new(big.Int).Set(curGasPrice) )
nextGasPrice.Add(nextGasPrice, gasRetryIncrement)
if nextGasPrice.Cmp(maxGasPrice) == 1 {
nextGasPrice.Set(maxGasPrice)
}
return nextGasPrice
} }
This diff is collapsed.
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)
}
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))
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment