package sender import ( "context" "crypto/ecdsa" "crypto/rand" "errors" "fmt" "math" "math/big" "os" "testing" "time" "github.com/agiledragon/gomonkey/v2" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" gokzg4844 "github.com/crate-crypto/go-kzg-4844" "github.com/holiman/uint256" "github.com/scroll-tech/go-ethereum/accounts/abi/bind" "github.com/scroll-tech/go-ethereum/common" gethTypes "github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/crypto" "github.com/scroll-tech/go-ethereum/crypto/kzg4844" "github.com/scroll-tech/go-ethereum/ethclient" "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/rpc" "github.com/stretchr/testify/assert" "gorm.io/gorm" "scroll-tech/database/migrate" "scroll-tech/common/testcontainers" "scroll-tech/common/types" bridgeAbi "scroll-tech/rollup/abi" "scroll-tech/rollup/internal/config" "scroll-tech/rollup/mock_bridge" ) var ( privateKeyString string privateKey *ecdsa.PrivateKey signerConfig *config.SignerConfig cfg *config.Config testApps *testcontainers.TestcontainerApps txTypes = []string{"LegacyTx", "DynamicFeeTx", "DynamicFeeTx"} txBlob = []*kzg4844.Blob{nil, nil, randBlobs(2)[0]} txUint8Types = []uint8{0, 2, 3} db *gorm.DB testContractsAddress common.Address ) func TestMain(m *testing.M) { defer func() { if testApps != nil { testApps.Free() } if testAppsSignerTest != nil { testAppsSignerTest.Free() } }() m.Run() } func setupEnv(t *testing.T) { glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat())) glogger.Verbosity(log.LvlInfo) log.Root().SetHandler(glogger) var err error cfg, err = config.NewConfig("../../../conf/config.json") assert.NoError(t, err) privateKeyString = "1212121212121212121212121212121212121212121212121212121212121212" signerConfig = &config.SignerConfig{ SignerType: "PrivateKey", PrivateKeySignerConfig: &config.PrivateKeySignerConfig{ PrivateKey: privateKeyString, }, } priv, err := crypto.HexToECDSA(privateKeyString) assert.NoError(t, err) privateKey = priv testApps = testcontainers.NewTestcontainerApps() assert.NoError(t, testApps.StartPostgresContainer()) assert.NoError(t, testApps.StartL2GethContainer()) assert.NoError(t, testApps.StartPoSL1Container()) cfg.L2Config.RelayerConfig.SenderConfig.Endpoint, err = testApps.GetPoSL1EndPoint() assert.NoError(t, err) db, err = testApps.GetGormDBClient() assert.NoError(t, err) sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) l1RawClient, err := testApps.GetPoSL1Client() assert.NoError(t, err) l1Client := ethclient.NewClient(l1RawClient) chainID, err := l1Client.ChainID(context.Background()) assert.NoError(t, err) auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) assert.NoError(t, err) nonce, err := l1Client.PendingNonceAt(context.Background(), auth.From) assert.NoError(t, err) testContractsAddress = crypto.CreateAddress(auth.From, nonce) tx := gethTypes.NewContractCreation(nonce, big.NewInt(0), 10000000, big.NewInt(10000000000), common.FromHex(mock_bridge.MockBridgeMetaData.Bin)) signedTx, err := auth.Signer(auth.From, tx) assert.NoError(t, err) err = l1Client.SendTransaction(context.Background(), signedTx) assert.NoError(t, err) assert.Eventually(t, func() bool { _, isPending, err := l1Client.TransactionByHash(context.Background(), signedTx.Hash()) return err == nil && !isPending }, 30*time.Second, time.Second) assert.Eventually(t, func() bool { receipt, err := l1Client.TransactionReceipt(context.Background(), signedTx.Hash()) return err == nil && receipt.Status == gethTypes.ReceiptStatusSuccessful }, 30*time.Second, time.Second) assert.Eventually(t, func() bool { code, err := l1Client.CodeAt(context.Background(), testContractsAddress, nil) return err == nil && len(code) > 0 }, 30*time.Second, time.Second) } func TestSender(t *testing.T) { setupEnv(t) t.Run("test new sender", testNewSender) t.Run("test send and retrieve transaction", testSendAndRetrieveTransaction) t.Run("test access list transaction gas limit", testAccessListTransactionGasLimit) 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 dynamic fee transaction with rising base fee", testResubmitDynamicFeeTransactionWithRisingBaseFee) t.Run("test resubmit blob transaction with rising base fee and blob base fee", testResubmitBlobTransactionWithRisingBaseFeeAndBlobBaseFee) t.Run("test resubmit nonce gapped transaction", testResubmitNonceGappedTransaction) t.Run("test check pending transaction tx confirmed", testCheckPendingTransactionTxConfirmed) t.Run("test check pending transaction resubmit tx confirmed", testCheckPendingTransactionResubmitTxConfirmed) t.Run("test check pending transaction replaced tx confirmed", testCheckPendingTransactionReplacedTxConfirmed) t.Run("test check pending transaction multiple times with only one transaction pending", testCheckPendingTransactionTxMultipleTimesWithOnlyOneTxPending) t.Run("test blob transaction with blobhash op contract call", testBlobTransactionWithBlobhashOpContractCall) t.Run("test test send blob-carrying tx over limit", testSendBlobCarryingTxOverLimit) } func testNewSender(t *testing.T) { for _, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) // exit by Stop() cfgCopy1 := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy1.TxType = txType newSender1, err := NewSender(context.Background(), &cfgCopy1, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) newSender1.Stop() // exit by ctx.Done() cfgCopy2 := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy2.TxType = txType subCtx, cancel := context.WithCancel(context.Background()) _, err = NewSender(subCtx, &cfgCopy2, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) cancel() } } func testSendAndRetrieveTransaction(t *testing.T) { for i, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) var blobs []*kzg4844.Blob if txBlob[i] != nil { blobs = []*kzg4844.Blob{txBlob[i]} } hash, _, err := s.SendTransaction("0", &common.Address{}, nil, blobs) assert.NoError(t, err) txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 1) assert.Equal(t, "0", txs[0].ContextID) assert.Equal(t, hash.String(), txs[0].Hash) assert.Equal(t, txUint8Types[i], txs[0].Type) assert.Equal(t, types.TxStatusPending, txs[0].Status) assert.Equal(t, "0x1C5A77d9FA7eF466951B2F01F724BCa3A5820b63", txs[0].SenderAddress) assert.Equal(t, types.SenderTypeUnknown, txs[0].SenderType) assert.Equal(t, "test", txs[0].SenderService) assert.Equal(t, "test", txs[0].SenderName) assert.Eventually(t, func() bool { txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 100) assert.NoError(t, err) return len(txs) == 0 }, 30*time.Second, time.Second) s.Stop() } } func testResubmitZeroGasPriceTransaction(t *testing.T) { for i, txType := range txTypes { if txBlob[i] != nil { continue } sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(0), gasTipCap: big.NewInt(0), gasFeeCap: big.NewInt(0), gasLimit: 50000, } tx, err := s.createTx(feeData, &common.Address{}, nil, nil, s.transactionSigner.GetNonce()) assert.NoError(t, err) assert.NotNil(t, tx) err = s.client.SendTransaction(s.ctx, tx) assert.NoError(t, err) txHashes := []common.Hash{tx.Hash()} // Increase at least 1 wei in gas price, gas tip cap and gas fee cap. // Bumping the fees enough times to let the transaction be included in a block. for i := 0; i < 30; i++ { tx, err = s.createReplacingTransaction(tx, 0, 0) assert.NoError(t, err) err = s.client.SendTransaction(s.ctx, tx) assert.NoError(t, err) txHashes = append(txHashes, tx.Hash()) } assert.Eventually(t, func() bool { for _, txHash := range txHashes { _, isPending, err := s.client.TransactionByHash(context.Background(), txHash) if err == nil && !isPending { return true } } return false }, 30*time.Second, time.Second) assert.Eventually(t, func() bool { for _, txHash := range txHashes { receipt, err := s.client.TransactionReceipt(context.Background(), txHash) if err == nil && receipt != nil { return true } } return false }, 30*time.Second, time.Second) s.Stop() } } func testAccessListTransactionGasLimit(t *testing.T) { for i, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) l2GasOracleABI, err := bridgeAbi.L2GasPriceOracleMetaData.GetAbi() assert.NoError(t, err) data, err := l2GasOracleABI.Pack("setL2BaseFee", big.NewInt(int64(i+1))) assert.NoError(t, err) var sidecar *gethTypes.BlobTxSidecar if txBlob[i] != nil { sidecar, err = makeSidecar(gethTypes.BlobSidecarVersion0, []*kzg4844.Blob{txBlob[i]}) assert.NoError(t, err) } gasLimit, accessList, err := s.estimateGasLimit(&testContractsAddress, data, sidecar, nil, big.NewInt(1000000000), big.NewInt(1000000000), big.NewInt(1000000000)) assert.NoError(t, err) if txType == LegacyTxType { // Legacy transactions can not have an access list. assert.Equal(t, uint64(43935), gasLimit) assert.Nil(t, accessList) } else { // Dynamic fee and blob transactions can have an access list. assert.Equal(t, uint64(43458), gasLimit) assert.NotNil(t, accessList) } s.Stop() } } func testResubmitNonZeroGasPriceTransaction(t *testing.T) { for i, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.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, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(1000000000), gasTipCap: big.NewInt(1000000000), gasFeeCap: big.NewInt(1000000000), blobGasFeeCap: big.NewInt(1000000000), gasLimit: 50000, } var sidecar *gethTypes.BlobTxSidecar if txBlob[i] != nil { sidecar, err = makeSidecar(gethTypes.BlobSidecarVersion0, []*kzg4844.Blob{txBlob[i]}) assert.NoError(t, err) } tx, err := s.createTx(feeData, &common.Address{}, nil, sidecar, s.transactionSigner.GetNonce()) assert.NoError(t, err) assert.NotNil(t, tx) err = s.client.SendTransaction(s.ctx, tx) assert.NoError(t, err) resubmittedTx, err := s.createReplacingTransaction(tx, 0, 0) assert.NoError(t, err) err = s.client.SendTransaction(s.ctx, resubmittedTx) assert.NoError(t, err) assert.Eventually(t, func() bool { _, isPending, err := s.client.TransactionByHash(context.Background(), resubmittedTx.Hash()) return err == nil && !isPending }, 30*time.Second, time.Second) assert.Eventually(t, func() bool { receipt, err := s.client.TransactionReceipt(context.Background(), resubmittedTx.Hash()) return err == nil && receipt != nil }, 30*time.Second, time.Second) s.Stop() } } func testResubmitUnderpricedTransaction(t *testing.T) { for i, txType := range txTypes { if txBlob[i] != nil { continue } sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.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, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) feeData := &FeeData{ gasPrice: big.NewInt(1000000000), gasTipCap: big.NewInt(1000000000), gasFeeCap: big.NewInt(1000000000), gasLimit: 50000, } tx, err := s.createTx(feeData, &common.Address{}, nil, nil, s.transactionSigner.GetNonce()) assert.NoError(t, err) assert.NotNil(t, tx) err = s.client.SendTransaction(s.ctx, tx) assert.NoError(t, err) resubmittedTx, err := s.createReplacingTransaction(tx, 0, 0) assert.NoError(t, err) err = s.client.SendTransaction(s.ctx, resubmittedTx) assert.Error(t, err, "replacement transaction underpriced") assert.Eventually(t, func() bool { _, isPending, err := s.client.TransactionByHash(context.Background(), tx.Hash()) return err == nil && !isPending }, 30*time.Second, time.Second) assert.Eventually(t, func() bool { receipt, err := s.client.TransactionReceipt(context.Background(), tx.Hash()) return err == nil && receipt != nil }, 30*time.Second, time.Second) s.Stop() } } func testResubmitDynamicFeeTransactionWithRisingBaseFee(t *testing.T) { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) txType := "DynamicFeeTx" cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) patchGuard := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) defer patchGuard.Reset() tx := gethTypes.NewTx(&gethTypes.DynamicFeeTx{ Nonce: s.transactionSigner.GetNonce(), To: &common.Address{}, Data: nil, Gas: 21000, ChainID: s.chainID, GasTipCap: big.NewInt(0), GasFeeCap: big.NewInt(0), }) baseFeePerGas := uint64(1000) // bump the basefee by 10x baseFeePerGas *= 10 // resubmit and check that the gas fee has been adjusted accordingly resubmittedTx, err := s.createReplacingTransaction(tx, baseFeePerGas, 0) assert.NoError(t, err) err = s.client.SendTransaction(s.ctx, resubmittedTx) assert.NoError(t, err) maxGasPrice := new(big.Int).SetUint64(s.config.MaxGasPrice) expectedGasFeeCap := getGasFeeCap(new(big.Int).SetUint64(baseFeePerGas), tx.GasTipCap()) if expectedGasFeeCap.Cmp(maxGasPrice) > 0 { expectedGasFeeCap = maxGasPrice } assert.Equal(t, expectedGasFeeCap.Uint64(), resubmittedTx.GasFeeCap().Uint64()) s.Stop() } func testResubmitBlobTransactionWithRisingBaseFeeAndBlobBaseFee(t *testing.T) { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = DynamicFeeTxType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) patchGuard := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) defer patchGuard.Reset() sidecar, err := makeSidecar(gethTypes.BlobSidecarVersion0, randBlobs(1)) assert.NoError(t, err) tx := gethTypes.NewTx(&gethTypes.BlobTx{ ChainID: uint256.MustFromBig(s.chainID), Nonce: s.transactionSigner.GetNonce(), GasTipCap: uint256.MustFromBig(big.NewInt(0)), GasFeeCap: uint256.MustFromBig(big.NewInt(0)), Gas: 21000, To: common.Address{}, Data: nil, BlobFeeCap: uint256.MustFromBig(big.NewInt(1)), BlobHashes: sidecar.BlobHashes(), Sidecar: sidecar, }) baseFeePerGas := uint64(1000) blobBaseFeePerGas := uint64(10000000000000) // bounded by max blob base fee. // bump the basefee and blobbasefee by 10x baseFeePerGas *= 10 blobBaseFeePerGas *= 10 // resubmit and check that the gas fee has been adjusted accordingly resubmittedTx, err := s.createReplacingTransaction(tx, baseFeePerGas, blobBaseFeePerGas) assert.NoError(t, err) err = s.client.SendTransaction(s.ctx, resubmittedTx) assert.NoError(t, err) maxGasPrice := new(big.Int).SetUint64(s.config.MaxGasPrice) expectedGasFeeCap := getGasFeeCap(new(big.Int).SetUint64(baseFeePerGas), tx.GasTipCap()) if expectedGasFeeCap.Cmp(maxGasPrice) > 0 { expectedGasFeeCap = maxGasPrice } maxBlobGasPrice := new(big.Int).SetUint64(s.config.MaxBlobGasPrice) expectedBlobGasFeeCap := getBlobGasFeeCap(new(big.Int).SetUint64(blobBaseFeePerGas)) if expectedBlobGasFeeCap.Cmp(maxBlobGasPrice) > 0 { expectedBlobGasFeeCap = maxBlobGasPrice } assert.Equal(t, expectedGasFeeCap.Uint64(), resubmittedTx.GasFeeCap().Uint64()) assert.Equal(t, expectedBlobGasFeeCap.Uint64(), resubmittedTx.BlobGasFeeCap().Uint64()) s.Stop() } func testResubmitNonceGappedTransaction(t *testing.T) { for i, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.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 // resubmit immediately if not nonce gapped cfgCopy.Confirmations = rpc.LatestBlockNumber cfgCopy.EscalateBlocks = 0 // stop background check pending transaction cfgCopy.CheckPendingTime = math.MaxUint32 s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeUnknown, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) // simulating not confirmed transaction patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) { return nil, errors.New("simulated transaction receipt error") }) var blobs []*kzg4844.Blob if txBlob[i] != nil { blobs = []*kzg4844.Blob{txBlob[i]} } _, _, err = s.SendTransaction("test-1", &common.Address{}, nil, blobs) assert.NoError(t, err) _, _, err = s.SendTransaction("test-2", &common.Address{}, nil, blobs) assert.NoError(t, err) s.checkPendingTransaction() txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 10) assert.NoError(t, err) assert.Len(t, txs, 3) assert.Equal(t, txs[0].Nonce, txs[1].Nonce) assert.Equal(t, txs[0].Nonce+1, txs[2].Nonce) // the first 2 transactions have the same nonce, with one replaced and another pending assert.Equal(t, types.TxStatusReplaced, txs[0].Status) assert.Equal(t, types.TxStatusPending, txs[1].Status) // the third transaction has nonce + 1, which will not be replaced due to the nonce gap, // thus the status should be pending assert.Equal(t, types.TxStatusPending, txs[2].Status) s.Stop() patchGuard1.Reset() patchGuard2.Reset() } } func testCheckPendingTransactionTxConfirmed(t *testing.T) { for _, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeCommitBatch, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) _, _, err = s.SendTransaction("test", &common.Address{}, nil, randBlobs(1)) assert.NoError(t, err) txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 1) assert.Equal(t, types.TxStatusPending, txs[0].Status) assert.Equal(t, types.SenderTypeCommitBatch, txs[0].SenderType) patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) { return &gethTypes.Receipt{TxHash: hash, BlockNumber: big.NewInt(0), Status: gethTypes.ReceiptStatusSuccessful}, nil }) s.checkPendingTransaction() assert.NoError(t, err) txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 0) s.Stop() patchGuard1.Reset() patchGuard2.Reset() } } func testCheckPendingTransactionResubmitTxConfirmed(t *testing.T) { for _, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.EscalateBlocks = 0 s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeFinalizeBatch, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) originTxHash, _, err := s.SendTransaction("test", &common.Address{}, nil, randBlobs(1)) assert.NoError(t, err) txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 1) assert.Equal(t, types.TxStatusPending, txs[0].Status) assert.Equal(t, types.SenderTypeFinalizeBatch, txs[0].SenderType) patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) { if hash == originTxHash { return nil, errors.New("simulated transaction receipt error") } return &gethTypes.Receipt{TxHash: hash, BlockNumber: big.NewInt(0), Status: gethTypes.ReceiptStatusSuccessful}, nil }) // Attempt to resubmit the transaction. s.checkPendingTransaction() assert.NoError(t, err) status, err := s.pendingTransactionOrm.GetTxStatusByTxHash(context.Background(), originTxHash) assert.NoError(t, err) assert.Equal(t, types.TxStatusReplaced, status) txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 2) assert.NoError(t, err) assert.Len(t, txs, 2) assert.Equal(t, types.TxStatusReplaced, txs[0].Status) assert.Equal(t, types.TxStatusPending, txs[1].Status) // Check the pending transactions again after attempting to resubmit. s.checkPendingTransaction() assert.NoError(t, err) txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 0) s.Stop() patchGuard1.Reset() patchGuard2.Reset() } } func testCheckPendingTransactionReplacedTxConfirmed(t *testing.T) { for _, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.EscalateBlocks = 0 s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeL1GasOracle, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) txHash, _, err := s.SendTransaction("test", &common.Address{}, nil, randBlobs(1)) assert.NoError(t, err) txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 1) assert.Equal(t, types.TxStatusPending, txs[0].Status) assert.Equal(t, types.SenderTypeL1GasOracle, txs[0].SenderType) patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) { var status types.TxStatus status, err = s.pendingTransactionOrm.GetTxStatusByTxHash(context.Background(), hash) if err != nil { return nil, fmt.Errorf("failed to get transaction status, hash: %s, err: %w", hash.String(), err) } // If the transaction status is 'replaced', return a successful receipt. if status == types.TxStatusReplaced { return &gethTypes.Receipt{ TxHash: hash, BlockNumber: big.NewInt(0), Status: gethTypes.ReceiptStatusSuccessful, }, nil } return nil, errors.New("simulated transaction receipt error") }) // Attempt to resubmit the transaction. s.checkPendingTransaction() assert.NoError(t, err) status, err := s.pendingTransactionOrm.GetTxStatusByTxHash(context.Background(), txHash) assert.NoError(t, err) assert.Equal(t, types.TxStatusReplaced, status) txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 2) assert.NoError(t, err) assert.Len(t, txs, 2) assert.Equal(t, types.TxStatusReplaced, txs[0].Status) assert.Equal(t, types.TxStatusPending, txs[1].Status) // Check the pending transactions again after attempting to resubmit. s.checkPendingTransaction() assert.NoError(t, err) txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 0) s.Stop() patchGuard1.Reset() patchGuard2.Reset() } } func testCheckPendingTransactionTxMultipleTimesWithOnlyOneTxPending(t *testing.T) { for _, txType := range txTypes { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = txType cfgCopy.EscalateBlocks = 0 s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeCommitBatch, db, nil) assert.NoError(t, err) patchGuard1 := gomonkey.ApplyMethodFunc(s.client, "SendTransaction", func(_ context.Context, _ *gethTypes.Transaction) error { return nil }) _, _, err = s.SendTransaction("test", &common.Address{}, nil, randBlobs(1)) assert.NoError(t, err) txs, err := s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 1) assert.NoError(t, err) assert.Len(t, txs, 1) assert.Equal(t, types.TxStatusPending, txs[0].Status) assert.Equal(t, types.SenderTypeCommitBatch, txs[0].SenderType) patchGuard2 := gomonkey.ApplyMethodFunc(s.client, "TransactionReceipt", func(_ context.Context, hash common.Hash) (*gethTypes.Receipt, error) { return nil, errors.New("simulated transaction receipt error") }) for i := 1; i <= 6; i++ { s.checkPendingTransaction() assert.NoError(t, err) txs, err = s.pendingTransactionOrm.GetPendingOrReplacedTransactionsBySenderType(context.Background(), s.senderType, 100) assert.NoError(t, err) assert.Len(t, txs, i+1) for j := 0; j < i; j++ { assert.Equal(t, types.TxStatusReplaced, txs[j].Status) } assert.Equal(t, types.TxStatusPending, txs[i].Status) } s.Stop() patchGuard1.Reset() patchGuard2.Reset() } } func testBlobTransactionWithBlobhashOpContractCall(t *testing.T) { sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) blobs := randBlobs(1) sideCar, err := makeSidecar(gethTypes.BlobSidecarVersion0, blobs) assert.NoError(t, err) versionedHash := sideCar.BlobHashes()[0] blsModulo, ok := new(big.Int).SetString("52435875175126190479447740508185965837690552500527637822603658699938581184513", 10) assert.True(t, ok) pointHash := crypto.Keccak256Hash(versionedHash.Bytes()) pointBigInt := new(big.Int).SetBytes(pointHash.Bytes()) pointBytes := new(big.Int).Mod(pointBigInt, blsModulo).Bytes() start := 32 - len(pointBytes) var point kzg4844.Point copy(point[start:], pointBytes) commitment := sideCar.Commitments[0] proof, claim, err := kzg4844.ComputeProof(blobs[0], point) assert.NoError(t, err) var claimArray [32]byte copy(claimArray[:], claim[:]) demoContractMetaData := &bind.MetaData{ABI: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"claim\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"commitment\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"proof\",\"type\":\"bytes\"}],\"name\":\"verifyProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]"} demoContractABI, err := demoContractMetaData.GetAbi() assert.NoError(t, err) data, err := demoContractABI.Pack( "verifyProof", claimArray, commitment[:], proof[:], ) assert.NoError(t, err) cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = DynamicFeeTxType s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeL1GasOracle, db, nil) assert.NoError(t, err) defer s.Stop() _, _, err = s.SendTransaction("0", &testContractsAddress, data, blobs) assert.NoError(t, err) var txHash common.Hash assert.Eventually(t, func() bool { txs, err := s.pendingTransactionOrm.GetConfirmedTransactionsBySenderType(context.Background(), s.senderType, 100) assert.NoError(t, err) if len(txs) == 1 { txHash = common.HexToHash(txs[0].Hash) return true } return false }, 30*time.Second, time.Second) assert.Eventually(t, func() bool { receipt, err := s.client.TransactionReceipt(context.Background(), txHash) return err == nil && receipt.Status == gethTypes.ReceiptStatusSuccessful }, 30*time.Second, time.Second) } func randBlobs(count int) []*kzg4844.Blob { blobs := make([]*kzg4844.Blob, 0, count) for c := 0; c < count; c++ { var blob kzg4844.Blob for i := 0; i < len(blob); i += gokzg4844.SerializedScalarSize { fieldElementBytes := randFieldElement() copy(blob[i:i+gokzg4844.SerializedScalarSize], fieldElementBytes[:]) } blobs = append(blobs, &blob) } return blobs } func randFieldElement() [32]byte { bytes := make([]byte, 32) _, err := rand.Read(bytes) if err != nil { panic("failed to get random field element") } var r fr.Element r.SetBytes(bytes) return gokzg4844.SerializeScalar(r) } func testSendBlobCarryingTxOverLimit(t *testing.T) { cfgCopy := *cfg.L2Config.RelayerConfig.SenderConfig cfgCopy.TxType = "DynamicFeeTx" sqlDB, err := db.DB() assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) s, err := NewSender(context.Background(), &cfgCopy, signerConfig, "test", "test", types.SenderTypeCommitBatch, db, nil) assert.NoError(t, err) for i := 0; i < int(cfgCopy.MaxPendingBlobTxs); i++ { _, _, err = s.SendTransaction("0", &common.Address{}, nil, randBlobs(1)) assert.NoError(t, err) } _, _, err = s.SendTransaction("0", &common.Address{}, nil, randBlobs(1)) assert.ErrorIs(t, err, ErrTooManyPendingBlobTxs) s.Stop() }