Skip to content

Commit a17e3a9

Browse files
committed
fix(transaction): prevent concurrent map access in monitor
1 parent a54d62b commit a17e3a9

1 file changed

Lines changed: 24 additions & 18 deletions

File tree

pkg/transaction/monitor.go

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -190,46 +190,52 @@ func watchStart(watches []transactionWatch) time.Time {
190190
return start
191191
}
192192

193-
// check pending checks the given block (number) for confirmed or cancelled transactions
193+
// checkPending checks the given block for confirmed or cancelled transactions.
194194
func (tm *transactionMonitor) checkPending(block uint64) error {
195-
confirmedNonces := make(map[uint64]*types.Receipt)
196-
var cancelledNonces []uint64
197-
for nonceGroup, watchMap := range tm.watchesByNonce {
195+
// Snapshot nonces and tx hashes to check (releases lock during slow RPC calls).
196+
tm.lock.Lock()
197+
snapshot := make(map[uint64]map[common.Hash]time.Time, len(tm.watchesByNonce))
198+
for nonce, watchMap := range tm.watchesByNonce {
199+
snapshot[nonce] = make(map[common.Hash]time.Time, len(watchMap))
198200
for txHash, watches := range watchMap {
201+
snapshot[nonce][txHash] = watchStart(watches)
202+
}
203+
}
204+
tm.lock.Unlock()
205+
206+
// Check receipts without holding lock (RPC calls can be slow).
207+
confirmedNonces := make(map[uint64]*types.Receipt)
208+
for nonce, txMap := range snapshot {
209+
for txHash, start := range txMap {
199210
receipt, err := tm.backend.TransactionReceipt(tm.ctx, txHash)
200211
if err != nil {
201-
// wait for a few blocks to be mined before considering a transaction not existing
202-
transactionWatchNotFoundTimeout := 5 * tm.pollingInterval
203-
if errors.Is(err, ethereum.NotFound) && watchStart(watches).Before(time.Now().Add(transactionWatchNotFoundTimeout)) {
204-
// if both err and receipt are nil, there is no receipt
205-
// the reason why we consider this only potentially cancelled is to catch cases where after a reorg the original transaction wins
212+
if errors.Is(err, ethereum.NotFound) && start.Before(time.Now().Add(5*tm.pollingInterval)) {
206213
continue
207214
}
208215
return err
209216
}
210217
if receipt != nil {
211-
// if we have a receipt we have a confirmation
212-
confirmedNonces[nonceGroup] = receipt
218+
confirmedNonces[nonce] = receipt
213219
}
214220
}
215221
}
216222

217-
for nonceGroup := range tm.watchesByNonce {
218-
if _, ok := confirmedNonces[nonceGroup]; ok {
223+
// Check for cancellations.
224+
var cancelledNonces []uint64
225+
for nonce := range snapshot {
226+
if _, ok := confirmedNonces[nonce]; ok {
219227
continue
220228
}
221-
222229
oldNonce, err := tm.backend.NonceAt(tm.ctx, tm.sender, new(big.Int).SetUint64(block-tm.cancellationDepth))
223230
if err != nil {
224231
return err
225232
}
226-
227-
if nonceGroup < oldNonce {
228-
cancelledNonces = append(cancelledNonces, nonceGroup)
233+
if nonce < oldNonce {
234+
cancelledNonces = append(cancelledNonces, nonce)
229235
}
230236
}
231237

232-
// notify the subscribers and remove watches for confirmed or cancelled transactions
238+
// Notify subscribers and cleanup.
233239
tm.lock.Lock()
234240
defer tm.lock.Unlock()
235241

0 commit comments

Comments
 (0)