Commit b5a91778 authored by Joshua Gutow's avatar Joshua Gutow Committed by GitHub

Merge pull request #4830 from ethereum-optimism/jg/txmgr_cleanup_pt2

txmgr: Simplify resubmit logic and scaffolding
parents eac9b3ae 3468c6af
......@@ -72,6 +72,8 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*BatchSubmitte
ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
From: fromAddress,
Signer: signer(rcfg.L1ChainID),
}
batcherCfg := Config{
......
......@@ -54,14 +54,10 @@ func (t *TransactionManager) SendTransaction(ctx context.Context, data []byte) (
if err != nil {
return nil, fmt.Errorf("failed to create tx: %w", err)
}
// Construct a closure that will update the txn with the current gas prices.
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
return t.UpdateGasPrice(ctx, tx)
}
ctx, cancel := context.WithTimeout(ctx, 100*time.Second) // TODO: Select a timeout that makes sense here.
defer cancel()
if receipt, err := t.txMgr.Send(ctx, updateGasPrice, t.l1Client.SendTransaction); err != nil {
if receipt, err := t.txMgr.Send(ctx, tx); err != nil {
t.log.Warn("unable to publish tx", "err", err, "data_size", len(data))
return nil, err
} else {
......@@ -135,29 +131,3 @@ func (t *TransactionManager) CraftTx(ctx context.Context, data []byte) (*types.T
tx := types.NewTx(rawTx)
return t.signerFn(ctx, t.senderAddress, tx)
}
// UpdateGasPrice signs an otherwise identical txn to the one provided but with
// updated gas prices sampled from the existing network conditions.
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (t *TransactionManager) UpdateGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
gasTipCap, gasFeeCap, err := t.calcGasTipAndFeeCap(ctx)
if err != nil {
return nil, err
}
rawTx := &types.DynamicFeeTx{
ChainID: t.chainID,
Nonce: tx.Nonce(),
To: tx.To(),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: tx.Gas(),
Data: tx.Data(),
}
// Only log the new tip/fee cap because the updateGasPrice closure reuses the same initial transaction
t.log.Trace("updating gas price", "tip_cap", gasTipCap, "fee_cap", gasFeeCap)
finalTx := types.NewTx(rawTx)
return t.signerFn(ctx, t.senderAddress, finalTx)
}
......@@ -50,6 +50,8 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
ReceiptQueryInterval: time.Second,
NumConfirmations: 1,
SafeAbortNonceTooLowCount: 4,
From: from,
// Signer is loaded in `proposer.NewL2OutputSubmitter`
},
L1Client: l1,
RollupClient: rollupCl,
......@@ -85,7 +87,9 @@ func (p *L2Proposer) ActMakeProposalTx(t Testing) {
tx, err := p.driver.CreateProposalTx(t.Ctx(), output)
require.NoError(t, err)
err = p.driver.SendTransaction(t.Ctx(), tx)
// Note: Use L1 instead of the output submitter's transaction manager because
// this is non-blocking while the txmgr is blocking & deadlocks the tests
err = p.l1.SendTransaction(t.Ctx(), tx)
require.NoError(t, err)
p.lastTx = tx.Hash()
......
......@@ -171,6 +171,7 @@ func NewL2OutputSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger) (*L2OutputSu
ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
From: fromAddress,
}
proposerCfg := Config{
......@@ -198,6 +199,8 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger) (*L2OutputSubmitter, error)
cancel()
return nil, err
}
signer := cfg.SignerFnFactory(chainID)
cfg.TxManagerConfig.Signer = signer
l2ooContract, err := bindings.NewL2OutputOracle(cfg.L2OutputOracleAddr, cfg.L1Client)
if err != nil {
......@@ -227,7 +230,7 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger) (*L2OutputSubmitter, error)
allowNonFinalized: cfg.AllowNonFinalized,
from: cfg.From,
signerFn: cfg.SignerFnFactory(chainID),
signerFn: signer,
pollInterval: cfg.PollInterval,
}, nil
}
......@@ -261,12 +264,6 @@ func (l *L2OutputSubmitter) UpdateGasPrice(ctx context.Context, tx *types.Transa
return l.rawL2ooContract.RawTransact(opts, tx.Data())
}
// SendTransaction injects a signed transaction into the pending pool for execution.
func (l *L2OutputSubmitter) SendTransaction(ctx context.Context, tx *types.Transaction) error {
l.log.Info("proposer sending transaction", "tx", tx.Hash())
return l.l1Client.SendTransaction(ctx, tx)
}
// FetchNextOutputInfo gets the block number of the next proposal.
// It returns: the next block number, if the proposal should be made, error
func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.OutputResponse, bool, error) {
......@@ -356,22 +353,15 @@ func (l *L2OutputSubmitter) CreateProposalTx(ctx context.Context, output *eth.Ou
return tx, nil
}
// SendTransactionExt sends a transaction through the transaction manager which handles automatic
// SendTransaction sends a transaction through the transaction manager which handles automatic
// price bumping.
// It also hardcodes a timeout of 100s.
func (l *L2OutputSubmitter) SendTransactionExt(ctx context.Context, tx *types.Transaction) error {
// Construct the closure that will update the txn with the current gas prices.
nonce := tx.Nonce()
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
l.log.Info("proposer updating batch tx gas price", "nonce", nonce)
return l.UpdateGasPrice(ctx, tx)
}
func (l *L2OutputSubmitter) SendTransaction(ctx context.Context, tx *types.Transaction) error {
// Wait until one of our submitted transactions confirms. If no
// receipt is received it's likely our gas price was too low.
cCtx, cancel := context.WithTimeout(ctx, 100*time.Second)
defer cancel()
receipt, err := l.txMgr.Send(cCtx, updateGasPrice, l.SendTransaction)
receipt, err := l.txMgr.Send(cCtx, tx)
if err != nil {
l.log.Error("proposer unable to publish tx", "err", err)
return err
......@@ -411,7 +401,7 @@ func (l *L2OutputSubmitter) loop() {
cancel()
break
}
if err := l.SendTransactionExt(cCtx, tx); err != nil {
if err := l.SendTransaction(cCtx, tx); err != nil {
l.log.Error("Failed to send proposal transaction", "err", err)
cancel()
break
......
......@@ -2,14 +2,17 @@ package txmgr
import (
"context"
"errors"
"math/big"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
)
// UpdateGasPriceSendTxFunc defines a function signature for publishing a
......@@ -40,6 +43,10 @@ type Config struct {
// are required to give up on a tx at a particular nonce without receiving
// confirmation.
SafeAbortNonceTooLowCount uint64
// Signer is used to sign transactions when the gas price is increased.
Signer opcrypto.SignerFn
From common.Address
}
// TxManager is an interface that allows callers to reliably publish txs,
......@@ -50,15 +57,15 @@ type TxManager interface {
// until an invocation of sendTx returns (called with differing gas
// prices). The method may be canceled using the passed context.
//
// The initial transaction MUST be signed & ready to submit.
//
// NOTE: Send should be called by AT MOST one caller at a time.
Send(ctx context.Context, updateGasPrice UpdateGasPriceFunc, sendTxn SendTransactionFunc) (*types.Receipt, error)
Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error)
}
// ReceiptSource is a minimal function signature used to detect the confirmation
// of published txs.
//
// NOTE: This is a subset of bind.DeployBackend.
type ReceiptSource interface {
// ETHBackend is the set of methods that the transaction manager uses to resubmit gas & determine
// when transactions are included on L1.
type ETHBackend interface {
// BlockNumber returns the most recent block number.
BlockNumber(ctx context.Context) (uint64, error)
......@@ -66,6 +73,14 @@ type ReceiptSource interface {
// txHash. If lookup does not fail, but the transaction is not found,
// nil should be returned for both values.
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
// SendTransaction submits a signed transaction to L1.
SendTransaction(ctx context.Context, tx *types.Transaction) error
// These functions are used to estimate what the basefee & priority fee should be set to.
// TODO(CLI-3318): Maybe need a generic interface to support different RPC providers
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
SuggestGasTipCap(ctx context.Context) (*big.Int, error)
}
// SimpleTxManager is a implementation of TxManager that performs linear fee
......@@ -74,12 +89,55 @@ type SimpleTxManager struct {
Config // embed the config directly
name string
backend ReceiptSource
backend ETHBackend
l log.Logger
}
// IncreaseGasPrice takes the previous transaction & potentially clones then signs it with a higher tip.
// If the basefee + priority fee did not increase by a minimum percent (geth's replacement percent) an
// error will be returned.
// We do not re-estimate the amount of gas used because for some stateful transactions (like output proposals) the
// act of including the transaction renders the repeat of the transaction invalid.
func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var gasTipCap, gasFeeCap *big.Int
if tip, err := m.backend.SuggestGasTipCap(ctx); err != nil {
return nil, err
} else if tip == nil {
return nil, errors.New("the suggested tip was nil")
} else {
gasTipCap = tip
}
if head, err := m.backend.HeaderByNumber(ctx, nil); err != nil {
return nil, err
} else if head.BaseFee == nil {
return nil, errors.New("txmgr does not support pre-london blocks that do not have a basefee")
} else {
gasFeeCap = CalcGasFeeCap(head.BaseFee, gasTipCap)
}
// TODO (CLI-2630): Check for a large enough price bump
rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(),
Nonce: tx.Nonce(),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: tx.Gas(),
To: tx.To(),
Value: tx.Value(),
Data: tx.Data(),
AccessList: tx.AccessList(),
}
return m.Signer(ctx, m.From, types.NewTx(rawTx))
}
// NewSimpleTxManager initializes a new SimpleTxManager with the passed Config.
func NewSimpleTxManager(name string, l log.Logger, cfg Config, backend ReceiptSource) *SimpleTxManager {
func NewSimpleTxManager(name string, l log.Logger, cfg Config, backend ETHBackend) *SimpleTxManager {
if cfg.NumConfirmations == 0 {
panic("txmgr: NumConfirmations cannot be zero")
}
......@@ -97,8 +155,12 @@ func NewSimpleTxManager(name string, l log.Logger, cfg Config, backend ReceiptSo
// invocation of sendTx returns (called with differing gas prices). The method
// may be canceled using the passed context.
//
// The initially supplied transaction must be signed, have gas estimation done, and have a reasonable gas fee.
// When the transaction is resubmitted the tx manager will re-sign the transaction at a different gas pricing
// but retain the gas used, the nonce, and the data.
//
// NOTE: Send should be called by AT MOST one caller at a time.
func (m *SimpleTxManager) Send(ctx context.Context, updateGasPrice UpdateGasPriceFunc, sendTx SendTransactionFunc) (*types.Receipt, error) {
func (m *SimpleTxManager) Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) {
// Initialize a wait group to track any spawned goroutines, and ensure
// we properly clean up any dangling resources this method generates.
......@@ -114,22 +176,13 @@ func (m *SimpleTxManager) Send(ctx context.Context, updateGasPrice UpdateGasPric
sendState := NewSendState(m.SafeAbortNonceTooLowCount)
// Create a closure that will block on passed sendTx function in the
// Create a closure that will block on submitting the tx in the
// background, returning the first successfully mined receipt back to
// the main event loop via receiptChan.
receiptChan := make(chan *types.Receipt, 1)
sendTxAsync := func() {
sendTxAsync := func(tx *types.Transaction) {
defer wg.Done()
tx, err := updateGasPrice(ctx)
if err != nil {
if err == context.Canceled || strings.Contains(err.Error(), "context canceled") {
return
}
m.l.Error("unable to update txn gas price", "err", err)
return
}
txHash := tx.Hash()
nonce := tx.Nonce()
gasTipCap := tx.GasTipCap()
......@@ -137,12 +190,14 @@ func (m *SimpleTxManager) Send(ctx context.Context, updateGasPrice UpdateGasPric
log := m.l.New("txHash", txHash, "nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
log.Info("publishing transaction")
// Sign and publish transaction with current gas price.
err = sendTx(ctx, tx)
err := m.backend.SendTransaction(ctx, tx)
sendState.ProcessSendError(err)
if err != nil {
if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") {
if errors.Is(err, context.Canceled) {
return
}
if errors.Is(err, txpool.ErrAlreadyKnown) {
log.Info("resubmitted already known transaction")
return
}
log.Error("unable to publish transaction", "err", err)
......@@ -177,7 +232,7 @@ func (m *SimpleTxManager) Send(ctx context.Context, updateGasPrice UpdateGasPric
// background, before entering the event loop and waiting out the
// resubmission timeout.
wg.Add(1)
go sendTxAsync()
go sendTxAsync(tx)
ticker := time.NewTicker(m.ResubmissionTimeout)
defer ticker.Stop()
......@@ -196,9 +251,17 @@ func (m *SimpleTxManager) Send(ctx context.Context, updateGasPrice UpdateGasPric
continue
}
// Submit and wait for the bumped traction to confirm.
// Increase the gas price & submit the new transaction
newTx, err := m.IncreaseGasPrice(ctx, tx)
if err != nil {
m.l.Error("Failed to increase the gas price for the tx", "err", err)
// Don't `continue` here so we resubmit the transaction with the same gas price.
} else {
// Save the tx so we know it's gas price.
tx = newTx
}
wg.Add(1)
go sendTxAsync()
go sendTxAsync(tx)
// The passed context has been canceled, i.e. in the event of a
// shutdown.
......@@ -235,7 +298,7 @@ func (m *SimpleTxManager) waitMined(ctx context.Context, tx *types.Transaction,
break
}
m.l.Trace("Transaction mined, checking confirmations", "txHash", txHash, "txHeight", txHeight,
m.l.Debug("Transaction mined, checking confirmations", "txHash", txHash, "txHeight", txHeight,
"tipHeight", tipHeight, "numConfirmations", m.NumConfirmations)
// The transaction is considered confirmed when
......@@ -252,7 +315,7 @@ func (m *SimpleTxManager) waitMined(ctx context.Context, tx *types.Transaction,
// Safe to subtract since we know the LHS above is greater.
confsRemaining := (txHeight + m.NumConfirmations) - (tipHeight + 1)
m.l.Info("Transaction not yet confirmed", "txHash", txHash, "confsRemaining", confsRemaining)
m.l.Debug("Transaction not yet confirmed", "txHash", txHash, "confsRemaining", confsRemaining)
case err != nil:
m.l.Trace("Receipt retrievel failed", "hash", txHash, "err", err)
......@@ -266,6 +329,7 @@ func (m *SimpleTxManager) waitMined(ctx context.Context, tx *types.Transaction,
select {
case <-ctx.Done():
m.l.Warn("context cancelled in waitMined")
return nil, ctx.Err()
case <-queryTicker.C:
}
......
......@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
......@@ -28,14 +29,15 @@ type testHarness struct {
// newTestHarnessWithConfig initializes a testHarness with a specific
// configuration.
func newTestHarnessWithConfig(t *testing.T, cfg Config) *testHarness {
backend := newMockBackend()
g := newGasPricer(3)
backend := newMockBackend(g)
mgr := NewSimpleTxManager("TEST", testlog.Logger(t, log.LvlCrit), cfg, backend)
return &testHarness{
cfg: cfg,
mgr: mgr,
backend: backend,
gasPricer: newGasPricer(3),
gasPricer: g,
}
}
......@@ -51,6 +53,10 @@ func configWithNumConfs(numConfirmations uint64) Config {
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
SafeAbortNonceTooLowCount: 3,
Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
return tx, nil
},
From: common.Address{},
}
}
......@@ -87,6 +93,12 @@ func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
return epochGasTipCap, epochGasFeeCap
}
func (g *gasPricer) basefee() *big.Int {
g.mu.Lock()
defer g.mu.Unlock()
return new(big.Int).Mul(g.baseBaseFee, big.NewInt(g.epoch))
}
func (g *gasPricer) sample() (*big.Int, *big.Int) {
g.mu.Lock()
defer g.mu.Unlock()
......@@ -107,6 +119,9 @@ type minedTxInfo struct {
type mockBackend struct {
mu sync.RWMutex
g *gasPricer
send SendTransactionFunc
// blockHeight tracks the current height of the chain.
blockHeight uint64
......@@ -115,12 +130,18 @@ type mockBackend struct {
}
// newMockBackend initializes a new mockBackend.
func newMockBackend() *mockBackend {
func newMockBackend(g *gasPricer) *mockBackend {
return &mockBackend{
g: g,
minedTxs: make(map[common.Hash]minedTxInfo),
}
}
// setTxSender sets the implementation for the SendTransactionFunction
func (b *mockBackend) setTxSender(s SendTransactionFunc) {
b.send = s
}
// 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.
......@@ -145,14 +166,30 @@ func (b *mockBackend) BlockNumber(ctx context.Context) (uint64, error) {
return b.blockHeight, nil
}
func (b *mockBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
return &types.Header{
BaseFee: b.g.basefee(),
}, nil
}
func (b *mockBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
tip, _ := b.g.sample()
return tip, nil
}
func (b *mockBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error {
if b.send == nil {
panic("set sender function was not set")
}
return b.send(ctx, tx)
}
// 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 gasFeeCap used in the GasUsed to make
// the value accessible from our test framework.
func (b *mockBackend) TransactionReceipt(
ctx context.Context,
txHash common.Hash,
) (*types.Receipt, error) {
func (b *mockBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
b.mu.RLock()
defer b.mu.RUnlock()
......@@ -180,13 +217,11 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
gasPricer := newGasPricer(1)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if gasPricer.shouldMine(tx.GasFeeCap()) {
......@@ -195,9 +230,11 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
}
return nil
}
h.backend.setTxSender(sendTx)
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, tx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
......@@ -211,23 +248,21 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
h := newTestHarness(t)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Don't publish tx to backend, simulating never being mined.
return nil
}
h.backend.setTxSender(sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
receipt, err := h.mgr.Send(ctx, tx)
require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt)
}
......@@ -239,14 +274,11 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
h := newTestHarness(t)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if h.gasPricer.shouldMine(tx.GasFeeCap()) {
txHash := tx.Hash()
......@@ -254,9 +286,11 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
}
return nil
}
h.backend.setTxSender(sendTx)
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, tx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
......@@ -273,22 +307,21 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
h := newTestHarness(t)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
return errRpcFailure
}
h.backend.setTxSender(sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
receipt, err := h.mgr.Send(ctx, tx)
require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt)
}
......@@ -301,13 +334,11 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
h := newTestHarness(t)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Fail all but the final attempt.
......@@ -319,9 +350,11 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
h.backend.mine(&txHash, tx.GasFeeCap())
return nil
}
h.backend.setTxSender(sendTx)
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, tx)
require.Nil(t, err)
require.NotNil(t, receipt)
......@@ -336,13 +369,11 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
h := newTestHarness(t)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Delay mining the tx with the min gas price.
......@@ -354,9 +385,11 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
}
return nil
}
h.backend.setTxSender(sendTx)
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, tx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
......@@ -368,13 +401,11 @@ func TestTxMgrDoesntAbortNonceTooLowAfterMiningTx(t *testing.T) {
h := newTestHarnessWithConfig(t, configWithNumConfs(2))
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
gasTipCap, gasFeeCap := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
sendTx := func(ctx context.Context, tx *types.Transaction) error {
switch {
......@@ -399,9 +430,11 @@ func TestTxMgrDoesntAbortNonceTooLowAfterMiningTx(t *testing.T) {
return core.ErrNonceTooLow
}
}
h.backend.setTxSender(sendTx)
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, tx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
......@@ -419,7 +452,8 @@ func TestWaitMinedReturnsReceiptOnFirstSuccess(t *testing.T) {
txHash := tx.Hash()
h.backend.mine(&txHash, new(big.Int))
ctx := context.Background()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
receipt, err := h.mgr.waitMined(ctx, tx, nil)
require.Nil(t, err)
require.NotNil(t, receipt)
......@@ -433,7 +467,7 @@ func TestWaitMinedCanBeCanceled(t *testing.T) {
h := newTestHarness(t)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Create an unimined tx.
......@@ -524,6 +558,18 @@ func (b *failingBackend) TransactionReceipt(
}, nil
}
func (b *failingBackend) HeaderByNumber(_ context.Context, _ *big.Int) (*types.Header, error) {
return nil, ethereum.NotFound
}
func (b *failingBackend) SendTransaction(_ context.Context, _ *types.Transaction) error {
return errors.New("unimplemented")
}
func (b *failingBackend) SuggestGasTipCap(_ context.Context) (*big.Int, error) {
return nil, errors.New("unimplemented")
}
// TestWaitMinedReturnsReceiptAfterFailure asserts that WaitMined is able to
// recover from failed calls to the backend. It uses the failedBackend to
// simulate an rpc call failure, followed by the successful return of a receipt.
......@@ -549,7 +595,8 @@ func TestWaitMinedReturnsReceiptAfterFailure(t *testing.T) {
tx := types.NewTx(&types.LegacyTx{})
txHash := tx.Hash()
ctx := context.Background()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
receipt, err := mgr.waitMined(ctx, tx, nil)
require.Nil(t, err)
require.NotNil(t, receipt)
......
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