Skip to content

Never report a successful broadcast as a failed send - #455

Open
j0ntz wants to merge 9 commits into
masterfrom
jon/send-post-broadcast-failure
Open

Never report a successful broadcast as a failed send#455
j0ntz wants to merge 9 commits into
masterfrom
jon/send-post-broadcast-failure

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Technical design doc

Fixes the engine half of Edge bug "Send - multiple transactions after network error": one intended Bitcoin send became five real payments because every failure the user saw happened AFTER a successful broadcast.

Asana: https://app.asana.com/1/9976422036640/project/1213843652804305/task/1217135300337949

Two defects fixed:

  1. saveTx failed on a disconnected engine. updateProgressRatio threw No addresses to process whenever zero addresses were subscribed (exactly the state of a wallet whose blockbook sockets are down), and saveTx reaches it via processUtxos after the transaction is already saved and its inputs marked spent. The throw is progress bookkeeping with no denominator, not a data error, so it is now a no-op instead of failing the caller's data write. Regression test: saveTx on a never-connected engine resolves (test/common/utxobased/engine/saveTx.spec.ts, red before the fix with the exact incident stack).

  2. Broadcast failure was ambiguous. ServerStates.broadcastTx multicasts to every connected blockbook (or every NOWNode HTTP fallback) and rejects only when all fail, but a server can relay the transaction and still return an error or time out, so "all servers errored" does not prove the transaction is absent from the network. The exhausted broadcast is now CLASSIFIED, with no added network calls: an already-known rejection (already in block chain / txn-already-in-mempool / txn-already-known) resolves the broadcast as a SUCCESS, since the server's own answer proves the transaction reached the network; when every relay-capable failure is an explicit Blockbook rejection the engine rejects with the original error (definitively failed, safe to retry); when any failure is a transport error it rejects with BroadcastAmbiguityError, which the GUI will use to lock the retry path and show may-have-worked messaging (tracked separately). Electrum stub refusals, which provably never sent anything, are excluded from the determination. An earlier revision queried the network for the txid before rejecting; that was removed after review, since an immediate query runs under the same network conditions that made the broadcast ambiguous and proves nothing either way. The post-broadcast txid-mismatch throw in UtxoEngine.broadcastTx (dead code today, failure-after-success if ever revived) now logs a warning and returns the transaction with the network's txid so the wallet tracks what was actually accepted.

The GUI half (send scene must not present post-broadcast errors as failed sends) is the EdgeApp/edge-react-gui companion PR on branch jon/send-post-broadcast-failure.


Note

High Risk
Changes UTXO send/broadcast and saveTx behavior on the payment path; incorrect classification could still allow duplicate sends or block legitimate retries.

Overview
Fixes cases where a successful broadcast was still reported as a failed send, which led to duplicate payments when users retried.

saveTx on a disconnected engine no longer fails after the transaction is already saved. When no addresses are subscribed, updateProgressRatio skips progress updates instead of throwing No addresses to process. stop() resets progress counters so leftover counts cannot emit a bogus fully-synced state during later saveTx-driven UTXO work.

Exhausted multicasts in ServerStates.broadcastTx are classified via new broadcastError helpers: all explicit Blockbook rejections still reject with the original error (safe to retry); any transport-style failure rejects with BroadcastAmbiguityError so the UI can treat the send as possibly on-network. Already-known mempool/chain rejections are treated as success. UtxoEngine.broadcastTx logs and returns the network’s txid on mismatch instead of throwing after relay.

Tests cover classification (broadcastError.spec.ts) and disconnected-engine saveTx (saveTx.spec.ts).

Reviewed by Cursor Bugbot for commit 51e5b66. Bugbot is set up for automated code reviews on this repo. Configure here.

UtxoEngineProcessor's updateProgressRatio threw 'No addresses to process'
whenever the engine had zero subscribed addresses, which is the state of a
wallet whose blockbook sockets are all down. saveTx reaches this code via
processUtxos after the transaction is already saved and its inputs marked
spent, so the throw turned an already-successful send into a reported
failure. Skip the progress update instead; there is no denominator to
compute a ratio from without subscribed addresses.
@j0ntz
j0ntz marked this pull request as ready for review August 4, 2026 19:23
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/common/utxobased/engine/ServerStates.ts Outdated

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The first commit fixes a real crash, but the return lands in a spot that lets setLookAhead run in a state it previously never reached — see the UtxoEngineProcessor.ts comment. The second commit's verification step is defensible in principle (a node can relay a tx and still error), but as written it is serial, untimed, and unconditional, so it turns a fast send failure into a long one on the money path.

Comment thread src/common/utxobased/engine/ServerStates.ts Outdated
Comment thread src/common/utxobased/engine/ServerStates.ts Outdated
Comment thread src/common/utxobased/engine/ServerStates.ts Outdated
Comment thread src/common/utxobased/engine/ServerStates.ts Outdated
Comment thread src/common/utxobased/engine/UtxoEngineProcessor.ts
Comment thread src/common/utxobased/engine/UtxoEngine.ts
Comment thread test/common/utxobased/engine/saveTx.spec.ts Outdated
@j0ntz
j0ntz force-pushed the jon/send-post-broadcast-failure branch from 0b5ce06 to 7b6c8e2 Compare August 14, 2026 18:04
@j0ntz

j0ntz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Both review points addressed: the saveTx return no longer counts denominator-less calls as progress and setLookAhead's new reachability is guarded by spec assertions (11c8184); the verification step is removed entirely in favor of zero-latency failure classification (7b6c8e2), so the send path rejects as fast as before, with ambiguous transport failures now distinguishable from explicit rejections.

Comment thread src/common/utxobased/engine/UtxoEngineProcessor.ts
j0ntz added 3 commits August 14, 2026 11:15
ServerStates.broadcastTx submits the signed transaction to every connected
blockbook (or every NOWNode HTTP fallback) and rejects only when all of
them fail, but a server can relay the transaction to the network and still
return an error or fail to respond. Before rejecting, query the network
for the txid and treat a known transaction as a successful broadcast.

Also stop throwing on a mismatched broadcast-response txid in
UtxoEngine.broadcastTx: the transaction is already on the network at that
point, so log a warning instead of reporting a send failure.
@j0ntz
j0ntz force-pushed the jon/send-post-broadcast-failure branch from 7b6c8e2 to b5255c0 Compare August 14, 2026 18:15

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b5255c0. Configure here.

Comment thread src/common/utxobased/engine/UtxoEngine.ts
Comment thread src/common/utxobased/engine/ServerStates.ts
j0ntz added 3 commits August 14, 2026 11:27
The immediate network query for the txid was unsound: it runs under the
same network conditions that made the broadcast ambiguous, so a negative
result proves nothing (the relayed transaction has had no time to
propagate) and a positive mostly fires when the network is healthy. It
also added serial, untimed network calls to the send path.

Classify the exhausted broadcast instead, with no added network calls:
collect every server's failure and reject with the original error when
all of them are explicit Blockbook rejections (definitively failed, safe
to retry), or with BroadcastAmbiguityError when any failure is a
transport error, since one of those servers may have relayed the
transaction before failing to answer. The GUI can branch on the error
name to lock the retry path for ambiguous failures.
@j0ntz
j0ntz force-pushed the jon/send-post-broadcast-failure branch from b5255c0 to 3a1ab78 Compare August 14, 2026 18:27

@cursor cursor 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.

Stale comment

Agentic security review of the broadcast-failure classification and post-broadcast txid handling. One medium finding on treating all Blockbook JSON errors as definitive, retry-safe rejections.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread src/common/utxobased/engine/broadcastError.ts
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.

2 participants