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,16 +171,16 @@ 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{
To: &walletAddr,
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
Value: nil,
Data: nil,
return types.NewTx(&types.DynamicFeeTx{
To: &walletAddr,
Nonce: nonce,
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
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 (
"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
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
// 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
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 (
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)),
)
}
......@@ -14,69 +14,12 @@ import (
"github.com/stretchr/testify/require"
)
// TestNextGasPrice asserts that NextGasPrice properly bumps the passed current
// gas price, and clamps it to the max gas price. It also tests that
// NextGasPrice doesn't mutate the passed curGasPrice argument.
func TestNextGasPrice(t *testing.T) {
t.Parallel()
tests := []struct {
name string
curGasPrice *big.Int
gasRetryIncrement *big.Int
maxGasPrice *big.Int
expGasPrice *big.Int
}{
{
name: "increment below max",
curGasPrice: new(big.Int).SetUint64(5),
gasRetryIncrement: new(big.Int).SetUint64(10),
maxGasPrice: new(big.Int).SetUint64(20),
expGasPrice: new(big.Int).SetUint64(15),
},
{
name: "increment equal max",
curGasPrice: new(big.Int).SetUint64(5),
gasRetryIncrement: new(big.Int).SetUint64(10),
maxGasPrice: new(big.Int).SetUint64(15),
expGasPrice: new(big.Int).SetUint64(15),
},
{
name: "increment above max",
curGasPrice: new(big.Int).SetUint64(5),
gasRetryIncrement: new(big.Int).SetUint64(10),
maxGasPrice: new(big.Int).SetUint64(12),
expGasPrice: new(big.Int).SetUint64(12),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Copy curGasPrice, as we will later test for mutation.
curGasPrice := new(big.Int).Set(test.curGasPrice)
nextGasPrice := txmgr.NextGasPrice(
curGasPrice, test.gasRetryIncrement,
test.maxGasPrice,
)
require.Equal(t, nextGasPrice, test.expGasPrice)
// Ensure curGasPrice hasn't been mutated. This check
// enforces that NextGasPrice creates a copy internally.
// Failure to do so could result in gas price bumps
// being read concurrently from other goroutines, and
// introduce race conditions.
require.Equal(t, curGasPrice, test.curGasPrice)
})
}
}
// testHarness houses the necessary resources to test the SimpleTxManager.
type testHarness struct {
cfg txmgr.Config
mgr txmgr.TxManager
backend *mockBackend
cfg txmgr.Config
mgr txmgr.TxManager
backend *mockBackend
gasPricer *gasPricer
}
// newTestHarnessWithConfig initializes a testHarness with a specific
......@@ -86,9 +29,10 @@ func newTestHarnessWithConfig(cfg txmgr.Config) *testHarness {
mgr := txmgr.NewSimpleTxManager("TEST", cfg, backend)
return &testHarness{
cfg: cfg,
mgr: mgr,
backend: backend,
cfg: cfg,
mgr: mgr,
backend: backend,
gasPricer: newGasPricer(3),
}
}
......@@ -100,17 +44,54 @@ func newTestHarness() *testHarness {
func configWithNumConfs(numConfirmations uint64) txmgr.Config {
return txmgr.Config{
MinGasPrice: new(big.Int).SetUint64(5),
MaxGasPrice: new(big.Int).SetUint64(50),
GasRetryIncrement: new(big.Int).SetUint64(5),
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
}
}
type gasPricer struct {
epoch int64
mineAtEpoch int64
baseGasTipFee *big.Int
baseBaseFee *big.Int
mu sync.Mutex
}
func newGasPricer(mineAtEpoch int64) *gasPricer {
return &gasPricer{
mineAtEpoch: mineAtEpoch,
baseGasTipFee: big.NewInt(5),
baseBaseFee: big.NewInt(7),
}
}
func (g *gasPricer) expGasFeeCap() *big.Int {
_, gasFeeCap := g.feesForEpoch(g.mineAtEpoch)
return gasFeeCap
}
func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
epochBaseFee := new(big.Int).Mul(g.baseBaseFee, big.NewInt(epoch))
epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, big.NewInt(epoch))
epochGasFeeCap := txmgr.CalcGasFeeCap(epochBaseFee, epochGasTipCap)
return epochGasTipCap, epochGasFeeCap
}
func (g *gasPricer) sample() (*big.Int, *big.Int, bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.epoch++
epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch)
shouldMine := g.epoch == g.mineAtEpoch
return epochGasTipCap, epochGasFeeCap, shouldMine
}
type minedTxInfo struct {
gasPrice *big.Int
gasFeeCap *big.Int
blockNumber uint64
}
......@@ -133,17 +114,17 @@ func newMockBackend() *mockBackend {
}
}
// mine records a (txHash, gasPrice) as confirmed. Subsequent calls to
// mine records a (txHash, gasFeeCap) as confirmed. Subsequent calls to
// TransactionReceipt with a matching txHash will result in a non-nil receipt.
// If a nil txHash is supplied this has the effect of mining an empty block.
func (b *mockBackend) mine(txHash *common.Hash, gasPrice *big.Int) {
func (b *mockBackend) mine(txHash *common.Hash, gasFeeCap *big.Int) {
b.mu.Lock()
defer b.mu.Unlock()
b.blockHeight++
if txHash != nil {
b.minedTxs[*txHash] = minedTxInfo{
gasPrice: gasPrice,
gasFeeCap: gasFeeCap,
blockNumber: b.blockHeight,
}
}
......@@ -159,7 +140,7 @@ func (b *mockBackend) BlockNumber(ctx context.Context) (uint64, error) {
// TransactionReceipt queries the mockBackend for a mined txHash. If none is
// found, nil is returned for both return values. Otherwise, it retruns a
// receipt containing the txHash and the gasPrice used in the GasUsed to make
// receipt containing the txHash and the gasFeeCap used in the GasUsed to make
// the value accessible from our test framework.
func (b *mockBackend) TransactionReceipt(
ctx context.Context,
......@@ -174,11 +155,11 @@ func (b *mockBackend) TransactionReceipt(
return nil, nil
}
// Return the gas price for the transaction in the GasUsed field so that
// Return the gas fee cap for the transaction in the GasUsed field so that
// we can assert the proper tx confirmed in our tests.
return &types.Receipt{
TxHash: txHash,
GasUsed: txInfo.gasPrice.Uint64(),
GasUsed: txInfo.gasFeeCap.Uint64(),
BlockNumber: big.NewInt(int64(txInfo.blockNumber)),
}, nil
}
......@@ -189,15 +170,16 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
t.Parallel()
h := newTestHarness()
gasFeeCap := big.NewInt(5)
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{
GasPrice: gasPrice,
tx := types.NewTx(&types.DynamicFeeTx{
GasFeeCap: gasFeeCap,
})
txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice)
h.backend.mine(&txHash, gasFeeCap)
return tx, nil
}
......@@ -205,7 +187,7 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, receipt.GasUsed, h.cfg.MinGasPrice.Uint64())
require.Equal(t, gasFeeCap.Uint64(), receipt.GasUsed)
}
// TestTxMgrNeverConfirmCancel asserts that a Send can be canceled even if no
......@@ -218,11 +200,10 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
// Don't publish tx to backend, simulating never being mined.
return types.NewTx(&types.LegacyTx{
GasPrice: gasPrice,
return types.NewTx(&types.DynamicFeeTx{
GasFeeCap: big.NewInt(5),
}), nil
}
......@@ -236,21 +217,22 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
// TestTxMgrConfirmsAtMaxGasPrice asserts that Send properly returns the max gas
// price receipt if none of the lower gas price txs were mined.
func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) {
func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
t.Parallel()
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{
GasPrice: gasPrice,
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
if gasPrice.Cmp(h.cfg.MaxGasPrice) == 0 {
if shouldMine {
txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice)
h.backend.mine(&txHash, gasFeeCap)
}
return tx, nil
}
......@@ -259,40 +241,7 @@ func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, receipt.GasUsed, h.cfg.MaxGasPrice.Uint64())
}
// TestTxMgrConfirmsAtMaxGasPriceDelayed asserts that after the maximum gas
// price tx has been published, and a resubmission timeout has elapsed, that an
// error is returned signaling that even our max gas price is taking too long.
func TestTxMgrConfirmsAtMaxGasPriceDelayed(t *testing.T) {
t.Parallel()
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{
GasPrice: gasPrice,
})
// Delay mining of the max gas price tx by more than the
// resubmission timeout. Default config uses 1 second. Send
// should still return an error beforehand.
if gasPrice.Cmp(h.cfg.MaxGasPrice) == 0 {
time.AfterFunc(2*time.Second, func() {
txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice)
})
}
return tx, nil
}
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Equal(t, err, txmgr.ErrPublishTimeout)
require.Nil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// errRpcFailure is a sentinel error used in testing to fail publications.
......@@ -308,14 +257,15 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
return nil, errRpcFailure
}
ctx := context.Background()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Equal(t, err, txmgr.ErrPublishTimeout)
require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt)
}
......@@ -329,18 +279,20 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
// Fail all but the final attempt.
if gasPrice.Cmp(h.cfg.MaxGasPrice) != 0 {
if !shouldMine {
return nil, errRpcFailure
}
tx := types.NewTx(&types.LegacyTx{
GasPrice: gasPrice,
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice)
h.backend.mine(&txHash, gasFeeCap)
return tx, nil
}
......@@ -349,7 +301,7 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, receipt.GasUsed, h.cfg.MaxGasPrice.Uint64())
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestTxMgrConfirmsMinGasPriceAfterBumping delays the mining of the initial tx
......@@ -362,16 +314,17 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{
GasPrice: gasPrice,
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
// Delay mining the tx with the min gas price.
if gasPrice.Cmp(h.cfg.MinGasPrice) == 0 {
if shouldMine {
time.AfterFunc(5*time.Second, func() {
txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice)
h.backend.mine(&txHash, gasFeeCap)
})
}
return tx, nil
......@@ -381,7 +334,7 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, receipt.GasUsed, h.cfg.MinGasPrice.Uint64())
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestWaitMinedReturnsReceiptOnFirstSuccess insta-mines a transaction and
......
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