diff options
| author | Adam Soltys <[email protected]> | 2026-09-06 18:43:50 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-06 18:43:50 -0700 |
| commit | 2e492ec499264ba84ec8cad30f788eff28556269 (patch) | |
| tree | 53fb558113dea60a6902c5bccd082e07f0b8f220 /src/i18n.js | |
| parent | 95cf88f1246118eb6a672a5b4b6545331c9ddef4 (diff) | |
| download | coinosv3-2e492ec499264ba84ec8cad30f788eff28556269.tar.gz coinosv3-2e492ec499264ba84ec8cad30f788eff28556269.zip | |
Fix the on-chain gift front-running vulnerability architecturally: ephemeral-key (v2) gifts.
The old on-chain gift presigned a SIGHASH_NONE/SINGLE spend of the sender's own coin — a blank cheque whose claimer output isn't committed, so anyone (from the link, or a mempool watcher once a claim broadcast) could redirect the presigned input to their own address via a higher-fee tx. A sweeper bot had been front-running coinos gift links for ~a month; it drained a real 5000-sat gift ("claimed! → empty wallet"). Root-caused via chain + code forensics: theft tx input0 sighash = SINGLE.
New design (v2, lazy ephemeral key): the sender presigns a FULLY COMMITTED (SIGHASH_ALL) funding tx paying the gift to a throwaway key E, and puts E's private key in the link. The claimer broadcasts the funding tx, then a fresh SIGHASH_ALL sweep of E into their own wallet. Both txs commit to every output, so neither is redirectable in the mempool — redirecting needs E's key, which lives only in the link (the intended bearer property: holding the link is the only way to claim). Lazy: the funding tx isn't broadcast until claim, so an unclaimed gift moves no coins and is revoked for free by spending the still-reserved funding coin. Sender pays the funding fee (from change / the swept total); claimer pays the sweep fee (from the gift).
- gifts-wallet.js: _buildGiftPsbt now builds+signs the ephemeral funding tx (coin -> E + change, SIGHASH_ALL); new GIFT_MAGIC_V2 codec (parseLazyGift/_encodeLazyGift: [magic][E priv:32][le32 len][funding tx]); giftClaimTxs returns the {funding, sweep} pair; giftOutpoints/previewGift read E's output; isRetiredGift flags the old v1 (compact + legacy PSBT) links. Fee threaded through _selectGiftCoins/createGift/createGiftAll/createGiftFromSplit.
- gifts.js doClaim: broadcast funding (tolerating "already in mempool" from a co-holder/retry) then sweep; refuse retired v1 links with a clear message; verifyClaimWon now watches E's output.
- i18n: giftChainWarn reworded (safe bearer link funding a fresh on-chain wallet); giftRetired added.
Verified: focused crypto test confirms codec round-trip + both inputs signed SIGHASH_ALL (committed). Full regtest end-to-end: funding + sweep both relayed (child accepted spending an unconfirmed parent), mined, claimer address credited the gift. UI morph smoke passes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src/i18n.js')
| -rw-r--r-- | src/i18n.js | 3 |
1 files changed, 2 insertions, 1 deletions
diff --git a/src/i18n.js b/src/i18n.js index 86fed95..dbc23f7 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -911,7 +911,8 @@ const STR = { lnPayNetworkFee: 'Network fee', lnPayTotal: 'Total', lnPayConfirm: 'Pay', - giftChainWarn: '\u26a0 On-chain gift links are bearer: anyone who gets the link — or sees the claim on the network — can sweep it. Share it privately and have the recipient claim right away. (A safer redesign is coming; Spending gifts aren\u2019t affected.)', + giftChainWarn: '⚠ This is a bearer link: whoever opens it can claim the gift, so share it privately. It funds a fresh on-chain wallet when claimed.', + giftRetired: 'This gift link uses an older format that’s no longer safe to claim. Ask the sender to void it and create a new one.', lnAutopayOffer: 'Auto-pay {name} up to {n} next time', lnPaySentTitle: 'Lightning payment sent', lnPaySentBody: 'The swap provider pays the invoice once the on-chain lockup confirms.', |
