Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 5380f48

Browse files
huangzhen1997amit-momin
authored andcommitted
Fix Nil pointer error in TXM stuck tx detector (#14685)
* add nil ptr check for findBroadcastedAttempts * add changeset * fix loop variable lint * remove comment * add nil ptr check for BroadcastBeforeBlockNum * add error log * update log fmt * break logs * update sanity check * add unit test coverage for empty BroadcastBeforeBlockNum * update comments * fix test name * fix make
1 parent f8f9513 commit 5380f48

File tree

3 files changed

+51
-4
lines changed

3 files changed

+51
-4
lines changed

.changeset/chilled-plants-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink": patch
3+
---
4+
5+
The findBroadcastedAttempts in detectStuckTransactionsHeuristic can returns uninitialized struct that potentially cause nil pointer error. Changed the return type of findBroadcastedAttempts to be pointers and added nil pointer check. #bugfix

core/chains/evm/txmgr/stuck_tx_detector.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,25 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context,
217217
}
218218
// Tx attempts are loaded from newest to oldest
219219
oldestBroadcastAttempt, newestBroadcastAttempt, broadcastedAttemptsCount := findBroadcastedAttempts(tx)
220+
d.lggr.Debugf("found %d broadcasted attempts for tx id %d in stuck transaction heuristic", broadcastedAttemptsCount, tx.ID)
221+
222+
// attempt shouldn't be nil as we validated in FindUnconfirmedTxWithLowestNonce, but added anyway for a "belts and braces" approach
223+
if oldestBroadcastAttempt == nil || newestBroadcastAttempt == nil {
224+
d.lggr.Debugw("failed to find broadcast attempt for tx in stuck transaction heuristic", "tx", tx)
225+
continue
226+
}
227+
228+
// sanity check
229+
if oldestBroadcastAttempt.BroadcastBeforeBlockNum == nil {
230+
d.lggr.Debugw("BroadcastBeforeBlockNum was not set for broadcast attempt in stuck transaction heuristic", "attempt", oldestBroadcastAttempt)
231+
continue
232+
}
233+
220234
// 2. Check if Threshold amount of blocks have passed since the oldest attempt's broadcast block num
221235
if *oldestBroadcastAttempt.BroadcastBeforeBlockNum > blockNum-int64(*d.cfg.Threshold()) {
222236
continue
223237
}
238+
224239
// 3. Check if the transaction has at least MinAttempts amount of broadcasted attempts
225240
if broadcastedAttemptsCount < *d.cfg.MinAttempts() {
226241
continue
@@ -246,17 +261,18 @@ func compareGasFees(attemptGas gas.EvmFee, marketGas gas.EvmFee) int {
246261
}
247262

248263
// Assumes tx attempts are loaded newest to oldest
249-
func findBroadcastedAttempts(tx Tx) (oldestAttempt TxAttempt, newestAttempt TxAttempt, broadcastedCount uint32) {
264+
func findBroadcastedAttempts(tx Tx) (oldestAttempt *TxAttempt, newestAttempt *TxAttempt, broadcastedCount uint32) {
250265
foundNewest := false
251-
for _, attempt := range tx.TxAttempts {
266+
for i := range tx.TxAttempts {
267+
attempt := tx.TxAttempts[i]
252268
if attempt.State != types.TxAttemptBroadcast {
253269
continue
254270
}
255271
if !foundNewest {
256-
newestAttempt = attempt
272+
newestAttempt = &attempt
257273
foundNewest = true
258274
}
259-
oldestAttempt = attempt
275+
oldestAttempt = &attempt
260276
broadcastedCount++
261277
}
262278
return

core/chains/evm/txmgr/stuck_tx_detector_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,15 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) {
278278
require.NoError(t, err)
279279
require.Len(t, txs, 1)
280280
})
281+
282+
t.Run("detects stuck transaction with empty BroadcastBeforeBlockNum in attempts will be skipped without panic", func(t *testing.T) {
283+
_, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore)
284+
enabledAddresses := []common.Address{fromAddress}
285+
mustInsertUnconfirmedTxWithBroadcastAttemptsContainsEmptyBroadcastBeforeBlockNum(t, txStore, 0, fromAddress, autoPurgeMinAttempts, marketGasPrice.Add(oneGwei))
286+
txs, err := stuckTxDetector.DetectStuckTransactions(ctx, enabledAddresses, blockNum)
287+
require.NoError(t, err)
288+
require.Len(t, txs, 0)
289+
})
281290
}
282291

283292
func TestStuckTxDetector_DetectStuckTransactionsZircuit(t *testing.T) {
@@ -537,6 +546,23 @@ func mustInsertUnconfirmedTxWithBroadcastAttempts(t *testing.T, txStore txmgr.Te
537546
return etx
538547
}
539548

549+
// helper function for edge case where broadcast attempt contains empty pointer
550+
func mustInsertUnconfirmedTxWithBroadcastAttemptsContainsEmptyBroadcastBeforeBlockNum(t *testing.T, txStore txmgr.TestEvmTxStore, nonce int64, fromAddress common.Address, numAttempts uint32, latestGasPrice *assets.Wei) txmgr.Tx {
551+
ctx := tests.Context(t)
552+
etx := cltest.MustInsertUnconfirmedEthTx(t, txStore, nonce, fromAddress)
553+
// Insert attempts from oldest to newest
554+
for i := int64(numAttempts - 1); i >= 0; i-- {
555+
attempt := cltest.NewLegacyEthTxAttempt(t, etx.ID)
556+
attempt.State = txmgrtypes.TxAttemptBroadcast
557+
attempt.BroadcastBeforeBlockNum = nil
558+
attempt.TxFee = gas.EvmFee{GasPrice: latestGasPrice.Sub(assets.NewWeiI(i))}
559+
require.NoError(t, txStore.InsertTxAttempt(ctx, &attempt))
560+
}
561+
etx, err := txStore.FindTxWithAttempts(ctx, etx.ID)
562+
require.NoError(t, err)
563+
return etx
564+
}
565+
540566
func mustInsertFatalErrorTxWithError(t *testing.T, txStore txmgr.TestEvmTxStore, nonce int64, fromAddress common.Address, blockNum int64) txmgr.Tx {
541567
etx := cltest.NewEthTx(fromAddress)
542568
etx.State = txmgrcommon.TxFatalError

0 commit comments

Comments
 (0)