Skip to content

fix(tx): honest broadcast result, and no name transaction hsd would reject - #81

Merged
DimazzzZ merged 2 commits into
mainfrom
fix/honest-broadcast-and-draft-lifecycle
Oct 8, 2026
Merged

DimazzzZ merged 2 commits into
mainfrom
fix/honest-broadcast-and-draft-lifecycle

Conversation

@DimazzzZ

@DimazzzZ DimazzzZ commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements docs/specs/2026-10-07-honest-broadcast-result.md (R1–R7) and fixes the name builders that the honest broadcast showed were building transactions hsd rejects.

1. A send is sent only once the node has it (4f33104)

hsd 8.0.0's sendrawtransaction returns the txid whatever its mempool does with the transaction.

  • R1–R3: after sending, the wallet looks the transaction up up to 5 times, 400 ms apart. If hsd's not-found comes back every time and no output of the transaction is mined, the broadcast errors "The node did not take the transaction" and the draft is broadcast_pending with its coins held. An output already mined (a node without a transaction index) confirms it. A check without hsd's answer gives no verdict.
  • R5: a Shakedex purchase, and its one automatic rebroadcast, go through the same check. A rebroadcast the node does not take loses the purchase with nothing paid, unless the chain traces to it.
  • R6: a broadcasted, confirmed, dropped or failed draft is not signed or broadcast again. Signing used to flip any draft back to signed.
  • R7: dropped and failed are no longer final. For 72 hours (hsd's mempool expiry) the confirmation poll keeps looking and confirms a transaction mined after the verdict.
  • The look-up and the chain evidence live in noncustodial::tx_evidence, so the purchase job can use them under the layering rule.

2. The builders refuse what hsd's consensus rejects (8c44af0)

Seven live tests built transactions hsd refuses and passed only because the broadcast never said so (bad-renewal-premature, bad-finalize-maturity, bad-redeem-owner, an orphan). Before persisting a draft, renew, finalize (single, batch, with payment) and redeem now ask the node (getnameinfo and the tip, judged at tip + 1) and refuse, saying how many blocks are left. The UI withholds Renew the same way. The seven tests now run valid scenarios (a rival outbids for redeem; renew and finalize mine the required blocks first) and assert the effect on chain.

Tests

  • Unit: 2959 passed. Each rule, guard and wrapper call fails its test when removed (mutation-checked).
  • Live regtest, run locally on a fresh chain with Shakedex and a second node without --index-tx included: 80 passed. New: double spend not sent, a dropped send revived on both nodes, purchase and finalize fee rate on vsize at 20 doos/byte, purchase rebroadcast refused after a price drop, redeem of the winning reveal refused.
  • cargo clippy --all-targets -D warnings, fmt and npx prettier --check . clean.

Known leftovers

  • The UI's Redeem button counts from the wallet's database, so it can appear on a name the wallet never tracked; the builder then refuses it with a clear message.
  • docs/REGTEST_TESTING.md Scenario 4 reads as if an unrevealed bid could be redeemed (REDEEM spends a REVEAL output). Not checked live; left as is.

… is never final

hsd 8.0.0 answers sendrawtransaction with the txid whatever its mempool does with the transaction, so a send whose coins were already spent, or one hsd refused for any other reason, read as sent. This implements docs/specs/2026-10-07-honest-broadcast-result.md:

- R1-R3: after sendrawtransaction the wallet looks the transaction up up to five times, 400 ms apart; the first find ends the window. hsd's not-found on every check, with no output of the transaction mined, is "not taken": the broadcast errors "The node did not take the transaction", the draft is broadcast_pending with its coins held. An output already mined (a node without a transaction index) confirms it; a check without hsd's answer is no verdict.
- R5: a Shakedex purchase goes through the same check, and so does its one automatic rebroadcast: not taken loses it with nothing paid, unless the chain traces to it.
- R6: a broadcasted, confirmed, dropped or failed draft is not signed or broadcast again. Signing flipped any draft back to "signed", so a confirmed one could be turned back and a dropped one's released coins spent again.
- R7: dropped and failed are no longer final. For 72 hours (hsd's mempool expiry) the confirmation poll keeps looking for such a transaction and confirms it once mined, so a payment that went through never keeps reading "not sent".

The look-up and the chain evidence move to noncustodial::tx_evidence, which the purchase job may use (CODING_STANDARDS layering).

Tests: unit and mock tests pin R1-R7 (each checked to fail with its rule removed); live regtest tests cover the double spend, the revived send on both nodes, a purchase and finalize paying their fee rate on vsize at 20 doos/byte, and a purchase rebroadcast refused after a price drop. Seven older live tests now fail because hsd refuses a transaction they build (bad-redeem-owner, bad-renewal-premature, bad-finalize-maturity, an orphan): they passed only because the broadcast never said so. They are fixed next.
…reject

The name builders built transactions hsd refuses: a RENEW within tree_interval of the name's last renewal (bad-renewal-premature), a FINALIZE before the transfer lockup is over (bad-finalize-maturity; the Finalize button was gated, the builders were not, and the gate allowed it when the transfer height was unknown), and a REDEEM of the winning reveal, which is the name's owner coin until REGISTER (bad-redeem-owner; the old filter read the wallet's database and missed a name the wallet never tracked). Each now asks the node (getnameinfo and the tip, judged at tip + 1 as hsd does) and refuses before a draft is persisted, saying how many blocks are left; single and batch renew, finalize, finalize-with-payment and redeem all go through the same checks (commands/draft_ctx.rs). The UI withholds Renew the same way ("can be renewed in N blocks").

Seven live regtest tests built exactly these transactions and passed only because the broadcast never said hsd refused them. They now run valid scenarios and assert the effect on chain: a rival bidder outbids for redeem, renew and finalize mine the required blocks first and check info.renewal / the new owner, and the batch transfer test syncs between its two finalizes (both had spent one funding coin, which hsd held as an orphan). New live test: live_redeem_of_the_winning_reveal_is_refused.

Tests: unit 2959 passed, each guard and wrapper call checked to fail its tests when removed; full live suite on a fresh regtest chain, Shakedex and the node without a transaction index included, 80 passed. clippy -D warnings, fmt and prettier clean.
@github-actions github-actions Bot added domain: wallet Balances, send/receive, accounts, HD derivation, coin selection. domain: names Auction lifecycle, batch ops, watchlist, paid swaps, lost-bid recovery. domain: node hsd full-node integration (RPC, chain scan) and SPV mode. labels Oct 8, 2026
@DimazzzZ DimazzzZ self-assigned this Oct 8, 2026
@DimazzzZ DimazzzZ added the type: bug Defect or unexpected behavior. label Oct 8, 2026
@DimazzzZ
DimazzzZ merged commit 1644c67 into main Oct 8, 2026
7 checks passed
@DimazzzZ
DimazzzZ deleted the fix/honest-broadcast-and-draft-lifecycle branch October 8, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: names Auction lifecycle, batch ops, watchlist, paid swaps, lost-bid recovery. domain: node hsd full-node integration (RPC, chain scan) and SPV mode. domain: wallet Balances, send/receive, accounts, HD derivation, coin selection. type: bug Defect or unexpected behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant