aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-27 14:42:11 -0700
committerAdam Soltys <[email protected]>2026-06-27 14:42:11 -0700
commit1beae75ebd34142cc63342fbf7c8a798af45bb5a (patch)
tree96f2962305dd1169b7c475a6ef2e9fabb97b92aa /src
parent6c817756c5cca849864119405be9fa3f9e968057 (diff)
downloadcoinosv3-1beae75ebd34142cc63342fbf7c8a798af45bb5a.tar.gz
coinosv3-1beae75ebd34142cc63342fbf7c8a798af45bb5a.zip
Add regtest SP tweak indexer; accept Core's array-witness in inputPubKey
tools/sp-indexer.js: a minimal BIP-352 oracle (Bun) that walks Core and serves, per block, one entry per eligible tx — the tweak (input_hash*A) plus that tx's taproot outputs — for the wallet to scan on-device. Shares src/silentpay.js so the tweak math is the vector-validated code. Added to the regtest compose as the 'sp' service (oven/bun, pointed at bc:18443). inputPubKey now accepts a witness as either serialized hex (vectors) or an array of stack items (Core getblock RPC). Verified end-to-end on regtest: a real SP payment is indexed and the scan recovers the output, value, and spend tweak. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/silentpay.js28
1 files changed, 17 insertions, 11 deletions
diff --git a/src/silentpay.js b/src/silentpay.js
index 1a58a41..16de66b 100644
--- a/src/silentpay.js
+++ b/src/silentpay.js
@@ -1,4 +1,4 @@
-// Silent Payments (BIP-352) — sending side only.
+// Silent Payments (BIP-352) — sending and receiving.
//
// A silent payment address (sp1…) is a static, reusable address the receiver
// publishes once. The sender derives a unique one-time taproot output for each
@@ -6,14 +6,12 @@
// so there's no on-chain link between payments and no address reuse — without
// any interaction with the receiver.
//
-// This module only does the SENDER half (deriving the output). Receiving needs
-// chain scanning (a per-tx ECDH tweak), which a light client can't do without a
-// dedicated index server — out of scope here.
+// Sending (silentPaymentScripts) derives the output; we only spend compressed
+// P2WPKH inputs, so each input contributes its private key as-is. Receiving (see
+// the second half) can't scan the chain in a browser, so an indexing server
+// supplies per-tx tweaks + taproot UTXOs and we match them on-device.
//
-// Validated against the official BIP-352 send test vectors. We only spend
-// compressed P2WPKH inputs, which keeps the eligible-input rules trivial: each
-// input contributes its private key as-is (no taproot even-Y negation, no
-// uncompressed-key or P2SH edge cases).
+// Validated against the official BIP-352 send + receive test vectors.
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
@@ -154,6 +152,14 @@ function parseWitness(hex) {
return items;
}
+// Witness stack items as byte arrays. Accepts either a serialized witness (hex
+// string, as in the BIP-352 vectors) or an array of hex stack items (as Core's
+// getblock RPC returns in txinwitness).
+function witnessItems(witness) {
+ if (Array.isArray(witness)) return witness.map(hexToBytes);
+ return parseWitness(witness);
+}
+
// Pull the data pushes out of a scriptSig (hex) — enough to recover the pubkey
// from P2PKH or the redeemScript from P2SH (push opcodes only).
function scriptPushes(hex) {
@@ -186,14 +192,14 @@ export function inputPubKey({ scriptSig = '', witness = '', scriptPubKey = '' })
const spk = hexToBytes(scriptPubKey);
// P2TR: OP_1 PUSH32 <x-only>
if (spk.length === 34 && spk[0] === 0x51 && spk[1] === 0x20) {
- let w = parseWitness(witness);
+ let w = witnessItems(witness);
if (w.length > 1 && w[w.length - 1][0] === 0x50) w = w.slice(0, -1); // strip annex
if (w.length > 1 && bytesToHex(w[w.length - 1].slice(1, 33)) === NUMS_H) return null; // NUMS script-path
return Point.fromHex(concatBytes(Uint8Array.of(0x02), spk.slice(2, 34))).toRawBytes(true); // even-Y lift
}
// P2WPKH: OP_0 PUSH20
if (spk.length === 22 && spk[0] === 0x00 && spk[1] === 0x14) {
- const w = parseWitness(witness);
+ const w = witnessItems(witness);
const pk = w[w.length - 1];
return looksLikePubKey(pk) ? Point.fromHex(pk).toRawBytes(true) : null;
}
@@ -201,7 +207,7 @@ export function inputPubKey({ scriptSig = '', witness = '', scriptPubKey = '' })
if (spk.length === 23 && spk[0] === 0xa9 && spk[1] === 0x14 && spk[22] === 0x87) {
const rs = scriptPushes(scriptSig)[0];
if (!rs || rs.length !== 22 || rs[0] !== 0x00 || rs[1] !== 0x14) return null;
- const w = parseWitness(witness);
+ const w = witnessItems(witness);
const pk = w[w.length - 1];
return looksLikePubKey(pk) ? Point.fromHex(pk).toRawBytes(true) : null;
}