From 33ed5fceef279a7bc3d1389093e9869041efcaca Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Tue, 29 Jul 2025 15:43:20 +0800 Subject: [PATCH 1/8] fix(gas-oracle): nonce too low when resubmission --- common/version/version.go | 2 +- rollup/internal/controller/sender/sender.go | 15 +++++++++++++++ rollup/internal/orm/pending_transaction.go | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/common/version/version.go b/common/version/version.go index b50743fd1d..f31fe8e48a 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -5,7 +5,7 @@ import ( "runtime/debug" ) -var tag = "v4.5.35" +var tag = "v4.5.36" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index d5a4db5d3b..0ce9522524 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -612,6 +612,21 @@ func (s *Sender) checkPendingTransaction() { } if err := s.client.SendTransaction(s.ctx, newSignedTx); err != nil { + // Check if it's a nonce too low error + if strings.Contains(err.Error(), "nonce too low") { + // nonce too low means a transaction with this nonce has already been mined + // Mark all non-confirmed transactions with the same nonce as failed + if updateErr := s.pendingTransactionOrm.UpdateNonConfirmedTransactionsAsFailedByNonce(s.ctx, txnToCheck.SenderAddress, originalTx.Nonce()); updateErr != nil { + log.Error("failed to update transactions as failed by nonce", "nonce", originalTx.Nonce(), "senderAddress", txnToCheck.SenderAddress, "err", updateErr) + return + } + + // Reset nonce + s.resetNonce(context.Background()) + + log.Info("nonce too low detected, marked all non-confirmed transactions with same nonce as failed", "nonce", originalTx.Nonce(), "address", s.transactionSigner.GetAddr().String()) + return + } // SendTransaction failed, need to rollback the previous database changes if rollbackErr := s.db.Transaction(func(tx *gorm.DB) error { // Restore original transaction status back to pending diff --git a/rollup/internal/orm/pending_transaction.go b/rollup/internal/orm/pending_transaction.go index df53682704..a94fce3c0e 100644 --- a/rollup/internal/orm/pending_transaction.go +++ b/rollup/internal/orm/pending_transaction.go @@ -207,3 +207,21 @@ func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context. } return nil } + +// UpdateNonConfirmedTransactionsAsFailedByNonce updates the status of all non-confirmed transactions to TxStatusConfirmedFailed +// for a specific nonce and sender address. +func (o *PendingTransaction) UpdateNonConfirmedTransactionsAsFailedByNonce(ctx context.Context, senderAddress string, nonce uint64, dbTX ...*gorm.DB) error { + db := o.db + if len(dbTX) > 0 && dbTX[0] != nil { + db = dbTX[0] + } + db = db.WithContext(ctx) + db = db.Model(&PendingTransaction{}) + db = db.Where("sender_address = ?", senderAddress) + db = db.Where("nonce = ?", nonce) + db = db.Where("status != ?", types.TxStatusConfirmed) // Don't update confirmed transactions + if err := db.Update("status", types.TxStatusConfirmedFailed).Error; err != nil { + return fmt.Errorf("failed to update non-confirmed transactions as failed by nonce, senderAddress: %s, nonce: %d, error: %w", senderAddress, nonce, err) + } + return nil +} From 637578aadbaf420fd5f4ed9891029bba908479d6 Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Tue, 29 Jul 2025 19:59:36 +0800 Subject: [PATCH 2/8] fixes --- rollup/internal/controller/sender/sender.go | 40 +++++++++++++-------- rollup/internal/orm/pending_transaction.go | 23 ++++++------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index 0ce9522524..25f1f159e0 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -105,12 +105,29 @@ func NewSender(ctx context.Context, config *config.SenderConfig, signerConfig *c return nil, fmt.Errorf("failed to create transaction signer, err: %w", err) } - // Set pending nonce - nonce, err := client.PendingNonceAt(ctx, transactionSigner.GetAddr()) + // Get maximum nonce from database + dbNonce, err := orm.NewPendingTransaction(db).GetMaxNonceBySenderAddress(ctx, transactionSigner.GetAddr().Hex()) if err != nil { - return nil, fmt.Errorf("failed to get pending nonce for address %s, err: %w", transactionSigner.GetAddr(), err) + return nil, fmt.Errorf("failed to get max nonce from database for address %s, err: %w", transactionSigner.GetAddr().Hex(), err) } - transactionSigner.SetNonce(nonce) + + // Get pending nonce from the client + pendingNonce, err := client.PendingNonceAt(ctx, transactionSigner.GetAddr()) + if err != nil { + return nil, fmt.Errorf("failed to get pending nonce for address %s, err: %w", transactionSigner.GetAddr().Hex(), err) + } + + // Take the maximum of both values + var finalNonce uint64 + if pendingNonce > dbNonce { + finalNonce = pendingNonce + } else { + finalNonce = dbNonce + } + + log.Info("nonce initialization", "address", transactionSigner.GetAddr().Hex(), "pendingNonce", pendingNonce, "dbNonce", dbNonce, "finalNonce", finalNonce) + + transactionSigner.SetNonce(finalNonce) sender := &Sender{ ctx: ctx, @@ -612,19 +629,14 @@ func (s *Sender) checkPendingTransaction() { } if err := s.client.SendTransaction(s.ctx, newSignedTx); err != nil { - // Check if it's a nonce too low error if strings.Contains(err.Error(), "nonce too low") { - // nonce too low means a transaction with this nonce has already been mined - // Mark all non-confirmed transactions with the same nonce as failed - if updateErr := s.pendingTransactionOrm.UpdateNonConfirmedTransactionsAsFailedByNonce(s.ctx, txnToCheck.SenderAddress, originalTx.Nonce()); updateErr != nil { - log.Error("failed to update transactions as failed by nonce", "nonce", originalTx.Nonce(), "senderAddress", txnToCheck.SenderAddress, "err", updateErr) + // When we receive a 'nonce too low' error but cannot find the transaction receipt, it indicates another transaction with this nonce has already been processed, so this transaction will never be mined and should be marked as failed. + log.Warn("nonce too low detected, marking all non-confirmed transactions with same nonce as failed", "nonce", originalTx.Nonce(), "address", s.transactionSigner.GetAddr().Hex(), "txHash", originalTx.Hash().Hex(), "err", err) + + if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, originalTx.Hash(), types.TxStatusConfirmedFailed); updateErr != nil { + log.Error("failed to update status of original transaction to confirmed failed", "txHash", originalTx.Hash().Hex(), "nonce", originalTx.Nonce(), "from", s.transactionSigner.GetAddr().Hex(), "err", updateErr) return } - - // Reset nonce - s.resetNonce(context.Background()) - - log.Info("nonce too low detected, marked all non-confirmed transactions with same nonce as failed", "nonce", originalTx.Nonce(), "address", s.transactionSigner.GetAddr().String()) return } // SendTransaction failed, need to rollback the previous database changes diff --git a/rollup/internal/orm/pending_transaction.go b/rollup/internal/orm/pending_transaction.go index a94fce3c0e..a84c3f3751 100644 --- a/rollup/internal/orm/pending_transaction.go +++ b/rollup/internal/orm/pending_transaction.go @@ -208,20 +208,17 @@ func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context. return nil } -// UpdateNonConfirmedTransactionsAsFailedByNonce updates the status of all non-confirmed transactions to TxStatusConfirmedFailed -// for a specific nonce and sender address. -func (o *PendingTransaction) UpdateNonConfirmedTransactionsAsFailedByNonce(ctx context.Context, senderAddress string, nonce uint64, dbTX ...*gorm.DB) error { - db := o.db - if len(dbTX) > 0 && dbTX[0] != nil { - db = dbTX[0] - } - db = db.WithContext(ctx) +// GetMaxNonceBySenderAddress retrieves the maximum nonce for a specific sender address. +// Returns 0 if no transactions are found for the given address. +func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (uint64, error) { + var maxNonce uint64 + db := o.db.WithContext(ctx) db = db.Model(&PendingTransaction{}) db = db.Where("sender_address = ?", senderAddress) - db = db.Where("nonce = ?", nonce) - db = db.Where("status != ?", types.TxStatusConfirmed) // Don't update confirmed transactions - if err := db.Update("status", types.TxStatusConfirmedFailed).Error; err != nil { - return fmt.Errorf("failed to update non-confirmed transactions as failed by nonce, senderAddress: %s, nonce: %d, error: %w", senderAddress, nonce, err) + + if err := db.Pluck("COALESCE(MAX(nonce), 0)", &maxNonce).Error; err != nil { + return 0, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err) } - return nil + + return maxNonce, nil } From 22a1108c5c8f6cfea1097b97979a9e3774e0db13 Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Tue, 29 Jul 2025 20:14:39 +0800 Subject: [PATCH 3/8] fixes --- rollup/internal/orm/orm_test.go | 58 ++++++++++++++++++++++ rollup/internal/orm/pending_transaction.go | 10 ++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/rollup/internal/orm/orm_test.go b/rollup/internal/orm/orm_test.go index b66c74db9d..2c1b8cf31d 100644 --- a/rollup/internal/orm/orm_test.go +++ b/rollup/internal/orm/orm_test.go @@ -597,3 +597,61 @@ func TestPendingTransactionOrm(t *testing.T) { err = pendingTransactionOrm.DeleteTransactionByTxHash(context.Background(), common.HexToHash("0x123")) assert.Error(t, err) // Should return error for non-existent transaction } + +func TestPendingTransaction_GetMaxNonceBySenderAddress(t *testing.T) { + sqlDB, err := db.DB() + assert.NoError(t, err) + assert.NoError(t, migrate.ResetDB(sqlDB)) + + // When there are no transactions for this sender address, should return 0 + maxNonce, err := pendingTransactionOrm.GetMaxNonceBySenderAddress(context.Background(), "0xdeadbeef") + assert.NoError(t, err) + assert.Equal(t, uint64(0), maxNonce) + + // Insert two transactions with different nonces for the same sender address + senderMeta := &SenderMeta{ + Name: "testName", + Service: "testService", + Address: common.HexToAddress("0xdeadbeef"), + Type: types.SenderTypeCommitBatch, + } + + tx0 := gethTypes.NewTx(&gethTypes.DynamicFeeTx{ + Nonce: 1, + To: &common.Address{}, + Data: []byte{}, + Gas: 21000, + AccessList: gethTypes.AccessList{}, + Value: big.NewInt(0), + ChainID: big.NewInt(1), + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(1), + V: big.NewInt(0), + R: big.NewInt(0), + S: big.NewInt(0), + }) + tx1 := gethTypes.NewTx(&gethTypes.DynamicFeeTx{ + Nonce: 3, + To: &common.Address{}, + Data: []byte{}, + Gas: 22000, + AccessList: gethTypes.AccessList{}, + Value: big.NewInt(0), + ChainID: big.NewInt(1), + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(2), + V: big.NewInt(0), + R: big.NewInt(0), + S: big.NewInt(0), + }) + + err = pendingTransactionOrm.InsertPendingTransaction(context.Background(), "test", senderMeta, tx0, 0) + assert.NoError(t, err) + err = pendingTransactionOrm.InsertPendingTransaction(context.Background(), "test", senderMeta, tx1, 0) + assert.NoError(t, err) + + // Now the max nonce for this sender should be 3 + maxNonce, err = pendingTransactionOrm.GetMaxNonceBySenderAddress(context.Background(), senderMeta.Address.String()) + assert.NoError(t, err) + assert.Equal(t, uint64(3), maxNonce) +} diff --git a/rollup/internal/orm/pending_transaction.go b/rollup/internal/orm/pending_transaction.go index a84c3f3751..bf3874af70 100644 --- a/rollup/internal/orm/pending_transaction.go +++ b/rollup/internal/orm/pending_transaction.go @@ -211,14 +211,16 @@ func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context. // GetMaxNonceBySenderAddress retrieves the maximum nonce for a specific sender address. // Returns 0 if no transactions are found for the given address. func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (uint64, error) { - var maxNonce uint64 db := o.db.WithContext(ctx) db = db.Model(&PendingTransaction{}) db = db.Where("sender_address = ?", senderAddress) - - if err := db.Pluck("COALESCE(MAX(nonce), 0)", &maxNonce).Error; err != nil { + var maxNonce uint64 + row := db.Model(&PendingTransaction{}). + Select("COALESCE(MAX(nonce), 0)"). + Where("sender_address = ?", senderAddress). + Row() + if err := row.Scan(&maxNonce); err != nil { return 0, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err) } - return maxNonce, nil } From 86b18d53035906df6b26ac540d94e3093f808f6d Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Tue, 29 Jul 2025 20:39:01 +0800 Subject: [PATCH 4/8] address comments --- rollup/internal/controller/sender/sender.go | 79 +++++++++++++-------- rollup/internal/orm/orm_test.go | 6 +- rollup/internal/orm/pending_transaction.go | 26 ++++--- 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index 25f1f159e0..deb6d880bf 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -63,7 +63,7 @@ type FeeData struct { gasLimit uint64 } -// Sender Transaction sender to send transaction to l1/l2 geth +// Sender Transaction sender to send transaction to l1/l2 type Sender struct { config *config.SenderConfig gethClient *gethclient.Client @@ -105,30 +105,7 @@ func NewSender(ctx context.Context, config *config.SenderConfig, signerConfig *c return nil, fmt.Errorf("failed to create transaction signer, err: %w", err) } - // Get maximum nonce from database - dbNonce, err := orm.NewPendingTransaction(db).GetMaxNonceBySenderAddress(ctx, transactionSigner.GetAddr().Hex()) - if err != nil { - return nil, fmt.Errorf("failed to get max nonce from database for address %s, err: %w", transactionSigner.GetAddr().Hex(), err) - } - - // Get pending nonce from the client - pendingNonce, err := client.PendingNonceAt(ctx, transactionSigner.GetAddr()) - if err != nil { - return nil, fmt.Errorf("failed to get pending nonce for address %s, err: %w", transactionSigner.GetAddr().Hex(), err) - } - - // Take the maximum of both values - var finalNonce uint64 - if pendingNonce > dbNonce { - finalNonce = pendingNonce - } else { - finalNonce = dbNonce - } - - log.Info("nonce initialization", "address", transactionSigner.GetAddr().Hex(), "pendingNonce", pendingNonce, "dbNonce", dbNonce, "finalNonce", finalNonce) - - transactionSigner.SetNonce(finalNonce) - + // Create sender instance first and then initialize nonce sender := &Sender{ ctx: ctx, config: config, @@ -144,8 +121,13 @@ func NewSender(ctx context.Context, config *config.SenderConfig, signerConfig *c service: service, senderType: senderType, } - sender.metrics = initSenderMetrics(reg) + // Initialize nonce using the new method + if err := sender.resetNonce(); err != nil { + return nil, fmt.Errorf("failed to reset nonce: %w", err) + } + + sender.metrics = initSenderMetrics(reg) go sender.loop(ctx) return sender, nil @@ -259,7 +241,10 @@ func (s *Sender) SendTransaction(contextID string, target *common.Address, data // Check if contain nonce, and reset nonce // only reset nonce when it is not from resubmit if strings.Contains(err.Error(), "nonce too low") { - s.resetNonce(context.Background()) + if err := s.resetNonce(); err != nil { + log.Warn("failed to reset nonce after failed send transaction", "address", s.transactionSigner.GetAddr().String(), "err", err) + return common.Hash{}, 0, fmt.Errorf("failed to reset nonce after failed send transaction, err: %w", err) + } } return common.Hash{}, 0, fmt.Errorf("failed to send transaction, err: %w", err) } @@ -344,14 +329,46 @@ func (s *Sender) createTx(feeData *FeeData, target *common.Address, data []byte, return signedTx, nil } +// initializeNonce initializes the nonce by taking the maximum of database nonce and pending nonce. +func (s *Sender) initializeNonce() (uint64, error) { + // Get maximum nonce from database + dbNonce, err := s.pendingTransactionOrm.GetMaxNonceBySenderAddress(s.ctx, s.transactionSigner.GetAddr().Hex()) + if err != nil { + return 0, fmt.Errorf("failed to get max nonce from database for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err) + } + + // Get pending nonce from the client + pendingNonce, err := s.client.PendingNonceAt(s.ctx, s.transactionSigner.GetAddr()) + if err != nil { + return 0, fmt.Errorf("failed to get pending nonce for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err) + } + + // Take the maximum of pending nonce and (db nonce + 1) + // Database stores the used nonce, so the next available nonce should be dbNonce + 1 + // When dbNonce is -1 (no records), dbNonce + 1 = 0, which is correct + nextDbNonce := uint64(dbNonce + 1) + var finalNonce uint64 + if pendingNonce > nextDbNonce { + finalNonce = pendingNonce + } else { + finalNonce = nextDbNonce + } + + log.Info("nonce initialization", "address", s.transactionSigner.GetAddr().Hex(), "maxDbNonce", dbNonce, "nextDbNonce", nextDbNonce, "pendingNonce", pendingNonce, "finalNonce", finalNonce) + + return finalNonce, nil +} + // resetNonce reset nonce if send signed tx failed. -func (s *Sender) resetNonce(ctx context.Context) { - nonce, err := s.client.PendingNonceAt(ctx, s.transactionSigner.GetAddr()) +func (s *Sender) resetNonce() error { + nonce, err := s.initializeNonce() if err != nil { - log.Warn("failed to reset nonce", "address", s.transactionSigner.GetAddr().String(), "err", err) - return + log.Error("failed to reset nonce", "address", s.transactionSigner.GetAddr().String(), "err", err) + return fmt.Errorf("failed to reset nonce, err: %w", err) } + log.Info("reset nonce", "address", s.transactionSigner.GetAddr().String(), "nonce", nonce) s.transactionSigner.SetNonce(nonce) + return nil } func (s *Sender) createReplacingTransaction(tx *gethTypes.Transaction, baseFee, blobBaseFee uint64) (*gethTypes.Transaction, error) { diff --git a/rollup/internal/orm/orm_test.go b/rollup/internal/orm/orm_test.go index 2c1b8cf31d..3a0c6009af 100644 --- a/rollup/internal/orm/orm_test.go +++ b/rollup/internal/orm/orm_test.go @@ -603,10 +603,10 @@ func TestPendingTransaction_GetMaxNonceBySenderAddress(t *testing.T) { assert.NoError(t, err) assert.NoError(t, migrate.ResetDB(sqlDB)) - // When there are no transactions for this sender address, should return 0 + // When there are no transactions for this sender address, should return -1 maxNonce, err := pendingTransactionOrm.GetMaxNonceBySenderAddress(context.Background(), "0xdeadbeef") assert.NoError(t, err) - assert.Equal(t, uint64(0), maxNonce) + assert.Equal(t, int64(-1), maxNonce) // Insert two transactions with different nonces for the same sender address senderMeta := &SenderMeta{ @@ -653,5 +653,5 @@ func TestPendingTransaction_GetMaxNonceBySenderAddress(t *testing.T) { // Now the max nonce for this sender should be 3 maxNonce, err = pendingTransactionOrm.GetMaxNonceBySenderAddress(context.Background(), senderMeta.Address.String()) assert.NoError(t, err) - assert.Equal(t, uint64(3), maxNonce) + assert.Equal(t, int64(3), maxNonce) } diff --git a/rollup/internal/orm/pending_transaction.go b/rollup/internal/orm/pending_transaction.go index bf3874af70..6dfd164c65 100644 --- a/rollup/internal/orm/pending_transaction.go +++ b/rollup/internal/orm/pending_transaction.go @@ -3,6 +3,7 @@ package orm import ( "bytes" "context" + "database/sql" "fmt" "time" @@ -209,18 +210,23 @@ func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context. } // GetMaxNonceBySenderAddress retrieves the maximum nonce for a specific sender address. -// Returns 0 if no transactions are found for the given address. -func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (uint64, error) { - db := o.db.WithContext(ctx) - db = db.Model(&PendingTransaction{}) - db = db.Where("sender_address = ?", senderAddress) - var maxNonce uint64 - row := db.Model(&PendingTransaction{}). - Select("COALESCE(MAX(nonce), 0)"). +// Returns -1 if no transactions are found for the given address. +func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (int64, error) { + var maxNonce sql.NullInt64 + + row := o.db.WithContext(ctx). + Model(&PendingTransaction{}). + Select("MAX(nonce)"). Where("sender_address = ?", senderAddress). Row() + if err := row.Scan(&maxNonce); err != nil { - return 0, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err) + return -1, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err) + } + + if !maxNonce.Valid { + return -1, nil } - return maxNonce, nil + + return maxNonce.Int64, nil } From a843f558505fbc9485df9e3ce8a93acbba35883b Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Tue, 29 Jul 2025 20:47:37 +0800 Subject: [PATCH 5/8] address AI's comment --- rollup/internal/controller/sender/sender.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index deb6d880bf..367c437550 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -648,10 +648,19 @@ func (s *Sender) checkPendingTransaction() { if err := s.client.SendTransaction(s.ctx, newSignedTx); err != nil { if strings.Contains(err.Error(), "nonce too low") { // When we receive a 'nonce too low' error but cannot find the transaction receipt, it indicates another transaction with this nonce has already been processed, so this transaction will never be mined and should be marked as failed. - log.Warn("nonce too low detected, marking all non-confirmed transactions with same nonce as failed", "nonce", originalTx.Nonce(), "address", s.transactionSigner.GetAddr().Hex(), "txHash", originalTx.Hash().Hex(), "err", err) - - if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, originalTx.Hash(), types.TxStatusConfirmedFailed); updateErr != nil { - log.Error("failed to update status of original transaction to confirmed failed", "txHash", originalTx.Hash().Hex(), "nonce", originalTx.Nonce(), "from", s.transactionSigner.GetAddr().Hex(), "err", updateErr) + log.Warn("nonce too low detected, marking all non-confirmed transactions with same nonce as failed", "nonce", originalTx.Nonce(), "address", s.transactionSigner.GetAddr().Hex(), "txHash", originalTx.Hash().Hex(), "newTxHash", newSignedTx.Hash().Hex(), "err", err) + + // Handle both original and replacement transactions in a database transaction + if dbErr := s.db.Transaction(func(dbTX *gorm.DB) error { + if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, originalTx.Hash(), types.TxStatusConfirmedFailed, dbTX); updateErr != nil { + return fmt.Errorf("failed to update original transaction status, hash: %s, err: %w", originalTx.Hash().Hex(), updateErr) + } + if updateErr := s.pendingTransactionOrm.DeleteTransactionByTxHash(s.ctx, newSignedTx.Hash(), dbTX); updateErr != nil { + return fmt.Errorf("failed to delete replacement transaction, hash: %s, err: %w", newSignedTx.Hash().Hex(), updateErr) + } + return nil + }); dbErr != nil { + log.Error("failed to handle nonce too low scenario in database", "err", dbErr) return } return From 260a3989a51afbae2a9b9fd8d9497aba1553e9ab Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Wed, 30 Jul 2025 13:26:32 +0800 Subject: [PATCH 6/8] address comments --- rollup/internal/controller/sender/sender.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index 367c437550..508917bf7d 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -655,8 +655,9 @@ func (s *Sender) checkPendingTransaction() { if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, originalTx.Hash(), types.TxStatusConfirmedFailed, dbTX); updateErr != nil { return fmt.Errorf("failed to update original transaction status, hash: %s, err: %w", originalTx.Hash().Hex(), updateErr) } - if updateErr := s.pendingTransactionOrm.DeleteTransactionByTxHash(s.ctx, newSignedTx.Hash(), dbTX); updateErr != nil { - return fmt.Errorf("failed to delete replacement transaction, hash: %s, err: %w", newSignedTx.Hash().Hex(), updateErr) + // Mark the replacement transaction as failed + if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, newSignedTx.Hash(), types.TxStatusConfirmedFailed, dbTX); updateErr != nil { + return fmt.Errorf("failed to update replacement transaction status, hash: %s, err: %w", newSignedTx.Hash().Hex(), updateErr) } return nil }); dbErr != nil { From 167ce20b51f2e3c80c1272f4c9ecdd936a69e2b9 Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Wed, 30 Jul 2025 13:55:51 +0800 Subject: [PATCH 7/8] address comments --- rollup/internal/orm/pending_transaction.go | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/rollup/internal/orm/pending_transaction.go b/rollup/internal/orm/pending_transaction.go index 6dfd164c65..cd92001730 100644 --- a/rollup/internal/orm/pending_transaction.go +++ b/rollup/internal/orm/pending_transaction.go @@ -3,7 +3,7 @@ package orm import ( "bytes" "context" - "database/sql" + "errors" "fmt" "time" @@ -212,21 +212,23 @@ func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context. // GetMaxNonceBySenderAddress retrieves the maximum nonce for a specific sender address. // Returns -1 if no transactions are found for the given address. func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (int64, error) { - var maxNonce sql.NullInt64 + var result struct { + Nonce int64 `gorm:"column:nonce"` + } - row := o.db.WithContext(ctx). + err := o.db.WithContext(ctx). Model(&PendingTransaction{}). - Select("MAX(nonce)"). + Select("nonce"). Where("sender_address = ?", senderAddress). - Row() + Order("nonce DESC"). + First(&result).Error - if err := row.Scan(&maxNonce); err != nil { + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return -1, nil + } return -1, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err) } - if !maxNonce.Valid { - return -1, nil - } - - return maxNonce.Int64, nil + return result.Nonce, nil } From 1115c51c9840b32a71d12bed5f340d098cfbd196 Mon Sep 17 00:00:00 2001 From: colinlyguo Date: Wed, 30 Jul 2025 14:13:01 +0800 Subject: [PATCH 8/8] address comments --- rollup/internal/controller/sender/sender.go | 16 +++------------- rollup/internal/orm/pending_transaction.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/rollup/internal/controller/sender/sender.go b/rollup/internal/controller/sender/sender.go index 508917bf7d..8be721b097 100644 --- a/rollup/internal/controller/sender/sender.go +++ b/rollup/internal/controller/sender/sender.go @@ -649,19 +649,9 @@ func (s *Sender) checkPendingTransaction() { if strings.Contains(err.Error(), "nonce too low") { // When we receive a 'nonce too low' error but cannot find the transaction receipt, it indicates another transaction with this nonce has already been processed, so this transaction will never be mined and should be marked as failed. log.Warn("nonce too low detected, marking all non-confirmed transactions with same nonce as failed", "nonce", originalTx.Nonce(), "address", s.transactionSigner.GetAddr().Hex(), "txHash", originalTx.Hash().Hex(), "newTxHash", newSignedTx.Hash().Hex(), "err", err) - - // Handle both original and replacement transactions in a database transaction - if dbErr := s.db.Transaction(func(dbTX *gorm.DB) error { - if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, originalTx.Hash(), types.TxStatusConfirmedFailed, dbTX); updateErr != nil { - return fmt.Errorf("failed to update original transaction status, hash: %s, err: %w", originalTx.Hash().Hex(), updateErr) - } - // Mark the replacement transaction as failed - if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHash(s.ctx, newSignedTx.Hash(), types.TxStatusConfirmedFailed, dbTX); updateErr != nil { - return fmt.Errorf("failed to update replacement transaction status, hash: %s, err: %w", newSignedTx.Hash().Hex(), updateErr) - } - return nil - }); dbErr != nil { - log.Error("failed to handle nonce too low scenario in database", "err", dbErr) + txHashes := []string{originalTx.Hash().Hex(), newSignedTx.Hash().Hex()} + if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHashes(s.ctx, txHashes, types.TxStatusConfirmedFailed); updateErr != nil { + log.Error("failed to update transaction status", "hashes", txHashes, "err", updateErr) return } return diff --git a/rollup/internal/orm/pending_transaction.go b/rollup/internal/orm/pending_transaction.go index cd92001730..7e1555c5a1 100644 --- a/rollup/internal/orm/pending_transaction.go +++ b/rollup/internal/orm/pending_transaction.go @@ -192,6 +192,25 @@ func (o *PendingTransaction) UpdateTransactionStatusByTxHash(ctx context.Context return nil } +// UpdateTransactionStatusByTxHashes updates the status of multiple transactions by their hashes in one SQL statement +func (o *PendingTransaction) UpdateTransactionStatusByTxHashes(ctx context.Context, txHashes []string, status types.TxStatus, dbTX ...*gorm.DB) error { + if len(txHashes) == 0 { + return nil + } + db := o.db + if len(dbTX) > 0 && dbTX[0] != nil { + db = dbTX[0] + } + db = db.WithContext(ctx) + db = db.Model(&PendingTransaction{}) + db = db.Where("hash IN ?", txHashes) + if err := db.Update("status", status).Error; err != nil { + return fmt.Errorf("failed to update transaction status for hashes %v to status %d: %w", txHashes, status, err) + } + + return nil +} + // UpdateOtherTransactionsAsFailedByNonce updates the status of all transactions to TxStatusConfirmedFailed for a specific nonce and sender address, excluding a specified transaction hash. func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context.Context, senderAddress string, nonce uint64, hash common.Hash, dbTX ...*gorm.DB) error { db := o.db