From a17e3a9ca24510f27a74d8ac11db51d53a91f073 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 26 Dec 2025 16:36:54 +0000 Subject: [PATCH 1/2] fix(transaction): prevent concurrent map access in monitor --- pkg/transaction/monitor.go | 42 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/pkg/transaction/monitor.go b/pkg/transaction/monitor.go index bbd4a50ec58..806f24e6cc6 100644 --- a/pkg/transaction/monitor.go +++ b/pkg/transaction/monitor.go @@ -190,46 +190,52 @@ func watchStart(watches []transactionWatch) time.Time { return start } -// check pending checks the given block (number) for confirmed or cancelled transactions +// checkPending checks the given block for confirmed or cancelled transactions. func (tm *transactionMonitor) checkPending(block uint64) error { - confirmedNonces := make(map[uint64]*types.Receipt) - var cancelledNonces []uint64 - for nonceGroup, watchMap := range tm.watchesByNonce { + // Snapshot nonces and tx hashes to check (releases lock during slow RPC calls). + tm.lock.Lock() + snapshot := make(map[uint64]map[common.Hash]time.Time, len(tm.watchesByNonce)) + for nonce, watchMap := range tm.watchesByNonce { + snapshot[nonce] = make(map[common.Hash]time.Time, len(watchMap)) for txHash, watches := range watchMap { + snapshot[nonce][txHash] = watchStart(watches) + } + } + tm.lock.Unlock() + + // Check receipts without holding lock (RPC calls can be slow). + confirmedNonces := make(map[uint64]*types.Receipt) + for nonce, txMap := range snapshot { + for txHash, start := range txMap { receipt, err := tm.backend.TransactionReceipt(tm.ctx, txHash) if err != nil { - // wait for a few blocks to be mined before considering a transaction not existing - transactionWatchNotFoundTimeout := 5 * tm.pollingInterval - if errors.Is(err, ethereum.NotFound) && watchStart(watches).Before(time.Now().Add(transactionWatchNotFoundTimeout)) { - // if both err and receipt are nil, there is no receipt - // the reason why we consider this only potentially cancelled is to catch cases where after a reorg the original transaction wins + if errors.Is(err, ethereum.NotFound) && start.Before(time.Now().Add(5*tm.pollingInterval)) { continue } return err } if receipt != nil { - // if we have a receipt we have a confirmation - confirmedNonces[nonceGroup] = receipt + confirmedNonces[nonce] = receipt } } } - for nonceGroup := range tm.watchesByNonce { - if _, ok := confirmedNonces[nonceGroup]; ok { + // Check for cancellations. + var cancelledNonces []uint64 + for nonce := range snapshot { + if _, ok := confirmedNonces[nonce]; ok { continue } - oldNonce, err := tm.backend.NonceAt(tm.ctx, tm.sender, new(big.Int).SetUint64(block-tm.cancellationDepth)) if err != nil { return err } - - if nonceGroup < oldNonce { - cancelledNonces = append(cancelledNonces, nonceGroup) + if nonce < oldNonce { + cancelledNonces = append(cancelledNonces, nonce) } } - // notify the subscribers and remove watches for confirmed or cancelled transactions + // Notify subscribers and cleanup. tm.lock.Lock() defer tm.lock.Unlock() From 1a9928167ac97fe81eed8262c78f8deaf97eeab3 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 2 Jan 2026 12:24:17 +0000 Subject: [PATCH 2/2] fix(transaction): optimize checkPending to reduce RPC calls --- pkg/transaction/monitor.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/transaction/monitor.go b/pkg/transaction/monitor.go index 806f24e6cc6..0c1e77f88f9 100644 --- a/pkg/transaction/monitor.go +++ b/pkg/transaction/monitor.go @@ -216,22 +216,29 @@ func (tm *transactionMonitor) checkPending(block uint64) error { } if receipt != nil { confirmedNonces[nonce] = receipt + break // found receipt for this nonce, no need to check other tx hashes } } } - // Check for cancellations. - var cancelledNonces []uint64 + var unconfirmedNonces []uint64 for nonce := range snapshot { - if _, ok := confirmedNonces[nonce]; ok { - continue + if _, ok := confirmedNonces[nonce]; !ok { + unconfirmedNonces = append(unconfirmedNonces, nonce) } + } + + // Check for cancellations. + var cancelledNonces []uint64 + if len(unconfirmedNonces) > 0 { oldNonce, err := tm.backend.NonceAt(tm.ctx, tm.sender, new(big.Int).SetUint64(block-tm.cancellationDepth)) if err != nil { return err } - if nonce < oldNonce { - cancelledNonces = append(cancelledNonces, nonce) + for _, nonce := range unconfirmedNonces { + if nonce < oldNonce { + cancelledNonces = append(cancelledNonces, nonce) + } } }