Skip to content

Add stuck-transaction monitor for wallet transactions#4174

Open
mswilkison wants to merge 9 commits into
threshold-network:mainfrom
mswilkison:feat/stuck-transaction-monitor
Open

Add stuck-transaction monitor for wallet transactions#4174
mswilkison wants to merge 9 commits into
threshold-network:mainfrom
mswilkison:feat/stuck-transaction-monitor

Conversation

@mswilkison

@mswilkison mswilkison commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Implements #4173. A broadcast wallet transaction that stays unconfirmed in the mempool locks the wallet's main UTXO and blocks all subsequent wallet transactions until it confirms. Today this is silent — the broadcast path waits only for mempool acceptance, never for confirmation.

Change

A Node-scoped transactionMonitor (pkg/tbtc/transaction_monitor.go):

  • The shared walletTransactionExecutor registers every broadcast transaction with the monitor — immediately after a successful BroadcastTransaction, and again once it's known on-chain (idempotent) — so all four wallet-action types are covered even if a confirmation lookup fails.
  • A background loop (started in runCoordinationLayer) polls GetTransactionConfirmations for each tracked tx on a wall-clock cadence. Confirmed → dropped. Unconfirmed past the stuck threshold → emit stuck_wallet_transactions_total + a Warn log naming the wallet and txid (once per tx, and before any give-up eviction). Never-confirming transactions are evicted after a maximum tracking age; a full tracking table surfaces unmonitored_wallet_transactions_total. Each check pass is time-budgeted so one slow chain call cannot stall the rest.

keep-client only exposes the metric + log; alerting/paging is left to the operator's existing Prometheus/Grafana. Additive to the coarse gated_sweep_stall_seconds signal — it surfaces the specific stuck txid so an operator can act in minutes.

Threshold is wall-clock

Tracking uses a wall-clock duration (defaultStuckTransactionThreshold = 6h), not a block count — no extra RPC per check. (An earlier revision of this PR used a block count; this supersedes it.)

Context-aware confirmation lookups

bitcoin.Chain.GetTransactionConfirmations now takes a context.Context so callers can bound and cancel the lookup. The Electrum implementation threads the caller context (combined with the connection-lifetime context) into its retry loop; the other callers pass context.Background(), which the combining preserves as their prior behavior.

This lets the monitor's check pass call the lookup synchronously, bounded by the per-pass time budget, instead of spawning a per-lookup goroutine guarded by an in-flight dedup map. When the budget expires the call is cancelled and the pass defers the remaining transactions to the next pass, so a slow or hung backend can no longer strand a goroutine or an in-flight map entry beyond a check pass.

Note: Electrum's GetTransactionConfirmations also makes an internal GetLatestBlockHeight call — a separate bitcoin.Chain method that is not context-threaded. That trailing lookup still self-bounds via Electrum's own request-retry timeout (~2 min) rather than cancelling with the caller context, so a pass can overrun its budget by up to that before being caught on the next iteration. Not a hang or a leak.

Tests

Cover: no alert below the threshold and at the boundary; exactly-once alert past it; untracking on confirmation; alert-before-eviction for a never-confirming transaction; the table-full metric; and a budgeted check pass cancelling a blocked confirmation lookup rather than stalling. go build + go test ./pkg/tbtc/... ./pkg/clientinfo/... pass.

Out of scope / follow-ups

Summary by CodeRabbit

  • New Features

    • Added background monitoring for broadcast wallet transactions that stay unconfirmed, including operator alerts for “stuck” transactions and capacity-based handling for untracked overflow.
    • Introduced two new performance counters for stuck and unmonitored wallet transactions, enabled when metrics are available.
    • Enabled monitoring across deposit sweeps, redemptions, moving funds, and moved-funds sweeps.
  • Tests

    • Added a dedicated transaction-monitor test suite (alerts, eviction, budgeted checks, capacity limits, snapshot ordering).
    • Updated existing action tests to match the new monitoring wiring.

