From 2af89785eab73efa3fe00d168cbb110c408f60b2 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Wed, 8 Oct 2025 18:12:22 +0200 Subject: [PATCH 01/21] chore(payment): Implement silent payments This PR implements bip352 and exposes the following functions: - scanForSilentPayments: to find if given outputs belong to our sp address - deriveOutput: to create a new silent payment output TODO: - [] Decide how we expose scanning and deriving to the developer i guess via the p2sp object ? - [] Decide whether we return the found hex endcoded or Uint8Array output - [] Figure out of we can remove `modN32` and `subBE` as they might exist? - [] Add derivation logic - [] Replace `getPubkeyFromInputTS` from test with something that exists? --- src/esm/crypto.js | 18 + src/esm/payments/index.js | 16 +- src/esm/payments/p2sp.js | 431 ++++ test/fixtures/p2sp.json | 3189 ++++++++++++++++++++++++ test/integration/silentpayment.spec.ts | 552 ++++ ts_src/crypto.ts | 18 + ts_src/payments/index.ts | 17 +- ts_src/payments/p2sp.ts | 558 +++++ 8 files changed, 4797 insertions(+), 2 deletions(-) create mode 100644 src/esm/payments/p2sp.js create mode 100644 test/fixtures/p2sp.json create mode 100644 test/integration/silentpayment.spec.ts create mode 100644 ts_src/payments/p2sp.ts diff --git a/src/esm/crypto.js b/src/esm/crypto.js index 9a55ce517..9b7296d5a 100644 --- a/src/esm/crypto.js +++ b/src/esm/crypto.js @@ -108,6 +108,24 @@ export const TAGGED_HASH_PREFIXES = { 201, 4, 3, 77, 28, 136, 232, 200, 14, 34, 229, 61, 36, 86, 109, 100, 130, 78, 214, 66, 114, 129, 192, 145, 0, 249, 77, 205, 82, 201, 129, ]), + 'BIP0352/Inputs': Uint8Array.from([ + 30, 123, 150, 235, 22, 10, 104, 129, 159, 151, 118, 75, 67, 213, 215, 126, + 102, 89, 215, 88, 119, 157, 67, 168, 167, 117, 95, 91, 228, 90, 126, 51, 30, + 123, 150, 235, 22, 10, 104, 129, 159, 151, 118, 75, 67, 213, 215, 126, 102, + 89, 215, 88, 119, 157, 67, 168, 167, 117, 95, 91, 228, 90, 126, 51, + ]), + 'BIP0352/SharedSecret': Uint8Array.from([ + 159, 109, 128, 17, 88, 30, 182, 45, 114, 230, 19, 96, 76, 51, 13, 202, 42, + 11, 211, 73, 226, 74, 70, 217, 162, 239, 36, 185, 169, 143, 65, 189, 159, + 109, 128, 17, 88, 30, 182, 45, 114, 230, 19, 96, 76, 51, 13, 202, 42, 11, + 211, 73, 226, 74, 70, 217, 162, 239, 36, 185, 169, 143, 65, 189, + ]), + 'BIP0352/Label': Uint8Array.from([ + 3, 73, 25, 70, 53, 194, 208, 62, 98, 212, 19, 186, 140, 205, 81, 152, 145, + 144, 23, 161, 233, 156, 190, 105, 31, 90, 52, 169, 147, 119, 224, 149, 3, + 73, 25, 70, 53, 194, 208, 62, 98, 212, 19, 186, 140, 205, 81, 152, 145, 144, + 23, 161, 233, 156, 190, 105, 31, 90, 52, 169, 147, 119, 224, 149, + ]), }; /** * Computes a tagged hash using the specified prefix and data. diff --git a/src/esm/payments/index.js b/src/esm/payments/index.js index 3c2fd1e5e..f48d430a9 100644 --- a/src/esm/payments/index.js +++ b/src/esm/payments/index.js @@ -6,6 +6,20 @@ import { p2sh } from './p2sh.js'; import { p2wpkh } from './p2wpkh.js'; import { p2wsh } from './p2wsh.js'; import { p2tr } from './p2tr.js'; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; +import { p2sp, scanForSilentPayments, deriveOutput } from './p2sp.js'; +export { + embed, + p2ms, + p2pk, + p2pkh, + p2sh, + p2wpkh, + p2wsh, + p2tr, + p2sp, + // TODO how should we expose the two functions below? + scanForSilentPayments, + deriveOutput, +}; // TODO // witness commitment diff --git a/src/esm/payments/p2sp.js b/src/esm/payments/p2sp.js new file mode 100644 index 000000000..8d29104be --- /dev/null +++ b/src/esm/payments/p2sp.js @@ -0,0 +1,431 @@ +import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import * as ecc from 'tiny-secp256k1'; +import { toXOnly } from '../psbt/bip371'; +import * as tools from 'uint8array-tools'; +import { toHex, writeUInt32 } from 'uint8array-tools'; +import { bech32m } from 'bech32'; +import * as lazy from './lazy'; +import { taggedHash } from '../crypto'; +// --- TYPE DEFINITIONS & UTILITIES --- +export const BECH32_SP_LIMIT = 150; +/** + * Create a 32Bit Unsigned integer + * @param n the number to put + * @returns a in [Uint8Array] big endian encoded + */ +export function ser32BE(n) { + const b = new Uint8Array(4); + writeUInt32(b, 0, n >>> 0, 'be'); + return b; +} +const N = tools.fromHex( + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', +); +function isZero32(a) { + for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; + return true; +} +//TODO I guess there already is a function for this ? +function subBE(a, b) { + const o = new Uint8Array(32); + let c = 0; + for (let i = 31; i >= 0; i--) { + let v = a[i] - b[i] - c; + if (v < 0) { + v += 256; + c = 1; + } else c = 0; + o[i] = v; + } + return o; +} +//TODO I guess there already is a function for this ? +function modN32(x) { + let r = new Uint8Array(x); + while (tools.compare(r, N) >= 0) r = subBE(r, N); + return r; +} +function hashToTweak(h) { + let t = modN32(h); + if (isZero32(t)) { + t = new Uint8Array(32); + t[31] = 1; + } + return t; +} +/** + * Smallest outpoint = lexicographic min of (txidLE || voutLE) + * @param inputs an array of inputs you want the first lexicographically sorted result + * @returns the first output after sorting lexicographically + */ +export const findSmallestOutpoint = inputs => + inputs + .map(v => serOutpointLE(v.hash, v.index)) + .sort((a, b) => tools.compare(a, b))[0]; +/** + * Serialize output with number little endian + * (used to sort outputs) + * @param txidHexBE - big endian encoded tx + * @param vout - output index + * @returns the serialized little endian encoded output + */ +export const serOutpointLE = (txidHexBE, vout) => { + const out = new Uint8Array(36); + const txidLE = tools.fromHex(txidHexBE); + if (txidLE.length !== 32) throw new Error('txid must be 32 bytes'); + txidLE.reverse(); // BE -> LE + out.set(txidLE, 0); + writeUInt32(out, 32, vout >>> 0, 'le'); + return out; +}; +/** + * Encodes spend and scan public keys into a Bech32m Silent Payment address. + * @param B_spend + * @param B_scan + * @param version + * @param network - testing, regtest or prod + * @returns bech32m encoded string + */ +export function encodeSilentPaymentAddress( + B_spend, + B_scan, + version = 0, + network = BITCOIN_NETWORK, +) { + if (version !== 0) + throw new Error(`Unsupported silent payment address version: ${version}`); + const SP_HRP = network.bech32 === 'bc' ? 'sp' : 'tsp'; + let data; + // To ensure encode/decode is symmetric, we use the legacy 66-byte format + // if full 33-byte keys are provided. This preserves the original y-parity. + if (B_spend.length === 33 && B_scan.length === 33) { + data = tools.concat([B_scan, B_spend]); + } else { + // Otherwise, we use the modern 64-byte format with x-only keys as per the spec. + const B_spend_xonly = B_spend.length === 33 ? B_spend.slice(1) : B_spend; + const B_scan_xonly = B_scan.length === 33 ? B_scan.slice(1) : B_scan; + if (B_spend_xonly.length !== 32 || B_scan_xonly.length !== 32) { + throw new Error('Invalid public key length for silent payment address'); + } + data = tools.concat([B_scan_xonly, B_spend_xonly]); + } + // The data is converted to 5-bit words, and the version is prepended to the words array. + const words = bech32m.toWords(data); + words.unshift(version); + // The default bech32 limit is 90, but silent payment addresses are longer. + // We pass a higher limit to accommodate this, as the underlying implementation allows it. + return bech32m.encode(SP_HRP, words, BECH32_SP_LIMIT); +} +/** + * Decodes a Bech32m Silent payment address into its spend and scan public keys. + * @param address - bech32m silent payment address format + * @returns { B_spend, B_scan, version } + */ +export function decodeSilentPaymentAddress(address) { + // The default bech32 limit is 90, but silent payment addresses are longer. + // We pass a higher limit to accommodate this, as the underlying implementation allows it. + const { prefix, words } = bech32m.decode(address, BECH32_SP_LIMIT); + if (prefix !== 'sp' && prefix !== 'tsp') { + throw new Error('Invalid silent payment address prefix'); + } + // The first word is the version, the rest are the data payload. + const version = words[0]; + const dataWords = words.slice(1); + const keysData = new Uint8Array(bech32m.fromWords(dataWords)); + let B_spend; + let B_scan; + switch (version) { + case 0: + // Handle both current (64 bytes for x-only keys) and legacy (66 bytes for full keys) payload lengths. + if (keysData.length === 64) { + // Current spec: 32-byte x-only keys. Reconstruct with even y-parity (0x02) as per convention. + const B_scan_xonly = keysData.slice(0, 32); + const B_spend_xonly = keysData.slice(32, 64); + B_spend = new Uint8Array(33); + B_spend[0] = 0x02; + B_spend.set(B_spend_xonly, 1); + B_scan = new Uint8Array(33); + B_scan[0] = 0x02; + B_scan.set(B_scan_xonly, 1); + } else if (keysData.length === 66) { + // Legacy spec: 33-byte full keys. We can return them directly to preserve the original y-parity. + B_scan = keysData.slice(0, 33); + B_spend = keysData.slice(33, 66); + } else { + throw new Error( + `Invalid silent payment address data length for v0: ${keysData.length}`, + ); + } + break; + default: + // Future versions might have different lengths. For now, we only support v0. + throw new Error(`Unsupported silent payment address version: ${version}`); + } + return { B_spend, B_scan, version }; +} +/** + * Main function for creating a Pay-to-Silent-Payment (P2SP) payment object. + * This function encapsulates the logic for handling silent payment addresses and keys. + * + * @param a - The payment object containing the necessary data for P2SP. + * @param opts - Optional payment options. + * @returns The P2SP payment object. + */ +export function p2sp(a, opts) { + if (!a.address && !(a.spendPubkey && a.scanPubkey)) { + throw new TypeError('Not enough data'); + } + opts = Object.assign({ validate: true }, opts || {}); + const network = a.network || BITCOIN_NETWORK; + const o = { name: 'p2sp', network }; + // Lazy load silent payment specific properties + lazy.prop(o, 'spendPubkey', () => { + if (a.address) return decodeSilentPaymentAddress(a.address).B_spend; + return a.spendPubkey; + }); + lazy.prop(o, 'scanPubkey', () => { + if (a.address) return decodeSilentPaymentAddress(a.address).B_scan; + return a.scanPubkey; + }); + lazy.prop(o, 'address', () => { + if (a.address) return a.address; + const version = a.version !== undefined ? a.version : 0; + return encodeSilentPaymentAddress( + o.spendPubkey, + o.scanPubkey, + version, + network, + ); + }); + if (opts.validate) { + if (a.address) { + const decoded = decodeSilentPaymentAddress(a.address); + if (a.spendPubkey && tools.compare(a.spendPubkey, decoded.B_spend) !== 0) + throw new TypeError('Spend pubkey mismatch'); + if (a.scanPubkey && tools.compare(a.scanPubkey, decoded.B_scan) !== 0) + throw new TypeError('Scan pubkey mismatch'); + const HRP = network.bech32 === 'bc' ? 'sp' : 'tsp'; + if (!a.address.startsWith(HRP)) { + throw new TypeError('Invalid prefix or Network mismatch'); + } + } + if (o.spendPubkey && o.spendPubkey.length !== 33) + throw new TypeError('Invalid spend pubkey length'); + if (o.scanPubkey && o.scanPubkey.length !== 33) + throw new TypeError('Invalid scan pubkey length'); + } + return Object.assign(o, a); +} +/** Calculate Input hash tweak + * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n + * @param smallestOutpoint - output of (findSmallestOutpoint) + * @param summedSenderPubkey + * @returns the input_hash tweak + */ +export function calculateInputHashTweak(smallestOutpoint, summedSenderPubkey) { + const ihRaw = taggedHash( + 'BIP0352/Inputs', + tools.concat([smallestOutpoint, summedSenderPubkey]), + ); + return hashToTweak(ihRaw); +} +/** + * Calculate the sum of input private keys + * @param inputPrivKeyTuples - an array of inputs to sum + * @returns aSum the resulting sum + */ +export function calculateSumA(inputPrivKeyTuples) { + if (inputPrivKeyTuples.length === 0) return null; + let aSum = null; + for (const { priv, isXOnly } of inputPrivKeyTuples) { + if (!ecc.isPrivate(priv)) continue; + let k = priv; + // only normalize when key will be used as x-only (Taproot key-spend). + if (isXOnly) { + const P = ecc.pointFromScalar(k, true); + if (!P) continue; // skip invalid + if (P[0] === 0x03) { + const neg = ecc.privateNegate(k); + if (!neg) return null; // defensive + k = neg; + } + } + aSum = aSum ? ecc.privateAdd(aSum, k) : k; + // If sum == 0 mod n, tiny-secp returns null. Treat as degenerate. + if (aSum == null || isZero32(aSum)) return null; + } + return aSum && !isZero32(aSum) ? aSum : null; +} +/** + * Calculate shared secret + * S = (inputHash * B_scan) * a_sum (compressed) + * @param inputHash + * @param scanPubkey - B_scan + * @param summedSenderPrivkey - a_Sum + * @returns S + */ +export function calculateSharedSecret( + inputHash, + scanPubkey, + summedSenderPrivkey, +) { + if (!summedSenderPrivkey) + throw new Error('summedSenderPrivkey was not provided?'); + const Si = ecc.pointMultiply(scanPubkey, inputHash, true); + if (!Si) throw new Error('pointMultiply(B_scan, ih) failed'); + let S; + S = ecc.pointMultiply(Si, summedSenderPrivkey, true); + if (!S) throw new Error('pointMultiply(Si, summedSenderPrivkey) failed'); + else return S; +} +/** + * Calculate the tweak key + * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n + * @constructor + * @param S + * @param k + * @returns input_hash tweak + */ +export function calculateT_k(S, k) { + if (!S) return null; + let t_k = taggedHash('BIP0352/SharedSecret', tools.concat([S, ser32BE(k)])); + return hashToTweak(t_k); +} +/** + * Calculate P_k + * P_k = B_spend + t_k·G (compressed) -> x-only for P2TR + * @param spendPubKey + * @param t_k + * @returns Pk + */ +export function calculateP_k(spendPubKey, t_k) { + const Pk = ecc.pointAddScalar(spendPubKey, t_k, true); + if (!Pk) throw new Error('pointAddScalar failed'); + return Pk; +} +/** + * derive a silent payment output for a transaction. + * @param S - shared secret = (inputHash * B_scan) * a_sum (compressed) + * @param spendPubkey - B_spend + * @param k - output number + * @returns the newly calculated output + */ +export function deriveOutput(S, spendPubkey, k) { + // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) -> reduce mod n + const t_k = calculateT_k(S, k); + // P_k = B_spend + t_k·G (compressed) -> x-only for P2TR + const P_k = calculateP_k(spendPubkey, t_k); + const P_xOnly = toXOnly(P_k); + if (!P_xOnly) throw new Error('pointAddScalar failed'); + return { pub_key: P_xOnly, tweak_key: t_k }; +} +/** + * Label scalar: TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) + * @param receiverScanPrivkey + * @param m + * @return tweaked label hash + */ +export function createLabelTweak( + receiverScanPrivkey, // b_scan (32B) + m, +) { + const raw = taggedHash( + 'BIP0352/Label', + tools.concat([receiverScanPrivkey, ser32BE(m)]), + ); + return hashToTweak(raw); +} +/** + * Generate labeled address Bm + * + * @param B_scan + * @param B_spend + * @param label + * @returns {L, Bm} - Label tweak and unencoded labeled address Bm + */ +export function generateLabelAndAddress(B_scan, B_spend, label) { + // TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) + const L = createLabelTweak(B_scan, label); + // Bm = B_spend + hashBIP0352/Label(ser256(b_scan) || ser32(m))·G + const Bm = ecc.pointAddScalar(B_spend, L, true); + if (!Bm) throw new Error('pointAddScalar(B_spend, L) failed'); + return { L, Bm }; +} +/** + * Scans a transaction's inputs and outputs to find any silent payments for the receiver. + * @param receiverScanPrivkey - b_scan + * @param receiverSpendPrivkey - b_spend + * @param smallestOutpoint + * @param inputHashTweak + * @param summedSenderPubkey - A_sum + * @param outputsToCheck - array of hex xOnly encoded outputs to check + * @param labelNonces + */ +export function scanForSilentPayments( + receiverScanPrivkey, + receiverSpendPrivkey, + smallestOutpoint, + inputHashTweak, + summedSenderPubkey, + outputsToCheck, + labelNonces = Array.from([]), +) { + let foundPayments = []; + // G + const baseSpendPubkey = ecc.pointFromScalar(receiverSpendPrivkey, true); + // Shared secret S = (inputHash * A_sum) * b_scan (order equivalent) + const S = calculateSharedSecret( + inputHashTweak, + summedSenderPubkey, + receiverScanPrivkey, + ); + if (!S) return []; + // First, scan for the base (unlabeled) address + foundPayments = foundPayments.concat( + performScan(baseSpendPubkey, S, outputsToCheck, null), + ); + // Then, scan for each labeled address + for (const m of labelNonces) { + const { L, Bm } = generateLabelAndAddress( + receiverScanPrivkey, + baseSpendPubkey, + m, + ); + const labeledResults = performScan(Bm, S, outputsToCheck, L); + // Add the label nonce to any found payments for identification + labeledResults.forEach(result => { + foundPayments.push({ ...result, labelNonce: m }); + }); + } + return foundPayments; +} +/** + * The core scanning logic, performed for a specific spend public key (B_spend). + * @param receiverSpendPubkey - G or B_m + * @param S + * @param outputsToCheck - array of hex xOnly encoded outputs to check + * @param labelScalar += */ +function performScan(receiverSpendPubkey, S, outputsToCheck, labelScalar) { + const found = []; + for (let k = 0; k < outputsToCheck.size; k++) { + const derivedOutput = deriveOutput(S, receiverSpendPubkey, k); + if (!derivedOutput.pub_key) break; + const xonlyHex = toHex(derivedOutput.pub_key).toLowerCase(); + if (outputsToCheck.size === 0 || outputsToCheck.has(xonlyHex)) { + // priv_key_tweak returned by L + t_k (mod n) for labeled, or t_k for unlabeled + let spendTweak = derivedOutput.tweak_key; + if (labelScalar != null && !isZero32(labelScalar)) { + const sum = ecc.privateAdd(labelScalar, derivedOutput.tweak_key); + if (!sum) throw new Error('privateAdd(label, t_k) failed'); + spendTweak = sum; + } + // TODO chose to return the hex encoded or raw Uint8Array + found.push({ + priv_key_tweak: spendTweak, + pub_key: derivedOutput.pub_key, + }); + } + } + return found; +} diff --git a/test/fixtures/p2sp.json b/test/fixtures/p2sp.json new file mode 100644 index 000000000..5ac9a07c2 --- /dev/null +++ b/test/fixtures/p2sp.json @@ -0,0 +1,3189 @@ +[ + { + "comment": "Simple send: two inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ], + "shared_secrets": [ + "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ], + "tweak": "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", + "shared_secret": "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Simple send: two inputs, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ], + "shared_secrets": [ + "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ], + "tweak": "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", + "shared_secret": "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ] + ], + "shared_secrets": [ + "03aa707f7b5e94b448abd28aa217e3d7a7cc6bb07f1a8d07be4de91bf7b1417469" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "4851455bfbe1ab4f80156570aa45063201aa5c9e1b1dcd29f0f8c33d10bf77ae", + "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", + "signature": "10332eea808b6a13f70059a8a73195808db782012907f5ba32b6eae66a2f66b4f65147e2b968a1678c5f73d57d5d195dbaf667b606ff80c8490eac1f3b710657" + } + ], + "tweak": "03aeea547819c08413974e2ab2b12212e007166bb2058f88b009e082b9b4914a58", + "shared_secret": "03aa707f7b5e94b448abd28aa217e3d7a7cc6bb07f1a8d07be4de91bf7b1417469", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ] + ], + "shared_secrets": [ + "03054f5c84b07182ba2a2e10a35e088778f95c04f059f4574b024c372eb8ce5468" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "ab0c9b87181bf527879f48db9f14a02233619b986f8e8f2d5d408ce68a709f51", + "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", + "signature": "398a9790865791a9db41a8015afad3a47d60fec5086c50557806a49a1bc038808632b8fe679a7bb65fc6b455be994502eed849f1da3729cd948fc7be73d67295" + } + ], + "tweak": "024cad5180a093d3af0f49f586bdf37f890920178e68e80561ed53351d0fa499ad", + "shared_secret": "03054f5c84b07182ba2a2e10a35e088778f95c04f059f4574b024c372eb8ce5468", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Outpoint ordering byte-lexicographically vs. vout-integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ] + ], + "shared_secrets": [ + "02cb25a6e7c9b7c6d550e0413da63834678465b5e80853a51d0335d318296ac182" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "c8ac0292997b5bca98b3ebd99a57e253071137550f270452cd3df8a3e2266d36", + "pub_key": "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c", + "signature": "c036ee38bfe46aba03234339ae7219b31b824b52ef9d5ce05810a0d6f62330dedc2b55652578aa5bdabf930fae941acd839d5a66f8fce7caa9710ccb446bddd1" + } + ], + "tweak": "031f9a80d0938cf980b51f7cc4fad713d49037f430646dff129c0570d75a40d8f0", + "shared_secret": "02cb25a6e7c9b7c6d550e0413da63834678465b5e80853a51d0335d318296ac182", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ] + ], + "shared_secrets": [ + "02f6b40ff17f4010fe732ac4b0f2f211281aa09c9a5fb41f1c151ec2606fee9ec2" + ], + "input_private_key_sum": "d5b8f02cbfe3f1d5295af9fb8a9320e859e9cb07115856486ab1a4e4fb89a621", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + } + ], + "outputs": [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f032695e2636619efa523fffaa9ef93c8802299181fd0461913c1b8daf9784cd", + "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", + "signature": "f238386c5d5e5444f8d2c75aabbcb28c346f208c76f60823f5de3b67b79e0ec72ea5de2d7caec314e0971d3454f122dda342b3eede01b3857e83654e36b25f76" + } + ], + "tweak": "0319949463fc6a2368d999a2a6a2bcb2dbf64a2ac6e00b3ba5659780c860a6d9e0", + "shared_secret": "02f6b40ff17f4010fe732ac4b0f2f211281aa09c9a5fb41f1c151ec2606fee9ec2", + "input_pub_key_sum": "03e40664e222ba71e29b80efc907fa22a3c6c64f45e89dbb8511dc7a3712b0a186" + } + } + ] + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ] + ], + "shared_secrets": [ + "02de9719785c6d09f71571dadf44bca59edba2af3e689c65cbc3bb5a4a387732ef" + ], + "input_private_key_sum": "e7638ebfda3ab3849a5707e240a6627671f7f6e609bf172691cf1e9780e51d47", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "02782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + } + ], + "outputs": [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3fb9ce5ce1746ced103c8ed254e81f6690764637ddbc876ec1f9b3ddab776b03", + "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", + "signature": "c5acd25a8f021a4192f93bc34403fd8b76484613466336fb259c72d04c169824f2690ca34e96cee86b69f376c8377003268fda56feeb1b873e5783d7e19bcca5" + } + ], + "tweak": "02dc59cc8e8873b65c1dd5c416d4fbeb647372c329bd84a70c05b310e222e2c183", + "shared_secret": "02de9719785c6d09f71571dadf44bca59edba2af3e689c65cbc3bb5a4a387732ef", + "input_pub_key_sum": "038180a2125f9d6dd116e1a6139be4d72fd5057dab6aaabaa5654817c11baeb3ba" + } + } + ] + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ] + ], + "shared_secrets": [ + "030e7f5ca4bf109fc35c8c2d878f756c891ac04c456cc5f0b05fcec4d3b2b1beb2" + ], + "input_private_key_sum": "cda4ff9a3480e1fbfc6edd61b222f280f9baa0652002c1ffdb612efcc45d2ff2", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "028c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + } + ], + "outputs": [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f5382508609771068ed079b24e1f72e4a17ee6d1c979066bf1d4e2a5676f09d4", + "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", + "signature": "ff65833b8fd1ed3ef9d0443b4f702b45a3f2dd457ba247687e8207745c3be9d2bdad0ab3f07118f8b2efc6a04b95f7b3e218daf8a64137ec91bd2fc67fc137a5" + } + ], + "tweak": "03b990f5b1d90ea8fd4bdd5c856a9dfe17035d196958062e2c6cb4c99e413f3548", + "shared_secret": "030e7f5ca4bf109fc35c8c2d878f756c891ac04c456cc5f0b05fcec4d3b2b1beb2", + "input_pub_key_sum": "020f0ab50f420ab1249bc2a21659c607f2873400853035aad0ca6d0ded04d62623" + } + } + ] + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ] + ], + "shared_secrets": [ + "021cd92ff153e638d0a97bcd11fafc81c321b111f5ba1efff593371b7b688efdd3" + ], + "input_private_key_sum": "7823ca0d4895515315a8e3bf602c080b6b732117272429e94751eb9b13a01943", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "b40017865c79b1fcbed68896791be93186d08f47e416b289b8c063777e14e8df", + "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", + "signature": "d1edeea28cf1033bcb3d89376cabaaaa2886cbd8fda112b5c61cc90a4e7f1878bdd62180b07d1dfc8ffee1863c525a0c7b5bcd413183282cfda756cb65787266" + } + ], + "tweak": "0233c2a447b8b244e4ffcfb59fe365eaa3bb22288b31e2113b9998861f40d4d6da", + "shared_secret": "021cd92ff153e638d0a97bcd11fafc81c321b111f5ba1efff593371b7b688efdd3", + "input_pub_key_sum": "031ecda9c64faaa6cd57c9f3d7c62bcfc0763c2627ed8dc0e2c3018e9ff37a0bf0" + } + } + ] + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ] + ], + "shared_secrets": [ + "03d9437eb3676cf5cc00feebe68bc44c4567332e4b89788dec9eceb3779054442b" + ], + "input_private_key_sum": "700fd97abd324179e8bcc72587bbd9a40b43f67535ce95a0b80175b2dc73a314", + "input_pub_keys": [ + "028c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4", + "03e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "a2f9dd05d1d398347c885d9c61a64d18a264de6d49cea4326bafc2791d627fa7", + "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", + "signature": "96038ad233d8befe342573a6e54828d863471fb2afbad575cc65271a2a649480ea14912b6abbd3fbf92efc1928c036f6e3eef927105af4ec1dd57cb909f360b8" + } + ], + "tweak": "02d4e4f2c4cdb71c9c39a700a9ee1a0fc05b98362a441183f5770af7d6e2b3038c", + "shared_secret": "03d9437eb3676cf5cc00feebe68bc44c4567332e4b89788dec9eceb3779054442b", + "input_pub_key_sum": "03bc118b1c8178915b716d6137633722c71adfe721551ec7b3938054691de6a2b9" + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "scan_pub_key": "02062d49ffc02787d586c608dfbec184aa91a6597d97b463ea5c6babd9d17a95a3", + "spend_pub_key": "0381eb9a9a9ec739d527c1631b31b421566f5c2a47b4ab5b1f6a686dfb68eab716" + }, + { + "address": "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "scan_pub_key": "02062d49ffc02787d586c608dfbec184aa91a6597d97b463ea5c6babd9d17a95a3", + "spend_pub_key": "0381eb9a9a9ec739d527c1631b31b421566f5c2a47b4ab5b1f6a686dfb68eab716" + } + ] + }, + "expected": { + "outputs": [ + [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2", + "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", + "scan_priv_key": "060b751d7892149006ed7b98606955a29fe284a1e900070c0971f5fb93dbf422" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ], + "outputs": [ + { + "priv_key_tweak": "72cd082cccb633bf85240a83494b32dc943a4d05647a6686d23ad4ca59c0ebe4", + "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "signature": "38745f3d9f5eef0b1cfb17ca314efa8c521efab28a23aa20ec5e3abb561d42804d539906dce60c4ee7977966184e6f2cab1faa0e5377ceb7148ec5218b4e7878" + }, + { + "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", + "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Receiving with labels: label with even parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0259352add837b6686e8d22b87017814a46b3ad308702167c65bd5c8599cd28d1c" + } + ] + }, + "expected": { + "outputs": [ + [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "51d4e9d0d482b5700109b4b2e16ff508269b03d800192a043d61dca4a0a72a52", + "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", + "signature": "c30fa63bad6f0a317f39a773a5cbf0b0f8193c71dfebba05ee6ae4ed28e3775e6e04c3ea70a83703bb888122855dc894cab61692e7fd10c9b3494d479a60785e" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0208a144a18433a83f633c822c1bf5ee4c8c8e24601d6ca75e20a7dc57a0ff9280" + } + ] + }, + "expected": { + "outputs": [ + [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "6024ae214876356b8d917716e7707d267ae16a0fdb07de2a786b74a7bbcddead", + "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", + "signature": "a86d554d0d6b7aa0907155f7e0b47f0182752472fffaeddd68da90e99b9402f166fd9b33039c302c7115098d971c1399e67c19e9e4de180b10ea0b9d6f0db832" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Receiving with labels: large label integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03d85092bbe3468f684ce1d8a2a66ebec96a9e6e09e7110720a5d5faa4aa7880d0" + } + ] + }, + "expected": { + "outputs": [ + [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "e336b92330c33030285ce42e4115ad92d5197913c88e06b9072b4a9b47c664a2", + "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", + "signature": "c9e80dd3bdd25ca2d352ce77510f1aed37ba3509dc8cc0677f2d7c2dd04090707950ce9dd6c83d2a428063063aff5c04f1744e334f661f2fc01b4ef80b50f739" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + } + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", + "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; same recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0244baa5cf5db444a9e922832ff2c88716b566a85d62e8235aebd91884d4f64942" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0244baa5cf5db444a9e922832ff2c88716b566a85d62e8235aebd91884d4f64942" + } + ] + }, + "expected": { + "outputs": [ + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1, + 1337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ], + "outputs": [ + { + "priv_key_tweak": "4e3352fbe0505c25e718d96007c259ef08db34f8c844e4ff742d9855ff03805a", + "pub_key": "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "signature": "6eeae1ea9eb826e3d0e812f65937100e0836ea188c04f36fabc4981eda29de8d3d3529390a0a8b3d830f7bca4f5eae5994b9788ddaf05ad259ffe26d86144b4b" + }, + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "bf709f98d4418f8a67e738154ae48818dad44689cd37fbc070891a396dd1c633", + "pub_key": "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "signature": "42a19fd8a63dde1824966a95d65a28203e631e49bf96ca5dae1b390e7a0ace2cc8709c9b0c5715047032f57f536a3c80273cbecf4c05be0b5456c183fa122c06" + }, + { + "priv_key_tweak": "736f05e4e3072c3b8656bedef2e9bf54cbcaa2b6fe5320d3e86f5b96874dda71", + "pub_key": "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "signature": "2e61bb3d79418ecf55f68847cf121bfc12d397b39d1da8643246b2f0a9b96c3daa4bfe9651beb5c9ce20e1f29282c4566400a4b45ee6657ec3b18fdc554da0b4" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Single recipient: use silent payments for sender change", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr", + "scan_pub_key": "03b4cc0b090b6f49a684558852db60ee5eb1c5f74352839c3d18a8fc04ef7354e0", + "spend_pub_key": "03ecd43b9fdad484ff57278b21878b844276ce390622d03dd0cfb4288b7e02a6f5" + } + ] + }, + "expected": { + "outputs": [ + [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "037d12c02c3aed482658a28b8d1be030dac1daf995551491d74c00543af98572fb" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", + "scan_priv_key": "11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqauj52ymtc4xdkmx3tgyhrsemg2g3303xk2gtzfy8h8ejet8fz8jcw23zua", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ], + "outputs": [ + { + "priv_key_tweak": "80cd767ed20bd0bb7d8ea5e803f8c381293a62e8a073cf46fb0081da46e64e1f", + "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "signature": "7fbd5074cf1377273155eefafc7c330cb61b31da252f22206ac27530d2b2567040d9af7808342ed4a09598c26d8307446e4ed77079e6a2e61fea736e44da5f5a" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "037d12c02c3aed482658a28b8d1be030dac1daf995551491d74c00543af98572fb", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Single recipient: taproot input with NUMS point", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ] + ], + "shared_secrets": [ + "036f040608cd1e5ee79c54e78bea85904c895591f547beae080d0c5f6946c2730d" + ], + "input_private_key_sum": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7", + "input_pub_keys": [ + "02782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + } + } + ], + "outputs": [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3ddec3232609d348d6b8b53123b4f40f6d4f5398ca586f087b0416ec3b851496", + "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", + "signature": "d7d06e3afb68363031e4eb18035c46ceae41bdbebe7888a4754bc9848c596436869aeaecff0527649a1f458b71c9ceecec10b535c09d01d720229aa228547706" + } + ], + "tweak": "02213b872c9a6ee28a0d861384a1b3e3ec7257f4855ed09b4323e3899f3b028989", + "shared_secret": "036f040608cd1e5ee79c54e78bea85904c895591f547beae080d0c5f6946c2730d", + "input_pub_key_sum": "02782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + ] + }, + { + "comment": "Pubkey extraction from malleated p2pkh", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ] + ], + "shared_secrets": [ + "034773b97ccad9791cb4213964ff9896ccd6581ee69345de5d114786d9d86b03a2" + ], + "input_private_key_sum": "610e0f75fd05e5e80e088b57af0a46da06cb0700c0c5907aa6d29c6b4ce46348", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "02e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + } + } + ], + "outputs": [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "10bde9781def20d7701e7603ef1b1e5e71c67bae7154818814e3c81ef5b1a3d3", + "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", + "signature": "6137969f810e9e8ef6c9755010e808f5dd1aed705882e44d7f0ae64eb0c509ec8b62a0671bee0d5914ac27d2c463443e28e999d82dc3d3a4919f093872d947bb" + } + ], + "tweak": "028d6617f9bfe08604beb2188f4eebec923f5f8cc436fa6d14e4256e49bc32e7c8", + "shared_secret": "034773b97ccad9791cb4213964ff9896ccd6581ee69345de5d114786d9d86b03a2", + "input_pub_key_sum": "038b0d201fe111bdc0e6953772bd02a41959d25d5b2f66bcbe348af27bbdd42735" + } + } + ] + }, + { + "comment": "P2PKH and P2WPKH Uncompressed Keys are skipped", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ], + "shared_secrets": [ + "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64" + ], + "input_private_key_sum": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ], + "tweak": "02b04034f00da0678507d1345b7d56fecef825a1151f9dc7d8ca6946452a9e1f43", + "shared_secret": "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64", + "input_pub_key_sum": "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + ] + }, + { + "comment": "Skip invalid P2SH inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ], + "shared_secrets": [ + "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64" + ], + "input_private_key_sum": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ], + "tweak": "02b04034f00da0678507d1345b7d56fecef825a1151f9dc7d8ca6946452a9e1f43", + "shared_secret": "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64", + "input_pub_key_sum": "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + ] + }, + { + "comment": "Recipient ignores unrelated outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "scan_pub_key": "02062d49ffc02787d586c608dfbec184aa91a6597d97b463ea5c6babd9d17a95a3", + "spend_pub_key": "0381eb9a9a9ec739d527c1631b31b421566f5c2a47b4ab5b1f6a686dfb68eab716" + } + ] + }, + "expected": { + "outputs": [ + [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" + ] + ], + "shared_secrets": [ + "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "No valid inputs, sender generates no outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [] + ], + "shared_secrets": [ + null + ], + "input_pub_keys": [] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + } + ], + "outputs": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [], + "tweak": null, + "shared_secret": null + } + } + ] + }, + { + "comment": "Input keys sum up to zero / point at infinity: sending fails, receiver skips tx", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + }, + "private_key": "592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456" + } + ], + "recipients": [ + { + "address": "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s", + "scan_pub_key": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "spend_pub_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ] + }, + "expected": { + "outputs": [ + [] + ], + "shared_secrets": [ + null + ], + "input_pub_keys": [ + "02557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "03557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + } + } + ], + "outputs": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "key_material": { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ], + "outputs": [], + "tweak": null, + "shared_secret": null + } + } + ] + } +] \ No newline at end of file diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts new file mode 100644 index 000000000..148c90998 --- /dev/null +++ b/test/integration/silentpayment.spec.ts @@ -0,0 +1,552 @@ +/* eslint-env mocha */ +import assert from 'assert'; +import * as ecc from 'tiny-secp256k1'; +import * as bitcoin from 'bitcoinjs-lib'; +import { fromHex, toHex } from 'uint8array-tools'; +import * as fixtures from '../fixtures/p2sp.json'; +import { hash160 } from '../../ts_src/crypto.js'; +import { + calculateInputHashTweak, + calculateSharedSecret, + calculateSumA, + deriveOutput, + decodeSilentPaymentAddress, + encodeSilentPaymentAddress, + findSmallestOutpoint, + modN32, + generateLabelAndAddress, + scanForSilentPayments, +} from '../../ts_src/payments/p2sp.js'; +import { Input } from '../../ts_src/transaction.js'; + +// ---- init ecc for bitcoinjs (even if we use tiny-secp directly) ---- +bitcoin.initEccLib(ecc); + +interface TestCase { + comment?: string; + sending: Array; + receiving: Array; +} + +interface TestInput { + txid: string; + vout: number; + scriptSig: string; + txinwitness: string; + prevout: { + scriptPubKey: { + hex: String; + }; + }; + private_key?: string; +} + +interface Sender { + given: { + vin: Array; + recipients: Array; + }; + expected: { + outputs: Array>; + shared_secrets: Array; + input_private_key_sum: string; + input_pub_keys: Array; + }; +} + +interface SenderRecipient { + address: string; + scan_pub_key: string; + spend_pub_key: string; +} + +interface Recipient { + given: { + vin: Array; + outputs: Array; + key_material: { + spend_priv_key: string; + scan_priv_key: string; + }; + labels: Array; + }; + expected: { + addresses: Array; + outputs: Array; + tweak: string; + shared_secret: string; + input_pub_key_sum: string; + }; +} + +interface RecipientOutput { + priv_key_tweak: string; + pub_key: string; + signature: String; +} + +// ================================================================ +// =================== scalar helpers (mod n) ================== +// ================================================================ +const N = fromHex( + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', +); + +function isZero32(a: Uint8Array) { + for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; + return true; +} + +// Detect patterns +function isP2PKH(spkHex?: string): boolean { + const u = fromHex(spkHex || ''); + return ( + u.length === 25 && + u[0] === 0x76 && + u[1] === 0xa9 && + u[2] === 0x14 && + u[23] === 0x88 && + u[24] === 0xac + ); +} + +function isP2SH(spkHex?: string): boolean { + const u = fromHex(spkHex || ''); + return u.length === 23 && u[0] === 0xa9 && u[1] === 0x14 && u[22] === 0x87; // OP_HASH160 20 OP_EQUAL +} + +function isP2WPKH(spkHexOrScriptHex?: string): boolean { + const u = fromHex(spkHexOrScriptHex || ''); + return u.length === 22 && u[0] === 0x00 && u[1] === 0x14; +} + +function isP2TR(spkHex?: string): boolean { + const u = fromHex(spkHex || ''); + return u.length === 34 && u[0] === 0x51 && u[1] === 0x20; // OP_1 PUSH32 +} + +function readVarInt( + u: Uint8Array, + off: number, +): { val: number; next: number } | null { + if (off >= u.length) return null; + const fb = u[off]; + if (fb < 0xfd) return { val: fb, next: off + 1 }; + if (fb === 0xfd) { + if (off + 2 >= u.length) return null; + return { val: u[off + 1] | (u[off + 2] << 8), next: off + 3 }; + } + if (fb === 0xfe) { + if (off + 4 >= u.length) return null; + return { + val: + (u[off + 1] | + (u[off + 2] << 8) | + (u[off + 3] << 16) | + (u[off + 4] << 24)) >>> + 0, + next: off + 5, + }; + } + return null; // 0xff not expected here +} + +function parseWitnessItems(witHex: string): Uint8Array[] { + const u = fromHex(witHex || ''); + const items: Uint8Array[] = []; + let off = 0; + const c = readVarInt(u, off); + if (!c) return items; + off = c.next; + for (let i = 0; i < c.val; i++) { + const L = readVarInt(u, off); + if (!L) break; + off = L.next; + items.push(u.slice(off, off + L.val)); + off += L.val; + } + return items; +} + +// ---- helper: sum compressed pubkeys exactly as given (33B) ---- +function sumPointsCompressed(points33: Uint8Array[]): Uint8Array { + let acc: Uint8Array | null = null; + for (const P of points33) { + if (!P || P.length !== 33 || (P[0] !== 0x02 && P[0] !== 0x03)) { + throw new Error('bad compressed pubkey in input_pub_keys'); + } + if (acc === null) acc = P; + else { + const next = ecc.pointAdd(acc, P, true) as Uint8Array | null; + if (!next) throw new Error('pointAdd failed when summing inputs'); + acc = next; + } + } + if (!acc) throw new Error('no input_pub_keys to sum'); + return acc; // 33B compressed A_sum +} + +/** + * Return a single compressed pubkey for display, or null if none. + * + * @param prevoutHex vin.prevout.scriptPubKey.hex + * @param scriptSigHex vin.scriptSig (hex) + * @param txinwitnessHex vin.txinwitness (hex-serialized witness) + */ +function getPubkeyFromInputTS( + prevoutHex?: string, + scriptSigHex?: string, + txinwitnessHex?: string, +): Uint8Array | null { + // P2PKH + if (isP2PKH(prevoutHex)) { + const spk = fromHex(prevoutHex!); + const spk_hash = spk.slice(3, 3 + 20); // hash160 in SPK + const ss = fromHex(scriptSigHex || ''); + for (let i = ss.length; i >= 33; i--) { + const cand = ss.slice(i - 33, i); + if ( + (cand[0] === 0x02 || cand[0] === 0x03) && + hash160(cand).every((b, j) => b === spk_hash[j]) + ) { + // compressed & hash matches SPK + return cand; + } + } + } + + // P2SH -> redeem_script is scriptSig[1:], if redeemScript is P2WPKH, take last witness item + if (isP2SH(prevoutHex)) { + const scriptSig = fromHex(scriptSigHex || ''); + const redeem = scriptSig.slice(1); // drop the first push opcode + if (isP2WPKH(toHex(redeem))) { + const items = parseWitnessItems(txinwitnessHex || ''); + if (items.length > 0) { + const last = items[items.length - 1]; + if (last.length === 33 && (last[0] === 0x02 || last[0] === 0x03)) + return last; + } + } + } + + // P2WPKH: last witness item is pubkey + if (isP2WPKH(prevoutHex)) { + const items = parseWitnessItems(txinwitnessHex || ''); + if (items.length > 0) { + const last = items[items.length - 1]; + if (last.length === 33 && (last[0] === 0x02 || last[0] === 0x03)) + return last; + } + } + + // P2TR: if script-path spend, SKIP (mirrors NUMS-case behavior in vectors); + // otherwise return internal key from prevout (x-only -> compressed even-Y) + if (isP2TR(prevoutHex)) { + const wit = parseWitnessItems(txinwitnessHex || ''); + let stack = [...wit]; + if (stack.length >= 1) { + // Annex (BIP342): last item starting with 0x50 + if (stack.length > 1 && stack[stack.length - 1][0] === 0x50) { + stack = stack.slice(0, -1); + } + if (stack.length > 1) { + // Script-path spend -> in these vectors, it's the NUMS case -> skip + return null; + } + } + // Key-path spend: take internal x-only from prevout at [2..33], render as 02||x + const spk = fromHex(prevoutHex!); + const out = new Uint8Array(33); + out[0] = 0x02; + out.set(spk.slice(2, 34), 1); + return out; + } + + return null; +} + +// ================================================================ +// ========================= sender ============================= +// ================================================================ +function runSenderCase(tc: Sender) { + const { given, expected } = tc; + + // 1) input_pub_keys = [get_pubkey_from_input(vin) for vin if valid] + const inputPubKeysHex: string[] = []; + const inputPrivKeyTuples: Array<{ priv: Uint8Array; isXOnly: boolean }> = []; + + for (const inp of given.vin) { + const priv = fromHex(inp.private_key); + const prevoutHex = inp.prevout?.scriptPubKey?.hex || ''; + const scriptSigHex = inp.scriptSig || ''; + const txinwitnessHex = inp.txinwitness || ''; + + const disp = getPubkeyFromInputTS(prevoutHex, scriptSigHex, txinwitnessHex); + + // include in the *display* list if present + if (disp) inputPubKeysHex.push(toHex(disp)); + + if (disp) { + const isXOnly = isP2TR(prevoutHex); + inputPrivKeyTuples.push({ priv, isXOnly }); + } + } + + if (expected.input_pub_keys) { + assert.deepStrictEqual( + inputPubKeysHex.map(h => h.toLowerCase()), + expected.input_pub_keys.map((h: string) => h.toLowerCase()), + 'input_pub_keys did not match expected', + ); + } + + // Early exit (no usable inputs) + if (inputPrivKeyTuples.length === 0) { + const sending_outputs: string[] = []; + assert( + expected.outputs && + expected.outputs[0] && + expected.outputs[0].length === 0 && + sending_outputs.length === 0, + 'Sending test failed (no inputs case)', + ); + return; + } + + // -------- 2) a_sum with Taproot-only odd-Y conditional negation -------- + // 2) a_sum = Σ negated (mod n) via tiny-secp + let aSum: Uint8Array = calculateSumA(inputPrivKeyTuples); + + // (optional) fixture check stays the same + if (expected.input_private_key_sum) { + assert.strictEqual( + toHex(aSum).toLowerCase(), + expected.input_private_key_sum.toLowerCase(), + 'a_sum did not match expected input_private_key_sum', + ); + } + // sl + if (!aSum) return; + + // -------- 3) H_inputs(outpoint_L || ser_P(A)) -------- + const A = ecc.pointFromScalar(aSum, true) as Uint8Array; // compressed 33B + const vinArray = given.vin.map( + ({ txid, vout }): Array => ({ hash: fromHex(txid), index: vout }), + ); + const outpointL = findSmallestOutpoint(vinArray); + + // -------- 4) group recipients by B_scan (first-seen order) -------- + type Group = { B_scan: Uint8Array; firstIdx: number; B_m_list: Uint8Array[] }; + const scanMap = new Map(); + const groups: Group[] = []; + + for (let i = 0; i < given.recipients.length; i++) { + const r = given.recipients[i]; + const B_spend_pub = fromHex(r.spend_pub_key); + const B_scan = fromHex(r.scan_pub_key); + + const key = toHex(B_scan).toLowerCase(); + let g = scanMap.get(key); + if (!g) { + g = { B_scan, firstIdx: i, B_m_list: [] }; + scanMap.set(key, g); + groups.push(g); + } + g.B_m_list.push(B_spend_pub); + } + + const inputHashTweak = calculateInputHashTweak(outpointL, A); + + // -------- 5) per-group S once; then derive P_k for each B_m with k=0,1,… -------- + const sending_outputs: Set = new Set(); + + for (const g of groups) { + const S = calculateSharedSecret(inputHashTweak, g.B_scan, aSum); + // optional shared-secret assert at group's first index + if (expected.shared_secrets && expected.shared_secrets[g.firstIdx]) { + assert.strictEqual( + toHex(S).toLowerCase(), + expected.shared_secrets[g.firstIdx].toLowerCase(), + `ecdh_shared_secret mismatch for recipient[${g.firstIdx}]`, + ); + } + + for (let k = 0; k < g.B_m_list.length; k++) { + const { pub_key, tweak_key } = deriveOutput(S, g.B_m_list[k]); + sending_outputs.add(toHex(pub_key)); // x-only hex + } + } + + // -------- 6) compare with ANY expected variant (set-equality) -------- + const expectedSet = new Set(expected.outputs.flat()); + const intersection = new Set( + [...sending_outputs].filter(x => expectedSet.has(x)), + ); + assert.strictEqual( + intersection.size > 0, + true, + 'Sending test failed (derived outputs are not in the expected outputs)', + ); +} + +// ---- receiver runner ---- +function runReceiverCase(tc: Recipient) { + const { given, expected } = tc; + + // -------- key material -------- + const b_scan = fromHex(given.key_material.scan_priv_key); + const b_spend = fromHex(given.key_material.spend_priv_key); + const B_spend = ecc.pointFromScalar(b_spend, true) as Uint8Array; + + // -------- outpoint_L -------- + const vinArray = given.vin.map( + ({ txid, vout }): Array => ({ hash: fromHex(txid), index: vout }), + ); + const outpointL = findSmallestOutpoint(vinArray); + + // -------- A_sum_point (compressed) -------- + let A_sum_point: Uint8Array; + if (expected.input_pub_key_sum) { + A_sum_point = fromHex(expected.input_pub_key_sum); + if (A_sum_point.length !== 33) + throw new Error('expected.input_pub_key_sum must be 33 bytes'); + } else { + assert.strictEqual( + expected.outputs?.length || 0, + 0, + 'receiver: expected no outputs with no inputs', + ); + return; + } + + // -------- Inputs tag → tweak -------- + const inputHashTweak = calculateInputHashTweak(outpointL, A_sum_point); + + // -------- Shared secret S (compressed) -------- + const S = calculateSharedSecret(inputHashTweak, A_sum_point, b_scan); + + if (expected.shared_secret) { + assert.strictEqual( + toHex(S).toLowerCase(), + expected.shared_secret.toLowerCase(), + 'receiver shared secret mismatch', + ); + } + + // -------- outputs_to_check = actual tx outputs we care about (x-only hex) -------- + const outputsToCheck = new Set( + (given.outputs || []).map((h: string) => h.toLowerCase()), + ); + + // -------- precompute labels from b_scan (array), -------- + const labelList: number[] = Array.isArray(given.labels) + ? given.labels.map((n: any) => Number(n) >>> 0) + : []; + + const foundScanned = scanForSilentPayments( + b_scan, + b_spend, + outpointL, + inputHashTweak, + A_sum_point, + outputsToCheck, + labelList, + ); + + const scannedResult: Set<{ pub_key: string; priv_key_tweak: string }> = + new Set( + foundScanned.map( + (v: { + priv_key_tweak: Uint8Array; + pub_key: Uint8Array; + lableNonce: Uint8Array; + }) => + JSON.stringify({ + pub_key: toHex(v.pub_key), + priv_key_tweak: toHex(v.priv_key_tweak), + }), + ), + ); + + const expectedOutputs: Set<{ pub_key: string; priv_key_tweak: string }> = + new Set( + expected.outputs.map( + (value: { pub_key: string; priv_key_tweak: string }) => + JSON.stringify({ + pub_key: value.pub_key, + priv_key_tweak: value.priv_key_tweak, + }), + ) || [], + ); + + assert.strictEqual( + scannedResult.size, + expectedOutputs.size, + `receiver: sizes mismatch (got ${scannedResult.size}, want ${expectedOutputs.size})`, + ); + // -------- compare sets of dicts (order-agnostic) -------- + for (const x of expectedOutputs) + assert.ok(scannedResult.has(x), `receiver: missing expected entry ${x}`); +} + +// ================================================================ +// ======================= mocha suite ========================== +// ================================================================ +describe('BIP-352 Silent Payments — sender vectors', () => { + const groups: TestCase[] = fixtures as TestCase[]; + let count = 0; + for (const g of groups) { + if (!g?.sending) continue; + console.log(g.comment); + for (const sc of g.sending) { + it('Test' + g.comment, () => { + runSenderCase(sc); + }); + for (const recipient of sc.given.recipients) { + const scan_pub_key = fromHex(recipient.scan_pub_key); + const spend_pub_key = fromHex(recipient.spend_pub_key); + it('Test address decoding for: ' + recipient.address, () => { + const decoded = decodeSilentPaymentAddress(recipient.address); + assert.deepStrictEqual( + scan_pub_key, + decoded.B_scan, + 'Decoding the sp address did not yield expected B_scan', + ); + assert.deepStrictEqual( + spend_pub_key, + decoded.B_spend, + 'Decoding the sp address did not yield expected B_spend_pub', + ); + }); + it('Test address encoding for: ' + recipient.address, () => { + const encoded = encodeSilentPaymentAddress( + spend_pub_key, + scan_pub_key, + ); + assert.deepStrictEqual( + recipient.address, + encoded, + 'Decoding the sp address did not yield expected B_spend_pub', + ); + }); + } + count++; + } + assert.ok(count > 0, 'no sender cases found'); + } +}); + +describe('BIP-352 Silent Payments — receiver vectors', () => { + const groups: TestCase[] = fixtures as TestCase[]; + let count = 0; + for (const g of groups) { + if (!g?.receiving) continue; + it('Test ' + g.comment, () => { + for (const rc of g.receiving) { + runReceiverCase(rc); + } + }); + count++; + } + assert.ok(count > 0, 'no receiver cases found'); +}); diff --git a/ts_src/crypto.ts b/ts_src/crypto.ts index f9e1c4e58..4ef3e3a76 100644 --- a/ts_src/crypto.ts +++ b/ts_src/crypto.ts @@ -116,6 +116,24 @@ export const TAGGED_HASH_PREFIXES: TaggedHashPrefixes = { 201, 4, 3, 77, 28, 136, 232, 200, 14, 34, 229, 61, 36, 86, 109, 100, 130, 78, 214, 66, 114, 129, 192, 145, 0, 249, 77, 205, 82, 201, 129, ]), + 'BIP0352/Inputs': Uint8Array.from([ + 30, 123, 150, 235, 22, 10, 104, 129, 159, 151, 118, 75, 67, 213, 215, 126, + 102, 89, 215, 88, 119, 157, 67, 168, 167, 117, 95, 91, 228, 90, 126, 51, 30, + 123, 150, 235, 22, 10, 104, 129, 159, 151, 118, 75, 67, 213, 215, 126, 102, + 89, 215, 88, 119, 157, 67, 168, 167, 117, 95, 91, 228, 90, 126, 51, + ]), + 'BIP0352/SharedSecret': Uint8Array.from([ + 159, 109, 128, 17, 88, 30, 182, 45, 114, 230, 19, 96, 76, 51, 13, 202, 42, + 11, 211, 73, 226, 74, 70, 217, 162, 239, 36, 185, 169, 143, 65, 189, 159, + 109, 128, 17, 88, 30, 182, 45, 114, 230, 19, 96, 76, 51, 13, 202, 42, 11, + 211, 73, 226, 74, 70, 217, 162, 239, 36, 185, 169, 143, 65, 189, + ]), + 'BIP0352/Label': Uint8Array.from([ + 3, 73, 25, 70, 53, 194, 208, 62, 98, 212, 19, 186, 140, 205, 81, 152, 145, + 144, 23, 161, 233, 156, 190, 105, 31, 90, 52, 169, 147, 119, 224, 149, 3, + 73, 25, 70, 53, 194, 208, 62, 98, 212, 19, 186, 140, 205, 81, 152, 145, 144, + 23, 161, 233, 156, 190, 105, 31, 90, 52, 169, 147, 119, 224, 149, + ]), }; /** diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index a9d79981f..a1cf79c48 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -7,6 +7,7 @@ * - P2WPKH (Pay-to-Witness-PubKey-Hash) * - P2WSH (Pay-to-Witness-Script-Hash) * - P2TR (Taproot) + * - P2SP (Silent Payments) * * The `Payment` interface defines the structure of a payment object used for constructing various * payment types, with fields for signatures, public keys, redeem scripts, and more. @@ -23,6 +24,7 @@ import { p2sh } from './p2sh.js'; import { p2wpkh } from './p2wpkh.js'; import { p2wsh } from './p2wsh.js'; import { p2tr } from './p2tr.js'; +import { p2sp, scanForSilentPayments, deriveOutput } from './p2sp.js'; export interface Payment { name?: string; @@ -58,7 +60,20 @@ export type StackElement = Uint8Array | number; export type Stack = StackElement[]; export type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; +export { + embed, + p2ms, + p2pk, + p2pkh, + p2sh, + p2wpkh, + p2wsh, + p2tr, + p2sp, + // TODO how should we expose the two functions below? + scanForSilentPayments, + deriveOutput, +}; // TODO // witness commitment diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts new file mode 100644 index 000000000..00ec5f427 --- /dev/null +++ b/ts_src/payments/p2sp.ts @@ -0,0 +1,558 @@ +import { bitcoin as BITCOIN_NETWORK, Network } from '../networks'; +import * as ecc from 'tiny-secp256k1'; +import { toXOnly } from '../psbt/bip371'; +import * as tools from 'uint8array-tools'; +import { toHex, writeUInt32 } from 'uint8array-tools'; +import { bech32m } from 'bech32'; + +// Explicitly import the Payment type for clarity +import { Payment, PaymentOpts } from './index'; +import * as lazy from './lazy'; +import * as payments from './index.ts'; +import { taggedHash } from '../crypto'; +import { Input } from '../transaction'; + +// --- TYPE DEFINITIONS & UTILITIES --- +export const BECH32_SP_LIMIT = 150; + +// Extend the Payment interface for silent payments +export interface SilentPayment extends Payment { + spendPubkey?: Uint8Array; + scanPubkey?: Uint8Array; + version?: number; +} + +/** + * Create a 32Bit Unsigned integer + * @param n the number to put + * @returns a in [Uint8Array] big endian encoded + */ +export function ser32BE(n: number): Uint8Array { + const b = new Uint8Array(4); + writeUInt32(b, 0, n >>> 0, 'be'); + return b; +} + +const N = tools.fromHex( + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', +); + +function isZero32(a: Uint8Array) { + for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; + return true; +} + +//TODO I guess there already is a function for this ? +function subBE(a: Uint8Array, b: Uint8Array) { + const o = new Uint8Array(32); + let c = 0; + for (let i = 31; i >= 0; i--) { + let v = a[i] - b[i] - c; + if (v < 0) { + v += 256; + c = 1; + } else c = 0; + o[i] = v; + } + return o; +} + +//TODO I guess there already is a function for this ? +function modN32(x: Uint8Array) { + let r = new Uint8Array(x); + while (tools.compare(r, N) >= 0) r = subBE(r, N); + return r; +} + +function hashToTweak(h: Uint8Array) { + let t = modN32(h); + if (isZero32(t)) { + t = new Uint8Array(32); + t[31] = 1; + } + return t; +} + +/** + * Smallest outpoint = lexicographic min of (txidLE || voutLE) + * @param inputs an array of inputs you want the first lexicographically sorted result + * @returns the first output after sorting lexicographically + */ +export const findSmallestOutpoint = (inputs: Array) => + inputs + .map(v => serOutpointLE(v.hash, v.index)) + .sort((a, b) => tools.compare(a, b))[0]; + +/** + * Serialize output with number little endian + * (used to sort outputs) + * @param txidHexBE - big endian encoded tx + * @param vout - output index + * @returns the serialized little endian encoded output + */ +export const serOutpointLE = (txidHexBE: string, vout: number) => { + const out = new Uint8Array(36); + const txidLE = tools.fromHex(txidHexBE); + if (txidLE.length !== 32) throw new Error('txid must be 32 bytes'); + txidLE.reverse(); // BE -> LE + out.set(txidLE, 0); + writeUInt32(out, 32, vout >>> 0, 'le'); + return out; +}; + +/** + * Encodes spend and scan public keys into a Bech32m Silent Payment address. + * @param B_spend + * @param B_scan + * @param version + * @param network - testing, regtest or prod + * @returns bech32m encoded string + */ +export function encodeSilentPaymentAddress( + B_spend: Uint8Array, + B_scan: Uint8Array, + version = 0, + network: Network = BITCOIN_NETWORK, +): string { + if (version !== 0) + throw new Error(`Unsupported silent payment address version: ${version}`); + + const SP_HRP = network.bech32 === 'bc' ? 'sp' : 'tsp'; + let data: Uint8Array; + + // To ensure encode/decode is symmetric, we use the legacy 66-byte format + // if full 33-byte keys are provided. This preserves the original y-parity. + if (B_spend.length === 33 && B_scan.length === 33) { + data = tools.concat([B_scan, B_spend]); + } else { + // Otherwise, we use the modern 64-byte format with x-only keys as per the spec. + const B_spend_xonly = B_spend.length === 33 ? B_spend.slice(1) : B_spend; + const B_scan_xonly = B_scan.length === 33 ? B_scan.slice(1) : B_scan; + + if (B_spend_xonly.length !== 32 || B_scan_xonly.length !== 32) { + throw new Error('Invalid public key length for silent payment address'); + } + data = tools.concat([B_scan_xonly, B_spend_xonly]); + } + + // The data is converted to 5-bit words, and the version is prepended to the words array. + const words = bech32m.toWords(data); + words.unshift(version); + + // The default bech32 limit is 90, but silent payment addresses are longer. + // We pass a higher limit to accommodate this, as the underlying implementation allows it. + return bech32m.encode(SP_HRP, words, BECH32_SP_LIMIT); +} + +/** + * Decodes a Bech32m Silent payment address into its spend and scan public keys. + * @param address - bech32m silent payment address format + * @returns { B_spend, B_scan, version } + */ +export function decodeSilentPaymentAddress(address: string): { + B_spend: Uint8Array; + B_scan: Uint8Array; + version: number; +} { + // The default bech32 limit is 90, but silent payment addresses are longer. + // We pass a higher limit to accommodate this, as the underlying implementation allows it. + const { prefix, words } = bech32m.decode(address, BECH32_SP_LIMIT); + if (prefix !== 'sp' && prefix !== 'tsp') { + throw new Error('Invalid silent payment address prefix'); + } + + // The first word is the version, the rest are the data payload. + const version = words[0]; + const dataWords = words.slice(1); + const keysData = new Uint8Array(bech32m.fromWords(dataWords)); + + let B_spend: Uint8Array; + let B_scan: Uint8Array; + + switch (version) { + case 0: + // Handle both current (64 bytes for x-only keys) and legacy (66 bytes for full keys) payload lengths. + if (keysData.length === 64) { + // Current spec: 32-byte x-only keys. Reconstruct with even y-parity (0x02) as per convention. + const B_scan_xonly = keysData.slice(0, 32); + const B_spend_xonly = keysData.slice(32, 64); + + B_spend = new Uint8Array(33); + B_spend[0] = 0x02; + B_spend.set(B_spend_xonly, 1); + + B_scan = new Uint8Array(33); + B_scan[0] = 0x02; + B_scan.set(B_scan_xonly, 1); + } else if (keysData.length === 66) { + // Legacy spec: 33-byte full keys. We can return them directly to preserve the original y-parity. + B_scan = keysData.slice(0, 33); + B_spend = keysData.slice(33, 66); + } else { + throw new Error( + `Invalid silent payment address data length for v0: ${keysData.length}`, + ); + } + break; + default: + // Future versions might have different lengths. For now, we only support v0. + throw new Error(`Unsupported silent payment address version: ${version}`); + } + + return { B_spend, B_scan, version }; +} + +/** + * Main function for creating a Pay-to-Silent-Payment (P2SP) payment object. + * This function encapsulates the logic for handling silent payment addresses and keys. + * + * @param a - The payment object containing the necessary data for P2SP. + * @param opts - Optional payment options. + * @returns The P2SP payment object. + */ +export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { + if (!a.address && !(a.spendPubkey && a.scanPubkey)) { + throw new TypeError('Not enough data'); + } + opts = Object.assign({ validate: true }, opts || {}); + + const network = a.network || BITCOIN_NETWORK; + const o: SilentPayment = { name: 'p2sp', network }; + + // Lazy load silent payment specific properties + lazy.prop(o, 'spendPubkey', () => { + if (a.address) return decodeSilentPaymentAddress(a.address).B_spend; + return a.spendPubkey!; + }); + lazy.prop(o, 'scanPubkey', () => { + if (a.address) return decodeSilentPaymentAddress(a.address).B_scan; + return a.scanPubkey!; + }); + lazy.prop(o, 'address', () => { + if (a.address) return a.address; + const version = a.version !== undefined ? a.version : 0; + return encodeSilentPaymentAddress( + o.spendPubkey!, + o.scanPubkey!, + version, + network, + ); + }); + + if (opts.validate) { + if (a.address) { + const decoded = decodeSilentPaymentAddress(a.address); + if (a.spendPubkey && tools.compare(a.spendPubkey, decoded.B_spend) !== 0) + throw new TypeError('Spend pubkey mismatch'); + if (a.scanPubkey && tools.compare(a.scanPubkey, decoded.B_scan) !== 0) + throw new TypeError('Scan pubkey mismatch'); + + const HRP = network.bech32 === 'bc' ? 'sp' : 'tsp'; + if (!a.address.startsWith(HRP)) { + throw new TypeError('Invalid prefix or Network mismatch'); + } + } + if (o.spendPubkey && o.spendPubkey.length !== 33) + throw new TypeError('Invalid spend pubkey length'); + if (o.scanPubkey && o.scanPubkey.length !== 33) + throw new TypeError('Invalid scan pubkey length'); + } + + return Object.assign(o, a); +} + +/** Calculate Input hash tweak + * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n + * @param smallestOutpoint - output of (findSmallestOutpoint) + * @param summedSenderPubkey + * @returns the input_hash tweak + */ +export function calculateInputHashTweak( + smallestOutpoint: Uint8Array, + summedSenderPubkey: Uint8Array, // A = a_sum·G (compressed 33B) +): Uint8Array { + const ihRaw = taggedHash( + 'BIP0352/Inputs', + tools.concat([smallestOutpoint, summedSenderPubkey]), + ); + return hashToTweak(ihRaw); +} + +/** + * Calculate the sum of input private keys + * @param inputPrivKeyTuples - an array of inputs to sum + * @returns aSum the resulting sum + */ +export function calculateSumA( + inputPrivKeyTuples: Array<{ priv: Uint8Array; isXOnly: boolean }>, +): Uint8Array | null { + if (inputPrivKeyTuples.length === 0) return null; + + let aSum: Uint8Array | null = null; + + for (const { priv, isXOnly } of inputPrivKeyTuples) { + if (!ecc.isPrivate(priv)) continue; + + let k = priv; + + // only normalize when key will be used as x-only (Taproot key-spend). + if (isXOnly) { + const P: Uint8Array | null = ecc.pointFromScalar( + k, + true, + ); + if (!P) continue; // skip invalid + if (P[0] === 0x03) { + const neg = ecc.privateNegate(k); + if (!neg) return null; // defensive + k = neg; + } + } + + aSum = aSum ? (ecc.privateAdd(aSum, k) as Uint8Array | null) : k; + + // If sum == 0 mod n, tiny-secp returns null. Treat as degenerate. + if (aSum == null || isZero32(aSum)) return null; + } + + return aSum && !isZero32(aSum) ? aSum : null; +} + +/** + * Calculate shared secret + * S = (inputHash * B_scan) * a_sum (compressed) + * @param inputHash + * @param scanPubkey - B_scan + * @param summedSenderPrivkey - a_Sum + * @returns S + */ +export function calculateSharedSecret( + inputHash: Uint8Array, + scanPubkey: Uint8Array, + summedSenderPrivkey: Uint8Array, // a_sum (32B) +): Uint8Array | null { + if (!summedSenderPrivkey) + throw new Error('summedSenderPrivkey was not provided?'); + const Si: Uint8Array | null = ecc.pointMultiply( + scanPubkey!, + inputHash, + true, + ); + if (!Si) throw new Error('pointMultiply(B_scan, ih) failed'); + const S: Uint8Array | null = ecc.pointMultiply( + Si, + summedSenderPrivkey, + true, + ); + if (!S) throw new Error('pointMultiply(Si, summedSenderPrivkey) failed'); + else return S; +} + +/** + * Calculate the tweak key + * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n + * @constructor + * @param S + * @param k + * @returns input_hash tweak + */ +export function calculateT_k(S: Uint8Array, k: number): Uint8Array | null { + const t_k: Uint8Array = taggedHash( + 'BIP0352/SharedSecret', + tools.concat([S, ser32BE(k)]), + ); + return hashToTweak(t_k); +} + +/** + * Calculate P_k + * P_k = B_spend + t_k·G (compressed) -> x-only for P2TR + * @param spendPubKey + * @param t_k + * @returns Pk + */ +export function calculateP_k( + spendPubKey: Uint8Array, + t_k: Uint8Array, +): Uint8Array { + const Pk: Uint8Array | null = ecc.pointAddScalar( + spendPubKey, + t_k, + true, + ); + if (!Pk) throw new Error('pointAddScalar failed'); + return Pk; +} + +/** + * derive a silent payment output for a transaction. + * @param S - shared secret = (inputHash * B_scan) * a_sum (compressed) + * @param spendPubkey - B_spend + * @param k - output number + * @returns the newly calculated output + */ +export function deriveOutput( + S: Uint8Array, + spendPubkey: Uint8Array, + k: number, +): { pub_key: Uint8Array; tweak_key } { + // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) -> reduce mod n + const t_k: Uint8Array | null = calculateT_k(S, k); + + // P_k = B_spend + t_k·G (compressed) -> x-only for P2TR + const P_k: Uint8Array = calculateP_k(spendPubkey, t_k); + const P_xOnly: Uint8Array = toXOnly(P_k); + if (!P_xOnly) throw new Error('pointAddScalar failed'); + + return { pub_key: P_xOnly, tweak_key: t_k }; +} + +/** + * Label scalar: TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) + * @param receiverScanPrivkey + * @param m + * @return tweaked label hash + */ +export function createLabelTweak( + receiverScanPrivkey: Uint8Array, // b_scan (32B) + m: number, // label integer +): Uint8Array { + const raw = taggedHash( + 'BIP0352/Label', + tools.concat([receiverScanPrivkey, ser32BE(m)]), + ); + return hashToTweak(raw); +} + +/** + * Generate labeled address Bm + * + * @param B_scan + * @param B_spend + * @param label + * @returns {L, Bm} - Label tweak and unencoded labeled address Bm + */ +export function generateLabelAndAddress( + B_scan: Uint8Array, + B_spend: Uint8Array, + label: number, +): { L: Uint8Array; Bm: Uint8Array } { + // TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) + const L: Uint8Array = createLabelTweak(B_scan, label); + // Bm = B_spend + hashBIP0352/Label(ser256(b_scan) || ser32(m))·G + const Bm: Uint8Array | null = ecc.pointAddScalar( + B_spend, + L, + true, + ); + if (!Bm) throw new Error('pointAddScalar(B_spend, L) failed'); + return { L, Bm }; +} + +/** + * Scans a transaction's inputs and outputs to find any silent payments for the receiver. + * @param receiverScanPrivkey - b_scan + * @param receiverSpendPrivkey - b_spend + * @param smallestOutpoint + * @param inputHashTweak + * @param summedSenderPubkey - A_sum + * @param outputsToCheck - array of hex xOnly encoded outputs to check + * @param labelNonces + */ +export function scanForSilentPayments( + receiverScanPrivkey: Uint8Array, + receiverSpendPrivkey: Uint8Array, + smallestOutpoint: Uint8Array, + inputHashTweak: Uint8Array, + summedSenderPubkey: Uint8Array, + outputsToCheck: Set, + labelNonces: Array = Array.from([]), +): { + priv_key_tweak: Uint8Array; + pub_key: Uint8Array; + labelNonce?: Uint8Array; +}[] { + let foundPayments: { + priv_key_tweak: Uint8Array; + pub_key: Uint8Array; + labelNonce?: Uint8Array; + }[] = []; + + // G + const baseSpendPubkey: Uint8Array = ecc.pointFromScalar( + receiverSpendPrivkey, + true, + )!; + + // Shared secret S = (inputHash * A_sum) * b_scan (order equivalent) + const S = calculateSharedSecret( + inputHashTweak, + summedSenderPubkey, + receiverScanPrivkey, + ); + if (!S) return []; + + // First, scan for the base (unlabeled) address + foundPayments = foundPayments.concat( + performScan(baseSpendPubkey, S, outputsToCheck, null), + ); + + // Then, scan for each labeled address + for (const m of labelNonces) { + const { L, Bm } = generateLabelAndAddress( + receiverScanPrivkey, + baseSpendPubkey, + m, + ); + + const labeledResults = performScan(Bm, S, outputsToCheck, L); + + // Add the label nonce to any found payments for identification + labeledResults.forEach(result => { + foundPayments.push({ ...result, labelNonce: m }); + }); + } + + return foundPayments; +} + +/** + * The core scanning logic, performed for a specific spend public key (B_spend). + * @param receiverSpendPubkey - G or B_m + * @param S + * @param outputsToCheck - array of hex xOnly encoded outputs to check + * @param labelScalar += */ +function performScan( + receiverSpendPubkey: Uint8Array, + S: Uint8Array, + outputsToCheck: Set, + labelScalar: Uint8Array | null, // L (or null for base) +): { priv_key_tweak: Uint8Array; pub_key: Uint8Array }[] { + const found: { payment: Payment; t: Uint8Array; P_xonly: Uint8Array }[] = []; + + for (let k = 0; k < outputsToCheck.size; k++) { + const derivedOutput = deriveOutput(S, receiverSpendPubkey, k); + if (!derivedOutput.pub_key) break; + const xonlyHex = toHex(derivedOutput.pub_key).toLowerCase(); + + if (outputsToCheck.size === 0 || outputsToCheck.has(xonlyHex)) { + // priv_key_tweak returned by L + t_k (mod n) for labeled, or t_k for unlabeled + let spendTweak = derivedOutput.tweak_key; + if (labelScalar != null && !isZero32(labelScalar)) { + const sum: Uint8Array | null = ecc.privateAdd( + labelScalar, + derivedOutput.tweak_key, + ); + if (!sum) throw new Error('privateAdd(label, t_k) failed'); + spendTweak = sum; + } + // TODO chose to return the hex encoded or raw Uint8Array + found.push({ + priv_key_tweak: spendTweak, + pub_key: derivedOutput.pub_key, + }); + } + } + return found; +} From d6f3297f7fcf559ed5052d4f1dbadafccd105fc0 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Wed, 8 Oct 2025 18:42:19 +0200 Subject: [PATCH 02/21] fix(crypto): Added missing tags Tags were missing from the TAGS --- src/esm/crypto.js | 3 +++ ts_src/crypto.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/esm/crypto.js b/src/esm/crypto.js index 9b7296d5a..47dcbcc60 100644 --- a/src/esm/crypto.js +++ b/src/esm/crypto.js @@ -35,6 +35,9 @@ export const TAGS = [ 'TapTweak', 'KeyAgg list', 'KeyAgg coefficient', + 'BIP0352/Inputs', + 'BIP0352/SharedSecret', + 'BIP0352/Label', ]; /** * A collection of tagged hash prefixes used in various BIP (Bitcoin Improvement Proposals) diff --git a/ts_src/crypto.ts b/ts_src/crypto.ts index 4ef3e3a76..87919733c 100644 --- a/ts_src/crypto.ts +++ b/ts_src/crypto.ts @@ -38,6 +38,9 @@ export const TAGS = [ 'TapTweak', 'KeyAgg list', 'KeyAgg coefficient', + 'BIP0352/Inputs', + 'BIP0352/SharedSecret', + 'BIP0352/Label', ] as const; export type TaggedHashPrefix = (typeof TAGS)[number]; type TaggedHashPrefixes = { From 52148d83ebb209090c83b78724d0f2ad8ac6aec0 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Wed, 8 Oct 2025 18:43:35 +0200 Subject: [PATCH 03/21] fix(p2sp): Function signature and output fixes Some outputs wernt matching what we expected them to be. --- src/esm/payments/p2sp.js | 16 ++++++---------- test/integration/silentpayment.spec.ts | 8 +++----- ts_src/payments/p2sp.ts | 23 ++++++++++------------- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/esm/payments/p2sp.js b/src/esm/payments/p2sp.js index 8d29104be..ac1e9f7c8 100644 --- a/src/esm/payments/p2sp.js +++ b/src/esm/payments/p2sp.js @@ -71,10 +71,9 @@ export const findSmallestOutpoint = inputs => */ export const serOutpointLE = (txidHexBE, vout) => { const out = new Uint8Array(36); - const txidLE = tools.fromHex(txidHexBE); - if (txidLE.length !== 32) throw new Error('txid must be 32 bytes'); - txidLE.reverse(); // BE -> LE - out.set(txidLE, 0); + if (txidHexBE.length !== 32) throw new Error('txid must be 32 bytes'); + txidHexBE.reverse(); // BE -> LE + out.set(txidHexBE, 0); writeUInt32(out, 32, vout >>> 0, 'le'); return out; }; @@ -273,8 +272,7 @@ export function calculateSharedSecret( throw new Error('summedSenderPrivkey was not provided?'); const Si = ecc.pointMultiply(scanPubkey, inputHash, true); if (!Si) throw new Error('pointMultiply(B_scan, ih) failed'); - let S; - S = ecc.pointMultiply(Si, summedSenderPrivkey, true); + const S = ecc.pointMultiply(Si, summedSenderPrivkey, true); if (!S) throw new Error('pointMultiply(Si, summedSenderPrivkey) failed'); else return S; } @@ -287,8 +285,7 @@ export function calculateSharedSecret( * @returns input_hash tweak */ export function calculateT_k(S, k) { - if (!S) return null; - let t_k = taggedHash('BIP0352/SharedSecret', tools.concat([S, ser32BE(k)])); + const t_k = taggedHash('BIP0352/SharedSecret', tools.concat([S, ser32BE(k)])); return hashToTweak(t_k); } /** @@ -313,6 +310,7 @@ export function calculateP_k(spendPubKey, t_k) { export function deriveOutput(S, spendPubkey, k) { // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) -> reduce mod n const t_k = calculateT_k(S, k); + if (!t_k) throw new Error('t_k: failed'); // P_k = B_spend + t_k·G (compressed) -> x-only for P2TR const P_k = calculateP_k(spendPubkey, t_k); const P_xOnly = toXOnly(P_k); @@ -355,7 +353,6 @@ export function generateLabelAndAddress(B_scan, B_spend, label) { * Scans a transaction's inputs and outputs to find any silent payments for the receiver. * @param receiverScanPrivkey - b_scan * @param receiverSpendPrivkey - b_spend - * @param smallestOutpoint * @param inputHashTweak * @param summedSenderPubkey - A_sum * @param outputsToCheck - array of hex xOnly encoded outputs to check @@ -364,7 +361,6 @@ export function generateLabelAndAddress(B_scan, B_spend, label) { export function scanForSilentPayments( receiverScanPrivkey, receiverSpendPrivkey, - smallestOutpoint, inputHashTweak, summedSenderPubkey, outputsToCheck, diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 148c90998..6060f96b8 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -14,7 +14,6 @@ import { encodeSilentPaymentAddress, findSmallestOutpoint, modN32, - generateLabelAndAddress, scanForSilentPayments, } from '../../ts_src/payments/p2sp.js'; import { Input } from '../../ts_src/transaction.js'; @@ -35,7 +34,7 @@ interface TestInput { txinwitness: string; prevout: { scriptPubKey: { - hex: String; + hex: string; }; }; private_key?: string; @@ -82,7 +81,7 @@ interface Recipient { interface RecipientOutput { priv_key_tweak: string; pub_key: string; - signature: String; + signature: string; } // ================================================================ @@ -315,7 +314,7 @@ function runSenderCase(tc: Sender) { // -------- 2) a_sum with Taproot-only odd-Y conditional negation -------- // 2) a_sum = Σ negated (mod n) via tiny-secp - let aSum: Uint8Array = calculateSumA(inputPrivKeyTuples); + const aSum: Uint8Array = calculateSumA(inputPrivKeyTuples); // (optional) fixture check stays the same if (expected.input_private_key_sum) { @@ -446,7 +445,6 @@ function runReceiverCase(tc: Recipient) { const foundScanned = scanForSilentPayments( b_scan, b_spend, - outpointL, inputHashTweak, A_sum_point, outputsToCheck, diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 00ec5f427..875788e0c 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -8,7 +8,6 @@ import { bech32m } from 'bech32'; // Explicitly import the Payment type for clarity import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; -import * as payments from './index.ts'; import { taggedHash } from '../crypto'; import { Input } from '../transaction'; @@ -90,12 +89,11 @@ export const findSmallestOutpoint = (inputs: Array) => * @param vout - output index * @returns the serialized little endian encoded output */ -export const serOutpointLE = (txidHexBE: string, vout: number) => { +export const serOutpointLE = (txidHexBE: Uint8Array, vout: number) => { const out = new Uint8Array(36); - const txidLE = tools.fromHex(txidHexBE); - if (txidLE.length !== 32) throw new Error('txid must be 32 bytes'); - txidLE.reverse(); // BE -> LE - out.set(txidLE, 0); + if (txidHexBE.length !== 32) throw new Error('txid must be 32 bytes'); + txidHexBE.reverse(); // BE -> LE + out.set(txidHexBE, 0); writeUInt32(out, 32, vout >>> 0, 'le'); return out; }; @@ -395,9 +393,10 @@ export function deriveOutput( S: Uint8Array, spendPubkey: Uint8Array, k: number, -): { pub_key: Uint8Array; tweak_key } { +): { pub_key: Uint8Array; tweak_key: Uint8Array } { // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) -> reduce mod n const t_k: Uint8Array | null = calculateT_k(S, k); + if (!t_k) throw new Error('t_k: failed'); // P_k = B_spend + t_k·G (compressed) -> x-only for P2TR const P_k: Uint8Array = calculateP_k(spendPubkey, t_k); @@ -453,7 +452,6 @@ export function generateLabelAndAddress( * Scans a transaction's inputs and outputs to find any silent payments for the receiver. * @param receiverScanPrivkey - b_scan * @param receiverSpendPrivkey - b_spend - * @param smallestOutpoint * @param inputHashTweak * @param summedSenderPubkey - A_sum * @param outputsToCheck - array of hex xOnly encoded outputs to check @@ -462,20 +460,19 @@ export function generateLabelAndAddress( export function scanForSilentPayments( receiverScanPrivkey: Uint8Array, receiverSpendPrivkey: Uint8Array, - smallestOutpoint: Uint8Array, inputHashTweak: Uint8Array, summedSenderPubkey: Uint8Array, outputsToCheck: Set, - labelNonces: Array = Array.from([]), + labelNonces: Array = Array.from([]), ): { priv_key_tweak: Uint8Array; pub_key: Uint8Array; - labelNonce?: Uint8Array; + labelNonce?: number; }[] { let foundPayments: { priv_key_tweak: Uint8Array; pub_key: Uint8Array; - labelNonce?: Uint8Array; + labelNonce?: number; }[] = []; // G @@ -529,7 +526,7 @@ function performScan( outputsToCheck: Set, labelScalar: Uint8Array | null, // L (or null for base) ): { priv_key_tweak: Uint8Array; pub_key: Uint8Array }[] { - const found: { payment: Payment; t: Uint8Array; P_xonly: Uint8Array }[] = []; + const found: { priv_key_tweak: Uint8Array; pub_key: Uint8Array }[] = []; for (let k = 0; k < outputsToCheck.size; k++) { const derivedOutput = deriveOutput(S, receiverSpendPubkey, k); From 77c07f7d799973c0466668804fbc690f03fe7379 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Wed, 8 Oct 2025 23:20:02 +0200 Subject: [PATCH 04/21] fix(tests): Properly load fixtures --- test/integration/silentpayment.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 6060f96b8..8e1dbff5d 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import * as ecc from 'tiny-secp256k1'; import * as bitcoin from 'bitcoinjs-lib'; import { fromHex, toHex } from 'uint8array-tools'; -import * as fixtures from '../fixtures/p2sp.json'; +import fixtures from '../fixtures/p2sp.json'; import { hash160 } from '../../ts_src/crypto.js'; import { calculateInputHashTweak, @@ -487,11 +487,8 @@ function runReceiverCase(tc: Recipient) { assert.ok(scannedResult.has(x), `receiver: missing expected entry ${x}`); } -// ================================================================ -// ======================= mocha suite ========================== -// ================================================================ describe('BIP-352 Silent Payments — sender vectors', () => { - const groups: TestCase[] = fixtures as TestCase[]; + const groups: TestCase[] = fixtures; let count = 0; for (const g of groups) { if (!g?.sending) continue; @@ -535,7 +532,7 @@ describe('BIP-352 Silent Payments — sender vectors', () => { }); describe('BIP-352 Silent Payments — receiver vectors', () => { - const groups: TestCase[] = fixtures as TestCase[]; + const groups : TestCase[]= fixtures; let count = 0; for (const g of groups) { if (!g?.receiving) continue; From bd2f200ff43efd51d69f90757c3ac279ceadd024 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Wed, 8 Oct 2025 23:25:25 +0200 Subject: [PATCH 05/21] chore(tests): Cleanup unused or redundant functions --- test/integration/silentpayment.spec.ts | 30 -------------------------- 1 file changed, 30 deletions(-) diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 8e1dbff5d..11024fb5d 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -84,18 +84,6 @@ interface RecipientOutput { signature: string; } -// ================================================================ -// =================== scalar helpers (mod n) ================== -// ================================================================ -const N = fromHex( - 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', -); - -function isZero32(a: Uint8Array) { - for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; - return true; -} - // Detect patterns function isP2PKH(spkHex?: string): boolean { const u = fromHex(spkHex || ''); @@ -167,24 +155,6 @@ function parseWitnessItems(witHex: string): Uint8Array[] { return items; } -// ---- helper: sum compressed pubkeys exactly as given (33B) ---- -function sumPointsCompressed(points33: Uint8Array[]): Uint8Array { - let acc: Uint8Array | null = null; - for (const P of points33) { - if (!P || P.length !== 33 || (P[0] !== 0x02 && P[0] !== 0x03)) { - throw new Error('bad compressed pubkey in input_pub_keys'); - } - if (acc === null) acc = P; - else { - const next = ecc.pointAdd(acc, P, true) as Uint8Array | null; - if (!next) throw new Error('pointAdd failed when summing inputs'); - acc = next; - } - } - if (!acc) throw new Error('no input_pub_keys to sum'); - return acc; // 33B compressed A_sum -} - /** * Return a single compressed pubkey for display, or null if none. * From 4042a596fdb3ea4b80847eda109115d73854ca46 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Fri, 10 Oct 2025 14:04:14 +0200 Subject: [PATCH 06/21] chore(test): Clean up unused variables --- test/integration/silentpayment.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 11024fb5d..36da93a89 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -271,13 +271,11 @@ function runSenderCase(tc: Sender) { // Early exit (no usable inputs) if (inputPrivKeyTuples.length === 0) { - const sending_outputs: string[] = []; assert( expected.outputs && expected.outputs[0] && expected.outputs[0].length === 0 && - sending_outputs.length === 0, - 'Sending test failed (no inputs case)', + 'Sending test failed (no inputs case)', ); return; } @@ -365,7 +363,6 @@ function runReceiverCase(tc: Recipient) { // -------- key material -------- const b_scan = fromHex(given.key_material.scan_priv_key); const b_spend = fromHex(given.key_material.spend_priv_key); - const B_spend = ecc.pointFromScalar(b_spend, true) as Uint8Array; // -------- outpoint_L -------- const vinArray = given.vin.map( @@ -502,7 +499,7 @@ describe('BIP-352 Silent Payments — sender vectors', () => { }); describe('BIP-352 Silent Payments — receiver vectors', () => { - const groups : TestCase[]= fixtures; + const groups: TestCase[] = fixtures; let count = 0; for (const g of groups) { if (!g?.receiving) continue; From c8518f32f3cb1b08aec91c3d26114b9ff0f484c0 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Fri, 10 Oct 2025 14:06:00 +0200 Subject: [PATCH 07/21] fix(test): Set k on deriveOutput Shouldent something have failed here? --- test/integration/silentpayment.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 36da93a89..31552e65b 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -339,7 +339,7 @@ function runSenderCase(tc: Sender) { } for (let k = 0; k < g.B_m_list.length; k++) { - const { pub_key, tweak_key } = deriveOutput(S, g.B_m_list[k]); + const { pub_key, tweak_key } = deriveOutput(S, g.B_m_list[k], k); sending_outputs.add(toHex(pub_key)); // x-only hex } } From 4f52dde9292fbbeb1a5a29e0da4fccf3b3448a13 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Fri, 10 Oct 2025 14:06:46 +0200 Subject: [PATCH 08/21] chore(p2sp): Move main function to top and add lazy prop Added lazy prop for outputs. --- ts_src/payments/p2sp.ts | 236 +++++++++++++++++++++++++++++----------- 1 file changed, 173 insertions(+), 63 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 875788e0c..44914c64c 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -9,16 +9,185 @@ import { bech32m } from 'bech32'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { taggedHash } from '../crypto'; -import { Input } from '../transaction'; +import { Input, Output } from '../transaction'; // --- TYPE DEFINITIONS & UTILITIES --- export const BECH32_SP_LIMIT = 150; -// Extend the Payment interface for silent payments +/** + * @property {Uint8Array} [S] - Shared secret between you and recipient + * @property {Uint8Array} [B_spend_pub] - Recipients spend pubkey B_scan + */ +interface Recipient { + S?: Uint8Array; + B_spend_pub?: Uint8Array; +} +/** + * Represents a Silent Payment transaction structure that extends a standard {@link Payment}. + * Includes additional cryptographic and metadata fields used for constructing + * or parsing silent payments. + * + * @property {Uint8Array} [spendPubkey] - Optional spend public key for the sender. + * @property {Uint8Array} [scanPubkey] - Optional scan public key used for recipient address derivation. + * @property {Input[]} [inputs] - Optional array of input UTXOs used in the transaction. + * @property {Output[]} [outputs] - Optional array of outputs generated in the transaction. + * @property {number} [version] - Optional version number of the silent payment scheme. + * @property {Uint8Array} [aSum] - Optional summed private key (see `calculateSumA`). + * @property {Uint8Array} [outpointL] - Optional first result of lexicographically sorted input transaction IDs. + * @property {{ priv: Uint8Array; isXOnly: boolean }[]} [privKeys] - Optional array of private keys associated with the transaction. + * Each object includes the private key and a flag indicating if it's x-only. + * @property {Recipient[]} recipients - Array of recipients for this silent payment. + */ export interface SilentPayment extends Payment { spendPubkey?: Uint8Array; scanPubkey?: Uint8Array; + inputs?: Input[]; + outputs?: Output[]; version?: number; + aSum?: Uint8Array; + outpointL?: Uint8Array; + privKeys?: Array<{ priv: Uint8Array; isXOnly: boolean }>; + recipients: Array; +} + +/** + * Main function for creating a Pay-to-Silent-Payment (P2SP) payment object. + * This function encapsulates the logic for handling silent payment addresses and keys. + * + * @param a - The payment object containing the necessary data for P2SP. + * @param opts - Optional payment options. + * @returns The P2SP payment object. + */ +export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { + if (!a.address && !(a.spendPubkey && a.scanPubkey)) { + throw new TypeError('Not enough data'); + } + opts = Object.assign({ validate: true }, opts || {}); + + const network = a.network || BITCOIN_NETWORK; + const o: SilentPayment = { name: 'p2sp', network }; + + // Lazy load silent payment specific properties + lazy.prop(o, 'spendPubkey', () => { + if (a.address) return decodeSilentPaymentAddress(a.address).B_spend; + return a.spendPubkey!; + }); + lazy.prop(o, 'scanPubkey', () => { + if (a.address) return decodeSilentPaymentAddress(a.address).B_scan; + return a.scanPubkey!; + }); + lazy.prop(o, 'address', () => { + if (a.address) return a.address; + const version = a.version !== undefined ? a.version : 0; + return encodeSilentPaymentAddress( + o.spendPubkey!, + o.scanPubkey!, + version, + network, + ); + }); + lazy.prop(o, 'outputs', () => { + if (a.outputs) return a.outputs; + const allRecipientsComplete = a.recipients.every( + r => r.S.length > 0 && r.B_spend_pub.length > 0, + ); + const allRecipientsHaveBSpend = a.recipients.every( + r => r.B_spend_pub.length > 0, + ); + // If we have both the secret and B_spend for each key we can derive directly + if (allRecipientsComplete) { + return a.recipients?.map((value, index) => { + deriveOutput(value.S, value.B_spend_pub, index); + }); + } + // If we have outpointL, aSum and only the spend keys for the recipients we need to calculate the input hash and secret + else if ( + a.outpointL != null && + a.outpointL?.length > 0 && + a.aSum != null && + a.aSum?.length > 0 && + a.recipients?.length > 0 && + allRecipientsHaveBSpend + ) { + const A: Uint8Array = ecc.pointFromScalar(a.aSum, true); // compressed 33B + const inputHashTweak: Uint8Array = calculateInputHashTweak( + a.outpointL, + A, + ); + return a.recipients?.map((value, index) => { + const S = calculateSharedSecret( + inputHashTweak, + value.B_spend_pub, + aSum, + ); + deriveOutput(S, value.B_spend_pub, index); + }); + } + // If we have all the inputs, aSum and only the spend keys for the recipients we need to calculate the input hash and secret + else if ( + a.inputs != null && + a.inputs?.length > 0 && + a.aSum != null && + a.aSum?.length > 0 && + a?.recipients.length > 0 && + allRecipientsHaveBSpend + ) { + const outpointL = findSmallestOutpoint(a.inputs); + const A: Uint8Array = ecc.pointFromScalar(a.aSum, true); // compressed 33B + const inputHashTweak: Uint8Array = calculateInputHashTweak(outpointL, A); + return a?.recipients.map((value, index) => { + const S = calculateSharedSecret( + inputHashTweak, + value.B_spend_pub, + a.aSum, + ); + deriveOutput(S, value.B_spend_pub, index); + }); + } + // If we have all the inputs, privKeys and only the spend keys for the recipients we need to calculate the Sum, the input hash and secret + else if ( + a.inputs != null && + a.inputs?.length > 0 && + a.privKeys != null && + a.privKeys?.length > 0 && + a?.recipients.length > 0 && + allRecipientsHaveBSpend + ) { + const aSum: Uint8Array = calculateSumA(a.privKeys); + const outpointL = findSmallestOutpoint(a.inputs); + const A: Uint8Array = ecc.pointFromScalar(aSum, true); // compressed 33B + const inputHashTweak: Uint8Array = calculateInputHashTweak(outpointL, A); + return a?.recipients.map((value, index) => { + const S = calculateSharedSecret( + inputHashTweak, + value.B_spend_pub, + aSum, + ); + deriveOutput(S, value.B_spend_pub, index); + }); + } else throw Error('Not enough data to derive outputs'); + }); + + if (opts.validate) { + if (a.address) { + const decoded = decodeSilentPaymentAddress(a.address); + if (a.spendPubkey && tools.compare(a.spendPubkey, decoded.B_spend) !== 0) + throw new TypeError('Spend pubkey mismatch'); + if (a.scanPubkey && tools.compare(a.scanPubkey, decoded.B_scan) !== 0) + throw new TypeError('Scan pubkey mismatch'); + + const HRP = network.bech32 === 'bc' ? 'sp' : 'tsp'; + if (!a.address.startsWith(HRP)) { + throw new TypeError('Invalid prefix or Network mismatch'); + } + } + if (o.spendPubkey && o.spendPubkey.length !== 33) + throw new TypeError('Invalid spend pubkey length'); + if (o.scanPubkey && o.scanPubkey.length !== 33) + throw new TypeError('Invalid scan pubkey length'); + } + + return Object.assign(o, a); } /** @@ -148,8 +317,8 @@ export function encodeSilentPaymentAddress( * @returns { B_spend, B_scan, version } */ export function decodeSilentPaymentAddress(address: string): { - B_spend: Uint8Array; - B_scan: Uint8Array; + B_spend: Uint8Array; // pub spend key + B_scan: Uint8Array; // pub scan key version: number; } { // The default bech32 limit is 90, but silent payment addresses are longer. @@ -200,65 +369,6 @@ export function decodeSilentPaymentAddress(address: string): { return { B_spend, B_scan, version }; } -/** - * Main function for creating a Pay-to-Silent-Payment (P2SP) payment object. - * This function encapsulates the logic for handling silent payment addresses and keys. - * - * @param a - The payment object containing the necessary data for P2SP. - * @param opts - Optional payment options. - * @returns The P2SP payment object. - */ -export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { - if (!a.address && !(a.spendPubkey && a.scanPubkey)) { - throw new TypeError('Not enough data'); - } - opts = Object.assign({ validate: true }, opts || {}); - - const network = a.network || BITCOIN_NETWORK; - const o: SilentPayment = { name: 'p2sp', network }; - - // Lazy load silent payment specific properties - lazy.prop(o, 'spendPubkey', () => { - if (a.address) return decodeSilentPaymentAddress(a.address).B_spend; - return a.spendPubkey!; - }); - lazy.prop(o, 'scanPubkey', () => { - if (a.address) return decodeSilentPaymentAddress(a.address).B_scan; - return a.scanPubkey!; - }); - lazy.prop(o, 'address', () => { - if (a.address) return a.address; - const version = a.version !== undefined ? a.version : 0; - return encodeSilentPaymentAddress( - o.spendPubkey!, - o.scanPubkey!, - version, - network, - ); - }); - - if (opts.validate) { - if (a.address) { - const decoded = decodeSilentPaymentAddress(a.address); - if (a.spendPubkey && tools.compare(a.spendPubkey, decoded.B_spend) !== 0) - throw new TypeError('Spend pubkey mismatch'); - if (a.scanPubkey && tools.compare(a.scanPubkey, decoded.B_scan) !== 0) - throw new TypeError('Scan pubkey mismatch'); - - const HRP = network.bech32 === 'bc' ? 'sp' : 'tsp'; - if (!a.address.startsWith(HRP)) { - throw new TypeError('Invalid prefix or Network mismatch'); - } - } - if (o.spendPubkey && o.spendPubkey.length !== 33) - throw new TypeError('Invalid spend pubkey length'); - if (o.scanPubkey && o.scanPubkey.length !== 33) - throw new TypeError('Invalid scan pubkey length'); - } - - return Object.assign(o, a); -} - /** Calculate Input hash tweak * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n * @param smallestOutpoint - output of (findSmallestOutpoint) From 5596888b02e23aa4cf36de4fbc99e08fbd125bc9 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Fri, 10 Oct 2025 16:12:38 +0200 Subject: [PATCH 09/21] fix(p2sp): Remove reduce mod add checks --- ts_src/payments/p2sp.ts | 133 ++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 74 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 44914c64c..062e8f52d 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -201,46 +201,11 @@ export function ser32BE(n: number): Uint8Array { return b; } -const N = tools.fromHex( - 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', -); - function isZero32(a: Uint8Array) { for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; return true; } -//TODO I guess there already is a function for this ? -function subBE(a: Uint8Array, b: Uint8Array) { - const o = new Uint8Array(32); - let c = 0; - for (let i = 31; i >= 0; i--) { - let v = a[i] - b[i] - c; - if (v < 0) { - v += 256; - c = 1; - } else c = 0; - o[i] = v; - } - return o; -} - -//TODO I guess there already is a function for this ? -function modN32(x: Uint8Array) { - let r = new Uint8Array(x); - while (tools.compare(r, N) >= 0) r = subBE(r, N); - return r; -} - -function hashToTweak(h: Uint8Array) { - let t = modN32(h); - if (isZero32(t)) { - t = new Uint8Array(32); - t[31] = 1; - } - return t; -} - /** * Smallest outpoint = lexicographic min of (txidLE || voutLE) * @param inputs an array of inputs you want the first lexicographically sorted result @@ -370,20 +335,40 @@ export function decodeSilentPaymentAddress(address: string): { } /** Calculate Input hash tweak - * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n - * @param smallestOutpoint - output of (findSmallestOutpoint) - * @param summedSenderPubkey + * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) * @returns the input_hash tweak + * @param outpointL36 + * @param summedSenderPubkey33 */ export function calculateInputHashTweak( - smallestOutpoint: Uint8Array, - summedSenderPubkey: Uint8Array, // A = a_sum·G (compressed 33B) + outpointL36: Uint8Array, // 36B = txid(LE 32B) || vout(LE 4B) + summedSenderPubkey33: Uint8Array, // ser_P(A): 33B compressed ): Uint8Array { + // Basic format checks + if (outpointL36.length !== 36) { + throw new Error('outpoint_L must be 36 bytes (txid||vout LE)'); + } + // Accept only compressed points for ser_P(A) + if ( + !( + summedSenderPubkey33.length === 33 && + (summedSenderPubkey33[0] === 0x02 || summedSenderPubkey33[0] === 0x03) + ) + ) { + throw new Error('ser_P(A) must be a 33-byte compressed pubkey'); + } + const ihRaw = taggedHash( 'BIP0352/Inputs', - tools.concat([smallestOutpoint, summedSenderPubkey]), + tools.concat([outpointL36, summedSenderPubkey33]), ); - return hashToTweak(ihRaw); + + // BIP-352 rule: must be in [1..n-1] + if (!ecc.isPrivate(ihRaw)) { + throw new Error('input_hash scalar is 0 or >= n'); + } + + return ihRaw; } /** @@ -435,41 +420,37 @@ export function calculateSumA( * @returns S */ export function calculateSharedSecret( - inputHash: Uint8Array, - scanPubkey: Uint8Array, - summedSenderPrivkey: Uint8Array, // a_sum (32B) -): Uint8Array | null { - if (!summedSenderPrivkey) - throw new Error('summedSenderPrivkey was not provided?'); - const Si: Uint8Array | null = ecc.pointMultiply( - scanPubkey!, - inputHash, - true, - ); - if (!Si) throw new Error('pointMultiply(B_scan, ih) failed'); - const S: Uint8Array | null = ecc.pointMultiply( - Si, - summedSenderPrivkey, - true, - ); - if (!S) throw new Error('pointMultiply(Si, summedSenderPrivkey) failed'); - else return S; -} + inputHash: Uint8Array, // 32B scalar + scanPubkey: Uint8Array, // 33B compressed B_scan + summedSenderPrivkey: Uint8Array, // 32B a_sum (even-Y normalized upstream) +): Uint8Array { + if (!ecc.isPrivate(inputHash)) + throw new Error('input_hash scalar is 0 or >= n'); + if (!ecc.isPrivate(summedSenderPrivkey)) throw new Error('a_sum invalid'); + if (!ecc.isPointCompressed(scanPubkey)) + throw new Error('B_scan must be compressed'); + + const Si = ecc.pointMultiply(scanPubkey, inputHash, true); + if (Si === null) + throw new Error('pointMultiply(B_scan, input_hash) -> infinity'); + + const S = ecc.pointMultiply(Si, summedSenderPrivkey, true); + if (S === null) throw new Error('pointMultiply(Si, a_sum) -> infinity'); + return S; // 33B compressed ser_P(S) +} /** * Calculate the tweak key - * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n + * input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) * @constructor * @param S * @param k * @returns input_hash tweak */ -export function calculateT_k(S: Uint8Array, k: number): Uint8Array | null { - const t_k: Uint8Array = taggedHash( - 'BIP0352/SharedSecret', - tools.concat([S, ser32BE(k)]), - ); - return hashToTweak(t_k); +export function calculateT_k(S: Uint8Array, k: number): Uint8Array { + const t_k = taggedHash('BIP0352/SharedSecret', tools.concat([S, ser32BE(k)])); + if (!ecc.isPrivate(t_k)) throw new Error('shared-secret scalar is 0 or >= n'); + return t_k; } /** @@ -504,7 +485,7 @@ export function deriveOutput( spendPubkey: Uint8Array, k: number, ): { pub_key: Uint8Array; tweak_key: Uint8Array } { - // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) -> reduce mod n + // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) const t_k: Uint8Array | null = calculateT_k(S, k); if (!t_k) throw new Error('t_k: failed'); @@ -523,14 +504,18 @@ export function deriveOutput( * @return tweaked label hash */ export function createLabelTweak( - receiverScanPrivkey: Uint8Array, // b_scan (32B) - m: number, // label integer + receiverScanPrivkey: Uint8Array, + m: number, ): Uint8Array { + // normalize m to uint32 per BIP + if (!Number.isInteger(m) || m < 0) throw new Error('label m must be uint32'); + const m32 = m >>> 0; const raw = taggedHash( 'BIP0352/Label', - tools.concat([receiverScanPrivkey, ser32BE(m)]), + tools.concat([receiverScanPrivkey, ser32BE(m32)]), ); - return hashToTweak(raw); + if (!ecc.isPrivate(raw)) throw new Error('label scalar is 0 or >= n'); + return raw; } /** From d0af90dedebade67f0f9ca0cd75c90bd107e3594 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 16:14:05 +0200 Subject: [PATCH 10/21] chore(p2sp): Remove dependency on inputs We dont want to drag in transactions into the base of p2sp. --- ts_src/payments/p2sp.ts | 47 +---------------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 062e8f52d..d84709b77 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -29,7 +29,6 @@ interface Recipient { * * @property {Uint8Array} [spendPubkey] - Optional spend public key for the sender. * @property {Uint8Array} [scanPubkey] - Optional scan public key used for recipient address derivation. - * @property {Input[]} [inputs] - Optional array of input UTXOs used in the transaction. * @property {Output[]} [outputs] - Optional array of outputs generated in the transaction. * @property {number} [version] - Optional version number of the silent payment scheme. * @property {Uint8Array} [aSum] - Optional summed private key (see `calculateSumA`). @@ -41,7 +40,6 @@ interface Recipient { export interface SilentPayment extends Payment { spendPubkey?: Uint8Array; scanPubkey?: Uint8Array; - inputs?: Input[]; outputs?: Output[]; version?: number; aSum?: Uint8Array; @@ -115,55 +113,12 @@ export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { A, ); return a.recipients?.map((value, index) => { - const S = calculateSharedSecret( - inputHashTweak, - value.B_spend_pub, - aSum, - ); - deriveOutput(S, value.B_spend_pub, index); - }); - } - // If we have all the inputs, aSum and only the spend keys for the recipients we need to calculate the input hash and secret - else if ( - a.inputs != null && - a.inputs?.length > 0 && - a.aSum != null && - a.aSum?.length > 0 && - a?.recipients.length > 0 && - allRecipientsHaveBSpend - ) { - const outpointL = findSmallestOutpoint(a.inputs); - const A: Uint8Array = ecc.pointFromScalar(a.aSum, true); // compressed 33B - const inputHashTweak: Uint8Array = calculateInputHashTweak(outpointL, A); - return a?.recipients.map((value, index) => { const S = calculateSharedSecret( inputHashTweak, value.B_spend_pub, a.aSum, ); - deriveOutput(S, value.B_spend_pub, index); - }); - } - // If we have all the inputs, privKeys and only the spend keys for the recipients we need to calculate the Sum, the input hash and secret - else if ( - a.inputs != null && - a.inputs?.length > 0 && - a.privKeys != null && - a.privKeys?.length > 0 && - a?.recipients.length > 0 && - allRecipientsHaveBSpend - ) { - const aSum: Uint8Array = calculateSumA(a.privKeys); - const outpointL = findSmallestOutpoint(a.inputs); - const A: Uint8Array = ecc.pointFromScalar(aSum, true); // compressed 33B - const inputHashTweak: Uint8Array = calculateInputHashTweak(outpointL, A); - return a?.recipients.map((value, index) => { - const S = calculateSharedSecret( - inputHashTweak, - value.B_spend_pub, - aSum, - ); - deriveOutput(S, value.B_spend_pub, index); + deriveSilentOutput(S, value.B_spend_pub, index); }); } else throw Error('Not enough data to derive outputs'); }); From ef20410368d6c6033d763dc5bc4a48f2a111f4e9 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 16:21:17 +0200 Subject: [PATCH 11/21] chore(p2sp): Type Uint8Array remove ArrayBufferLike --- ts_src/payments/p2sp.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index d84709b77..7fbfaed90 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -345,7 +345,7 @@ export function calculateSumA( // only normalize when key will be used as x-only (Taproot key-spend). if (isXOnly) { - const P: Uint8Array | null = ecc.pointFromScalar( + const P: Uint8Array | null = ecc.pointFromScalar( k, true, ); @@ -419,7 +419,7 @@ export function calculateP_k( spendPubKey: Uint8Array, t_k: Uint8Array, ): Uint8Array { - const Pk: Uint8Array | null = ecc.pointAddScalar( + const Pk: Uint8Array | null = ecc.pointAddScalar( spendPubKey, t_k, true, @@ -441,12 +441,12 @@ export function deriveOutput( k: number, ): { pub_key: Uint8Array; tweak_key: Uint8Array } { // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) - const t_k: Uint8Array | null = calculateT_k(S, k); + const t_k: Uint8Array | null = calculateT_k(S, k); if (!t_k) throw new Error('t_k: failed'); // P_k = B_spend + t_k·G (compressed) -> x-only for P2TR - const P_k: Uint8Array = calculateP_k(spendPubkey, t_k); - const P_xOnly: Uint8Array = toXOnly(P_k); + const P_k: Uint8Array = calculateP_k(spendPubkey, t_k); + const P_xOnly: Uint8Array = toXOnly(P_k); if (!P_xOnly) throw new Error('pointAddScalar failed'); return { pub_key: P_xOnly, tweak_key: t_k }; @@ -487,9 +487,9 @@ export function generateLabelAndAddress( label: number, ): { L: Uint8Array; Bm: Uint8Array } { // TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) - const L: Uint8Array = createLabelTweak(B_scan, label); + const L: Uint8Array = createLabelTweak(B_scan, label); // Bm = B_spend + hashBIP0352/Label(ser256(b_scan) || ser32(m))·G - const Bm: Uint8Array | null = ecc.pointAddScalar( + const Bm: Uint8Array | null = ecc.pointAddScalar( B_spend, L, true, @@ -587,7 +587,7 @@ function performScan( // priv_key_tweak returned by L + t_k (mod n) for labeled, or t_k for unlabeled let spendTweak = derivedOutput.tweak_key; if (labelScalar != null && !isZero32(labelScalar)) { - const sum: Uint8Array | null = ecc.privateAdd( + const sum: Uint8Array | null = ecc.privateAdd( labelScalar, derivedOutput.tweak_key, ); From 0c9b7d81ff0c8214b07e1a750afe4159944ee239 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:08:27 +0200 Subject: [PATCH 12/21] chore(p2sp): Renames Some basic renames hopefully more readable. --- ts_src/payments/p2sp.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 7fbfaed90..371b7f9a9 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -89,7 +89,7 @@ export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { const allRecipientsComplete = a.recipients.every( r => r.S.length > 0 && r.B_spend_pub.length > 0, ); - const allRecipientsHaveBSpend = a.recipients.every( + const allRecipientsHaveB_spend = a.recipients.every( r => r.B_spend_pub.length > 0, ); // If we have both the secret and B_spend for each key we can derive directly @@ -105,7 +105,7 @@ export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { a.aSum != null && a.aSum?.length > 0 && a.recipients?.length > 0 && - allRecipientsHaveBSpend + allRecipientsHaveB_spend ) { const A: Uint8Array = ecc.pointFromScalar(a.aSum, true); // compressed 33B const inputHashTweak: Uint8Array = calculateInputHashTweak( @@ -261,16 +261,16 @@ export function decodeSilentPaymentAddress(address: string): { // Handle both current (64 bytes for x-only keys) and legacy (66 bytes for full keys) payload lengths. if (keysData.length === 64) { // Current spec: 32-byte x-only keys. Reconstruct with even y-parity (0x02) as per convention. - const B_scan_xonly = keysData.slice(0, 32); - const B_spend_xonly = keysData.slice(32, 64); + const B_scan_xOnly = keysData.slice(0, 32); + const B_spend_xOnly = keysData.slice(32, 64); B_spend = new Uint8Array(33); B_spend[0] = 0x02; - B_spend.set(B_spend_xonly, 1); + B_spend.set(B_spend_xOnly, 1); B_scan = new Uint8Array(33); B_scan[0] = 0x02; - B_scan.set(B_scan_xonly, 1); + B_scan.set(B_scan_xOnly, 1); } else if (keysData.length === 66) { // Legacy spec: 33-byte full keys. We can return them directly to preserve the original y-parity. B_scan = keysData.slice(0, 33); @@ -371,17 +371,17 @@ export function calculateSumA( * S = (inputHash * B_scan) * a_sum (compressed) * @param inputHash * @param scanPubkey - B_scan - * @param summedSenderPrivkey - a_Sum + * @param summedSenderPrivKey - a_Sum * @returns S */ export function calculateSharedSecret( inputHash: Uint8Array, // 32B scalar scanPubkey: Uint8Array, // 33B compressed B_scan - summedSenderPrivkey: Uint8Array, // 32B a_sum (even-Y normalized upstream) + summedSenderPrivKey: Uint8Array, // 32B a_sum (even-Y normalized upstream) ): Uint8Array { if (!ecc.isPrivate(inputHash)) throw new Error('input_hash scalar is 0 or >= n'); - if (!ecc.isPrivate(summedSenderPrivkey)) throw new Error('a_sum invalid'); + if (!ecc.isPrivate(summedSenderPrivKey)) throw new Error('a_sum invalid'); if (!ecc.isPointCompressed(scanPubkey)) throw new Error('B_scan must be compressed'); @@ -389,7 +389,7 @@ export function calculateSharedSecret( if (Si === null) throw new Error('pointMultiply(B_scan, input_hash) -> infinity'); - const S = ecc.pointMultiply(Si, summedSenderPrivkey, true); + const S = ecc.pointMultiply(Si, summedSenderPrivKey, true); if (S === null) throw new Error('pointMultiply(Si, a_sum) -> infinity'); return S; // 33B compressed ser_P(S) From e6d0823956ed7a8126c26cb25bf26c66511855e1 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 16:13:09 +0200 Subject: [PATCH 13/21] chore(p2sp): Properly type silent payment outputs Added typings for SilentOutput and removed usage of Output. This SilentOutput is information used to create (or find) an actual UTXO. Its a building block instead of a final output. --- test/integration/silentpayment.spec.ts | 4 +-- ts_src/payments/index.ts | 4 +-- ts_src/payments/p2sp.ts | 50 +++++++++++++++----------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 31552e65b..5050f7278 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -9,7 +9,7 @@ import { calculateInputHashTweak, calculateSharedSecret, calculateSumA, - deriveOutput, + deriveSilentOutput, decodeSilentPaymentAddress, encodeSilentPaymentAddress, findSmallestOutpoint, @@ -339,7 +339,7 @@ function runSenderCase(tc: Sender) { } for (let k = 0; k < g.B_m_list.length; k++) { - const { pub_key, tweak_key } = deriveOutput(S, g.B_m_list[k], k); + const { pub_key, tweak_key } = deriveSilentOutput(S, g.B_m_list[k], k); sending_outputs.add(toHex(pub_key)); // x-only hex } } diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index a1cf79c48..ad4c4aba9 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -24,7 +24,7 @@ import { p2sh } from './p2sh.js'; import { p2wpkh } from './p2wpkh.js'; import { p2wsh } from './p2wsh.js'; import { p2tr } from './p2tr.js'; -import { p2sp, scanForSilentPayments, deriveOutput } from './p2sp.js'; +import { p2sp, scanForSilentPayments, deriveSilentOutput } from './p2sp.js'; export interface Payment { name?: string; @@ -72,7 +72,7 @@ export { p2sp, // TODO how should we expose the two functions below? scanForSilentPayments, - deriveOutput, + deriveSilentOutput, }; // TODO diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 371b7f9a9..c0121a72b 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -22,6 +22,22 @@ interface Recipient { S?: Uint8Array; B_spend_pub?: Uint8Array; } + +/** + * @property {Uint8Array} [pub_key] - Resulting xOnly P_k of {@link calculateP_k} + * @property {Uint8Array} [tweak_key] - t_k + * @property {Uint8Array} [priv_key_tweak] - labeled t_k when labeled are used else the same as t_k + * @property {number} [labelNonce] - + * @property {Uint8Array} [labelScalar] - result of TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) {@link createLabelTweak} + */ +interface SilentOutput { + pub_key: Uint8Array; + tweak_key: Uint8Array; + priv_key_tweak?: Uint8Array; + labelNonce?: number; + labelScalar?: Uint8Array; +} + /** * Represents a Silent Payment transaction structure that extends a standard {@link Payment}. * Includes additional cryptographic and metadata fields used for constructing @@ -29,7 +45,7 @@ interface Recipient { * * @property {Uint8Array} [spendPubkey] - Optional spend public key for the sender. * @property {Uint8Array} [scanPubkey] - Optional scan public key used for recipient address derivation. - * @property {Output[]} [outputs] - Optional array of outputs generated in the transaction. + * @property {SilentOutput[]} [outputs] - Optional array of outputs generated for use in the transaction. * @property {number} [version] - Optional version number of the silent payment scheme. * @property {Uint8Array} [aSum] - Optional summed private key (see `calculateSumA`). * @property {Uint8Array} [outpointL] - Optional first result of lexicographically sorted input transaction IDs. @@ -40,7 +56,7 @@ interface Recipient { export interface SilentPayment extends Payment { spendPubkey?: Uint8Array; scanPubkey?: Uint8Array; - outputs?: Output[]; + outputs?: SilentOutput[]; version?: number; aSum?: Uint8Array; outpointL?: Uint8Array; @@ -95,7 +111,7 @@ export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { // If we have both the secret and B_spend for each key we can derive directly if (allRecipientsComplete) { return a.recipients?.map((value, index) => { - deriveOutput(value.S, value.B_spend_pub, index); + deriveSilentOutput(value.S, value.B_spend_pub, index); }); } // If we have outpointL, aSum and only the spend keys for the recipients we need to calculate the input hash and secret @@ -433,13 +449,13 @@ export function calculateP_k( * @param S - shared secret = (inputHash * B_scan) * a_sum (compressed) * @param spendPubkey - B_spend * @param k - output number - * @returns the newly calculated output + * @returns {SilentOutput} the newly calculated output */ -export function deriveOutput( +export function deriveSilentOutput( S: Uint8Array, spendPubkey: Uint8Array, k: number, -): { pub_key: Uint8Array; tweak_key: Uint8Array } { +): SilentOutput { // t_k = H_tag(SharedSecret, ser_P(S) || ser32BE(k)) const t_k: Uint8Array | null = calculateT_k(S, k); if (!t_k) throw new Error('t_k: failed'); @@ -514,16 +530,8 @@ export function scanForSilentPayments( summedSenderPubkey: Uint8Array, outputsToCheck: Set, labelNonces: Array = Array.from([]), -): { - priv_key_tweak: Uint8Array; - pub_key: Uint8Array; - labelNonce?: number; -}[] { - let foundPayments: { - priv_key_tweak: Uint8Array; - pub_key: Uint8Array; - labelNonce?: number; - }[] = []; +): SilentOutput[] { + let foundPayments: SilentOutput[] = []; // G const baseSpendPubkey: Uint8Array = ecc.pointFromScalar( @@ -575,11 +583,11 @@ function performScan( S: Uint8Array, outputsToCheck: Set, labelScalar: Uint8Array | null, // L (or null for base) -): { priv_key_tweak: Uint8Array; pub_key: Uint8Array }[] { - const found: { priv_key_tweak: Uint8Array; pub_key: Uint8Array }[] = []; +): SilentOutput[] { + const found: SilentOutput[] = []; for (let k = 0; k < outputsToCheck.size; k++) { - const derivedOutput = deriveOutput(S, receiverSpendPubkey, k); + const derivedOutput = deriveSilentOutput(S, receiverSpendPubkey, k); if (!derivedOutput.pub_key) break; const xonlyHex = toHex(derivedOutput.pub_key).toLowerCase(); @@ -594,10 +602,10 @@ function performScan( if (!sum) throw new Error('privateAdd(label, t_k) failed'); spendTweak = sum; } - // TODO chose to return the hex encoded or raw Uint8Array found.push({ + ...derivedOutput, priv_key_tweak: spendTweak, - pub_key: derivedOutput.pub_key, + labelScalar, }); } } From 0ace2d7183d0ae5ecd3cce2a4de8c800c1be3bc3 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:17:19 +0200 Subject: [PATCH 14/21] chore(p2sp): Doc improvements Documented more params. --- ts_src/payments/p2sp.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index c0121a72b..81484bc94 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -68,8 +68,8 @@ export interface SilentPayment extends Payment { * Main function for creating a Pay-to-Silent-Payment (P2SP) payment object. * This function encapsulates the logic for handling silent payment addresses and keys. * - * @param a - The payment object containing the necessary data for P2SP. - * @param opts - Optional payment options. + * @param {SilentPayment} [a] - The payment object containing the necessary data for P2SP. + * @param {PaymentOpts} [opts] - Optional payment options. * @returns The P2SP payment object. */ export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { @@ -205,9 +205,9 @@ export const serOutpointLE = (txidHexBE: Uint8Array, vout: number) => { /** * Encodes spend and scan public keys into a Bech32m Silent Payment address. - * @param B_spend - * @param B_scan - * @param version + * @param {Uint8Array} [B_spend] - spend public key. + * @param {Uint8Array} [B_scan] - public scan key. + * @param {number} [version] - Optional version number of the silent payment scheme. * @param network - testing, regtest or prod * @returns bech32m encoded string */ @@ -427,7 +427,7 @@ export function calculateT_k(S: Uint8Array, k: number): Uint8Array { /** * Calculate P_k * P_k = B_spend + t_k·G (compressed) -> x-only for P2TR - * @param spendPubKey + * @param spendPubKey - B_spend * @param t_k * @returns Pk */ From 6b8032c8820644ec6699532b4937a57e702bf74e Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:18:24 +0200 Subject: [PATCH 15/21] chore(p2sp): Add type for decoded Silent Payment address Added the type for more readability. --- ts_src/payments/p2sp.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 81484bc94..d147befdf 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -248,15 +248,24 @@ export function encodeSilentPaymentAddress( } /** - * Decodes a Bech32m Silent payment address into its spend and scan public keys. - * @param address - bech32m silent payment address format - * @returns { B_spend, B_scan, version } + * @property {Uint8Array} [B_spend] - spend public key. + * @property {Uint8Array} [B_scan] - public scan key. + * @property {number} [version] - silent payment version */ -export function decodeSilentPaymentAddress(address: string): { +interface DecodedSilentPaymentAddress { B_spend: Uint8Array; // pub spend key B_scan: Uint8Array; // pub scan key version: number; -} { +} + +/** + * Decodes a Bech32m Silent payment address into its spend and scan public keys. + * @param address - bech32m silent payment address format + * @returns { DecodedSilentPaymentAddress } - Decoded silent payment address + */ +export function decodeSilentPaymentAddress( + address: string, +): DecodedSilentPaymentAddress { // The default bech32 limit is 90, but silent payment addresses are longer. // We pass a higher limit to accommodate this, as the underlying implementation allows it. const { prefix, words } = bech32m.decode(address, BECH32_SP_LIMIT); From 7e3c0ee4d2702dd320ece34a5a6e8545c0b54ad5 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:19:45 +0200 Subject: [PATCH 16/21] chore(p2sp): Formatting --- ts_src/payments/p2sp.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index d147befdf..002a49c7f 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -370,10 +370,7 @@ export function calculateSumA( // only normalize when key will be used as x-only (Taproot key-spend). if (isXOnly) { - const P: Uint8Array | null = ecc.pointFromScalar( - k, - true, - ); + const P: Uint8Array | null = ecc.pointFromScalar(k, true); if (!P) continue; // skip invalid if (P[0] === 0x03) { const neg = ecc.privateNegate(k); @@ -444,11 +441,7 @@ export function calculateP_k( spendPubKey: Uint8Array, t_k: Uint8Array, ): Uint8Array { - const Pk: Uint8Array | null = ecc.pointAddScalar( - spendPubKey, - t_k, - true, - ); + const Pk: Uint8Array | null = ecc.pointAddScalar(spendPubKey, t_k, true); if (!Pk) throw new Error('pointAddScalar failed'); return Pk; } @@ -514,11 +507,7 @@ export function generateLabelAndAddress( // TaggedHash("BIP0352/Label", ser256(b_scan) || ser32BE(m)) const L: Uint8Array = createLabelTweak(B_scan, label); // Bm = B_spend + hashBIP0352/Label(ser256(b_scan) || ser32(m))·G - const Bm: Uint8Array | null = ecc.pointAddScalar( - B_spend, - L, - true, - ); + const Bm: Uint8Array | null = ecc.pointAddScalar(B_spend, L, true); if (!Bm) throw new Error('pointAddScalar(B_spend, L) failed'); return { L, Bm }; } From 87b8d5b344ef87d9889119bac28ab63ba6ab7a48 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:20:08 +0200 Subject: [PATCH 17/21] chore(p2sp): Add types --- ts_src/payments/p2sp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 002a49c7f..ba5359f05 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -338,7 +338,7 @@ export function calculateInputHashTweak( throw new Error('ser_P(A) must be a 33-byte compressed pubkey'); } - const ihRaw = taggedHash( + const ihRaw: Uint8Array = taggedHash( 'BIP0352/Inputs', tools.concat([outpointL36, summedSenderPubkey33]), ); @@ -366,7 +366,7 @@ export function calculateSumA( for (const { priv, isXOnly } of inputPrivKeyTuples) { if (!ecc.isPrivate(priv)) continue; - let k = priv; + let k: Uint8Array = priv; // only normalize when key will be used as x-only (Taproot key-spend). if (isXOnly) { From 58e9630c3b14a1b2534ec35014bf7d0cfbb3db87 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:20:56 +0200 Subject: [PATCH 18/21] chore(p2sp): Make recipients optional --- ts_src/payments/p2sp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index ba5359f05..2d1cb4839 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -61,7 +61,7 @@ export interface SilentPayment extends Payment { aSum?: Uint8Array; outpointL?: Uint8Array; privKeys?: Array<{ priv: Uint8Array; isXOnly: boolean }>; - recipients: Array; + recipients?: Array; } /** From 2c4e8e1e7ae2c3bfc159fcc60503221d6b4c9e82 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 17:48:42 +0200 Subject: [PATCH 19/21] chore(p2sp): Remove Output import Forgot to remove the import. --- ts_src/payments/p2sp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 2d1cb4839..66098fee4 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -9,7 +9,7 @@ import { bech32m } from 'bech32'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { taggedHash } from '../crypto'; -import { Input, Output } from '../transaction'; +import { Input } from '../transaction'; // --- TYPE DEFINITIONS & UTILITIES --- export const BECH32_SP_LIMIT = 150; From 797c16d238d1a0c0aafca101134d86b05d9f2beb Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Thu, 16 Oct 2025 18:00:56 +0200 Subject: [PATCH 20/21] chore(p2sp): Clarify the output type and distinquish from regular outputs This is not the final name but hopefully makes this should be used as a building block to create a UTXO instead of final output. --- ts_src/payments/p2sp.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 66098fee4..09bad90c8 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -45,7 +45,7 @@ interface SilentOutput { * * @property {Uint8Array} [spendPubkey] - Optional spend public key for the sender. * @property {Uint8Array} [scanPubkey] - Optional scan public key used for recipient address derivation. - * @property {SilentOutput[]} [outputs] - Optional array of outputs generated for use in the transaction. + * @property {SilentOutput[]} [silentOutputs] - Optional array of silentOutputs generated for use in the transaction. * @property {number} [version] - Optional version number of the silent payment scheme. * @property {Uint8Array} [aSum] - Optional summed private key (see `calculateSumA`). * @property {Uint8Array} [outpointL] - Optional first result of lexicographically sorted input transaction IDs. @@ -56,7 +56,7 @@ interface SilentOutput { export interface SilentPayment extends Payment { spendPubkey?: Uint8Array; scanPubkey?: Uint8Array; - outputs?: SilentOutput[]; + silentOutputs?: SilentOutput[]; version?: number; aSum?: Uint8Array; outpointL?: Uint8Array; @@ -100,8 +100,8 @@ export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment { network, ); }); - lazy.prop(o, 'outputs', () => { - if (a.outputs) return a.outputs; + lazy.prop(o, 'silentOutputs', () => { + if (a.silentOutputs) return a.silentOutputs; const allRecipientsComplete = a.recipients.every( r => r.S.length > 0 && r.B_spend_pub.length > 0, ); From dadd54bd90ef8d5ee152d36ea83ad2be9b6107f0 Mon Sep 17 00:00:00 2001 From: Jasper van Gelder Date: Tue, 21 Oct 2025 18:23:06 +0200 Subject: [PATCH 21/21] chore(p2sp): Moved functionality to SilentPaymentTransaction We did not want to drag in inputs in payments so instead created a class that extends Transaction. This makes more sense for detecting (scanning) which UTXOs belong to our keys and as well for creating outputs who depend on the given inputs. --- test/integration/silentpayment.spec.ts | 7 +- ts_src/SilentPaymentTransaction.ts | 374 +++++++++++++++++++++++++ ts_src/bufferutils.ts | 6 + ts_src/payments/index.ts | 2 - ts_src/payments/p2sp.ts | 133 +-------- 5 files changed, 386 insertions(+), 136 deletions(-) create mode 100644 ts_src/SilentPaymentTransaction.ts diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 5050f7278..22fd9b796 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -12,11 +12,12 @@ import { deriveSilentOutput, decodeSilentPaymentAddress, encodeSilentPaymentAddress, - findSmallestOutpoint, - modN32, - scanForSilentPayments, } from '../../ts_src/payments/p2sp.js'; import { Input } from '../../ts_src/transaction.js'; +import { + findSmallestOutpoint, + scanForSilentPayments, +} from '../../ts_src/SilentPaymentTransaction.js'; // ---- init ecc for bitcoinjs (even if we use tiny-secp directly) ---- bitcoin.initEccLib(ecc); diff --git a/ts_src/SilentPaymentTransaction.ts b/ts_src/SilentPaymentTransaction.ts new file mode 100644 index 000000000..af7c89f0a --- /dev/null +++ b/ts_src/SilentPaymentTransaction.ts @@ -0,0 +1,374 @@ +import { Input, Transaction } from './transaction'; +import { fromHex, toHex, writeUInt32 } from 'uint8array-tools'; +import * as ecc from 'tiny-secp256k1'; +import { deriveSilentOutput } from './payments'; +import { + calculateInputHashTweak, + calculateSharedSecret, + calculateSumA, + decodeSilentPaymentAddress, + generateLabelAndAddress, +} from './payments/p2sp'; +import * as tools from 'uint8array-tools'; +import { isZero32 } from './bufferutils'; +import { bitcoin } from './networks'; + +/** + * Serialize output with number little endian + * (used to sort outputs) + * @param txidHexBE - big endian encoded tx + * @param vout - output index + * @returns the serialized little endian encoded output + */ +export const serOutpointLE = (txidHexBE: Uint8Array, vout: number) => { + const out = new Uint8Array(36); + if (txidHexBE.length !== 32) throw new Error('txid must be 32 bytes'); + txidHexBE.reverse(); // BE -> LE + out.set(txidHexBE, 0); + writeUInt32(out, 32, vout >>> 0, 'le'); + return out; +}; + +/** + * Smallest outpoint = lexicographic min of (txidLE || voutLE) + * @param inputs an array of inputs you want the first lexicographically sorted result + * @returns the first output after sorting lexicographically + */ +export const findSmallestOutpoint = (inputs: Array) => + inputs + .map(v => serOutpointLE(v.hash, v.index)) + .sort((a, b) => tools.compare(a, b))[0]; + +/** + * Scans a transaction's inputs and outputs to find any silent payments for the receiver. + * @param receiverScanPrivkey - b_scan + * @param receiverSpendPrivkey - b_spend + * @param inputHashTweak + * @param summedSenderPubkey - A_sum + * @param outputsToCheck - array of hex xOnly encoded outputs to check + * @param labelNonces + */ +export function scanForSilentPayments( + receiverScanPrivkey: Uint8Array, + receiverSpendPrivkey: Uint8Array, + inputHashTweak: Uint8Array, + summedSenderPubkey: Uint8Array, + outputsToCheck: Set, + labelNonces: Array = Array.from([]), +): SilentOutput[] { + let foundPayments: SilentOutput[] = []; + + // G + const baseSpendPubkey: Uint8Array = ecc.pointFromScalar( + receiverSpendPrivkey, + true, + )!; + + // Shared secret S = (inputHash * A_sum) * b_scan (order equivalent) + const S = calculateSharedSecret( + inputHashTweak, + summedSenderPubkey, + receiverScanPrivkey, + ); + if (!S) return []; + + // First, scan for the base (unlabeled) address + foundPayments = foundPayments.concat( + performScan(baseSpendPubkey, S, outputsToCheck, null), + ); + + // Then, scan for each labeled address + for (const m of labelNonces) { + const { L, Bm } = generateLabelAndAddress( + receiverScanPrivkey, + baseSpendPubkey, + m, + ); + + const labeledResults = performScan(Bm, S, outputsToCheck, L); + + // Add the label nonce to any found payments for identification + labeledResults.forEach(result => { + foundPayments.push({ ...result, labelNonce: m }); + }); + } + + return foundPayments; +} + +/** + * The core scanning logic, performed for a specific spend public key (B_spend). + * @param receiverSpendPubkey - G or B_m + * @param S + * @param outputsToCheck - array of hex xOnly encoded outputs to check + * @param labelScalar + = */ +function performScan( + receiverSpendPubkey: Uint8Array, + S: Uint8Array, + outputsToCheck: Set, + labelScalar: Uint8Array | null, // L (or null for base) +): SilentOutput[] { + const found: SilentOutput[] = []; + + for (let k = 0; k < outputsToCheck.size; k++) { + const derivedOutput = deriveSilentOutput(S, receiverSpendPubkey, k); + if (!derivedOutput.pub_key) break; + const xonlyHex = toHex(derivedOutput.pub_key).toLowerCase(); + + if (outputsToCheck.size === 0 || outputsToCheck.has(xonlyHex)) { + // priv_key_tweak returned by L + t_k (mod n) for labeled, or t_k for unlabeled + let spendTweak = derivedOutput.tweak_key; + if (labelScalar != null && !isZero32(labelScalar)) { + const sum: Uint8Array | null = ecc.privateAdd( + labelScalar, + derivedOutput.tweak_key, + ); + if (!sum) throw new Error('privateAdd(label, t_k) failed'); + spendTweak = sum; + } + found.push({ + ...derivedOutput, + priv_key_tweak: spendTweak, + labelScalar, + }); + } + } + return found; +} + +/** A Previous Output object, containing the scriptPubKey and value needed to scan an input. */ +export interface Prevout { + txid: string; // The transaction id of the previous output. + vout: number; // The output index of the previous output. + script: Uint8Array; // The scriptPubKey of the previous output. + value: number; // The value of the previous output. +} + +/** The receiver's private keys needed for scanning. */ +interface ReceiverKeys { + scanPrivKey: Uint8Array; + spendPrivKey: Uint8Array; +} + +/** + * The result of a successful scan, providing the data needed to spend the output. + * Extends the base SilentOutput from p2sp.ts to include transaction-specific UTXO info. + */ +export interface SilentOutput extends P2SPSilentOutput { + utxo: { + txid: string; + vout: number; + value: number; + }; +} + +/** + * Extends the bitcoinjs-lib Transaction class to provide methods + * for creating and scanning for BIP-352 Silent Payments. + */ +export class SilentPaymentTransaction extends bitcoin.Transaction { + private _senderPrivateKeys: { key: Uint8Array; isXOnly: boolean }[] = []; + private _silentIntents: { + recipientAddress: string; + value: number; + vout: number; // The index of the placeholder output + }[] = []; + private _outputsDirty: boolean = true; + + /** + * Adds an input to the transaction and tracks its private key for SP calculations. + * NOTE: Adding an input invalidates any previously calculated silent payment outputs. + * Call `finalizeSilentOutputs()` after all inputs have been added. + * @param hash Transaction hash of the input. + * @param index Output index of the input. + * @param sequence The sequence number. + * @param scriptSig The script signature (for non-witness inputs). + * @param prevoutScript The scriptPubKey of the UTXO being spent. + * @param senderPrivateKey The private key corresponding to this input. + */ + addSilentInput( + hash: Uint8Array, + index: number, + sequence: number | undefined, + scriptSig: Uint8Array | undefined, + prevoutScript: Uint8Array, + senderPrivateKey: Uint8Array, + ): number { + const isP2TR = prevoutScript.length === 34 && prevoutScript[0] === 0x51; + this._senderPrivateKeys.push({ key: senderPrivateKey, isXOnly: isP2TR }); + this._outputsDirty = true; // Mark outputs as dirty whenever an input changes. + // The parent class expects Buffers. Casting to `any` to match user's request. + return super.addInput(hash as any, index, sequence, scriptSig as any); + } + + /** + * Calculates the sum of the sender's private keys using the helper from p2sp.ts. + * This is the `a_sum` scalar. + */ + private _calculateSummedPrivateKey(): Uint8Array | null { + if (this._senderPrivateKeys.length === 0) { + throw new Error('Cannot create a silent payment without inputs.'); + } + return calculateSumA( + this._senderPrivateKeys.map(pk => ({ + priv: pk.key, + isXOnly: pk.isXOnly, + })), + ); + } + + /** + * Adds a placeholder for a silent payment output and stores the intent to calculate it later. + * The actual script will be generated and inserted when `finalizeSilentOutputs()` is called. + * @param recipientAddress The bech32m-encoded silent payment address of the recipient. + * @param value The amount in satoshis to send. + * @returns The vout (output index) of the placeholder output. + */ + addSilentPaymentOutput(recipientAddress: string, value: number): number { + // Add a placeholder output (empty script) to reserve the vout and value. + const placeholderScript = new Uint8Array(0); + const vout = this.addOutput(placeholderScript, value); + + // Store the intent to be finalized later. + this._silentIntents.push({ + recipientAddress, + value, + vout, + }); + + return vout; + } + + /** + * Finalizes all pending silent payment outputs. This method calculates the correct + * output scripts based on the final set of inputs and updates the transaction. + * It is idempotent and will only perform calculations if inputs have changed. + * + * THIS METHOD MUST BE CALLED after all inputs have been added and before signing. + */ + finalizeSilentOutputs(): void { + if (!this._outputsDirty || this._silentIntents.length === 0) { + return; // Nothing to do or already up-to-date. + } + + const a_sum = this._calculateSummedPrivateKey(); + if (!a_sum) { + throw new Error( + 'Sender private keys sum to zero; cannot create silent payment outputs.', + ); + } + + const smallestOutpoint = findSmallestOutpoint(this.ins); + const A_sum_point = ecc.pointFromScalar(a_sum, true)!; + const input_hash = calculateInputHashTweak(smallestOutpoint, A_sum_point); + + // This map tracks the 'k' value for each recipient (B_scan) during this finalization. + const kValues = new Map(); + + for (const intent of this._silentIntents) { + const { recipientAddress, vout } = intent; + + const { B_scan, B_spend } = decodeSilentPaymentAddress(recipientAddress); + const bScanHex = toHex(B_scan); + const k = kValues.get(bScanHex) || 0; + + // S = (input_hash * B_scan) * a_sum + const S = calculateSharedSecret(input_hash, B_scan, a_sum); + + // { pub_key, tweak_key } = deriveSilentOutput(...) + const { pub_key } = deriveSilentOutput(S, B_spend, k); + + const p2tr = bitcoin.payments.p2tr({ pubkey: pub_key }); + if (!p2tr.output) { + throw new Error( + `Failed to create P2TR output script for recipient ${recipientAddress}`, + ); + } + + // Update the placeholder output with the correct script. + this.outs[vout].script = p2tr.output; + + // Increment k for the next payment to this same recipient in this batch. + kValues.set(bScanHex, k + 1); + } + + // Reset the dirty flag after successful finalization. + this._outputsDirty = false; + } + + /** + * Scans a finalized transaction to find any outputs belonging to the receiver. + * @param tx The finalized bitcoinjs-lib Transaction object. + * @param prevouts An array of Previous Outputs being spent by the transaction, used to get prevout scripts. + * @param receiverKeys The receiver's scan and spend private keys. + * @param labelNonces An optional array of label nonces to scan for. + * @returns An array of found silent payment outputs with spending information. + */ + static scan( + tx: Transaction, + prevouts: Prevout[], + receiverKeys: ReceiverKeys, + labelNonces: number[] = [], + ): SilentOutput[] { + const validPubkeys = tx.ins + .map(input => { + const prevout = prevouts.find( + p => + tools.equals(tools.fromHex(p.txid).reverse(), input.hash) && + p.vout === input.index, + ); + if (!prevout) return null; + // The getPublicKeyFromInput function expects Buffers. + // Casting to `any` to match user's request to avoid Buffer.from() + return getPublicKeyFromInput({ + prevoutScript: prevout.script as any, + scriptSig: input.script as any, + witness: input.witness as any, + }); + }) + .filter((pk): pk is Uint8Array => pk !== null); + + if (validPubkeys.length === 0) return []; + + const A_sum = validPubkeys.reduce((acc, pk) => ecc.pointAdd(acc, pk)!); + if (!A_sum) return []; + + const smallestOutpoint = findSmallestOutpoint(this.ins); + const input_hash = calculateInputHashTweak(smallestOutpoint, A_sum); + + const txOutputXOnlyPubkeys = tx.outs.map(out => toHex(out.script.slice(2))); + const outputsToCheck = new Set(txOutputXOnlyPubkeys); + + // Delegate the core scanning logic to the tested helper function. + const foundP2SPOutputs = scanForSilentPayments( + receiverKeys.scanPrivKey, + receiverKeys.spendPrivKey, + input_hash, + A_sum, + outputsToCheck, + labelNonces, + ); + + // Map the results from the generic helper to the transaction-specific SilentOutput type. + return foundP2SPOutputs.map(p2spOutput => { + const pubkeyHex = toHex(p2spOutput.pub_key); + const vout = txOutputXOnlyPubkeys.findIndex(hex => hex === pubkeyHex); + + if (vout === -1) { + throw new Error( + 'Logic error: Found output not present in transaction.', + ); + } + + return { + ...p2spOutput, + utxo: { + txid: tx.getId(), + vout, + value: tx.outs[vout].value, + }, + }; + }); + } +} diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index d8b7dfb0e..89718c1ef 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -6,6 +6,12 @@ import * as tools from 'uint8array-tools'; const MAX_JS_NUMBER = 0x001fffffffffffff; +// TODO figure out a proper spot or existing function with the same functionally +export function isZero32(a: Uint8Array) { + for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; + return true; +} + // https://github.com/feross/buffer/blob/master/index.js#L1127 function verifuint(value: number | bigint, max: number): void { if (typeof value !== 'number' && typeof value !== 'bigint') diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index ad4c4aba9..054446016 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -70,8 +70,6 @@ export { p2wsh, p2tr, p2sp, - // TODO how should we expose the two functions below? - scanForSilentPayments, deriveSilentOutput, }; diff --git a/ts_src/payments/p2sp.ts b/ts_src/payments/p2sp.ts index 09bad90c8..f46162546 100644 --- a/ts_src/payments/p2sp.ts +++ b/ts_src/payments/p2sp.ts @@ -2,14 +2,14 @@ import { bitcoin as BITCOIN_NETWORK, Network } from '../networks'; import * as ecc from 'tiny-secp256k1'; import { toXOnly } from '../psbt/bip371'; import * as tools from 'uint8array-tools'; -import { toHex, writeUInt32 } from 'uint8array-tools'; +import { writeUInt32 } from 'uint8array-tools'; import { bech32m } from 'bech32'; // Explicitly import the Payment type for clarity import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { taggedHash } from '../crypto'; -import { Input } from '../transaction'; +import { isZero32 } from '../bufferutils'; // --- TYPE DEFINITIONS & UTILITIES --- export const BECH32_SP_LIMIT = 150; @@ -172,37 +172,6 @@ export function ser32BE(n: number): Uint8Array { return b; } -function isZero32(a: Uint8Array) { - for (let i = 0; i < 32; i++) if (a[i] !== 0) return false; - return true; -} - -/** - * Smallest outpoint = lexicographic min of (txidLE || voutLE) - * @param inputs an array of inputs you want the first lexicographically sorted result - * @returns the first output after sorting lexicographically - */ -export const findSmallestOutpoint = (inputs: Array) => - inputs - .map(v => serOutpointLE(v.hash, v.index)) - .sort((a, b) => tools.compare(a, b))[0]; - -/** - * Serialize output with number little endian - * (used to sort outputs) - * @param txidHexBE - big endian encoded tx - * @param vout - output index - * @returns the serialized little endian encoded output - */ -export const serOutpointLE = (txidHexBE: Uint8Array, vout: number) => { - const out = new Uint8Array(36); - if (txidHexBE.length !== 32) throw new Error('txid must be 32 bytes'); - txidHexBE.reverse(); // BE -> LE - out.set(txidHexBE, 0); - writeUInt32(out, 32, vout >>> 0, 'le'); - return out; -}; - /** * Encodes spend and scan public keys into a Bech32m Silent Payment address. * @param {Uint8Array} [B_spend] - spend public key. @@ -511,101 +480,3 @@ export function generateLabelAndAddress( if (!Bm) throw new Error('pointAddScalar(B_spend, L) failed'); return { L, Bm }; } - -/** - * Scans a transaction's inputs and outputs to find any silent payments for the receiver. - * @param receiverScanPrivkey - b_scan - * @param receiverSpendPrivkey - b_spend - * @param inputHashTweak - * @param summedSenderPubkey - A_sum - * @param outputsToCheck - array of hex xOnly encoded outputs to check - * @param labelNonces - */ -export function scanForSilentPayments( - receiverScanPrivkey: Uint8Array, - receiverSpendPrivkey: Uint8Array, - inputHashTweak: Uint8Array, - summedSenderPubkey: Uint8Array, - outputsToCheck: Set, - labelNonces: Array = Array.from([]), -): SilentOutput[] { - let foundPayments: SilentOutput[] = []; - - // G - const baseSpendPubkey: Uint8Array = ecc.pointFromScalar( - receiverSpendPrivkey, - true, - )!; - - // Shared secret S = (inputHash * A_sum) * b_scan (order equivalent) - const S = calculateSharedSecret( - inputHashTweak, - summedSenderPubkey, - receiverScanPrivkey, - ); - if (!S) return []; - - // First, scan for the base (unlabeled) address - foundPayments = foundPayments.concat( - performScan(baseSpendPubkey, S, outputsToCheck, null), - ); - - // Then, scan for each labeled address - for (const m of labelNonces) { - const { L, Bm } = generateLabelAndAddress( - receiverScanPrivkey, - baseSpendPubkey, - m, - ); - - const labeledResults = performScan(Bm, S, outputsToCheck, L); - - // Add the label nonce to any found payments for identification - labeledResults.forEach(result => { - foundPayments.push({ ...result, labelNonce: m }); - }); - } - - return foundPayments; -} - -/** - * The core scanning logic, performed for a specific spend public key (B_spend). - * @param receiverSpendPubkey - G or B_m - * @param S - * @param outputsToCheck - array of hex xOnly encoded outputs to check - * @param labelScalar -= */ -function performScan( - receiverSpendPubkey: Uint8Array, - S: Uint8Array, - outputsToCheck: Set, - labelScalar: Uint8Array | null, // L (or null for base) -): SilentOutput[] { - const found: SilentOutput[] = []; - - for (let k = 0; k < outputsToCheck.size; k++) { - const derivedOutput = deriveSilentOutput(S, receiverSpendPubkey, k); - if (!derivedOutput.pub_key) break; - const xonlyHex = toHex(derivedOutput.pub_key).toLowerCase(); - - if (outputsToCheck.size === 0 || outputsToCheck.has(xonlyHex)) { - // priv_key_tweak returned by L + t_k (mod n) for labeled, or t_k for unlabeled - let spendTweak = derivedOutput.tweak_key; - if (labelScalar != null && !isZero32(labelScalar)) { - const sum: Uint8Array | null = ecc.privateAdd( - labelScalar, - derivedOutput.tweak_key, - ); - if (!sum) throw new Error('privateAdd(label, t_k) failed'); - spendTweak = sum; - } - found.push({ - ...derivedOutput, - priv_key_tweak: spendTweak, - labelScalar, - }); - } - } - return found; -}