Skip to content

Commit fc75299

Browse files
authored
fix(gas-oracle): nonce too low when resubmission (#1712)
1 parent 4bfcd35 commit fc75299

File tree

3 files changed

+160
-14
lines changed

3 files changed

+160
-14
lines changed

rollup/internal/controller/sender/sender.go

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type FeeData struct {
6363
gasLimit uint64
6464
}
6565

66-
// Sender Transaction sender to send transaction to l1/l2 geth
66+
// Sender Transaction sender to send transaction to l1/l2
6767
type Sender struct {
6868
config *config.SenderConfig
6969
gethClient *gethclient.Client
@@ -105,13 +105,7 @@ func NewSender(ctx context.Context, config *config.SenderConfig, signerConfig *c
105105
return nil, fmt.Errorf("failed to create transaction signer, err: %w", err)
106106
}
107107

108-
// Set pending nonce
109-
nonce, err := client.PendingNonceAt(ctx, transactionSigner.GetAddr())
110-
if err != nil {
111-
return nil, fmt.Errorf("failed to get pending nonce for address %s, err: %w", transactionSigner.GetAddr(), err)
112-
}
113-
transactionSigner.SetNonce(nonce)
114-
108+
// Create sender instance first and then initialize nonce
115109
sender := &Sender{
116110
ctx: ctx,
117111
config: config,
@@ -127,8 +121,13 @@ func NewSender(ctx context.Context, config *config.SenderConfig, signerConfig *c
127121
service: service,
128122
senderType: senderType,
129123
}
130-
sender.metrics = initSenderMetrics(reg)
131124

125+
// Initialize nonce using the new method
126+
if err := sender.resetNonce(); err != nil {
127+
return nil, fmt.Errorf("failed to reset nonce: %w", err)
128+
}
129+
130+
sender.metrics = initSenderMetrics(reg)
132131
go sender.loop(ctx)
133132

134133
return sender, nil
@@ -242,7 +241,10 @@ func (s *Sender) SendTransaction(contextID string, target *common.Address, data
242241
// Check if contain nonce, and reset nonce
243242
// only reset nonce when it is not from resubmit
244243
if strings.Contains(err.Error(), "nonce too low") {
245-
s.resetNonce(context.Background())
244+
if err := s.resetNonce(); err != nil {
245+
log.Warn("failed to reset nonce after failed send transaction", "address", s.transactionSigner.GetAddr().String(), "err", err)
246+
return common.Hash{}, 0, fmt.Errorf("failed to reset nonce after failed send transaction, err: %w", err)
247+
}
246248
}
247249
return common.Hash{}, 0, fmt.Errorf("failed to send transaction, err: %w", err)
248250
}
@@ -327,14 +329,46 @@ func (s *Sender) createTx(feeData *FeeData, target *common.Address, data []byte,
327329
return signedTx, nil
328330
}
329331

332+
// initializeNonce initializes the nonce by taking the maximum of database nonce and pending nonce.
333+
func (s *Sender) initializeNonce() (uint64, error) {
334+
// Get maximum nonce from database
335+
dbNonce, err := s.pendingTransactionOrm.GetMaxNonceBySenderAddress(s.ctx, s.transactionSigner.GetAddr().Hex())
336+
if err != nil {
337+
return 0, fmt.Errorf("failed to get max nonce from database for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err)
338+
}
339+
340+
// Get pending nonce from the client
341+
pendingNonce, err := s.client.PendingNonceAt(s.ctx, s.transactionSigner.GetAddr())
342+
if err != nil {
343+
return 0, fmt.Errorf("failed to get pending nonce for address %s, err: %w", s.transactionSigner.GetAddr().Hex(), err)
344+
}
345+
346+
// Take the maximum of pending nonce and (db nonce + 1)
347+
// Database stores the used nonce, so the next available nonce should be dbNonce + 1
348+
// When dbNonce is -1 (no records), dbNonce + 1 = 0, which is correct
349+
nextDbNonce := uint64(dbNonce + 1)
350+
var finalNonce uint64
351+
if pendingNonce > nextDbNonce {
352+
finalNonce = pendingNonce
353+
} else {
354+
finalNonce = nextDbNonce
355+
}
356+
357+
log.Info("nonce initialization", "address", s.transactionSigner.GetAddr().Hex(), "maxDbNonce", dbNonce, "nextDbNonce", nextDbNonce, "pendingNonce", pendingNonce, "finalNonce", finalNonce)
358+
359+
return finalNonce, nil
360+
}
361+
330362
// resetNonce reset nonce if send signed tx failed.
331-
func (s *Sender) resetNonce(ctx context.Context) {
332-
nonce, err := s.client.PendingNonceAt(ctx, s.transactionSigner.GetAddr())
363+
func (s *Sender) resetNonce() error {
364+
nonce, err := s.initializeNonce()
333365
if err != nil {
334-
log.Warn("failed to reset nonce", "address", s.transactionSigner.GetAddr().String(), "err", err)
335-
return
366+
log.Error("failed to reset nonce", "address", s.transactionSigner.GetAddr().String(), "err", err)
367+
return fmt.Errorf("failed to reset nonce, err: %w", err)
336368
}
369+
log.Info("reset nonce", "address", s.transactionSigner.GetAddr().String(), "nonce", nonce)
337370
s.transactionSigner.SetNonce(nonce)
371+
return nil
338372
}
339373

340374
func (s *Sender) createReplacingTransaction(tx *gethTypes.Transaction, baseFee, blobBaseFee uint64) (*gethTypes.Transaction, error) {
@@ -612,6 +646,16 @@ func (s *Sender) checkPendingTransaction() {
612646
}
613647

614648
if err := s.client.SendTransaction(s.ctx, newSignedTx); err != nil {
649+
if strings.Contains(err.Error(), "nonce too low") {
650+
// 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.
651+
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)
652+
txHashes := []string{originalTx.Hash().Hex(), newSignedTx.Hash().Hex()}
653+
if updateErr := s.pendingTransactionOrm.UpdateTransactionStatusByTxHashes(s.ctx, txHashes, types.TxStatusConfirmedFailed); updateErr != nil {
654+
log.Error("failed to update transaction status", "hashes", txHashes, "err", updateErr)
655+
return
656+
}
657+
return
658+
}
615659
// SendTransaction failed, need to rollback the previous database changes
616660
if rollbackErr := s.db.Transaction(func(tx *gorm.DB) error {
617661
// Restore original transaction status back to pending

rollup/internal/orm/orm_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,3 +597,61 @@ func TestPendingTransactionOrm(t *testing.T) {
597597
err = pendingTransactionOrm.DeleteTransactionByTxHash(context.Background(), common.HexToHash("0x123"))
598598
assert.Error(t, err) // Should return error for non-existent transaction
599599
}
600+
601+
func TestPendingTransaction_GetMaxNonceBySenderAddress(t *testing.T) {
602+
sqlDB, err := db.DB()
603+
assert.NoError(t, err)
604+
assert.NoError(t, migrate.ResetDB(sqlDB))
605+
606+
// When there are no transactions for this sender address, should return -1
607+
maxNonce, err := pendingTransactionOrm.GetMaxNonceBySenderAddress(context.Background(), "0xdeadbeef")
608+
assert.NoError(t, err)
609+
assert.Equal(t, int64(-1), maxNonce)
610+
611+
// Insert two transactions with different nonces for the same sender address
612+
senderMeta := &SenderMeta{
613+
Name: "testName",
614+
Service: "testService",
615+
Address: common.HexToAddress("0xdeadbeef"),
616+
Type: types.SenderTypeCommitBatch,
617+
}
618+
619+
tx0 := gethTypes.NewTx(&gethTypes.DynamicFeeTx{
620+
Nonce: 1,
621+
To: &common.Address{},
622+
Data: []byte{},
623+
Gas: 21000,
624+
AccessList: gethTypes.AccessList{},
625+
Value: big.NewInt(0),
626+
ChainID: big.NewInt(1),
627+
GasTipCap: big.NewInt(0),
628+
GasFeeCap: big.NewInt(1),
629+
V: big.NewInt(0),
630+
R: big.NewInt(0),
631+
S: big.NewInt(0),
632+
})
633+
tx1 := gethTypes.NewTx(&gethTypes.DynamicFeeTx{
634+
Nonce: 3,
635+
To: &common.Address{},
636+
Data: []byte{},
637+
Gas: 22000,
638+
AccessList: gethTypes.AccessList{},
639+
Value: big.NewInt(0),
640+
ChainID: big.NewInt(1),
641+
GasTipCap: big.NewInt(1),
642+
GasFeeCap: big.NewInt(2),
643+
V: big.NewInt(0),
644+
R: big.NewInt(0),
645+
S: big.NewInt(0),
646+
})
647+
648+
err = pendingTransactionOrm.InsertPendingTransaction(context.Background(), "test", senderMeta, tx0, 0)
649+
assert.NoError(t, err)
650+
err = pendingTransactionOrm.InsertPendingTransaction(context.Background(), "test", senderMeta, tx1, 0)
651+
assert.NoError(t, err)
652+
653+
// Now the max nonce for this sender should be 3
654+
maxNonce, err = pendingTransactionOrm.GetMaxNonceBySenderAddress(context.Background(), senderMeta.Address.String())
655+
assert.NoError(t, err)
656+
assert.Equal(t, int64(3), maxNonce)
657+
}

rollup/internal/orm/pending_transaction.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package orm
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"time"
89

@@ -191,6 +192,25 @@ func (o *PendingTransaction) UpdateTransactionStatusByTxHash(ctx context.Context
191192
return nil
192193
}
193194

195+
// UpdateTransactionStatusByTxHashes updates the status of multiple transactions by their hashes in one SQL statement
196+
func (o *PendingTransaction) UpdateTransactionStatusByTxHashes(ctx context.Context, txHashes []string, status types.TxStatus, dbTX ...*gorm.DB) error {
197+
if len(txHashes) == 0 {
198+
return nil
199+
}
200+
db := o.db
201+
if len(dbTX) > 0 && dbTX[0] != nil {
202+
db = dbTX[0]
203+
}
204+
db = db.WithContext(ctx)
205+
db = db.Model(&PendingTransaction{})
206+
db = db.Where("hash IN ?", txHashes)
207+
if err := db.Update("status", status).Error; err != nil {
208+
return fmt.Errorf("failed to update transaction status for hashes %v to status %d: %w", txHashes, status, err)
209+
}
210+
211+
return nil
212+
}
213+
194214
// UpdateOtherTransactionsAsFailedByNonce updates the status of all transactions to TxStatusConfirmedFailed for a specific nonce and sender address, excluding a specified transaction hash.
195215
func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context.Context, senderAddress string, nonce uint64, hash common.Hash, dbTX ...*gorm.DB) error {
196216
db := o.db
@@ -207,3 +227,27 @@ func (o *PendingTransaction) UpdateOtherTransactionsAsFailedByNonce(ctx context.
207227
}
208228
return nil
209229
}
230+
231+
// GetMaxNonceBySenderAddress retrieves the maximum nonce for a specific sender address.
232+
// Returns -1 if no transactions are found for the given address.
233+
func (o *PendingTransaction) GetMaxNonceBySenderAddress(ctx context.Context, senderAddress string) (int64, error) {
234+
var result struct {
235+
Nonce int64 `gorm:"column:nonce"`
236+
}
237+
238+
err := o.db.WithContext(ctx).
239+
Model(&PendingTransaction{}).
240+
Select("nonce").
241+
Where("sender_address = ?", senderAddress).
242+
Order("nonce DESC").
243+
First(&result).Error
244+
245+
if err != nil {
246+
if errors.Is(err, gorm.ErrRecordNotFound) {
247+
return -1, nil
248+
}
249+
return -1, fmt.Errorf("failed to get max nonce by sender address, address: %s, err: %w", senderAddress, err)
250+
}
251+
252+
return result.Nonce, nil
253+
}

0 commit comments

Comments
 (0)