Commit c1eba2e6 authored by Conner Fromknecht's avatar Conner Fromknecht

feat: modify txmgr to send EIP-1559 txns

parent 1b2897d3
---
'@eth-optimism/batch-submitter-service': patch
---
use EIP-1559 txns for tx/state batches
...@@ -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, 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
} }
} }
...@@ -130,23 +130,23 @@ func SignClearingTx( ...@@ -130,23 +130,23 @@ func SignClearingTx(
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, return nil, err
Value: nil, }
Data: nil,
}) 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,15 +158,15 @@ func SignClearingTx( ...@@ -158,15 +158,15 @@ 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,
}) })
......
...@@ -12,7 +12,6 @@ import ( ...@@ -12,7 +12,6 @@ import (
"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-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 +26,6 @@ func init() { ...@@ -27,8 +26,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 +33,22 @@ var ( ...@@ -36,21 +33,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 +57,31 @@ func TestCraftClearingTx(t *testing.T) { ...@@ -59,21 +57,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, context.Background(), testWalletAddr, testNonce, l1Client, testPrivKey,
testPrivKey, testChainID, 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 +91,44 @@ func TestSignClearingTxEstimateGasSuccess(t *testing.T) { ...@@ -83,22 +91,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, context.Background(), testWalletAddr, testNonce, l1Client, testPrivKey,
testPrivKey, testChainID, testChainID,
)
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(
context.Background(), testWalletAddr, testNonce, l1Client, testPrivKey,
testChainID,
) )
require.Equal(t, errEstimateGas, err) require.Equal(t, errHeaderByNumber, err)
require.Nil(t, tx) require.Nil(t, tx)
} }
...@@ -117,14 +147,21 @@ func newClearPendingTxHarnessWithNumConfs( ...@@ -117,14 +147,21 @@ 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
} }
} }
...@@ -200,11 +237,14 @@ func TestClearPendingTxTimeout(t *testing.T) { ...@@ -200,11 +237,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 +265,15 @@ func TestClearPendingTxMultipleConfs(t *testing.T) { ...@@ -225,12 +265,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)
......
...@@ -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,7 +196,6 @@ func (d *Driver) CraftBatchTx( ...@@ -197,7 +196,6 @@ 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)
...@@ -206,13 +204,12 @@ func (d *Driver) CraftBatchTx( ...@@ -206,13 +204,12 @@ func (d *Driver) CraftBatchTx(
return d.sccContract.AppendStateBatch(opts, stateRoots, offsetStartsAtIndex) return d.sccContract.AppendStateBatch(opts, stateRoots, offsetStartsAtIndex)
} }
// 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 +220,6 @@ func (d *Driver) SubmitBatchTx( ...@@ -223,7 +220,6 @@ 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()) return d.rawSccContract.RawTransact(opts, tx.Data())
} }
...@@ -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,7 +232,6 @@ func (d *Driver) CraftBatchTx( ...@@ -233,7 +232,6 @@ 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) return d.rawCtcContract.RawTransact(opts, batchCallData)
...@@ -241,12 +239,11 @@ func (d *Driver) CraftBatchTx( ...@@ -241,12 +239,11 @@ func (d *Driver) CraftBatchTx(
} }
// 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 +254,6 @@ func (d *Driver) SubmitBatchTx( ...@@ -257,7 +254,6 @@ 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()) return d.rawCtcContract.RawTransact(opts, tx.Data())
} }
...@@ -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,27 +2,21 @@ package txmgr ...@@ -2,27 +2,21 @@ 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 {
...@@ -135,25 +129,29 @@ func (m *SimpleTxManager) Send( ...@@ -135,25 +129,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 +161,7 @@ func (m *SimpleTxManager) Send( ...@@ -163,7 +161,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 +169,17 @@ func (m *SimpleTxManager) Send( ...@@ -171,20 +169,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 +187,9 @@ func (m *SimpleTxManager) Send( ...@@ -192,24 +187,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 +203,13 @@ func (m *SimpleTxManager) Send( ...@@ -223,6 +203,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 +276,12 @@ func WaitMined( ...@@ -289,17 +276,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
} }
...@@ -14,69 +14,12 @@ import ( ...@@ -14,69 +14,12 @@ import (
"github.com/stretchr/testify/require" "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. // testHarness houses the necessary resources to test the SimpleTxManager.
type testHarness struct { type testHarness struct {
cfg txmgr.Config cfg txmgr.Config
mgr txmgr.TxManager mgr txmgr.TxManager
backend *mockBackend backend *mockBackend
gasPricer *gasPricer
} }
// newTestHarnessWithConfig initializes a testHarness with a specific // newTestHarnessWithConfig initializes a testHarness with a specific
...@@ -89,6 +32,7 @@ func newTestHarnessWithConfig(cfg txmgr.Config) *testHarness { ...@@ -89,6 +32,7 @@ func newTestHarnessWithConfig(cfg txmgr.Config) *testHarness {
cfg: cfg, cfg: cfg,
mgr: mgr, mgr: mgr,
backend: backend, backend: backend,
gasPricer: newGasPricer(3),
} }
} }
...@@ -109,8 +53,48 @@ func configWithNumConfs(numConfirmations uint64) txmgr.Config { ...@@ -109,8 +53,48 @@ func configWithNumConfs(numConfirmations uint64) txmgr.Config {
} }
} }
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 { type minedTxInfo struct {
gasPrice *big.Int gasFeeCap *big.Int
blockNumber uint64 blockNumber uint64
} }
...@@ -133,17 +117,17 @@ func newMockBackend() *mockBackend { ...@@ -133,17 +117,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. // 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. // 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() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
b.blockHeight++ b.blockHeight++
if txHash != nil { if txHash != nil {
b.minedTxs[*txHash] = minedTxInfo{ b.minedTxs[*txHash] = minedTxInfo{
gasPrice: gasPrice, gasFeeCap: gasFeeCap,
blockNumber: b.blockHeight, blockNumber: b.blockHeight,
} }
} }
...@@ -159,7 +143,7 @@ func (b *mockBackend) BlockNumber(ctx context.Context) (uint64, error) { ...@@ -159,7 +143,7 @@ func (b *mockBackend) BlockNumber(ctx context.Context) (uint64, error) {
// TransactionReceipt queries the mockBackend for a mined txHash. If none is // TransactionReceipt queries the mockBackend for a mined txHash. If none is
// found, nil is returned for both return values. Otherwise, it retruns a // 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. // the value accessible from our test framework.
func (b *mockBackend) TransactionReceipt( func (b *mockBackend) TransactionReceipt(
ctx context.Context, ctx context.Context,
...@@ -174,11 +158,11 @@ func (b *mockBackend) TransactionReceipt( ...@@ -174,11 +158,11 @@ func (b *mockBackend) TransactionReceipt(
return nil, nil 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. // we can assert the proper tx confirmed in our tests.
return &types.Receipt{ return &types.Receipt{
TxHash: txHash, TxHash: txHash,
GasUsed: txInfo.gasPrice.Uint64(), GasUsed: txInfo.gasFeeCap.Uint64(),
BlockNumber: big.NewInt(int64(txInfo.blockNumber)), BlockNumber: big.NewInt(int64(txInfo.blockNumber)),
}, nil }, nil
} }
...@@ -189,15 +173,16 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) { ...@@ -189,15 +173,16 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
t.Parallel() t.Parallel()
h := newTestHarness() h := newTestHarness()
gasFeeCap := big.NewInt(5)
sendTxFunc := func( sendTxFunc := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{ tx := types.NewTx(&types.DynamicFeeTx{
GasPrice: gasPrice, GasFeeCap: gasFeeCap,
}) })
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice) h.backend.mine(&txHash, gasFeeCap)
return tx, nil return tx, nil
} }
...@@ -205,7 +190,7 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) { ...@@ -205,7 +190,7 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) 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 // TestTxMgrNeverConfirmCancel asserts that a Send can be canceled even if no
...@@ -218,11 +203,10 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) { ...@@ -218,11 +203,10 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
sendTxFunc := func( sendTxFunc := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
// Don't publish tx to backend, simulating never being mined. // Don't publish tx to backend, simulating never being mined.
return types.NewTx(&types.LegacyTx{ return types.NewTx(&types.DynamicFeeTx{
GasPrice: gasPrice, GasFeeCap: big.NewInt(5),
}), nil }), nil
} }
...@@ -236,21 +220,22 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) { ...@@ -236,21 +220,22 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
// TestTxMgrConfirmsAtMaxGasPrice asserts that Send properly returns the max gas // TestTxMgrConfirmsAtMaxGasPrice asserts that Send properly returns the max gas
// price receipt if none of the lower gas price txs were mined. // price receipt if none of the lower gas price txs were mined.
func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) { func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
t.Parallel() t.Parallel()
h := newTestHarness() h := newTestHarness()
sendTxFunc := func( sendTxFunc := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{ gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
GasPrice: gasPrice, tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}) })
if gasPrice.Cmp(h.cfg.MaxGasPrice) == 0 { if shouldMine {
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice) h.backend.mine(&txHash, gasFeeCap)
} }
return tx, nil return tx, nil
} }
...@@ -259,40 +244,7 @@ func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) { ...@@ -259,40 +244,7 @@ func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
require.Equal(t, receipt.GasUsed, h.cfg.MaxGasPrice.Uint64()) require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// 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)
} }
// errRpcFailure is a sentinel error used in testing to fail publications. // errRpcFailure is a sentinel error used in testing to fail publications.
...@@ -308,14 +260,15 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) { ...@@ -308,14 +260,15 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
sendTxFunc := func( sendTxFunc := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
return nil, errRpcFailure return nil, errRpcFailure
} }
ctx := context.Background() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Equal(t, err, txmgr.ErrPublishTimeout) require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt) require.Nil(t, receipt)
} }
...@@ -329,18 +282,20 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) { ...@@ -329,18 +282,20 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
sendTxFunc := func( sendTxFunc := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
// Fail all but the final attempt. // Fail all but the final attempt.
if gasPrice.Cmp(h.cfg.MaxGasPrice) != 0 { if !shouldMine {
return nil, errRpcFailure return nil, errRpcFailure
} }
tx := types.NewTx(&types.LegacyTx{ tx := types.NewTx(&types.DynamicFeeTx{
GasPrice: gasPrice, GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}) })
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice) h.backend.mine(&txHash, gasFeeCap)
return tx, nil return tx, nil
} }
...@@ -349,7 +304,7 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) { ...@@ -349,7 +304,7 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) 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 // TestTxMgrConfirmsMinGasPriceAfterBumping delays the mining of the initial tx
...@@ -362,16 +317,17 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) { ...@@ -362,16 +317,17 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
sendTxFunc := func( sendTxFunc := func(
ctx context.Context, ctx context.Context,
gasPrice *big.Int,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
tx := types.NewTx(&types.LegacyTx{ gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
GasPrice: gasPrice, tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}) })
// Delay mining the tx with the min gas price. // Delay mining the tx with the min gas price.
if gasPrice.Cmp(h.cfg.MinGasPrice) == 0 { if shouldMine {
time.AfterFunc(5*time.Second, func() { time.AfterFunc(5*time.Second, func() {
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, gasPrice) h.backend.mine(&txHash, gasFeeCap)
}) })
} }
return tx, nil return tx, nil
...@@ -381,7 +337,7 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) { ...@@ -381,7 +337,7 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) 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 // TestWaitMinedReturnsReceiptOnFirstSuccess insta-mines a transaction and
......
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