A broadcast wallet transaction (deposit sweep, redemption, moving funds)
that stays unconfirmed in the mempool locks the wallet's main UTXO and blocks
all subsequent wallet transactions until it confirms. Today this is silent:
the broadcast path waits only for mempool acceptance and never watches for
confirmation.

Add a Node-scoped transaction monitor that tracks broadcast wallet
transactions and, if one stays unconfirmed for longer than a threshold number
of Bitcoin blocks, emits a metric (stuck_wallet_transactions_total) and a warn
log identifying the wallet and txid so operators can intervene (e.g. mempool
acceleration or CPFP). Detection and alerting only; automated fee-bumping is
out of scope (Part B of threshold-network#4171).

Refs threshold-network#4173

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds an in-memory transaction monitor, connects wallet transaction executors and node coordination to it, makes confirmation lookups context-aware, and exposes metrics for stuck and unmonitored wallet transactions. Tests cover alerting, eviction, lookup budgets, ordering, and capacity limits.

Changes

Wallet transaction monitoring

Layer / File(s) Summary
Context-aware confirmation lookups
pkg/bitcoin/..., pkg/maintainer/..., pkg/tbtc/..., pkg/tbtcpg/...
GetTransactionConfirmations accepts a context, Electrum binds requests to caller cancellation and connection shutdown, and callers and test implementations use the updated contract.
Monitor state, polling, and metrics
pkg/tbtc/transaction_monitor.go, pkg/tbtc/transaction_monitor_test.go, pkg/clientinfo/performance.go
The monitor tracks broadcasts, polls confirmations, removes confirmed or stale transactions, enforces lookup and capacity limits, and records stuck or unmonitored metrics covered by tests.
Wallet executor tracking
pkg/tbtc/wallet.go
Wallet transaction executors register successfully broadcast or on-chain-observed transactions with the monitor.
Node lifecycle and wallet-action wiring
pkg/tbtc/node.go, pkg/tbtc/deposit_sweep.go, pkg/tbtc/redemption.go, pkg/tbtc/moving_funds.go, pkg/tbtc/moved_funds_sweep.go, pkg/tbtc/*_test.go
The node initializes and starts the monitor, configures its metrics recorder, and passes it to wallet-action executors; affected tests update constructor calls.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WalletAction
  participant walletTransactionExecutor
  participant transactionMonitor
  participant BitcoinChain
  WalletAction->>walletTransactionExecutor: broadcast transaction
  walletTransactionExecutor->>transactionMonitor: track transaction
  transactionMonitor->>BitcoinChain: poll confirmations with bounded context
  BitcoinChain-->>transactionMonitor: return confirmation count
  transactionMonitor-->>transactionMonitor: alert, evict, or remove transaction
Loading

Possibly related issues

  • Issue 4175: Proposes persisting the monitor’s tracked transactions across restarts; this PR adds only in-memory tracking.

Suggested reviewers: piotr-roslaniec

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a wallet transaction monitor for stuck transactions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- Track by wall-clock time instead of block height. This removes the
  synchronous GetLatestBlockHeight call from the broadcast path and, more
  importantly, the silent-skip on its failure - a transaction is now always
  tracked once broadcast, including during chain instability when it is most
  likely to get stuck.
- Give up on transactions that stay unconfirmed past a maximum tracking age
  (e.g. dropped from the mempool) so never-confirming transactions cannot fill
  the tracking table and starve monitoring of new ones.
- Emit the metric outside the mutex rather than holding the lock during the
  recorder call.
- Document the single-goroutine invariant of check(), the per-operator nature
  of the metric/log, and the in-memory (cross-restart) limitation.
- Rework the unit test accordingly and add coverage for the give-up eviction
  and the capacity bound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread pkg/tbtc/wallet.go Outdated

broadcastTxLogger.Infof("transaction is known on Bitcoin chain")

if wte.transactionMonitor != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

track() here only runs after GetTransactionConfirmations (line 447) succeeds. If that call keeps failing, line 454 continues and this transaction is never registered — even though BroadcastTransaction at line 420 may have already succeeded. Two paths need to register the transaction: right after a successful broadcast (don't wait on the confirmation lookup for that case), and also here when a later lookup finds the transaction known even if this operator's own broadcast call errored. track() is already safe to call more than once on the same hash, so both call sites can register it without special-casing.


// Give up on transactions that have been unconfirmed for too long (e.g.
// dropped from the mempool) so they cannot fill the tracking table.
if outstanding > transactionMonitorMaxTrackingAge {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 24-hour give-up check here runs and continues before the 6-hour stuck-alert check at line 185, in the same pass. If a transaction's first check() happens after 24 hours already passed, it gets evicted with only a "giving up" log — the stuck alert and the stuck_wallet_transactions_total metric never fire for it. At the defaults, each Electrum operation has a 30-second attempt timeout and a two-minute retry window, but GetTransactionConfirmations can perform several such operations. Reaching eviction before the first alert still requires substantial accumulated delay across the serialized checks, or a long process-level pause, so this is a defensive robustness fix rather than a normal-operation blocker. Still worth fixing: swap the order so the alert check runs before eviction.

Comment thread pkg/tbtc/transaction_monitor.go Outdated
tm.mu.Unlock()

for txHash, t := range snapshot {
confirmations, err := tm.btcChain.GetTransactionConfirmations(txHash)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although Electrum bounds each individual operation by default, checks here are serialized, so one slow call delays every transaction behind it in the same pass. Strongest fix is a monitor-specific end-to-end timeout budget for the whole check. Bounded concurrency would help too, though the Electrum client's shared connection mutex means the underlying wire requests stay serialized either way.

// transaction tracks it independently, so the metric and log are emitted per
// operator and should be de-duplicated by transaction hash downstream.
//
// The tracked set is in-memory only. A transaction that is already stuck when

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already called out in the comment, so not a new finding — flagging because restarts (deploys, upgrades, crashes) aren't rare, so this is a monitoring gap on a fairly regular cadence, not just an edge case. Recommend filing a follow-up issue to persist the tracked set and rehydrate it on startup before this ships to mainnet.

return
}

if len(tm.tracked) >= transactionMonitorMaxTracked {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the tracking table is full, this just logs a warning and returns — no metric, and the caller in wallet.go has no way to know registration failed. A full table means a real broadcast transaction goes unmonitored, so suggest adding a dedicated metric here, not just a log line.

// defaultStuckTransactionThreshold is how long a broadcast wallet
// transaction may remain unconfirmed before it is considered stuck and an
// alert is raised (~6 hours).
defaultStuckTransactionThreshold = 6 * time.Hour

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description's "Open for review" section mentions defaultStuckTransactionThresholdBlocks = 36, but the constant actually implemented here is defaultStuckTransactionThreshold = 6 * time.Hour — a wall-clock duration, not a block count, and a different name. Worth updating the description to match, or confirming wall-clock is the intended final approach (tracking by block height would need an extra RPC per check). On the config question you raised: since none of these four constants can be tuned without a code change today, recommend moving them into node config with the current values as defaults.

t.Fatalf("expected no alert for a fresh transaction; got counter [%v]", got)
}

// Past the threshold: flagged as stuck exactly once across repeated checks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more small description/test mismatch: the body says this covers "no alert at the threshold," but this test checks a fresh transaction and then jumps to threshold + 1 minute — the exact boundary itself isn't tested. Please add a case at exactly defaultStuckTransactionThreshold, or update that line in the description.

…st track path

- Register a broadcast transaction with the monitor immediately after a
  successful BroadcastTransaction, not only after the confirmation lookup
  succeeds, so a failing lookup no longer leaves a broadcast transaction
  unmonitored (lrsaturnino review).
- Fire the stuck alert before the give-up eviction so a transaction first
  observed after the maximum tracking age still alerts exactly once instead of
  being silently evicted.
- Emit an unmonitored-transactions metric (not just a log) when the tracking
  table is full, so a real broadcast transaction going unmonitored is visible.
- Bound each check pass with a time budget so one slow chain call cannot stall
  monitoring of the remaining transactions.
- Extend tests to cover the alert-before-eviction path, the table-full metric,
  and the threshold boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Note that each GetTransactionConfirmations call is separately bounded by
the Electrum client's own operation timeouts, so the per-pass budget plus
per-call bound together keep one slow call from stalling a check pass.

Correct the boundary-test comment to state it exercises just-below the
threshold and explain why exactly-at is not tested deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lrsaturnino
lrsaturnino previously approved these changes Jul 19, 2026

@lrsaturnino lrsaturnino left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mswilkison
mswilkison marked this pull request as ready for review July 20, 2026 14:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/tbtc/transaction_monitor.go`:
- Around line 168-190: Update the transaction-monitor check pass after building
the snapshot in the relevant monitoring function to iterate transactions
deterministically by ascending broadcastAt, rather than directly over the
randomized map. Import and use sort as needed, ensuring older transactions are
checked first and deferred newer transactions do not starve older ones across
passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ebc33ba8-2553-4618-ad4f-c06ca2560eac

📥 Commits

Reviewing files that changed from the base of the PR and between 038b7ce and b35cc05.

📒 Files selected for processing (13)
  • pkg/clientinfo/performance.go
  • pkg/tbtc/deposit_sweep.go
  • pkg/tbtc/deposit_sweep_test.go
  • pkg/tbtc/moved_funds_sweep.go
  • pkg/tbtc/moved_funds_sweep_test.go
  • pkg/tbtc/moving_funds.go
  • pkg/tbtc/moving_funds_test.go
  • pkg/tbtc/node.go
  • pkg/tbtc/redemption.go
  • pkg/tbtc/redemption_test.go
  • pkg/tbtc/transaction_monitor.go
  • pkg/tbtc/transaction_monitor_test.go
  • pkg/tbtc/wallet.go

Comment thread pkg/tbtc/transaction_monitor.go Outdated
Go map iteration is randomized, so when a check pass hits its time budget
and defers the rest, an unlucky old transaction could be skipped pass after
pass and miss its stuck threshold entirely. Iterate a snapshot ordered by
broadcast time (oldest first) via snapshotByAge, so the transactions closest
to alerting are always checked first and only the newest are ever deferred
(per CodeRabbit review on threshold-network#4174).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mswilkison
mswilkison dismissed stale reviews from piotr-roslaniec and lrsaturnino via 3834410 July 20, 2026 15:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/tbtc/transaction_monitor_test.go (1)

226-242: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the second check() call with a timeout, like the first.

This block asserts the core "later pass skips the in-flight lookup instead of stalling" behavior, but monitor.check(context.Background()) at line 235 is called synchronously with no timeout. If this exact behavior regresses, the call blocks indefinitely and the test fails via a global Go test timeout instead of a clear, fast assertion — losing the diagnostic value this test is meant to provide. The first checkWithBudget call a few lines above (lines 203-220) already uses a checkDone/select+time.After pattern for the same reason; mirror it here.

♻️ Proposed fix
 	confirmedTxHash := confirmedTx.Hash()
 	monitor.track(confirmedTxHash, [20]byte{})
 
-	monitor.check(context.Background())
+	secondCheckDone := make(chan struct{})
+	go func() {
+		monitor.check(context.Background())
+		close(secondCheckDone)
+	}()
+	select {
+	case <-secondCheckDone:
+	case <-time.After(2 * time.Second):
+		t.Fatal("second check pass stalled on the in-flight lookup")
+	}
 
 	if got := chain.getLookupCount(); got != 1 {
 		t.Fatalf("expected one in-flight lookup; got [%d]", got)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/tbtc/transaction_monitor_test.go` around lines 226 - 242, Wrap the second
monitor check in the same timeout-based completion pattern used by the earlier
checkWithBudget block: run monitor.check in a goroutine, wait on its completion
channel, and fail with a short time.After timeout if it stalls. Preserve the
existing lookup-count and tracking assertions after the check completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/tbtc/transaction_monitor_test.go`:
- Around line 226-242: Wrap the second monitor check in the same timeout-based
completion pattern used by the earlier checkWithBudget block: run monitor.check
in a goroutine, wait on its completion channel, and fail with a short time.After
timeout if it stalls. Preserve the existing lookup-count and tracking assertions
after the check completes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ea421f54-43f6-44aa-a4f8-6f1e077c83cc

📥 Commits

Reviewing files that changed from the base of the PR and between 3834410 and 9fe32b1.

📒 Files selected for processing (2)
  • pkg/tbtc/transaction_monitor.go
  • pkg/tbtc/transaction_monitor_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/tbtc/transaction_monitor.go

mswilkison and others added 2 commits July 21, 2026 14:59
Add a context parameter to bitcoin.Chain.GetTransactionConfirmations so
callers can bound and cancel the lookup. The electrum implementation
threads the caller context (combined with the connection lifetime
context) into its retry loop; other callers pass context.Background(),
which the combining preserves as their prior behavior.

This lets the transaction monitor drop its per-lookup goroutine and the
inFlightConfirmationLookups dedup map. The confirmation lookup is now a
synchronous call bounded by the check budget, so a slow or hung backend
can no longer strand a goroutine or a map entry beyond a check pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/bitcoin/electrum/electrum.go (1)

1290-1301: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make mutex acquisition cancellation-aware.

requestWithRetry derives requestCtx only after reconnectIfShutdown() and c.clientMutex.Lock(). Because the mutex is held across the network request, an unrelated slow request can keep a monitor lookup blocked even after its parentCtx expires. Avoid non-cancellable lock waits in this path, or use a context-aware semaphore/connection design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/bitcoin/electrum/electrum.go` around lines 1290 - 1301, Update
requestWithRetry so mutex acquisition is cancellation-aware: derive/use the
request context before waiting for c.clientMutex and replace the blocking Lock
path with a context-aware acquisition mechanism. Ensure an expired parentCtx
returns promptly without waiting behind another network request, while
preserving the existing mutex protection around the request and retry flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/bitcoin/electrum/electrum.go`:
- Around line 143-152: Update GetTransactionConfirmations to use a context-aware
internal latest-block-height helper with reqCtx instead of
c.GetLatestBlockHeight(), while preserving the existing public method behavior.
Implement the helper using the supplied context and add a regression test that
blocks the tip lookup and verifies cancellation within the monitor check budget.

In `@pkg/tbtc/transaction_monitor.go`:
- Around line 216-221: Update the transaction-processing loop around
GetTransactionConfirmations so a lookup that ends with checkCtx expiration does
not remain first on every pass. Defer or rotate that timed-out transaction, or
continue processing subsequent entries as unconfirmed so newer transactions
still receive confirmation removal, stuck alerts, and maximum-age eviction; add
coverage for a blocked oldest transaction and a newer eligible transaction.

---

Outside diff comments:
In `@pkg/bitcoin/electrum/electrum.go`:
- Around line 1290-1301: Update requestWithRetry so mutex acquisition is
cancellation-aware: derive/use the request context before waiting for
c.clientMutex and replace the blocking Lock path with a context-aware
acquisition mechanism. Ensure an expired parentCtx returns promptly without
waiting behind another network request, while preserving the existing mutex
protection around the request and retry flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a015f496-4bcd-4f21-aa3c-b868d78811b1

📥 Commits

Reviewing files that changed from the base of the PR and between 6022795 and 7f46a53.

📒 Files selected for processing (17)
  • pkg/bitcoin/chain.go
  • pkg/bitcoin/chain_test.go
  • pkg/bitcoin/electrum/electrum.go
  • pkg/bitcoin/electrum/electrum_integration_test.go
  • pkg/bitcoin/spv_proof.go
  • pkg/clientinfo/rpc_health_test.go
  • pkg/maintainer/btcdiff/bitcoin_chain_test.go
  • pkg/maintainer/spv/bitcoin_chain_test.go
  • pkg/maintainer/spv/spv.go
  • pkg/tbtc/bitcoin_chain_test.go
  • pkg/tbtc/deposit_sweep.go
  • pkg/tbtc/transaction_monitor.go
  • pkg/tbtc/transaction_monitor_test.go
  • pkg/tbtc/wallet.go
  • pkg/tbtc/wallet_test.go
  • pkg/tbtcpg/bitcoin_chain_test.go
  • pkg/tbtcpg/deposit_sweep.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/tbtc/wallet.go
  • pkg/tbtc/deposit_sweep.go
  • pkg/tbtc/transaction_monitor_test.go

Comment on lines +143 to +152
// Bound the network calls below by both the caller's context and the
// connection lifetime context, so a cancelled caller aborts the lookup while
// connection shutdown still cancels it as before.
reqCtx, cancel := context.WithCancel(ctx)
defer cancel()
stopOnParentDone := context.AfterFunc(c.parentCtx, cancel)
defer stopOnParentDone()

rawTransaction, err := requestWithRetry(
reqCtx,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Propagate the caller context through the latest-block lookup.

GetTransactionConfirmations bounds the raw transaction and history calls with reqCtx, but its later c.GetLatestBlockHeight() call uses c.parentCtx. A slow tip request can therefore continue past the monitor’s check budget and stall the monitoring pass. Add a context-aware internal latest-height helper and call it with reqCtx; add a regression test covering a blocked tip lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/bitcoin/electrum/electrum.go` around lines 143 - 152, Update
GetTransactionConfirmations to use a context-aware internal latest-block-height
helper with reqCtx instead of c.GetLatestBlockHeight(), while preserving the
existing public method behavior. Implement the helper using the supplied context
and add a regression test that blocks the tip lookup and verifies cancellation
within the monitor check budget.

Comment on lines +216 to +221
// The chain call is bounded by checkCtx, so a slow or hung backend cannot
// keep this run loop blocked past the check budget: when the budget
// expires the call is cancelled and returns, and the checkCtx.Err() guard
// below defers the remaining transactions to the next pass.
confirmations, err :=
tm.btcChain.GetTransactionConfirmations(checkCtx, txHash)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Prevent a timed-out oldest lookup from starving the rest of the table.

Because each pass starts with the oldest transaction, a lookup that blocks until checkCtx expires is retried first on every pass; the function returns before reaching newer transactions. Their confirmation removal, stuck alerts, and maximum-age eviction can then be delayed indefinitely. Rotate/defer the timed-out entry or process remaining entries as unconfirmed for local alert/eviction handling, and add a test with a blocked old transaction plus a newer eligible one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/tbtc/transaction_monitor.go` around lines 216 - 221, Update the
transaction-processing loop around GetTransactionConfirmations so a lookup that
ends with checkCtx expiration does not remain first on every pass. Defer or
rotate that timed-out transaction, or continue processing subsequent entries as
unconfirmed so newer transactions still receive confirmation removal, stuck
alerts, and maximum-age eviction; add coverage for a blocked oldest transaction
and a newer eligible transaction.

Run the age-based stuck-alert and give-up eviction for the current
transaction before deferring the rest when a check pass hits its time
budget. Previously the pass returned immediately after a cancelled
confirmation lookup, so a backend that consistently hung on the oldest
tracked transaction would keep it (the one closest to the stuck
threshold) from ever alerting or being evicted, and newer transactions
were never reached either.

The remaining transactions are still deferred rather than alerted on
without a lookup, so they cannot false-alert on age alone. Also thread
broadcastCtx into the post-broadcast confirmation check so a hung backend
cannot block the broadcast goroutine past its timeout, and make the
oldest-first snapshot sort stable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants