diff options
| author | Adam Soltys <[email protected]> | 2026-06-28 04:14:07 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-28 04:14:07 -0700 |
| commit | 76daca707a7b9cb9aafab1ceb8999ec3c708a63d (patch) | |
| tree | 461014db1815482c101561a316f0df50bb957c91 /src/silentpay.js | |
| parent | a6e6083e4c758a9c07dc7a4ea38f880c561cb365 (diff) | |
| download | coinosv3-76daca707a7b9cb9aafab1ceb8999ec3c708a63d.tar.gz coinosv3-76daca707a7b9cb9aafab1ceb8999ec3c708a63d.zip | |
Spend SP coins like any coin — confirmed-only, and able to fund SP payments
Two things:
- Only confirmed SP coins enter the spend pool. Unconfirmed ones are excluded
(the sender's tx could be replaced, invalidating our spend) — matching the
spendable headline, which already counted confirmed-only.
- SP→SP sending: our own silent-payment coins can now fund a payment TO an sp1
address (previously excluded). _applySilentPayments includes SP inputs with
their key d = spend_priv + t_k, and silentPaymentScripts negates a taproot
input's key when it gives odd Y, per BIP-352 (so the sender's A = a·G matches
what the indexer reconstructs). signing was already SP-aware.
Validated: BIP-352 sending vectors pass for all clean key-path-taproot-input
cases (the remaining vector failures are NUMS script-path / labels / uncompressed
keys — input types the wallet never spends). End-to-end on regtest: an SP coin
funds a payment to another sp address, testmempoolaccept passes, and the recipient
scans and finds it.
Note: silentPaymentScripts derives input_hash from the eligible inputs it's given,
which is correct because the wallet only ever spends all-eligible coins (P2WPKH +
key-path SP); it isn't a general-purpose sender for txs with ineligible inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/silentpay.js')
| -rw-r--r-- | src/silentpay.js | 15 |
1 files changed, 11 insertions, 4 deletions
diff --git a/src/silentpay.js b/src/silentpay.js index 1732ced..eeabc1a 100644 --- a/src/silentpay.js +++ b/src/silentpay.js @@ -69,17 +69,24 @@ export function decodeSilentPaymentAddress(addr) { // Derive the one-time taproot output scripts for a set of silent-payment outputs // that this transaction's inputs are funding. -// inputs: [{ txid (display hex), vout, priv (Uint8Array 32) }] — ALL eligible -// (P2WPKH) inputs of the tx; the derivation commits to every one. +// inputs: [{ txid (display hex), vout, priv (Uint8Array 32), taproot? }] — ALL +// eligible inputs of the tx; the derivation commits to every one. Set +// taproot:true for P2TR inputs (e.g. our own silent-payment coins) so +// their key is taken with even Y, per BIP-352. // outputs: [{ scan, spend }] — one entry per desired output (repeat the same // recipient to pay it multiple times; the k counter is handled here). // Returns a taproot scriptPubKey (Uint8Array) for each entry, in order. export function silentPaymentScripts(inputs, outputs) { if (!inputs.length) throw new Error('Silent payment needs at least one input.'); - // a = Σ input private keys (mod n); A = a·G + // a = Σ input private keys (mod n); A = a·G. A taproot input contributes the + // key for its even-Y x-only pubkey, so negate when the key gives odd Y. let a = 0n; - for (const i of inputs) a = modN(a + toInt(i.priv)); + for (const i of inputs) { + let d = toInt(i.priv); + if (i.taproot && Number(G.multiply(d).toRawBytes(true)[0]) === 0x03) d = modN(-d); + a = modN(a + d); + } if (a === 0n) throw new Error('Silent payment input key sum is zero.'); const A = G.multiply(a); |
