Skip to content

Commit

Permalink
add FailingEthBackend mock
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasArrachea committed Nov 11, 2024
1 parent b46a766 commit 328beca
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 24 deletions.
3 changes: 2 additions & 1 deletion chainio/txmgr/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (m *SimpleTxManager) Send(

// SendWithRetry is used to send a transaction to the Ethereum node, same as Send but adding retry logic.
// If the transaction fails, it will retry sending the transaction until it gets a receipt, using
// exponential backoff with factor 2, starting with retryTimeout.
// **exponential backoff** with factor 2, starting with retryTimeout.
func (m *SimpleTxManager) SendWithRetry(
ctx context.Context,
tx *types.Transaction,
Expand All @@ -109,6 +109,7 @@ func (m *SimpleTxManager) SendWithRetry(
}
// if sending failed, backoff and try again
m.logger.Error("failed to send transaction", err)
m.logger.Debugf("waiting %f seconds for backoff", retryTimeout.Seconds())
time.Sleep(retryTimeout)
retryTimeout *= time.Duration(exponentialFactor)
}
Expand Down
109 changes: 86 additions & 23 deletions chainio/txmgr/simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,50 @@ package txmgr_test

import (
"context"
"fmt"
"math/big"
"os"
"testing"
"time"

"github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet"
"github.com/Layr-Labs/eigensdk-go/chainio/txmgr"
"github.com/Layr-Labs/eigensdk-go/logging"
"github.com/Layr-Labs/eigensdk-go/signerv2"
"github.com/Layr-Labs/eigensdk-go/testutils"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSendWithRetry(t *testing.T) {
ecdsaPrivKeyHex := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
ecdsaPrivateKey, err := crypto.HexToECDSA(ecdsaPrivKeyHex)
require.NoError(t, err)
func newTx() *types.Transaction {
return types.NewTx(&types.DynamicFeeTx{
ChainID: big.NewInt(31337),
Nonce: 0,
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(1_000_000_000),
Gas: 21000,
To: testutils.ZeroAddress(),
Value: big.NewInt(1),
})
}

testConfig := testutils.GetDefaultTestConfig()
func TestSendWithRetryWithNoError(t *testing.T) {
// Test SendWithRetry with a non-failing transaction to verify normal behavior
ecdsaPrivateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
require.NoError(t, err)
anvilC, err := testutils.StartAnvilContainer("")
require.NoError(t, err)
anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http")
require.NoError(t, err)
logger := logging.NewTextSLogger(os.Stdout, &logging.SLoggerOptions{Level: testConfig.LogLevel})
logger := testutils.NewTestLogger()

ethHttpClient, err := ethclient.Dial(anvilHttpEndpoint)
require.NoError(t, err)

rpcCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
chainid, err := ethHttpClient.ChainID(rpcCtx)
chainid, err := ethHttpClient.ChainID(context.Background())
require.NoError(t, err)

signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainid)
Expand All @@ -46,20 +56,73 @@ func TestSendWithRetry(t *testing.T) {

txMgr := txmgr.NewSimpleTxManager(pkWallet, ethHttpClient, logger, addr)

tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainid,
Nonce: 0,
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(1_000_000_000),
Gas: 21000,
To: testutils.ZeroAddress(),
Value: big.NewInt(1),
})
retryTimeout := 3 * time.Second // TODO: this is enough to fetch receipt on first attempt. If lower, fails with nonce too low. Fix it
tx := newTx()
retryTimeout := 2 * time.Second
maxRetries := uint32(10)

receipt, err := txMgr.SendWithRetry(context.Background(), tx, retryTimeout, maxRetries)
_, err = txMgr.SendWithRetry(context.Background(), tx, retryTimeout, maxRetries, 2)
require.NoError(t, err)
}

func TestSendWithRetryDoesBackoff(t *testing.T) {
// Test SendWithRetry using a FailingEthBackend to simulate errors when sending transactions
logger := testutils.NewTestLogger()
ethBackend := NewFailingEthBackend(3)

chainid := big.NewInt(31337)
ecdsaSk, _, err := testutils.NewEcdsaSkAndAddress()
require.NoError(t, err)

signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaSk}, chainid)
require.NoError(t, err)

pkWallet, err := wallet.NewPrivateKeyWallet(ethBackend, signerV2, addr, logger)
require.NoError(t, err)

txMgr := txmgr.NewSimpleTxManager(pkWallet, ethBackend, logger, addr)

tx := newTx()
retryTimeout := 2 * time.Second
maxRetries := uint32(10)

_, err = txMgr.SendWithRetry(context.Background(), tx, retryTimeout, maxRetries, 2)
require.NoError(t, err)
assert.Equal(t, ethBackend.pendingFailures, uint32(0))
}

// Mock of the EthBackend that returns an error when sending transactions.
// Once pendingFailures reaches zero, SendTransaction will no longer fail
type FailingEthBackend struct {
pendingFailures uint32
}

func NewFailingEthBackend(pendingFailures uint32) *FailingEthBackend {
backend := &FailingEthBackend{pendingFailures: pendingFailures}
return backend
}

func (s *FailingEthBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error {
if s.pendingFailures == 0 {
return nil
}
s.pendingFailures--
return fmt.Errorf("did not send tx")
}

func (s *FailingEthBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
return &types.Receipt{}, nil
}

func (s *FailingEthBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
return 0, nil
}

func (s *FailingEthBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
return &types.Header{
BaseFee: big.NewInt(0),
}, nil
}

t.Log(receipt)
func (s *FailingEthBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return big.NewInt(0), nil
}

0 comments on commit 328beca

Please sign in to comment.