diff --git a/package.json b/package.json index 9344b29..3adb390 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-payments", - "version": "0.3.1", + "version": "0.3.2", "description": "BIP-352", "main": "src/index.ts", "scripts": { diff --git a/src/index.ts b/src/index.ts index 7898e34..fd7aa58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,30 +7,30 @@ import ecc from "./noble_ecc"; const ECPair = ECPairFactory(ecc); bitcoin.initEccLib(ecc); -export type UTXOType = 'p2wpkh' | 'p2sh-p2wpkh' | 'p2pkh' | 'p2tr' | 'non-eligible'; -type UTXO = { +export type UTXOType = "p2wpkh" | "p2sh-p2wpkh" | "p2pkh" | "p2tr" | "non-eligible"; + +export type UTXO = { txid: string; vout: number; - WIF: string; + wif: string; utxoType: UTXOType; }; -type Target = { - silentPaymentCode?: string; - address?: string; +export type Target = { + address: string; // either address or payment code value?: number; }; -type SilentPaymentGroup = { +export type SilentPaymentGroup = { Bscan: Buffer; BmValues: Array<[Buffer, number | undefined]>; }; function taggedHash(tag: string, data: Buffer): Buffer { - const hash = crypto.createHash('sha256'); - const tagHash = hash.update(tag, 'utf-8').digest(); - const ss = Buffer.concat([tagHash, tagHash, data]); - return crypto.createHash('sha256').update(ss).digest(); + const hash = crypto.createHash("sha256"); + const tagHash = hash.update(tag, "utf-8").digest(); + const ss = Buffer.concat([tagHash, tagHash, data]); + return crypto.createHash("sha256").update(ss).digest(); } const G = Buffer.from("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "hex"); @@ -49,12 +49,12 @@ export class SilentPayment { const silentPaymentGroups: Array = []; for (const target of targets) { - if (!target.silentPaymentCode) { + if (!target.address.startsWith("sp1")) { ret.push(target); // passthrough continue; } - const result = bech32m.decode(target.silentPaymentCode, 118); + const result = bech32m.decode(target.address, 118); const version = result.words.shift(); if (version !== 0) { throw new Error("Unexpected version of silent payment code"); @@ -88,10 +88,7 @@ export class SilentPayment { let k = 0; for (const [Bm, amount] of group.BmValues) { - const tk = taggedHash( - "BIP0352/SharedSecret", - Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(k)]) - ); + const tk = taggedHash("BIP0352/SharedSecret", Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(k)])); // Let Pmk = tk·G + Bm const Pmk = Buffer.from(ecc.pointAdd(ecc.pointMultiply(G, tk) as Uint8Array, Bm) as Uint8Array); @@ -141,18 +138,18 @@ export class SilentPayment { throw new Error("No UTXOs provided"); } - const keys: Array = [] + const keys: Array = []; for (const utxo of utxos) { - let key = ECPair.fromWIF(utxo.WIF).privateKey; + let key = ECPair.fromWIF(utxo.wif).privateKey; switch (utxo.utxoType) { - case 'non-eligible': - // Non-eligible UTXOs can be spent in the transaction, but are not used for the - // shared secret derivation. Note: we don't check that the private key is valid - // for non-eligible utxos because its possible the sender is following a different - // signing protocol for these utxos. For silent payments eligible utxos, we require - // access to the private key. - break; - case 'p2tr': + case "non-eligible": + // Non-eligible UTXOs can be spent in the transaction, but are not used for the + // shared secret derivation. Note: we don't check that the private key is valid + // for non-eligible utxos because its possible the sender is following a different + // signing protocol for these utxos. For silent payments eligible utxos, we require + // access to the private key. + break; + case "p2tr": if (key === undefined) { throw new Error("No private key found for eligible UTXO"); } @@ -161,9 +158,9 @@ export class SilentPayment { if (ecc.pointFromScalar(key)![0] === 0x03) { key = Buffer.from(ecc.privateNegate(key)); } - case 'p2wpkh': - case 'p2sh-p2wpkh': - case 'p2pkh': + case "p2wpkh": + case "p2sh-p2wpkh": + case "p2pkh": if (key === undefined) { throw new Error("No private key found for eligible UTXO"); } @@ -181,7 +178,7 @@ export class SilentPayment { return Buffer.from(ecc.privateAdd(acc, key) as Uint8Array); }); - return ret + return ret; } static isPaymentCodeValid(pc: string) { diff --git a/tests/silent-payment.test.ts b/tests/silent-payment.test.ts index 88a41d7..57dd5ff 100644 --- a/tests/silent-payment.test.ts +++ b/tests/silent-payment.test.ts @@ -8,13 +8,13 @@ import jsonInput from "./data/sending_test_vectors.json"; const ECPair = ECPairFactory(ecc); function exactMatch(a: string[], b: string[]): boolean { - const sortedA = a.sort(); - const sortedB = b.sort(); - return sortedA.length === sortedB.length && sortedA.every((value, index) => value === sortedB[index]); + const sortedA = a.sort(); + const sortedB = b.sort(); + return sortedA.length === sortedB.length && sortedA.every((value, index) => value === sortedB[index]); } function matchSubset(generated: string[], expected: string[][]): boolean { - return expected.some(subArray => exactMatch(generated, subArray)); + return expected.some((subArray) => exactMatch(generated, subArray)); } type Given = { @@ -32,8 +32,8 @@ type Sending = { }; type TestCase = { - comment: string; - sending: Sending[]; + comment: string; + sending: Sending[]; }; const tests = jsonInput as unknown as Array; @@ -46,34 +46,34 @@ it("smoke test", () => { /* Sending tests from the BIP352 test vectors */ tests.forEach((testCase, index) => { // Prepare the 'inputs' array - testCase.sending.forEach(sending => { - const utxos = sending.given.vin.map((input) => ({ - txid: input.txid, - vout: input.vout, - WIF: ECPair.fromPrivateKey(Buffer.from(input.private_key, "hex")).toWIF(), - utxoType: getUTXOType(input) as UTXOType, - })); - const noEligibleUtxos = utxos.every(utxo => utxo.utxoType === 'non-eligible'); - - // Prepare the 'recipients' array - const recipients = sending.given.recipients.map((recipient) => ({ - silentPaymentCode: recipient, - value: 1, - })); - - it(`Test Case: ${testCase.comment}`, () => { - const sp = new SilentPayment(); - if (noEligibleUtxos) { - expect(() => { - sp.createTransaction(utxos, recipients); - }).toThrow("No eligible UTXOs with private keys found"); - } else { - const generated = sp.createTransaction(utxos, recipients); - const generated_pubkeys: string[] = generated.map(obj => obj.address).filter(Boolean) as string[]; - assert(matchSubset(generated_pubkeys, sending.expected.outputs)); - } - }); + testCase.sending.forEach((sending) => { + const utxos = sending.given.vin.map((input) => ({ + txid: input.txid, + vout: input.vout, + wif: ECPair.fromPrivateKey(Buffer.from(input.private_key, "hex")).toWIF(), + utxoType: getUTXOType(input) as UTXOType, + })); + const noEligibleUtxos = utxos.every((utxo) => utxo.utxoType === "non-eligible"); + + // Prepare the 'recipients' array + const recipients = sending.given.recipients.map((recipient) => ({ + address: recipient, + value: 1, + })); + + it(`Test Case: ${testCase.comment}`, () => { + const sp = new SilentPayment(); + if (noEligibleUtxos) { + expect(() => { + sp.createTransaction(utxos, recipients); + }).toThrow("No eligible UTXOs with private keys found"); + } else { + const generated = sp.createTransaction(utxos, recipients); + const generated_pubkeys: string[] = generated.map((obj) => obj.address).filter(Boolean) as string[]; + assert(matchSubset(generated_pubkeys, sending.expected.outputs)); + } }); + }); }); it("2 inputs - 0 SP outputs (just a passthrough)", () => { @@ -84,13 +84,13 @@ it("2 inputs - 0 SP outputs (just a passthrough)", () => { { txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", vout: 0, - WIF: ECPair.fromPrivateKey(Buffer.from("1cd5e8f6b3f29505ed1da7a5806291ebab6491c6a172467e44debe255428a192", "hex")).toWIF(), + wif: ECPair.fromPrivateKey(Buffer.from("1cd5e8f6b3f29505ed1da7a5806291ebab6491c6a172467e44debe255428a192", "hex")).toWIF(), utxoType: "p2wpkh", }, { txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", vout: 0, - WIF: ECPair.fromPrivateKey(Buffer.from("7416ef4d92e4dd09d680af6999d1723816e781c030f4b4ecb5bf46939ca30056", "hex")).toWIF(), + wif: ECPair.fromPrivateKey(Buffer.from("7416ef4d92e4dd09d680af6999d1723816e781c030f4b4ecb5bf46939ca30056", "hex")).toWIF(), utxoType: "p2wpkh", }, ], @@ -121,33 +121,37 @@ it("2 inputs - 0 SP outputs (just a passthrough)", () => { it("SilentPayment._outpointHash() works", () => { const A = ECPair.fromWIF("L4cJGJp4haLbS46ZKMKrjt7HqVuYTSHkChykdMrni955Fs3Sb8vq").publicKey; assert.deepStrictEqual( - SilentPayment._outpointsHash([ - { - txid: "a2365547d16b555593e3f58a2b67143fc8ab84e7e1257b1c13d2a9a2ec3a2efb", - vout: 0, - WIF: "", - utxoType: "p2wpkh", - }, - ], - A).toString("hex"), + SilentPayment._outpointsHash( + [ + { + txid: "a2365547d16b555593e3f58a2b67143fc8ab84e7e1257b1c13d2a9a2ec3a2efb", + vout: 0, + wif: "", + utxoType: "p2wpkh", + }, + ], + A + ).toString("hex"), "94d5923201f2f239e4d2d5a44239e0377325a343e4c068cfd078217adc663d7c" ); assert.deepStrictEqual( - SilentPayment._outpointsHash([ - { - txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - vout: 0, - WIF: "", - utxoType: "non-eligible" - }, - { - txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - vout: 0, - WIF: "", - utxoType: "p2wpkh", - }, - ], - A).toString("hex"), + SilentPayment._outpointsHash( + [ + { + txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + vout: 0, + wif: "", + utxoType: "non-eligible", + }, + { + txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + vout: 0, + wif: "", + utxoType: "p2wpkh", + }, + ], + A + ).toString("hex"), "3ea0693eeb0c7e848ad7b875f1998e9ed02905e88a6f5c45f25fa187b7f073d2" ); });