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 (
"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 +158,6 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) {
}
txManagerConfig := txmgr.Config{
MinGasPrice: utils.GasPriceFromGwei(1),
MaxGasPrice: utils.GasPriceFromGwei(cfg.MaxGasPriceInGwei),
GasRetryIncrement: utils.GasPriceFromGwei(cfg.GasRetryIncrement),
ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations,
......
......@@ -133,14 +133,6 @@ type Config struct {
// blocks.
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
// transactions to the CTC contract.
SequencerPrivateKey string
......@@ -199,8 +191,6 @@ func NewConfig(ctx *cli.Context) (Config, error) {
SentryDsn: ctx.GlobalString(flags.SentryDsnFlag.Name),
SentryTraceRate: ctx.GlobalDuration(flags.SentryTraceRateFlag.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),
ProposerPrivateKey: ctx.GlobalString(flags.ProposerPrivateKeyFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
......
......@@ -8,7 +8,6 @@ import (
"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"
......@@ -50,20 +49,20 @@ func ClearPendingTx(
// price.
sendTx := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
log.Info(name+" clearing pending tx", "nonce", nonce,
"gasPrice", gasPrice)
log.Info(name+" clearing pending tx", "nonce", nonce)
signedTx, err := SignClearingTx(
ctx, walletAddr, nonce, gasPrice, l1Client, privKey, chainID,
name, ctx, walletAddr, nonce, l1Client, privKey, chainID,
)
if err != nil {
log.Error(name+" unable to sign clearing tx", "nonce", nonce,
"gasPrice", gasPrice, "err", err)
"err", err)
return nil, err
}
txHash := signedTx.Hash()
gasTipCap := signedTx.GasTipCap()
gasFeeCap := signedTx.GasFeeCap()
err = l1Client.SendTransaction(ctx, signedTx)
switch {
......@@ -71,7 +70,8 @@ func ClearPendingTx(
// Clearing transaction successfully confirmed.
case err == nil:
log.Info(name+" submitted clearing tx", "nonce", nonce,
"gasPrice", gasPrice, "txHash", txHash)
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash)
return signedTx, nil
......@@ -91,8 +91,8 @@ func ClearPendingTx(
// 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)
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash, "err", err)
return nil, err
}
}
......@@ -127,26 +127,39 @@ func ClearPendingTx(
// 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(
name string,
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,
})
gasTipCap, err := l1Client.SuggestGasTipCap(ctx)
if err != nil {
if !IsMaxPriorityFeePerGasNotFoundError(err) {
return nil, err
}
// 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 {
return nil, err
}
tx := CraftClearingTx(walletAddr, nonce, gasPrice, gasLimit)
gasFeeCap := txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
tx := CraftClearingTx(walletAddr, nonce, gasFeeCap, gasTipCap)
return types.SignTx(
tx, types.LatestSignerForChainID(chainID), privKey,
......@@ -158,15 +171,15 @@ func SignClearingTx(
func CraftClearingTx(
walletAddr common.Address,
nonce uint64,
gasPrice *big.Int,
gasLimit uint64,
gasFeeCap *big.Int,
gasTipCap *big.Int,
) *types.Transaction {
return types.NewTx(&types.LegacyTx{
return types.NewTx(&types.DynamicFeeTx{
To: &walletAddr,
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: nil,
Data: nil,
})
......
......@@ -11,8 +11,6 @@ import (
"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"
......@@ -27,8 +25,6 @@ func init() {
}
testPrivKey = privKey
testWalletAddr = crypto.PubkeyToAddress(privKey.PublicKey)
testChainID = new(big.Int).SetUint64(1)
testGasPrice = new(big.Int).SetUint64(3)
}
var (
......@@ -36,21 +32,22 @@ var (
testWalletAddr common.Address
testChainID = big.NewInt(1)
testNonce = uint64(2)
testGasPrice = big.NewInt(3)
testGasLimit = uint64(4)
testGasFeeCap = big.NewInt(3)
testGasTipCap = big.NewInt(4)
testBlockNumber = uint64(5)
testBaseFee = big.NewInt(6)
)
// TestCraftClearingTx asserts that CraftClearingTx produces the expected
// unsigned clearing transaction.
func TestCraftClearingTx(t *testing.T) {
tx := drivers.CraftClearingTx(
testWalletAddr, testNonce, testGasPrice, testGasLimit,
testWalletAddr, testNonce, testGasFeeCap, testGasTipCap,
)
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, testGasFeeCap, tx.GasFeeCap())
require.Equal(t, testGasTipCap, tx.GasTipCap())
require.Equal(t, new(big.Int), tx.Value())
require.Nil(t, tx.Data())
}
......@@ -59,21 +56,31 @@ func TestCraftClearingTx(t *testing.T) {
// 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
HeaderByNumber: func(_ context.Context, _ *big.Int) (*types.Header, error) {
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(
context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client,
"TEST", context.Background(), testWalletAddr, testNonce, 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, expGasFeeCap, tx.GasFeeCap())
require.Equal(t, testGasTipCap, tx.GasTipCap())
require.Equal(t, new(big.Int), tx.Value())
require.Nil(t, tx.Data())
......@@ -83,22 +90,44 @@ func TestSignClearingTxEstimateGasSuccess(t *testing.T) {
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")
// TestSignClearingTxSuggestGasTipCapFail asserts that signing a clearing
// transaction will fail if the underlying call to SuggestGasTipCap fails.
func TestSignClearingTxSuggestGasTipCapFail(t *testing.T) {
errSuggestGasTipCap := errors.New("suggest gas tip cap")
l1Client := mock.NewL1Client(mock.L1ClientConfig{
EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) {
return 0, errEstimateGas
SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
return nil, errSuggestGasTipCap
},
})
tx, err := drivers.SignClearingTx(
context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client,
"TEST", context.Background(), testWalletAddr, testNonce, l1Client,
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)
}
......@@ -117,22 +146,26 @@ func newClearPendingTxHarnessWithNumConfs(
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 {
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
if l1ClientConfig.SuggestGasTipCap == nil {
l1ClientConfig.SuggestGasTipCap = func(_ context.Context) (*big.Int, error) {
return testGasTipCap, 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,
NumConfirmations: numConfirmations,
......@@ -200,11 +233,14 @@ func TestClearPendingTxTimeout(t *testing.T) {
},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := drivers.ClearPendingTx(
"test", context.Background(), h.txMgr, h.l1Client, testWalletAddr,
testPrivKey, testChainID,
"test", ctx, h.txMgr, h.l1Client, testWalletAddr, testPrivKey,
testChainID,
)
require.Equal(t, txmgr.ErrPublishTimeout, err)
require.Equal(t, context.DeadlineExceeded, err)
}
// TestClearPendingTxMultipleConfs tests we wait the appropriate number of
......@@ -225,12 +261,15 @@ func TestClearPendingTxMultipleConfs(t *testing.T) {
},
}, numConfs)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// The txmgr should timeout waiting for the txn to confirm.
err := drivers.ClearPendingTx(
"test", context.Background(), h.txMgr, h.l1Client, testWalletAddr,
testPrivKey, testChainID,
"test", ctx, h.txMgr, h.l1Client, testWalletAddr, testPrivKey,
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
// considered sufficiently confirmed.
......
......@@ -4,7 +4,6 @@ import (
"context"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
......@@ -12,12 +11,9 @@ import (
// 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)
// HeaderByNumber returns a block header from the current canonical chain.
// If number is nil, the latest known header is returned.
HeaderByNumber(context.Context, *big.Int) (*types.Header, 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.
......@@ -30,6 +26,10 @@ type L1Client interface {
// method to get the contract address after the transaction has been mined.
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
// hash. Note that the receipt is not available for pending transactions.
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 (
"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-optimism/optimism/l2geth/params"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -197,22 +196,43 @@ func (d *Driver) CraftBatchTx(
}
opts.Context = ctx
opts.Nonce = nonce
opts.GasPrice = big.NewInt(params.GWei) // dummy
opts.NoSend = true
blockOffset := new(big.Int).SetUint64(d.cfg.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
// an otherwise identical transaction after setting the provided gas price. The
// final transaction is returned to the caller.
// SubmitBatchTx using the passed transaction as a template, signs and
// publishes the transaction unmodified apart from sampling the current gas
// price. The final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx(
ctx context.Context,
tx *types.Transaction,
gasPrice *big.Int,
) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID(
......@@ -223,7 +243,25 @@ func (d *Driver) SubmitBatchTx(
}
opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.GasPrice = gasPrice
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 (
"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/params"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -233,20 +232,37 @@ func (d *Driver) CraftBatchTx(
}
opts.Context = ctx
opts.Nonce = nonce
opts.GasPrice = big.NewInt(params.GWei) // dummy
opts.NoSend = true
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
// 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.
func (d *Driver) SubmitBatchTx(
ctx context.Context,
tx *types.Transaction,
gasPrice *big.Int,
) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID(
......@@ -257,7 +273,25 @@ func (d *Driver) SubmitBatchTx(
}
opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.GasPrice = gasPrice
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 (
Value: 1,
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{
Name: "sequencer-private-key",
Usage: "The private key to use for sending to the sequencer contract",
......@@ -235,8 +223,6 @@ var optionalFlags = []cli.Flag{
SentryDsnFlag,
SentryTraceRateFlag,
BlockOffsetFlag,
MaxGasPriceInGweiFlag,
GasRetryIncrementFlag,
SequencerPrivateKeyFlag,
ProposerPrivateKeyFlag,
MnemonicFlag,
......
......@@ -5,7 +5,6 @@ import (
"math/big"
"sync"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
......@@ -16,12 +15,9 @@ type L1ClientConfig struct {
// BlockNumber returns the most recent block number.
BlockNumber func(context.Context) (uint64, error)
// 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)
// HeaderByNumber returns a block header from the current canonical chain.
// If number is nil, the latest known header is returned.
HeaderByNumber func(context.Context, *big.Int) (*types.Header, 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.
......@@ -34,6 +30,10 @@ type L1ClientConfig struct {
// method to get the contract address after the transaction has been mined.
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
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error)
......@@ -61,12 +61,13 @@ func (c *L1Client) BlockNumber(ctx context.Context) (uint64, error) {
return c.cfg.BlockNumber(ctx)
}
// EstimateGas executes the mock EstimateGas method.
func (c *L1Client) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) {
// HeaderByNumber returns a block header from the current canonical chain. If
// 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()
defer c.mu.RUnlock()
return c.cfg.EstimateGas(ctx, call)
return c.cfg.HeaderByNumber(ctx, blockNumber)
}
// NonceAt executes the mock NonceAt method.
......@@ -85,6 +86,15 @@ func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) e
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.
func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.mu.RLock()
......@@ -103,17 +113,17 @@ func (c *L1Client) SetBlockNumberFunc(
c.cfg.BlockNumber = f
}
// SetEstimateGasFunc overrwrites the mock EstimateGas method.
func (c *L1Client) SetEstimateGasFunc(
f func(context.Context, ethereum.CallMsg) (uint64, error)) {
// SetHeaderByNumberFunc overwrites the mock HeaderByNumber method.
func (c *L1Client) SetHeaderByNumberFunc(
f func(ctx context.Context, blockNumber *big.Int) (*types.Header, error)) {
c.mu.Lock()
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(
f func(context.Context, common.Address, *big.Int) (uint64, error)) {
......@@ -123,7 +133,7 @@ func (c *L1Client) SetNonceAtFunc(
c.cfg.NonceAt = f
}
// SetSendTransactionFunc overrwrites the mock SendTransaction method.
// SetSendTransactionFunc overwrites the mock SendTransaction method.
func (c *L1Client) SetSendTransactionFunc(
f func(context.Context, *types.Transaction) error) {
......@@ -133,6 +143,16 @@ func (c *L1Client) SetSendTransactionFunc(
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.
func (c *L1Client) SetTransactionReceiptFunc(
f func(context.Context, common.Hash) (*types.Receipt, error)) {
......
......@@ -55,12 +55,11 @@ type Driver interface {
) (*types.Transaction, error)
// SubmitBatchTx using the passed transaction as a template, signs and
// publishes an otherwise identical transaction after setting the provided
// gas price. The final transaction is returned to the caller.
// publishes the transaction unmodified apart from sampling the current gas
// price. The final transaction is returned to the caller.
SubmitBatchTx(
ctx context.Context,
tx *types.Transaction,
gasPrice *big.Int,
) (*types.Transaction, error)
}
......@@ -194,15 +193,11 @@ func (s *Service) eventLoop() {
// Construct the transaction submission clousure that will attempt
// to send the next transaction at the given nonce and gas price.
sendTx := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
sendTx := func(ctx context.Context) (*types.Transaction, error) {
log.Info(name+" attempting batch tx", "start", start,
"end", end, "nonce", nonce,
"gasPrice", gasPrice)
"end", end, "nonce", nonce)
tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx, gasPrice)
tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx)
if err != nil {
return nil, err
}
......@@ -213,7 +208,6 @@ func (s *Service) eventLoop() {
"end", end,
"nonce", nonce,
"tx_hash", tx.Hash(),
"gasPrice", gasPrice,
)
return tx, nil
......
......@@ -2,46 +2,27 @@ package txmgr
import (
"context"
"errors"
"math/big"
"strings"
"sync"
"time"
"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"
)
// 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
// specific gas price. Implementations of this signature should also return
// promptly when the context is canceled.
type SendTxFunc = func(
ctx context.Context, gasPrice *big.Int) (*types.Transaction, error)
type SendTxFunc = func(ctx context.Context) (*types.Transaction, error)
// Config houses parameters for altering the behavior of a SimpleTxManager.
type Config struct {
// Name the name of the driver to appear in log lines.
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
// published transaction has been mined, the new tx with a bumped gas
// price will be published. Only one publication at MaxGasPrice will be
......@@ -135,25 +116,29 @@ func (m *SimpleTxManager) Send(
// background, returning the first successfully mined receipt back to
// the main event loop via receiptChan.
receiptChan := make(chan *types.Receipt, 1)
sendTxAsync := func(gasPrice *big.Int) {
sendTxAsync := func() {
defer wg.Done()
// Sign and publish transaction with current gas price.
tx, err := sendTx(ctxc, gasPrice)
tx, err := sendTx(ctxc)
if err != nil {
if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") {
return
}
log.Error(name+" unable to publish transaction",
"gas_price", gasPrice, "err", err)
log.Error(name+" unable to publish transaction", "err", err)
if shouldAbortImmediately(err) {
cancel()
}
// TODO(conner): add retry?
return
}
txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
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
// back to the main event loop if found.
......@@ -163,7 +148,7 @@ func (m *SimpleTxManager) Send(
)
if err != nil {
log.Debug(name+" send tx failed", "hash", txHash,
"gas_price", gasPrice, "err", err)
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap, "err", err)
}
if receipt != nil {
// Use non-blocking select to ensure function can exit
......@@ -171,20 +156,17 @@ func (m *SimpleTxManager) Send(
select {
case receiptChan <- receipt:
log.Trace(name+" send tx succeeded", "hash", txHash,
"gas_price", gasPrice)
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
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
// background, before entering the event loop and waiting out the
// resubmission timeout.
wg.Add(1)
go sendTxAsync(curGasPrice)
go sendTxAsync()
for {
select {
......@@ -192,24 +174,9 @@ func (m *SimpleTxManager) Send(
// Whenever a resubmission timeout has elapsed, bump the gas
// price and publish a new transaction.
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.
wg.Add(1)
go sendTxAsync(curGasPrice)
go sendTxAsync()
// The passed context has been canceled, i.e. in the event of a
// shutdown.
......@@ -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
// the tx receipt. Queries are made every queryInterval, regardless of whether
// the backend returns an error. This method can be canceled using the passed
......@@ -289,17 +263,12 @@ func WaitMined(
}
}
// NextGasPrice bumps the current gas price using an additive gasRetryIncrement,
// clamping the resulting value to maxGasPrice.
//
// NOTE: This method does not mutate curGasPrice, but instead returns a copy.
// This removes the possiblity of races occuring from goroutines sharing access
// to the same underlying big.Int.
func NextGasPrice(curGasPrice, gasRetryIncrement, maxGasPrice *big.Int) *big.Int {
nextGasPrice := new(big.Int).Set(curGasPrice)
nextGasPrice.Add(nextGasPrice, gasRetryIncrement)
if nextGasPrice.Cmp(maxGasPrice) == 1 {
nextGasPrice.Set(maxGasPrice)
}
return nextGasPrice
// CalcGasFeeCap deterministically computes the recommended gas fee cap given
// the base fee and gasTipCap. The resulting gasFeeCap is equal to:
// gasTipCap + 2*baseFee.
func CalcGasFeeCap(baseFee, gasTipCap *big.Int) *big.Int {
return new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(baseFee, big.NewInt(2)),
)
}
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