package sender import ( "context" "crypto/ecdsa" "errors" "math/big" "strconv" "testing" "github.com/agiledragon/gomonkey/v2" "github.com/scroll-tech/go-ethereum/accounts/abi/bind" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/crypto" "github.com/scroll-tech/go-ethereum/ethclient" "github.com/scroll-tech/go-ethereum/rpc" "github.com/stretchr/testify/assert" "scroll-tech/common/docker" "scroll-tech/rollup/internal/config" ) const TXBatch = 50 var ( privateKey *ecdsa.PrivateKey cfg *config.Config base *docker.App txTypes = []string{"LegacyTx", "AccessListTx", "DynamicFeeTx"} ) func TestMain(m *testing.M) { base = docker.NewDockerApp() m.Run() base.Free() } func setupEnv(t *testing.T) { var err error cfg, err = config.NewConfig("../../../conf/config.json") assert.NoError(t, err) base.RunL1Geth(t) priv, err := crypto.HexToECDSA("1212121212121212121212121212121212121212121212121212121212121212") assert.NoError(t, err) // Load default private key. privateKey = priv cfg.L1Config.RelayerConfig.SenderConfig.Endpoint = base.L1gethImg.Endpoint() cfg.L1Config.RelayerConfig.SenderConfig.CheckBalanceTime = 1 } func TestSender(t *testing.T) { // Setup setupEnv(t) t.Run("test new sender", testNewSender) t.Run("test pending limit", testPendLimit) t.Run("test fallback gas limit", testFallbackGasLimit) t.Run("test resubmit zero gas price transaction", testResubmitZeroGasPriceTransaction) t.Run("test resubmit non-zero gas price transaction", testResubmitNonZeroGasPriceTransaction) t.Run("test resubmit under priced transaction", testResubmitUnderpricedTransaction) t.Run("test resubmit transaction with rising base fee", testResubmitTransactionWithRisingBaseFee) t.Run("test check pending transaction", testCheckPendingTransaction) } func testNewSender(t *testing.T) { for _, txType := range txTypes { // exit by Stop() cfgCopy1 := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy1.TxType = txType newSender1, err := NewSender(context.Background(), &cfgCopy1, privateKey, "test", "test", nil) assert.NoError(t, err) newSender1.Stop() // exit by ctx.Done() cfgCopy2 := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy2.TxType = txType subCtx, cancel := context.WithCancel(context.Background()) _, err = NewSender(subCtx, &cfgCopy2, privateKey, "test", "test", nil) assert.NoError(t, err) cancel() } } func testPendLimit(t *testing.T) { for _, txType := range txTypes { cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.Confirmations = rpc.LatestBlockNumber cfgCopy.PendingLimit = 2 newSender, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) for i := 0; i < 2*newSender.PendingLimit(); i++ { _, err = newSender.SendTransaction(strconv.Itoa(i), &common.Address{}, big.NewInt(1), nil, 0) assert.True(t, err == nil || (err != nil && err.Error() == "sender's pending pool is full")) } assert.True(t, newSender.PendingCount() <= newSender.PendingLimit()) newSender.Stop() } } func testFallbackGasLimit(t *testing.T) { for _, txType := range txTypes { cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.Confirmations = rpc.LatestBlockNumber s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) client, err := ethclient.Dial(cfgCopy.Endpoint) assert.NoError(t, err) // FallbackGasLimit = 0 txHash0, err := s.SendTransaction("0", &common.Address{}, big.NewInt(1), nil, 0) assert.NoError(t, err) tx0, _, err := client.TransactionByHash(context.Background(), txHash0) assert.NoError(t, err) assert.Greater(t, tx0.Gas(), uint64(0)) // FallbackGasLimit = 100000 patchGuard := gomonkey.ApplyPrivateMethod(s, "estimateGasLimit", func(opts *bind.TransactOpts, contract *common.Address, input []byte, gasPrice, gasTipCap, gasFeeCap, value *big.Int) (uint64, error) { return 0, errors.New("estimateGasLimit error") }, ) txHash1, err := s.SendTransaction("1", &common.Address{}, big.NewInt(1), nil, 100000) assert.NoError(t, err) tx1, _, err := client.TransactionByHash(context.Background(), txHash1) assert.NoError(t, err) assert.Equal(t, uint64(100000), tx1.Gas()) s.Stop() patchGuard.Reset() } } func testResubmitZeroGasPriceTransaction(t *testing.T) { for _, txType := range txTypes { cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(0), gasTipCap: big.NewInt(0), gasFeeCap: big.NewInt(0), gasLimit: 50000, } tx, err := s.createAndSendTx(s.auth, feeData, &common.Address{}, big.NewInt(0), nil, nil) assert.NoError(t, err) assert.NotNil(t, tx) // Increase at least 1 wei in gas price, gas tip cap and gas fee cap. _, err = s.resubmitTransaction(feeData, s.auth, tx) assert.NoError(t, err) s.Stop() } } func testResubmitNonZeroGasPriceTransaction(t *testing.T) { for _, txType := range txTypes { cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig // Bump gas price, gas tip cap and gas fee cap just touch the minimum threshold of 10% (default config of geth). cfgCopy.EscalateMultipleNum = 110 cfgCopy.EscalateMultipleDen = 100 cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(100000), gasTipCap: big.NewInt(100000), gasFeeCap: big.NewInt(100000), gasLimit: 50000, } tx, err := s.createAndSendTx(s.auth, feeData, &common.Address{}, big.NewInt(0), nil, nil) assert.NoError(t, err) assert.NotNil(t, tx) _, err = s.resubmitTransaction(feeData, s.auth, tx) assert.NoError(t, err) s.Stop() } } func testResubmitUnderpricedTransaction(t *testing.T) { for _, txType := range txTypes { cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig // Bump gas price, gas tip cap and gas fee cap less than 10% (default config of geth). cfgCopy.EscalateMultipleNum = 109 cfgCopy.EscalateMultipleDen = 100 cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(100000), gasTipCap: big.NewInt(100000), gasFeeCap: big.NewInt(100000), gasLimit: 50000, } tx, err := s.createAndSendTx(s.auth, feeData, &common.Address{}, big.NewInt(0), nil, nil) assert.NoError(t, err) assert.NotNil(t, tx) _, err = s.resubmitTransaction(feeData, s.auth, tx) assert.Error(t, err, "replacement transaction underpriced") s.Stop() } } func testResubmitTransactionWithRisingBaseFee(t *testing.T) { txType := "DynamicFeeTx" cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) tx := types.NewTransaction(s.auth.Nonce.Uint64(), common.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) s.baseFeePerGas = 1000 feeData, err := s.getFeeData(s.auth, &common.Address{}, big.NewInt(0), nil, 0) assert.NoError(t, err) // bump the basefee by 10x s.baseFeePerGas *= 10 // resubmit and check that the gas fee has been adjusted accordingly newTx, err := s.resubmitTransaction(feeData, s.auth, tx) assert.NoError(t, err) escalateMultipleNum := new(big.Int).SetUint64(s.config.EscalateMultipleNum) escalateMultipleDen := new(big.Int).SetUint64(s.config.EscalateMultipleDen) maxGasPrice := new(big.Int).SetUint64(s.config.MaxGasPrice) adjBaseFee := new(big.Int) adjBaseFee.SetUint64(s.baseFeePerGas) adjBaseFee = adjBaseFee.Mul(adjBaseFee, escalateMultipleNum) adjBaseFee = adjBaseFee.Div(adjBaseFee, escalateMultipleDen) expectedGasFeeCap := new(big.Int).Add(feeData.gasTipCap, adjBaseFee) if expectedGasFeeCap.Cmp(maxGasPrice) > 0 { expectedGasFeeCap = maxGasPrice } assert.Equal(t, expectedGasFeeCap.Int64(), newTx.GasFeeCap().Int64()) s.Stop() } func testCheckPendingTransaction(t *testing.T) { for _, txType := range txTypes { cfgCopy := *cfg.L1Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, privateKey, "test", "test", nil) assert.NoError(t, err) header := &types.Header{Number: big.NewInt(100), BaseFee: big.NewInt(100)} confirmed := uint64(100) receipt := &types.Receipt{Status: types.ReceiptStatusSuccessful, BlockNumber: big.NewInt(90)} tx := types.NewTransaction(s.auth.Nonce.Uint64(), common.Address{}, big.NewInt(0), 0, big.NewInt(0), nil) testCases := []struct { name string receipt *types.Receipt receiptErr error resubmitErr error expectedCount int expectedFound bool }{ { name: "Normal case, transaction receipt exists and successful", receipt: receipt, receiptErr: nil, resubmitErr: nil, expectedCount: 0, expectedFound: false, }, { name: "Resubmit case, resubmitTransaction error (not nonce) case", receipt: receipt, receiptErr: errors.New("receipt error"), resubmitErr: errors.New("resubmit error"), expectedCount: 1, expectedFound: true, }, { name: "Resubmit case, resubmitTransaction success case", receipt: receipt, receiptErr: errors.New("receipt error"), resubmitErr: nil, expectedCount: 1, expectedFound: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var c *ethclient.Client patchGuard := gomonkey.ApplyMethodFunc(c, "TransactionReceipt", func(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { return tc.receipt, tc.receiptErr }) patchGuard.ApplyPrivateMethod(s, "resubmitTransaction", func(feeData *FeeData, auth *bind.TransactOpts, tx *types.Transaction) (*types.Transaction, error) { return tx, tc.resubmitErr }, ) pendingTx := &PendingTransaction{id: "abc", tx: tx, signer: s.auth, submitAt: header.Number.Uint64() - s.config.EscalateBlocks - 1} s.pendingTxs.Set(pendingTx.id, pendingTx) s.checkPendingTransaction(header, confirmed) if tc.receiptErr == nil { expectedConfirmation := &Confirmation{ ID: pendingTx.id, IsSuccessful: tc.receipt.Status == types.ReceiptStatusSuccessful, TxHash: pendingTx.tx.Hash(), } actualConfirmation := <-s.confirmCh assert.Equal(t, expectedConfirmation, actualConfirmation) } if tc.expectedFound && tc.resubmitErr == nil { actualPendingTx, found := s.pendingTxs.Get(pendingTx.id) assert.Equal(t, true, found) assert.Equal(t, header.Number.Uint64(), actualPendingTx.submitAt) } _, found := s.pendingTxs.Get(pendingTx.id) assert.Equal(t, tc.expectedFound, found) assert.Equal(t, tc.expectedCount, s.pendingTxs.Count()) patchGuard.Reset() }) } s.Stop() } }