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,16 +158,16 @@ func SignClearingTx( ...@@ -158,16 +158,16 @@ func SignClearingTx(
func CraftClearingTx( func CraftClearingTx(
walletAddr common.Address, walletAddr common.Address,
nonce uint64, nonce uint64,
gasPrice *big.Int, gasFeeCap *big.Int,
gasLimit uint64, gasTipCap *big.Int,
) *types.Transaction { ) *types.Transaction {
return types.NewTx(&types.LegacyTx{ return types.NewTx(&types.DynamicFeeTx{
To: &walletAddr, To: &walletAddr,
Nonce: nonce, Nonce: nonce,
GasPrice: gasPrice, GasFeeCap: gasFeeCap,
Gas: gasLimit, GasTipCap: gasTipCap,
Value: nil, Value: nil,
Data: nil, Data: nil,
}) })
} }
...@@ -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
} }
This diff is collapsed.
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