-
Notifications
You must be signed in to change notification settings - Fork 18
Never report a successful broadcast as a failed send #455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
j0ntz
wants to merge
9
commits into
master
Choose a base branch
from
jon/send-post-broadcast-failure
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3ee0776
Fix saveTx failing on a disconnected engine
j0ntz 11c8184
fixup! Fix saveTx failing on a disconnected engine
j0ntz 707f649
fixup! Fix saveTx failing on a disconnected engine
j0ntz 5989abc
Resolve broadcast ambiguity before reporting failure
j0ntz 80e9342
fixup! Resolve broadcast ambiguity before reporting failure
j0ntz 2982e18
fixup! Resolve broadcast ambiguity before reporting failure
j0ntz dd22df6
Replace the broadcast txid query with failure classification
j0ntz 3a1ab78
fixup! Replace the broadcast txid query with failure classification
j0ntz 51e5b66
fixup! Replace the broadcast txid query with failure classification
j0ntz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /** | ||
| * Classification of an all-servers-failed broadcast. | ||
| * | ||
| * An explicit rejection is a server answering the broadcast with a Blockbook | ||
| * error response: the server received the transaction and refused it, so the | ||
| * same signed bytes will be refused again and cannot be on the network from | ||
| * this attempt. Anything else (a request timeout, a dropped connection, an | ||
| * HTTP status error whose body was never read) leaves relay possible: a | ||
| * server can relay the transaction to the network and still fail to answer. | ||
| * | ||
| * The classification is pure error-shape inspection. It adds no network | ||
| * calls, so it cannot add latency to the send path. | ||
| */ | ||
|
|
||
| /** | ||
| * A broadcast that failed on every server where at least one failure was a | ||
| * transport error rather than an explicit rejection. The transaction cannot | ||
| * be assumed absent from the network, so a retry could produce a second real | ||
| * payment. Consumers branch on `name === 'BroadcastAmbiguityError'`; class | ||
| * identity does not survive the core bridge, names and properties do. | ||
| */ | ||
| export class BroadcastAmbiguityError extends Error { | ||
| readonly causes: string[] | ||
|
|
||
| constructor(causes: string[]) { | ||
| super('Broadcast failed, but the transaction may have reached the network') | ||
| this.name = 'BroadcastAmbiguityError' | ||
| this.causes = causes | ||
| } | ||
| } | ||
|
|
||
| export const isExplicitBroadcastRejection = (error: unknown): boolean => | ||
| String(error instanceof Error ? error.message : error).includes( | ||
| 'Blockbook Error: ' | ||
| ) | ||
|
|
||
| /** | ||
| * A rejection that means the server ALREADY HAS the transaction | ||
| * ("transaction already in block chain", "txn-already-in-mempool", | ||
| * "txn-already-known"). This is a confirmation the transaction reached the | ||
| * network, from this attempt or an earlier one with the same signed bytes, | ||
| * so the broadcast must be treated as a success: presenting it as a | ||
| * failure invites the duplicate-payment retry this work exists to stop. | ||
| */ | ||
| export const isAlreadyKnownRejection = (error: unknown): boolean => | ||
| isExplicitBroadcastRejection(error) && | ||
| /alread/i.test(String(error instanceof Error ? error.message : error)) | ||
|
|
||
| /** | ||
| * A failure from a server that provably never accepted the payload, so it | ||
| * cannot have relayed the transaction: the Electrum stub refuses | ||
| * broadcastTx synchronously. Such failures say nothing about relay and are | ||
| * excluded from the ambiguity determination. (A not-yet-connected blockbook | ||
| * is NOT in this class: its queued request can still send once the | ||
| * connection completes, so its timeout stays ambiguous.) | ||
| */ | ||
| export const isNonRelayFailure = (error: unknown): boolean => | ||
| String(error instanceof Error ? error.message : error).includes( | ||
| 'not supported for Electrum connections' | ||
| ) | ||
|
|
||
| export const classifyBroadcastFailure = ( | ||
| errors: unknown[] | ||
| ): 'rejected' | 'ambiguous' => { | ||
| if (errors.length === 0) return 'ambiguous' | ||
| const relayCapable = errors.filter(error => !isNonRelayFailure(error)) | ||
| return relayCapable.every(isExplicitBroadcastRejection) | ||
| ? 'rejected' | ||
| : 'ambiguous' | ||
|
j0ntz marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { assert } from 'chai' | ||
| import { describe, it } from 'mocha' | ||
|
|
||
| import { | ||
| BroadcastAmbiguityError, | ||
| classifyBroadcastFailure, | ||
| isAlreadyKnownRejection, | ||
| isExplicitBroadcastRejection, | ||
| isNonRelayFailure | ||
| } from '../../../../src/common/utxobased/engine/broadcastError' | ||
|
|
||
| describe('broadcast failure classification', function () { | ||
| it('classifies all-explicit-rejection sets as rejected', function () { | ||
| assert.equal( | ||
| classifyBroadcastFailure([ | ||
| new Error('Blockbook Error: -26: dust'), | ||
| new Error('Blockbook Error: -25: missing inputs') | ||
| ]), | ||
| 'rejected' | ||
| ) | ||
| }) | ||
|
|
||
| it('classifies any transport failure in the set as ambiguous', function () { | ||
| assert.equal( | ||
| classifyBroadcastFailure([ | ||
| new Error('Blockbook Error: -26: dust'), | ||
| new Error('Timeout for request 42') | ||
| ]), | ||
| 'ambiguous' | ||
| ) | ||
| assert.equal( | ||
| classifyBroadcastFailure([new Error('Timeout for request 42')]), | ||
| 'ambiguous' | ||
| ) | ||
| assert.equal( | ||
| classifyBroadcastFailure([ | ||
| new Error('Failed to broadcast transaction via Blockbook: HTTP 503') | ||
| ]), | ||
| 'ambiguous' | ||
| ) | ||
| }) | ||
|
|
||
| it('treats missing or empty error information as ambiguous', function () { | ||
| assert.equal(classifyBroadcastFailure([]), 'ambiguous') | ||
| assert.equal(classifyBroadcastFailure([undefined]), 'ambiguous') | ||
| }) | ||
|
|
||
| it('excludes non-relay failures from the ambiguity determination', function () { | ||
| const electrumStub = new Error( | ||
| 'broadcastTx not supported for Electrum connections' | ||
| ) | ||
| assert.isTrue(isNonRelayFailure(electrumStub)) | ||
| // An Electrum stub alongside explicit rejections must not turn a | ||
| // definitively failed broadcast into an ambiguous one. | ||
| assert.equal( | ||
| classifyBroadcastFailure([ | ||
| electrumStub, | ||
| new Error('Blockbook Error: -26: dust') | ||
| ]), | ||
| 'rejected' | ||
| ) | ||
| // All-stub sets never sent anything anywhere: definitively failed. | ||
| assert.equal(classifyBroadcastFailure([electrumStub]), 'rejected') | ||
| // A transport failure still dominates. | ||
| assert.equal( | ||
| classifyBroadcastFailure([ | ||
| electrumStub, | ||
| new Error('Timeout for request 42') | ||
| ]), | ||
| 'ambiguous' | ||
| ) | ||
| }) | ||
|
|
||
| it('recognizes explicit rejections by the Blockbook error marker', function () { | ||
| assert.isTrue( | ||
| isExplicitBroadcastRejection(new Error('Blockbook Error: -26: dust')) | ||
| ) | ||
| assert.isFalse(isExplicitBroadcastRejection(new Error('socket closed'))) | ||
| assert.isFalse(isExplicitBroadcastRejection(undefined)) | ||
| }) | ||
|
|
||
| it('recognizes already-known rejections as network confirmation', function () { | ||
| assert.isTrue( | ||
| isAlreadyKnownRejection( | ||
| new Error('Blockbook Error: -27: transaction already in block chain') | ||
| ) | ||
| ) | ||
| assert.isTrue( | ||
| isAlreadyKnownRejection( | ||
| new Error('Blockbook Error: txn-already-in-mempool') | ||
| ) | ||
| ) | ||
| assert.isTrue( | ||
| isAlreadyKnownRejection(new Error('Blockbook Error: txn-already-known')) | ||
| ) | ||
| assert.isFalse( | ||
| isAlreadyKnownRejection(new Error('Blockbook Error: -26: dust')) | ||
| ) | ||
| // "already" in a transport error is not a Blockbook rejection. | ||
| assert.isFalse(isAlreadyKnownRejection(new Error('socket already closed'))) | ||
| }) | ||
|
|
||
| it('keeps a bridge-stable name and carries its causes', function () { | ||
| const error = new BroadcastAmbiguityError(['Timeout for request 42']) | ||
| assert.equal(error.name, 'BroadcastAmbiguityError') | ||
| assert.deepEqual(error.causes, ['Timeout for request 42']) | ||
| assert.instanceOf(error, Error) | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.