mirror of
https://github.com/scroll-tech/scroll.git
synced 2026-01-09 14:08:03 -05:00
336 lines
11 KiB
Go
336 lines
11 KiB
Go
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()
|
|
}
|
|
}
|