From 2a69c9d0a64e261d828f6d6ae2e09d7a78e9ac2c Mon Sep 17 00:00:00 2001 From: Louis Singer Date: Tue, 19 Sep 2023 11:16:21 +0200 Subject: [PATCH 1/5] silent payment basic scheme --- package.json | 4 +- src/bip341.js | 2 +- src/index.d.ts | 18 +- src/index.js | 28 +- src/silentpayment.d.ts | 53 ++ src/silentpayment.js | 248 ++++++++ test/fixtures/silent_payments.json | 823 +++++++++++++++++++++++++ test/integration/_regtest.ts | 41 ++ test/integration/psetv2.spec.ts | 93 ++- test/integration/silentpayment.spec.ts | 209 +++++++ test/silent-payment.spec.ts | 123 ++++ ts_src/index.ts | 27 +- ts_src/psetv2/pset.ts | 2 +- ts_src/psetv2/updater.ts | 3 +- ts_src/silentpayment.ts | 269 ++++++++ yarn.lock | 10 +- 16 files changed, 1851 insertions(+), 102 deletions(-) create mode 100644 src/silentpayment.d.ts create mode 100644 src/silentpayment.js create mode 100644 test/fixtures/silent_payments.json create mode 100644 test/integration/silentpayment.spec.ts create mode 100644 test/silent-payment.spec.ts create mode 100644 ts_src/silentpayment.ts diff --git a/package.json b/package.json index 5128396fa..cda16923c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "blech32": "^1.0.1", "bs58check": "^2.0.0", "create-hash": "^1.2.0", - "ecpair": "^2.1.0", "slip77": "^0.2.0", "typeforce": "^1.11.3", "varuint-bitcoin": "^1.1.2" @@ -78,6 +77,7 @@ "bn.js": "^4.11.8", "bs58": "^4.0.0", "dhttp": "^3.0.0", + "ecpair": "^2.1.0", "hoodwink": "^2.0.0", "minimaldata": "^1.0.2", "mocha": "^10.1.0", @@ -88,7 +88,7 @@ "randombytes": "^2.1.0", "regtest-client": "0.2.0", "rimraf": "^2.6.3", - "tiny-secp256k1": "^2.2.1", + "tiny-secp256k1": "^2.2.3", "ts-node": "^10.9.1", "tslint": "^6.1.3", "typescript": "^4.4.4" diff --git a/src/bip341.js b/src/bip341.js index 6255f4d2a..1335c5242 100644 --- a/src/bip341.js +++ b/src/bip341.js @@ -120,7 +120,7 @@ function taprootSignScriptStack(ecc) { return (internalPublicKey, leaf, treeRootHash, path) => { const { parity } = tweakPublicKey(internalPublicKey, treeRootHash, ecc); const parityBit = Buffer.of( - leaf.version || exports.LEAF_VERSION_TAPSCRIPT + parity, + (leaf.version || exports.LEAF_VERSION_TAPSCRIPT) + parity, ); const control = Buffer.concat([ parityBit, diff --git a/src/index.d.ts b/src/index.d.ts index 2692730a0..52c1eb706 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,12 +1,12 @@ -import * as address from './address'; -import * as crypto from './crypto'; -import * as networks from './networks'; -import * as payments from './payments'; -import * as script from './script'; -import * as issuance from './issuance'; -import * as bip341 from './bip341'; -import * as confidential from './confidential'; -export { address, confidential, crypto, networks, payments, script, issuance, bip341, }; +export * as address from './address'; +export * as crypto from './crypto'; +export * as networks from './networks'; +export * as payments from './payments'; +export * as script from './script'; +export * as issuance from './issuance'; +export * as bip341 from './bip341'; +export * as confidential from './confidential'; +export * as silentpayment from './silentpayment'; export { OPS as opcodes } from './ops'; export { Input as TxInput, Output as TxOutput, Transaction, } from './transaction'; export * from './asset'; diff --git a/src/index.js b/src/index.js index e4a9c75e3..cc99d419f 100644 --- a/src/index.js +++ b/src/index.js @@ -53,31 +53,25 @@ var __exportStar = Object.defineProperty(exports, '__esModule', { value: true }); exports.Transaction = exports.opcodes = + exports.silentpayment = + exports.confidential = exports.bip341 = exports.issuance = exports.script = exports.payments = exports.networks = exports.crypto = - exports.confidential = exports.address = void 0; -const address = __importStar(require('./address')); -exports.address = address; -const crypto = __importStar(require('./crypto')); -exports.crypto = crypto; -const networks = __importStar(require('./networks')); -exports.networks = networks; -const payments = __importStar(require('./payments')); -exports.payments = payments; -const script = __importStar(require('./script')); -exports.script = script; -const issuance = __importStar(require('./issuance')); -exports.issuance = issuance; -const bip341 = __importStar(require('./bip341')); -exports.bip341 = bip341; -const confidential = __importStar(require('./confidential')); -exports.confidential = confidential; +exports.address = __importStar(require('./address')); +exports.crypto = __importStar(require('./crypto')); +exports.networks = __importStar(require('./networks')); +exports.payments = __importStar(require('./payments')); +exports.script = __importStar(require('./script')); +exports.issuance = __importStar(require('./issuance')); +exports.bip341 = __importStar(require('./bip341')); +exports.confidential = __importStar(require('./confidential')); +exports.silentpayment = __importStar(require('./silentpayment')); var ops_1 = require('./ops'); Object.defineProperty(exports, 'opcodes', { enumerable: true, diff --git a/src/silentpayment.d.ts b/src/silentpayment.d.ts new file mode 100644 index 000000000..02ddeb766 --- /dev/null +++ b/src/silentpayment.d.ts @@ -0,0 +1,53 @@ +/// +import { bip341 } from '.'; +export declare type Target = { + silentPaymentAddress: string; + value: number; + asset: string; +}; +export declare type Output = { + scriptPubKey: string; + value: number; + asset: string; +}; +export interface TinySecp256k1Interface extends bip341.BIP341Secp256k1Interface { + privateMultiply: (key: Uint8Array, tweak: Uint8Array) => Uint8Array; + pointMultiply: (point: Uint8Array, tweak: Uint8Array) => Uint8Array | null; + pointAdd: (point1: Uint8Array, point2: Uint8Array) => Uint8Array | null; + pointFromScalar: (key: Uint8Array) => Uint8Array | null; + privateAdd: (key: Uint8Array, tweak: Uint8Array) => Uint8Array | null; + privateNegate: (key: Uint8Array) => Uint8Array; + ecdh: (pubkey: Uint8Array, privkey: Uint8Array) => Uint8Array; +} +export declare class SilentPaymentAddress { + readonly spendPublicKey: Buffer; + readonly scanPublicKey: Buffer; + constructor(spendPublicKey: Buffer, scanPublicKey: Buffer); + static decode(str: string): SilentPaymentAddress; + encode(): string; +} +export declare class SilentPayment { + private ecc; + constructor(ecc: TinySecp256k1Interface); + /** + * create the transaction outputs sending outpoints identified by *outpointHash* to the *targets* + * @param inputsOutpointsHash hash of the input outpoints sent to the targets + * @param sumInputsPrivKeys sum of input private keys + * @param targets silent payment addresses receiving value/asset pair + * @returns a list of "silent-payment" taproot outputs + */ + pay(inputsOutpointsHash: Buffer, sumInputsPrivKeys: Buffer, targets: Target[]): Output[]; + sumSecretKeys(outpointKeys: { + key: Buffer; + isTaproot?: boolean; + }[]): Buffer; + sumPublicKeys(keys: Buffer[]): Buffer; + makeSharedSecret(inputsOutpointsHash: Buffer, inputPubKey: Buffer, scanSecretKey: Buffer): Buffer; + makePublicKey(spendPubKey: Buffer, index: number, ecdhSharedSecret: Buffer): Buffer; + makeSecretKey(spendPrivKey: Buffer, index: number, ecdhSharedSecret: Buffer): Buffer; +} +export declare function ser32(i: number): Buffer; +export declare function outpointsHash(parameters: { + txid: string; + vout: number; +}[]): Buffer; diff --git a/src/silentpayment.js b/src/silentpayment.js new file mode 100644 index 000000000..532ed83f8 --- /dev/null +++ b/src/silentpayment.js @@ -0,0 +1,248 @@ +'use strict'; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if ( + !desc || + ('get' in desc ? !m.__esModule : desc.writable || desc.configurable) + ) { + desc = { + enumerable: true, + get: function () { + return m[k]; + }, + }; + } + Object.defineProperty(o, k2, desc); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.outpointsHash = + exports.ser32 = + exports.SilentPayment = + exports.SilentPaymentAddress = + void 0; +const crypto = __importStar(require('crypto')); +const bech32_1 = require('bech32'); +const crypto_1 = require('./crypto'); +const G = Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', +); +class SilentPaymentAddress { + constructor(spendPublicKey, scanPublicKey) { + this.spendPublicKey = spendPublicKey; + this.scanPublicKey = scanPublicKey; + if (spendPublicKey.length !== 33 || scanPublicKey.length !== 33) { + throw new Error( + 'Invalid public key length, expected 33 bytes public key', + ); + } + } + static decode(str) { + const result = bech32_1.bech32m.decode(str, 118); + const version = result.words.shift(); + if (version !== 0) { + throw new Error('Unexpected version of silent payment code'); + } + const data = bech32_1.bech32m.fromWords(result.words); + const scanPubKey = Buffer.from(data.slice(0, 33)); + const spendPubKey = Buffer.from(data.slice(33)); + return new SilentPaymentAddress(spendPubKey, scanPubKey); + } + encode() { + const data = Buffer.concat([this.scanPublicKey, this.spendPublicKey]); + const words = bech32_1.bech32m.toWords(data); + words.unshift(0); + return bech32_1.bech32m.encode('sp', words, 118); + } +} +exports.SilentPaymentAddress = SilentPaymentAddress; +class SilentPayment { + constructor(ecc) { + this.ecc = ecc; + } + /** + * create the transaction outputs sending outpoints identified by *outpointHash* to the *targets* + * @param inputsOutpointsHash hash of the input outpoints sent to the targets + * @param sumInputsPrivKeys sum of input private keys + * @param targets silent payment addresses receiving value/asset pair + * @returns a list of "silent-payment" taproot outputs + */ + pay(inputsOutpointsHash, sumInputsPrivKeys, targets) { + const silentPaymentGroups = []; + for (const target of targets) { + const addr = SilentPaymentAddress.decode(target.silentPaymentAddress); + // Addresses with the same Bscan key all belong to the same recipient + // *Liquid* also sort by asset + const recipient = silentPaymentGroups.find( + (group) => + Buffer.compare(group.scanPublicKey, addr.scanPublicKey) === 0, + ); + const newTarget = { ...target, address: addr }; + if (recipient) { + recipient.targets.push(newTarget); + } else { + silentPaymentGroups.push({ + scanPublicKey: addr.scanPublicKey, + targets: [newTarget], + }); + } + } + const outputs = []; + // Generating Pmn for each Bm in the group + for (const group of silentPaymentGroups) { + // Bscan * a * outpoint_hash + const ecdhSharedSecretStep = Buffer.from( + this.ecc.privateMultiply(inputsOutpointsHash, sumInputsPrivKeys), + ); + const ecdhSharedSecret = this.ecc.pointMultiply( + group.scanPublicKey, + ecdhSharedSecretStep, + ); + if (!ecdhSharedSecret) { + throw new Error('Invalid ecdh shared secret'); + } + let n = 0; + for (const target of group.targets) { + const tn = (0, crypto_1.sha256)( + Buffer.concat([ecdhSharedSecret, ser32(n)]), + ); + // Let Pmn = tn·G + Bm + const pubkey = Buffer.from( + this.ecc.pointAdd( + this.ecc.pointMultiply(G, tn), + target.address.spendPublicKey, + ), + ); + const output = { + // Encode as a BIP341 taproot output + scriptPubKey: Buffer.concat([ + Buffer.from([0x51, 0x20]), + pubkey.slice(1), + ]).toString('hex'), + value: target.value, + asset: target.asset, + }; + outputs.push(output); + n += 1; + } + } + return outputs; + } + sumSecretKeys(outpointKeys) { + const keys = []; + for (const { key, isTaproot } of outpointKeys) { + // If taproot, check if the seckey results in an odd y-value and negate if so + if (isTaproot && this.ecc.pointFromScalar(key)?.at(0) === 0x03) { + const negated = Buffer.from(this.ecc.privateNegate(key)); + keys.push(negated); + continue; + } + keys.push(key); + } + if (keys.length === 0) { + throw new Error('No UTXOs with private keys found'); + } + // summary of every item in array + const ret = keys.reduce((acc, key) => { + const sum = this.ecc.privateAdd(acc, key); + if (!sum) throw new Error('Invalid private key sum'); + return Buffer.from(sum); + }); + return ret; + } + // sum of public keys + sumPublicKeys(keys) { + return keys.reduce((acc, key) => { + const sum = this.ecc.pointAdd(acc, key); + if (!sum) throw new Error('Invalid public key sum'); + return Buffer.from(sum); + }); + } + // compute the ecdh shared secret from scan private keys + public tx data (outpoints & pubkeys) + // it may be useful to scan and spend coins owned by silent addresses. + makeSharedSecret(inputsOutpointsHash, inputPubKey, scanSecretKey) { + const ecdhSharedSecretStep = Buffer.from( + this.ecc.privateMultiply(inputsOutpointsHash, scanSecretKey), + ); + const ecdhSharedSecret = this.ecc.pointMultiply( + inputPubKey, + ecdhSharedSecretStep, + ); + if (!ecdhSharedSecret) { + throw new Error('Invalid ecdh shared secret'); + } + return Buffer.from(ecdhSharedSecret); + } + makePublicKey(spendPubKey, index, ecdhSharedSecret) { + const tn = (0, crypto_1.sha256)( + Buffer.concat([ecdhSharedSecret, ser32(index)]), + ); + const Tn = this.ecc.pointMultiply(G, tn); + if (!Tn) throw new Error('Invalid Tn'); + const pubkey = this.ecc.pointAdd(Tn, spendPubKey); + if (!pubkey) throw new Error('Invalid pubkey'); + return Buffer.from(pubkey); + } + makeSecretKey(spendPrivKey, index, ecdhSharedSecret) { + const tn = (0, crypto_1.sha256)( + Buffer.concat([ecdhSharedSecret, ser32(index)]), + ); + const privkey = this.ecc.privateAdd(spendPrivKey, tn); + if (!privkey) throw new Error('Invalid privkey'); + return Buffer.from(privkey); + } +} +exports.SilentPayment = SilentPayment; +function ser32(i) { + const returnValue = Buffer.allocUnsafe(4); + returnValue.writeUInt32BE(i); + return returnValue; +} +exports.ser32 = ser32; +function outpointsHash(parameters) { + let bufferConcat = Buffer.alloc(0); + const outpoints = []; + for (const parameter of parameters) { + outpoints.push( + Buffer.concat([ + Buffer.from(parameter.txid, 'hex').reverse(), + ser32(parameter.vout).reverse(), + ]), + ); + } + outpoints.sort(Buffer.compare); + for (const outpoint of outpoints) { + bufferConcat = Buffer.concat([bufferConcat, outpoint]); + } + return crypto.createHash('sha256').update(bufferConcat).digest(); +} +exports.outpointsHash = outpointsHash; diff --git a/test/fixtures/silent_payments.json b/test/fixtures/silent_payments.json new file mode 100644 index 000000000..9cc10f9a9 --- /dev/null +++ b/test/fixtures/silent_payments.json @@ -0,0 +1,823 @@ +[ + { + "comment": "Simple send: two inputs", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", + 1 + ] + ] + } + }, + { + "comment": "Simple send: two inputs, regtest", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sprt1qqw4c54wvvwt6m38rg6vz3gs46ukhtrqr6qttd9akwaqjkx0znnme6qertwh7y9gq3zy4hhlk7tlgssyuvvmgl9stw8catdph0zc3wayd9y4rrztx", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "848b21b6ab50bf12ea5829955b9557cd6d7c3e0e2b7a5448d9e46a6a935b69a2", + 1 + ] + ] + } + }, + { + "comment": "Simple send: two inputs, order reversed", + "given": { + "outpoints": [ + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ], + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", + 1 + ] + ] + } + }, + { + "comment": "Simple send: two inputs from the same transaction", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 3 + ], + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 7 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "162f2298705b3ddca01ce1d214eedff439df3927582938d08e29e464908db00b", + 1 + ] + ] + } + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "given": { + "outpoints": [ + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 7 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 3 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "d9ede52f7e1e64e36ccf895ca0250daad96b174987079c903519b17852b21a3f", + 1 + ] + ] + } + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "0aafdcdb5893ae813299b16eea75f34ec16653ac39171da04d7c4e6d2e09ab8e", + 1 + ] + ] + } + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + true + ], + [ + "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7", + true + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", + 1 + ] + ] + } + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + true + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + true + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", + 1 + ] + ] + } + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + true + ], + [ + "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "2b4ff8e5bc608cbdd12117171e7d265b6882ad597559caf67b5ecfaf15301dd0", + 1 + ] + ] + } + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + true + ], + [ + "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "75f501f319db549aaa613717bd7af44da566d4d859b67fe436946564fafc47a3", + 1 + ] + ] + } + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 2 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 3 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 2 + ], + [ + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + 3 + ] + ] + } + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 2 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 3 + ], + [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + 4 + ], + [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + 5 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 2 + ], + [ + "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", + 3 + ], + [ + "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", + 4 + ], + [ + "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b", + 5 + ] + ] + } + }, + { + "comment": "Receiving with labels: label with even parity", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqhmem6grvs4nacsu0v5v5mjs934j7qfgkdkj8c95gyuru3tjpulvcwky2dz", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "2cbceeab2a4982841eb7dc34b8b4f19c04bf3bc083ebf984f5664366778eb50f", + 1 + ] + ] + } + }, + { + "comment": "Receiving with labels: label with odd parity", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqc389f45lq7jyqt8jxq6fkskfukr2tlruf6w8cpcx2krntwe4fr9ykagp3j", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "6b4455de119f51bf4d4a12dea555f14a5dc2c1369af5fba4871c5367264c028d", + 1 + ] + ] + } + }, + { + "comment": "Receiving with labels: large label integer", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq4umqa5feskydh9xadc9jlc22c89tu0apcv72u2vkuwtsrgzf0uesq45zq9", + 1 + ] + ] + }, + "expected": { + "outputs": [ + [ + "c3473bfcbe5e4d20d0790ae91f1b339bc15b46de64ca068d140118d0e325b849", + 1 + ] + ] + } + }, + { + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 2 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 1 + ], + [ + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + 2 + ] + ] + } + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 3 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 4 + ] + ] + }, + "expected": { + "outputs": [ + [ + "8890c19f005d6f6add5fef92d37ac6b161b7fdd5c1aef6eed1d32be3f216ac4c", + 3 + ], + [ + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + 4 + ] + ] + } + }, + { + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; multiple recipients", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 5 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", + 6 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj", + 7 + ], + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj", + 8 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 5 + ], + [ + "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", + 6 + ], + [ + "1b90a42136fef9ff2ca192abffc7be4536dc83d4e61cf18ae078f7e92b297cce", + 7 + ], + [ + "87a82600c08a255bc97d172e10816e322967eed6a77c9f37dd926492d7fdc106", + 8 + ] + ] + } + }, + { + "comment": "Single recipient: use silent payments for sender change", + "given": { + "outpoints": [ + [ + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + 0 + ], + [ + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + 0 + ] + ], + "input_priv_keys": [ + [ + "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + false + ], + [ + "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", + false + ] + ], + "recipients": [ + [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + 1 + ], + [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqll5497pp2gcr4cmq0v5nv07x8u5jswmf8ap2q0kxmx8628mkqanyu63ck8", + 2 + ] + ] + }, + "expected": { + "outputs": [ + [ + "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", + 1 + ], + [ + "0050c52a32566c0dfb517e473c68fedce4bd4543d219348d3bbdceeeb5755e34", + 2 + ] + ] + } + } +] diff --git a/test/integration/_regtest.ts b/test/integration/_regtest.ts index 6a0779187..cc57124db 100644 --- a/test/integration/_regtest.ts +++ b/test/integration/_regtest.ts @@ -1,4 +1,15 @@ import axios from 'axios'; +import * as ecc from 'tiny-secp256k1'; +import { + BIP174SigningData, + Extractor, + Finalizer, + Pset, + Signer, + Transaction, + script, +} from '../../ts_src'; +import { ECDSAVerifier, SchnorrVerifier } from '../../ts_src/psetv2/pset'; const APIURL = process.env.APIURL || 'http://localhost:3001'; export const TESTNET_APIURL = 'https://blockstream.info/liquidtestnet/api'; @@ -101,3 +112,33 @@ export async function broadcast( function sleep(ms: number): Promise { return new Promise((res: any): any => setTimeout(res, ms)); } + +export function signTransaction( + pset: Pset, + signers: any[], + sighashType: number, + ecclib: ECDSAVerifier & SchnorrVerifier = ecc, +): Transaction { + const signer = new Signer(pset); + + signers.forEach((keyPairs, i) => { + const preimage = pset.getInputPreimage(i, sighashType); + keyPairs.forEach((kp: any) => { + const partialSig: BIP174SigningData = { + partialSig: { + pubkey: kp.publicKey, + signature: script.signature.encode(kp.sign(preimage), sighashType), + }, + }; + signer.addSignature(i, partialSig, Pset.ECDSASigValidator(ecclib)); + }); + }); + + if (!pset.validateAllSignatures(Pset.ECDSASigValidator(ecclib))) { + throw new Error('Failed to sign pset'); + } + + const finalizer = new Finalizer(pset); + finalizer.finalize(); + return Extractor.extract(pset); +} diff --git a/test/integration/psetv2.spec.ts b/test/integration/psetv2.spec.ts index 5955b370b..9faf8bc41 100644 --- a/test/integration/psetv2.spec.ts +++ b/test/integration/psetv2.spec.ts @@ -27,7 +27,6 @@ import { BIP371SigningData } from '../../ts_src/psetv2'; import { toXOnly } from '../../ts_src/psetv2/bip371'; import secp256k1 from '@vulpemventures/secp256k1-zkp'; import { issuanceEntropyFromInput } from '../../ts_src/issuance'; -import { ECDSAVerifier, SchnorrVerifier } from '../../ts_src/psetv2/pset'; const OPS = bscript.OPS; const { BIP341Factory } = bip341; @@ -72,7 +71,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { updater.addInputs(inputs); updater.addOutputs(outputs); - const rawTx = signTransaction( + const rawTx = regtestUtils.signTransaction( pset, [alice.keys], Transaction.SIGHASH_ALL, @@ -134,7 +133,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction(pset, [alice.keys], Transaction.SIGHASH_ALL); + const rawTx = regtestUtils.signTransaction( + pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(rawTx.toHex()); }); @@ -189,7 +192,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction(pset, [alice.keys], Transaction.SIGHASH_ALL); + const rawTx = regtestUtils.signTransaction( + pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(rawTx.toHex()); }); @@ -255,7 +262,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction( + const rawTx = regtestUtils.signTransaction( pset, [alice.keys, unconfidentialAlice.keys], Transaction.SIGHASH_ALL, @@ -314,7 +321,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction(pset, [alice.keys], Transaction.SIGHASH_ALL); + const rawTx = regtestUtils.signTransaction( + pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(rawTx.toHex()); }); @@ -370,7 +381,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction(pset, [alice.keys], Transaction.SIGHASH_ALL); + const rawTx = regtestUtils.signTransaction( + pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(rawTx.toHex()); }); @@ -426,7 +441,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction(pset, [alice.keys], Transaction.SIGHASH_ALL); + const rawTx = regtestUtils.signTransaction( + pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(rawTx.toHex()); }); @@ -484,7 +503,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ issuanceBlindingArgs, outputBlindingArgs }); - const issuanceTx = signTransaction( + const issuanceTx = regtestUtils.signTransaction( pset, [alice.keys], Transaction.SIGHASH_ALL, @@ -559,7 +578,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { outputBlindingArgs: reissuanceOutputBlindingArgs, }); - const reissuanceTx = signTransaction( + const reissuanceTx = regtestUtils.signTransaction( reissuancePset, [alice.keys, alice.keys], Transaction.SIGHASH_ALL, @@ -622,7 +641,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ issuanceBlindingArgs, outputBlindingArgs }); - const issuanceTx = signTransaction( + const issuanceTx = regtestUtils.signTransaction( pset, [alice.keys], Transaction.SIGHASH_ALL, @@ -697,7 +716,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { outputBlindingArgs: reissuanceOutputBlindingArgs, }); - const reissuanceTx = signTransaction( + const reissuanceTx = regtestUtils.signTransaction( reissuancePset, [alice.keys, alice.keys], Transaction.SIGHASH_ALL, @@ -757,7 +776,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const issuanceTx = signTransaction( + const issuanceTx = regtestUtils.signTransaction( pset, [alice.keys], Transaction.SIGHASH_ALL, @@ -826,7 +845,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { outputBlindingArgs: reissuanceOutputBlindingArgs, }); - const reissuanceTx = signTransaction( + const reissuanceTx = regtestUtils.signTransaction( reissuancePset, [alice.keys, alice.keys], Transaction.SIGHASH_ALL, @@ -889,7 +908,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ issuanceBlindingArgs, outputBlindingArgs }); - const rawTx = signTransaction(pset, [alice.keys], Transaction.SIGHASH_ALL); + const rawTx = regtestUtils.signTransaction( + pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(rawTx.toHex()); }); @@ -986,7 +1009,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction( + const rawTx = regtestUtils.signTransaction( pset, [alice.keys, bob.keys], Transaction.SIGHASH_ALL, @@ -1289,7 +1312,11 @@ describe('liquidjs-lib (transactions with psetv2)', () => { // need 3 signers const signersKeys = [...p2sh.keys].slice(0, 3); - const tx = signTransaction(pset, [signersKeys], Transaction.SIGHASH_ALL); + const tx = regtestUtils.signTransaction( + pset, + [signersKeys], + Transaction.SIGHASH_ALL, + ); await regtestUtils.broadcast(tx.toHex()); }, ); @@ -1348,7 +1375,7 @@ describe('liquidjs-lib (transactions with psetv2)', () => { zkpGenerator, ); blinder.blindLast({ outputBlindingArgs }); - const rawTx = signTransaction( + const rawTx = regtestUtils.signTransaction( pset, [alice.keys, bob.keys], Transaction.SIGHASH_ALL, @@ -1357,36 +1384,6 @@ describe('liquidjs-lib (transactions with psetv2)', () => { }); }); -function signTransaction( - pset: Pset, - signers: any[], - sighashType: number, - ecclib: ECDSAVerifier & SchnorrVerifier = ecc, -): Transaction { - const signer = new PsetSigner(pset); - - signers.forEach((keyPairs, i) => { - const preimage = pset.getInputPreimage(i, sighashType); - keyPairs.forEach((kp: any) => { - const partialSig: BIP174SigningData = { - partialSig: { - pubkey: kp.publicKey, - signature: bscript.signature.encode(kp.sign(preimage), sighashType), - }, - }; - signer.addSignature(i, partialSig, Pset.ECDSASigValidator(ecclib)); - }); - }); - - if (!pset.validateAllSignatures(Pset.ECDSASigValidator(ecclib))) { - throw new Error('Failed to sign pset'); - } - - const finalizer = new PsetFinalizer(pset); - finalizer.finalize(); - return PsetExtractor.extract(pset); -} - const serializeSchnnorrSig = (sig: Buffer, hashtype: number) => Buffer.concat([ sig, diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts new file mode 100644 index 000000000..1fa09ee34 --- /dev/null +++ b/test/integration/silentpayment.spec.ts @@ -0,0 +1,209 @@ +import secp256k1 from '@vulpemventures/secp256k1-zkp'; +import * as tinyecc from 'tiny-secp256k1'; +import * as assert from 'assert'; +import { + BIP371SigningData, + Creator, + CreatorInput, + CreatorOutput, + Extractor, + Finalizer, + Pset, + Signer, + Transaction, + Updater, + networks, + silentpayment, +} from '../../ts_src'; +import { createPayment, getInputData } from './utils'; +import { TinySecp256k1Interface } from '../../ts_src/silentpayment'; +import { broadcast, signTransaction } from './_regtest'; +import { ECPair } from '../ecc'; + +describe('Silent Payments', () => { + let ecc: TinySecp256k1Interface; + before(async () => { + const { ecc: stepEcc, ecdh } = await secp256k1(); + ecc = { + ...stepEcc, + ecdh: ecdh, + privateMultiply: stepEcc.privateMul, + pointAdd: tinyecc.pointAdd, + pointMultiply: (p: Uint8Array, tweak: Uint8Array) => + tinyecc.pointMultiply(p, tweak), + }; + }); + + it('should send payment to silent address', async () => { + // create and faucet an alice wallet + // bob will create a silent address, alice will send some L-BTC to it + const alice = createPayment('p2wpkh', undefined, undefined, false); + const aliceInputData = await getInputData(alice.payment, true, 'noredeem'); + + const bobKeyPairSpend = ECPair.makeRandom(); // sec in cold storage, pub public + const bobKeyPairScan = ECPair.makeRandom(); // sec & pub in hot storage, pub public + + // bob creates a silent address and shares it with alice + const bob = new silentpayment.SilentPaymentAddress( + bobKeyPairSpend.publicKey, + bobKeyPairScan.publicKey, + ).encode(); + + // alice adds the input + const inputs = [aliceInputData].map(({ hash, index }) => { + const txid: string = Buffer.from(hash).reverse().toString('hex'); + return new CreatorInput(txid, index); + }); + + const pset = Creator.newPset({ inputs }); + const updater = new Updater(pset); + updater.addInWitnessUtxo(0, aliceInputData.witnessUtxo); + updater.addInUtxoRangeProof(0, aliceInputData.witnessUtxo.rangeProof); + updater.addInSighashType(0, Transaction.SIGHASH_ALL); + + // alice creates the taproot "silent payment" outputs associated to bob's silent address + const outpointsHash = silentpayment.outpointsHash( + inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), + ); + const sumPrivateKeys = new silentpayment.SilentPayment(ecc).sumSecretKeys([ + { + key: alice.keys[0].privateKey, + }, + ]); + + const sendAmount = 1000; + const fee = 400; + const change = 1_0000_0000 - sendAmount - fee; + + const outputs = new silentpayment.SilentPayment(ecc).pay( + outpointsHash, + sumPrivateKeys, + [ + { + silentPaymentAddress: bob, + asset: networks.regtest.assetHash, + value: sendAmount, + }, + ], + ); + + // alice adds the outputs + updater.addOutputs( + outputs.map((o) => ({ + amount: o.value, + asset: o.asset, + script: Buffer.from(o.scriptPubKey, 'hex'), + })), + ); + + // add change & fee outputs + updater.addOutputs([ + { + amount: change, + asset: networks.regtest.assetHash, + script: alice.payment.output, + }, + { + amount: fee, + asset: networks.regtest.assetHash, + }, + ]); + + // alice signs the transaction + const signed = signTransaction( + updater.pset, + [alice.keys], + Transaction.SIGHASH_ALL, + ); + const tx = signed.toHex(); + await broadcast(tx); + + // check if bob can spend the output (key spend using private key) + const outputToSpend = signed.outs[0]; + + const bobInput = new CreatorInput(signed.getId(), 0); + + const bobOutput = new CreatorOutput( + networks.regtest.assetHash, + 600, + alice.payment.output, + ); + + const feeOutput = new CreatorOutput(networks.regtest.assetHash, fee); + + const bobPset = Creator.newPset({ + inputs: [bobInput], + outputs: [bobOutput, feeOutput], + }); + + const bobUpdater = new Updater(bobPset); + bobUpdater.addInWitnessUtxo(0, outputToSpend); + bobUpdater.addInSighashType(0, Transaction.SIGHASH_DEFAULT); + + // to sign the input, bob has to compute the right privKey + + const sp = new silentpayment.SilentPayment(ecc); + + // 1. sum the outpoints public keys + const inputPubKey = sp.sumPublicKeys([alice.keys[0].publicKey]); + + // 2. compute the tweak + const ecdhSharedSecret = sp.makeSharedSecret( + outpointsHash, + inputPubKey, + bobKeyPairScan.privateKey!, + ); + + // bob may recompute the pubkey to scan the chain + const pubkey = sp.makePublicKey( + bobKeyPairSpend.publicKey, + 0, + ecdhSharedSecret, + ); + assert.deepStrictEqual(pubkey.slice(1), outputToSpend.script.slice(2)); + + // 3. compute the privKey + let privKey = sp.makeSecretKey( + bobKeyPairSpend.privateKey!, + 0, + ecdhSharedSecret, + ); + const pubeyFromPrv = Buffer.from(ecc.pointFromScalar(privKey)!); + assert.deepStrictEqual(pubeyFromPrv.slice(1), pubkey.slice(1)); + + // negate if necessary + if (ecc.pointFromScalar(privKey)?.at(0) === 0x03) { + privKey = Buffer.from(ecc.privateNegate(privKey)); + } + + const preimage = bobPset.getInputPreimage( + 0, + Transaction.SIGHASH_DEFAULT, + networks.regtest.genesisBlockHash, + ); + + const signature = Buffer.from( + ecc.signSchnorr(preimage, privKey, Buffer.alloc(32)), + ); + const signer = new Signer(bobPset); + + const partialSig: BIP371SigningData = { + tapKeySig: serializeSchnnorrSig(signature, Transaction.SIGHASH_DEFAULT), + genesisBlockHash: networks.regtest.genesisBlockHash, + }; + signer.addSignature(0, partialSig, Pset.SchnorrSigValidator(ecc)); + + const finalizer = new Finalizer(bobPset); + finalizer.finalize(); + const bobTx = Extractor.extract(bobPset); + const hex = bobTx.toHex(); + + await broadcast(hex); + }); +}); + +const serializeSchnnorrSig = (sig: Buffer, hashtype: number) => + Buffer.concat([ + sig, + hashtype !== 0x00 ? Buffer.of(hashtype) : Buffer.alloc(0), + ]); diff --git a/test/silent-payment.spec.ts b/test/silent-payment.spec.ts new file mode 100644 index 000000000..99865c445 --- /dev/null +++ b/test/silent-payment.spec.ts @@ -0,0 +1,123 @@ +import secp256k1 from '@vulpemventures/secp256k1-zkp'; +import * as tinyecc from 'tiny-secp256k1'; +import assert from 'node:assert'; +import { ECPairFactory } from 'ecpair'; +import { networks, silentpayment } from '../ts_src'; +const { SilentPayment } = silentpayment; + +import jsonImput from './fixtures/silent_payments.json'; + +const ECPair = ECPairFactory(tinyecc); + +type TestCase = { + comment: string; + given: { + outpoints: [string, number][]; + input_priv_keys: [string, boolean][]; + recipients: [string, number][]; + }; + expected: { + outputs: [string, number][]; + }; +}; + +const tests = jsonImput as unknown as Array; + +describe('silentPayments', () => { + let ecc: any; + + before(async () => { + ecc = (await secp256k1()).ecc; + ecc = { + ...ecc, + privateMultiply: ecc.privateMul, + pointAdd: tinyecc.pointAdd, + pointMultiply: tinyecc.pointMultiply, + }; + }); + + /* Sending tests from the BIP352 test vectors */ + tests.forEach((testCase) => { + // Prepare the 'inputs' array + const inputs = testCase.given.outpoints.map((outpoint, idx) => ({ + txid: outpoint[0], + vout: outpoint[1], + WIF: ECPair.fromPrivateKey( + Buffer.from(testCase.given.input_priv_keys[idx][0], 'hex'), + ).toWIF(), + isTaproot: testCase.given.input_priv_keys[idx][1], + })); + + // Prepare the 'recipients' array + const recipients: silentpayment.Target[] = testCase.given.recipients.map( + (recipient) => ({ + silentPaymentAddress: recipient[0], + value: recipient[1], + asset: networks.regtest.assetHash, + }), + ); + + it(`Test Case: ${testCase.comment} works`, () => { + const sp = new SilentPayment(ecc); + const outpointsHash = silentpayment.outpointsHash(inputs); + const sumPrivateKeys = sp.sumSecretKeys(inputs.map(castWIF)); + + assert.deepStrictEqual( + sp.pay(outpointsHash, sumPrivateKeys, recipients), + testCase.expected.outputs.map((output) => { + const address = '5120' + output[0]; + const value = output[1]; + return { + scriptPubKey: address, + value: value, + asset: networks.regtest.assetHash, + }; + }), + ); + }); + }); + + it('silentpayment.outpointHash() works', () => { + assert.deepStrictEqual( + silentpayment + .outpointsHash([ + { + txid: 'a2365547d16b555593e3f58a2b67143fc8ab84e7e1257b1c13d2a9a2ec3a2efb', + vout: 0, + }, + ]) + .toString('hex'), + 'dc28dfeffd23899e1ec394a601ef543fa4f29c59e8548ceeca8f3b40fef5d041', + ); + + // multiple outpoints + + assert.deepStrictEqual( + silentpayment + .outpointsHash([ + { + txid: 'f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16', + vout: 0, + }, + { + txid: 'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d', + vout: 0, + }, + ]) + .toString('hex'), + '210fef5d624db17c965c7597e2c6c9f60ef440c831d149c43567c50158557f12', + ); + }); +}); + +function castWIF( + obj: T, +): Omit & { key: Buffer } { + const { WIF, ...rest } = obj; + const keyPair = ECPair.fromWIF(WIF); + if (!keyPair.privateKey) throw new Error('WIF is not a private key'); + return { + ...rest, + key: keyPair.privateKey, + }; +} diff --git a/ts_src/index.ts b/ts_src/index.ts index 9ef6bdc3f..ae87f2d14 100644 --- a/ts_src/index.ts +++ b/ts_src/index.ts @@ -1,21 +1,12 @@ -import * as address from './address'; -import * as crypto from './crypto'; -import * as networks from './networks'; -import * as payments from './payments'; -import * as script from './script'; -import * as issuance from './issuance'; -import * as bip341 from './bip341'; -import * as confidential from './confidential'; -export { - address, - confidential, - crypto, - networks, - payments, - script, - issuance, - bip341, -}; +export * as address from './address'; +export * as crypto from './crypto'; +export * as networks from './networks'; +export * as payments from './payments'; +export * as script from './script'; +export * as issuance from './issuance'; +export * as bip341 from './bip341'; +export * as confidential from './confidential'; +export * as silentpayment from './silentpayment'; export { OPS as opcodes } from './ops'; export { Input as TxInput, diff --git a/ts_src/psetv2/pset.ts b/ts_src/psetv2/pset.ts index da859ee2b..4565c8346 100644 --- a/ts_src/psetv2/pset.ts +++ b/ts_src/psetv2/pset.ts @@ -436,7 +436,7 @@ export class Pset { prevoutScripts, prevoutAssetsValues, sighashType, - genesisBlockHash!, + genesisBlockHash, leafHash, ); } diff --git a/ts_src/psetv2/updater.ts b/ts_src/psetv2/updater.ts index 989f2baaf..88fec407c 100644 --- a/ts_src/psetv2/updater.ts +++ b/ts_src/psetv2/updater.ts @@ -560,9 +560,10 @@ export class Updater { } const tweakedKey = input.getUtxo()!.script.slice(2); + const sighash = pset.getInputPreimage( inIndex, - input.sighashType!, + input.sighashType, genesisBlockHash, ); if (!validator(tweakedKey, sighash, sig)) { diff --git a/ts_src/silentpayment.ts b/ts_src/silentpayment.ts new file mode 100644 index 000000000..711e87ca6 --- /dev/null +++ b/ts_src/silentpayment.ts @@ -0,0 +1,269 @@ +import * as crypto from 'crypto'; +import { bech32m } from 'bech32'; +import { bip341 } from '.'; +import { sha256 } from './crypto'; + +export type Target = { + silentPaymentAddress: string; + value: number; + asset: string; +}; + +export type Output = { + scriptPubKey: string; + value: number; + asset: string; +}; + +// internal use only +type SilentPaymentGroup = { + scanPublicKey: Buffer; + targets: Array<{ + value: number; + address: SilentPaymentAddress; + asset: string; + }>; +}; + +const G = Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', +); + +export interface TinySecp256k1Interface + extends bip341.BIP341Secp256k1Interface { + privateMultiply: (key: Uint8Array, tweak: Uint8Array) => Uint8Array; + pointMultiply: (point: Uint8Array, tweak: Uint8Array) => Uint8Array | null; + pointAdd: (point1: Uint8Array, point2: Uint8Array) => Uint8Array | null; + pointFromScalar: (key: Uint8Array) => Uint8Array | null; + privateAdd: (key: Uint8Array, tweak: Uint8Array) => Uint8Array | null; + privateNegate: (key: Uint8Array) => Uint8Array; + ecdh: (pubkey: Uint8Array, privkey: Uint8Array) => Uint8Array; +} + +export class SilentPaymentAddress { + constructor(readonly spendPublicKey: Buffer, readonly scanPublicKey: Buffer) { + if (spendPublicKey.length !== 33 || scanPublicKey.length !== 33) { + throw new Error( + 'Invalid public key length, expected 33 bytes public key', + ); + } + } + + static decode(str: string): SilentPaymentAddress { + const result = bech32m.decode(str, 118); + const version = result.words.shift(); + if (version !== 0) { + throw new Error('Unexpected version of silent payment code'); + } + const data = bech32m.fromWords(result.words); + const scanPubKey = Buffer.from(data.slice(0, 33)); + const spendPubKey = Buffer.from(data.slice(33)); + return new SilentPaymentAddress(spendPubKey, scanPubKey); + } + + encode(): string { + const data = Buffer.concat([this.scanPublicKey, this.spendPublicKey]); + + const words = bech32m.toWords(data); + words.unshift(0); + return bech32m.encode('sp', words, 118); + } +} + +export class SilentPayment { + constructor(private ecc: TinySecp256k1Interface) {} + + /** + * create the transaction outputs sending outpoints identified by *outpointHash* to the *targets* + * @param inputsOutpointsHash hash of the input outpoints sent to the targets + * @param sumInputsPrivKeys sum of input private keys + * @param targets silent payment addresses receiving value/asset pair + * @returns a list of "silent-payment" taproot outputs + */ + pay( + inputsOutpointsHash: Buffer, + sumInputsPrivKeys: Buffer, + targets: Target[], + ): Output[] { + const silentPaymentGroups: Array = []; + for (const target of targets) { + const addr = SilentPaymentAddress.decode(target.silentPaymentAddress); + + // Addresses with the same Bscan key all belong to the same recipient + // *Liquid* also sort by asset + const recipient = silentPaymentGroups.find( + (group) => + Buffer.compare(group.scanPublicKey, addr.scanPublicKey) === 0, + ); + + const newTarget = { ...target, address: addr }; + + if (recipient) { + recipient.targets.push(newTarget); + } else { + silentPaymentGroups.push({ + scanPublicKey: addr.scanPublicKey, + targets: [newTarget], + }); + } + } + + const outputs: Output[] = []; + + // Generating Pmn for each Bm in the group + for (const group of silentPaymentGroups) { + // Bscan * a * outpoint_hash + const ecdhSharedSecretStep = Buffer.from( + this.ecc.privateMultiply(inputsOutpointsHash, sumInputsPrivKeys), + ); + const ecdhSharedSecret = this.ecc.pointMultiply( + group.scanPublicKey, + ecdhSharedSecretStep, + ); + + if (!ecdhSharedSecret) { + throw new Error('Invalid ecdh shared secret'); + } + + let n = 0; + for (const target of group.targets) { + const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(n)])); + + // Let Pmn = tn·G + Bm + const pubkey = Buffer.from( + this.ecc.pointAdd( + this.ecc.pointMultiply(G, tn)!, + target.address.spendPublicKey, + )!, + ); + + const output = { + // Encode as a BIP341 taproot output + scriptPubKey: Buffer.concat([ + Buffer.from([0x51, 0x20]), + pubkey.slice(1), + ]).toString('hex'), + value: target.value, + asset: target.asset, + }; + outputs.push(output); + n += 1; + } + } + return outputs; + } + + sumSecretKeys(outpointKeys: { key: Buffer; isTaproot?: boolean }[]): Buffer { + const keys: Array = []; + for (const { key, isTaproot } of outpointKeys) { + // If taproot, check if the seckey results in an odd y-value and negate if so + if (isTaproot && this.ecc.pointFromScalar(key)?.at(0) === 0x03) { + const negated = Buffer.from(this.ecc.privateNegate(key)); + keys.push(negated); + continue; + } + + keys.push(key); + } + + if (keys.length === 0) { + throw new Error('No UTXOs with private keys found'); + } + + // summary of every item in array + const ret = keys.reduce((acc, key) => { + const sum = this.ecc.privateAdd(acc, key); + if (!sum) throw new Error('Invalid private key sum'); + return Buffer.from(sum); + }); + + return ret; + } + + // sum of public keys + sumPublicKeys(keys: Buffer[]): Buffer { + return keys.reduce((acc, key) => { + const sum = this.ecc.pointAdd(acc, key); + if (!sum) throw new Error('Invalid public key sum'); + return Buffer.from(sum); + }); + } + + // compute the ecdh shared secret from scan private keys + public tx data (outpoints & pubkeys) + // it may be useful to scan and spend coins owned by silent addresses. + makeSharedSecret( + inputsOutpointsHash: Buffer, + inputPubKey: Buffer, + scanSecretKey: Buffer, + ): Buffer { + const ecdhSharedSecretStep = Buffer.from( + this.ecc.privateMultiply(inputsOutpointsHash, scanSecretKey), + ); + const ecdhSharedSecret = this.ecc.pointMultiply( + inputPubKey, + ecdhSharedSecretStep, + ); + + if (!ecdhSharedSecret) { + throw new Error('Invalid ecdh shared secret'); + } + + return Buffer.from(ecdhSharedSecret); + } + + makePublicKey( + spendPubKey: Buffer, + index: number, + ecdhSharedSecret: Buffer, + ): Buffer { + const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(index)])); + + const Tn = this.ecc.pointMultiply(G, tn); + if (!Tn) throw new Error('Invalid Tn'); + + const pubkey = this.ecc.pointAdd(Tn, spendPubKey); + if (!pubkey) throw new Error('Invalid pubkey'); + + return Buffer.from(pubkey); + } + + makeSecretKey( + spendPrivKey: Buffer, + index: number, + ecdhSharedSecret: Buffer, + ): Buffer { + const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(index)])); + + const privkey = this.ecc.privateAdd(spendPrivKey, tn); + if (!privkey) throw new Error('Invalid privkey'); + + return Buffer.from(privkey); + } +} + +export function ser32(i: number): Buffer { + const returnValue = Buffer.allocUnsafe(4); + returnValue.writeUInt32BE(i); + return returnValue; +} + +export function outpointsHash( + parameters: { txid: string; vout: number }[], +): Buffer { + let bufferConcat = Buffer.alloc(0); + const outpoints: Array = []; + for (const parameter of parameters) { + outpoints.push( + Buffer.concat([ + Buffer.from(parameter.txid, 'hex').reverse(), + ser32(parameter.vout).reverse(), + ]), + ); + } + outpoints.sort(Buffer.compare); + for (const outpoint of outpoints) { + bufferConcat = Buffer.concat([bufferConcat, outpoint]); + } + return crypto.createHash('sha256').update(bufferConcat).digest(); +} diff --git a/yarn.lock b/yarn.lock index 15675ca86..670ddd05b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -445,7 +445,7 @@ base-x@^3.0.2, base-x@^3.0.6: bech32@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== binary-extensions@^2.0.0: @@ -1810,10 +1810,10 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -tiny-secp256k1@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz#a61d4791b7031aa08a9453178a131349c3e10f9b" - integrity sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng== +tiny-secp256k1@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz#fe1dde11a64fcee2091157d4b78bcb300feb9b65" + integrity sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q== dependencies: uint8array-tools "0.0.7" From 0d938c2b20a8146bca7feef8d029a8dab467b970 Mon Sep 17 00:00:00 2001 From: Louis Singer Date: Tue, 19 Sep 2023 12:15:54 +0200 Subject: [PATCH 2/5] fix tests linting --- src/silentpayment.d.ts | 1 - test/integration/silentpayment.spec.ts | 3 +-- test/silent-payment.spec.ts | 8 ++++---- ts_src/silentpayment.ts | 1 - 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/silentpayment.d.ts b/src/silentpayment.d.ts index 02ddeb766..6777c2ca2 100644 --- a/src/silentpayment.d.ts +++ b/src/silentpayment.d.ts @@ -17,7 +17,6 @@ export interface TinySecp256k1Interface extends bip341.BIP341Secp256k1Interface pointFromScalar: (key: Uint8Array) => Uint8Array | null; privateAdd: (key: Uint8Array, tweak: Uint8Array) => Uint8Array | null; privateNegate: (key: Uint8Array) => Uint8Array; - ecdh: (pubkey: Uint8Array, privkey: Uint8Array) => Uint8Array; } export declare class SilentPaymentAddress { readonly spendPublicKey: Buffer; diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 1fa09ee34..aa7d693a7 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -23,10 +23,9 @@ import { ECPair } from '../ecc'; describe('Silent Payments', () => { let ecc: TinySecp256k1Interface; before(async () => { - const { ecc: stepEcc, ecdh } = await secp256k1(); + const { ecc: stepEcc } = await secp256k1(); ecc = { ...stepEcc, - ecdh: ecdh, privateMultiply: stepEcc.privateMul, pointAdd: tinyecc.pointAdd, pointMultiply: (p: Uint8Array, tweak: Uint8Array) => diff --git a/test/silent-payment.spec.ts b/test/silent-payment.spec.ts index 99865c445..a53ff59fb 100644 --- a/test/silent-payment.spec.ts +++ b/test/silent-payment.spec.ts @@ -1,6 +1,6 @@ import secp256k1 from '@vulpemventures/secp256k1-zkp'; import * as tinyecc from 'tiny-secp256k1'; -import assert from 'node:assert'; +import * as assert from 'assert'; import { ECPairFactory } from 'ecpair'; import { networks, silentpayment } from '../ts_src'; const { SilentPayment } = silentpayment; @@ -65,11 +65,11 @@ describe('silentPayments', () => { assert.deepStrictEqual( sp.pay(outpointsHash, sumPrivateKeys, recipients), testCase.expected.outputs.map((output) => { - const address = '5120' + output[0]; + const scriptPubKey = '5120' + output[0]; const value = output[1]; return { - scriptPubKey: address, - value: value, + scriptPubKey, + value, asset: networks.regtest.assetHash, }; }), diff --git a/ts_src/silentpayment.ts b/ts_src/silentpayment.ts index 711e87ca6..5ed6850b8 100644 --- a/ts_src/silentpayment.ts +++ b/ts_src/silentpayment.ts @@ -38,7 +38,6 @@ export interface TinySecp256k1Interface pointFromScalar: (key: Uint8Array) => Uint8Array | null; privateAdd: (key: Uint8Array, tweak: Uint8Array) => Uint8Array | null; privateNegate: (key: Uint8Array) => Uint8Array; - ecdh: (pubkey: Uint8Array, privkey: Uint8Array) => Uint8Array; } export class SilentPaymentAddress { From be19faa6d4d4333ee18810c84cf2111136c7dfef Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 20 Sep 2023 16:22:17 +0200 Subject: [PATCH 3/5] modify API after review --- src/silentpayment.d.ts | 46 +- src/silentpayment.js | 217 ++++--- test/fixtures/silent_payments.json | 823 ------------------------- test/integration/silentpayment.spec.ts | 87 +-- test/silent-payment.spec.ts | 123 ---- ts_src/silentpayment.ts | 308 ++++----- 6 files changed, 302 insertions(+), 1302 deletions(-) delete mode 100644 test/fixtures/silent_payments.json delete mode 100644 test/silent-payment.spec.ts diff --git a/src/silentpayment.d.ts b/src/silentpayment.d.ts index 6777c2ca2..60d1c6d51 100644 --- a/src/silentpayment.d.ts +++ b/src/silentpayment.d.ts @@ -1,16 +1,9 @@ /// -import { bip341 } from '.'; -export declare type Target = { - silentPaymentAddress: string; - value: number; - asset: string; -}; -export declare type Output = { - scriptPubKey: string; - value: number; - asset: string; +export declare type Outpoint = { + txid: string; + vout: number; }; -export interface TinySecp256k1Interface extends bip341.BIP341Secp256k1Interface { +export interface TinySecp256k1Interface { privateMultiply: (key: Uint8Array, tweak: Uint8Array) => Uint8Array; pointMultiply: (point: Uint8Array, tweak: Uint8Array) => Uint8Array | null; pointAdd: (point1: Uint8Array, point2: Uint8Array) => Uint8Array | null; @@ -18,6 +11,11 @@ export interface TinySecp256k1Interface extends bip341.BIP341Secp256k1Interface privateAdd: (key: Uint8Array, tweak: Uint8Array) => Uint8Array | null; privateNegate: (key: Uint8Array) => Uint8Array; } +export interface SilentPayment { + makeScriptPubKey(inputs: Outpoint[], inputPrivateKey: Buffer, silentPaymentAddress: string, index?: number): Buffer; + isMine(scriptPubKey: Buffer, inputs: Outpoint[], inputPublicKey: Buffer, scanSecretKey: Buffer, index?: number): boolean; + makeSigningKey(inputs: Outpoint[], inputPublicKey: Buffer, spendSecretKey: Buffer, index?: number): Buffer; +} export declare class SilentPaymentAddress { readonly spendPublicKey: Buffer; readonly scanPublicKey: Buffer; @@ -25,28 +23,4 @@ export declare class SilentPaymentAddress { static decode(str: string): SilentPaymentAddress; encode(): string; } -export declare class SilentPayment { - private ecc; - constructor(ecc: TinySecp256k1Interface); - /** - * create the transaction outputs sending outpoints identified by *outpointHash* to the *targets* - * @param inputsOutpointsHash hash of the input outpoints sent to the targets - * @param sumInputsPrivKeys sum of input private keys - * @param targets silent payment addresses receiving value/asset pair - * @returns a list of "silent-payment" taproot outputs - */ - pay(inputsOutpointsHash: Buffer, sumInputsPrivKeys: Buffer, targets: Target[]): Output[]; - sumSecretKeys(outpointKeys: { - key: Buffer; - isTaproot?: boolean; - }[]): Buffer; - sumPublicKeys(keys: Buffer[]): Buffer; - makeSharedSecret(inputsOutpointsHash: Buffer, inputPubKey: Buffer, scanSecretKey: Buffer): Buffer; - makePublicKey(spendPubKey: Buffer, index: number, ecdhSharedSecret: Buffer): Buffer; - makeSecretKey(spendPrivKey: Buffer, index: number, ecdhSharedSecret: Buffer): Buffer; -} -export declare function ser32(i: number): Buffer; -export declare function outpointsHash(parameters: { - txid: string; - vout: number; -}[]): Buffer; +export declare function SPFactory(ecc: TinySecp256k1Interface): SilentPayment; diff --git a/src/silentpayment.js b/src/silentpayment.js index 532ed83f8..3f7057ba8 100644 --- a/src/silentpayment.js +++ b/src/silentpayment.js @@ -44,18 +44,10 @@ var __importStar = return result; }; Object.defineProperty(exports, '__esModule', { value: true }); -exports.outpointsHash = - exports.ser32 = - exports.SilentPayment = - exports.SilentPaymentAddress = - void 0; +exports.SPFactory = exports.SilentPaymentAddress = void 0; const crypto = __importStar(require('crypto')); const bech32_1 = require('bech32'); const crypto_1 = require('./crypto'); -const G = Buffer.from( - '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', - 'hex', -); class SilentPaymentAddress { constructor(spendPublicKey, scanPublicKey) { this.spendPublicKey = spendPublicKey; @@ -85,116 +77,97 @@ class SilentPaymentAddress { } } exports.SilentPaymentAddress = SilentPaymentAddress; -class SilentPayment { +// inject ecc dependency, returns a SilentPayment interface +function SPFactory(ecc) { + return new SilentPaymentImpl(ecc); +} +exports.SPFactory = SPFactory; +const SEGWIT_V1_SCRIPT_PREFIX = Buffer.from([0x51, 0x20]); +const G = Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', +); +class SilentPaymentImpl { constructor(ecc) { this.ecc = ecc; } /** - * create the transaction outputs sending outpoints identified by *outpointHash* to the *targets* - * @param inputsOutpointsHash hash of the input outpoints sent to the targets - * @param sumInputsPrivKeys sum of input private keys - * @param targets silent payment addresses receiving value/asset pair - * @returns a list of "silent-payment" taproot outputs + * Compute scriptPubKey used to send funds to a silent payment address + * @param inputs list of ALL outpoints of the transaction sending to the silent payment address + * @param inputPrivateKey private key owning the spent outpoint. Sum of all private keys if multiple inputs + * @param silentPaymentAddress target of the scriptPubKey + * @param index index of the silent payment address. Prevent address reuse if multiple silent addresses are in the same transaction. + * @returns the output scriptPubKey belonging to the silent payment address */ - pay(inputsOutpointsHash, sumInputsPrivKeys, targets) { - const silentPaymentGroups = []; - for (const target of targets) { - const addr = SilentPaymentAddress.decode(target.silentPaymentAddress); - // Addresses with the same Bscan key all belong to the same recipient - // *Liquid* also sort by asset - const recipient = silentPaymentGroups.find( - (group) => - Buffer.compare(group.scanPublicKey, addr.scanPublicKey) === 0, - ); - const newTarget = { ...target, address: addr }; - if (recipient) { - recipient.targets.push(newTarget); - } else { - silentPaymentGroups.push({ - scanPublicKey: addr.scanPublicKey, - targets: [newTarget], - }); - } - } - const outputs = []; - // Generating Pmn for each Bm in the group - for (const group of silentPaymentGroups) { - // Bscan * a * outpoint_hash - const ecdhSharedSecretStep = Buffer.from( - this.ecc.privateMultiply(inputsOutpointsHash, sumInputsPrivKeys), - ); - const ecdhSharedSecret = this.ecc.pointMultiply( - group.scanPublicKey, - ecdhSharedSecretStep, - ); - if (!ecdhSharedSecret) { - throw new Error('Invalid ecdh shared secret'); - } - let n = 0; - for (const target of group.targets) { - const tn = (0, crypto_1.sha256)( - Buffer.concat([ecdhSharedSecret, ser32(n)]), - ); - // Let Pmn = tn·G + Bm - const pubkey = Buffer.from( - this.ecc.pointAdd( - this.ecc.pointMultiply(G, tn), - target.address.spendPublicKey, - ), - ); - const output = { - // Encode as a BIP341 taproot output - scriptPubKey: Buffer.concat([ - Buffer.from([0x51, 0x20]), - pubkey.slice(1), - ]).toString('hex'), - value: target.value, - asset: target.asset, - }; - outputs.push(output); - n += 1; - } - } - return outputs; + makeScriptPubKey(inputs, inputPrivateKey, silentPaymentAddress, index = 0) { + const inputsHash = hashOutpoints(inputs); + const addr = SilentPaymentAddress.decode(silentPaymentAddress); + const sharedSecret = this.makeSharedSecret( + inputsHash, + addr.scanPublicKey, + inputPrivateKey, + ); + const outputPublicKey = this.makePublicKey( + addr.spendPublicKey, + index, + sharedSecret, + ); + return Buffer.concat([SEGWIT_V1_SCRIPT_PREFIX, outputPublicKey.slice(1)]); } - sumSecretKeys(outpointKeys) { - const keys = []; - for (const { key, isTaproot } of outpointKeys) { - // If taproot, check if the seckey results in an odd y-value and negate if so - if (isTaproot && this.ecc.pointFromScalar(key)?.at(0) === 0x03) { - const negated = Buffer.from(this.ecc.privateNegate(key)); - keys.push(negated); - continue; - } - keys.push(key); - } - if (keys.length === 0) { - throw new Error('No UTXOs with private keys found'); - } - // summary of every item in array - const ret = keys.reduce((acc, key) => { - const sum = this.ecc.privateAdd(acc, key); - if (!sum) throw new Error('Invalid private key sum'); - return Buffer.from(sum); - }); - return ret; + /** + * Check if a scriptPubKey belongs to a silent payment address + * @param scriptPubKey scriptPubKey to check + * @param inputs list of ALL outpoints of the transaction sending to the silent payment address + * @param inputPublicKey public key owning the spent outpoint. Sum of all public keys if multiple inputs + * @param scanSecretKey private key of the silent payment address + * @param index index of the silent payment address. + */ + isMine(scriptPubKey, inputs, inputPublicKey, scanSecretKey, index = 0) { + const inputsHash = hashOutpoints(inputs); + const sharedSecret = this.makeSharedSecret( + inputsHash, + inputPublicKey, + scanSecretKey, + ); + const outputPublicKey = this.makePublicKey( + inputPublicKey, + index, + sharedSecret, + ); + return ( + Buffer.compare( + scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length), + outputPublicKey.slice(1), + ) === 0 + ); } - // sum of public keys - sumPublicKeys(keys) { - return keys.reduce((acc, key) => { - const sum = this.ecc.pointAdd(acc, key); - if (!sum) throw new Error('Invalid public key sum'); - return Buffer.from(sum); - }); + /** + * Compute the secret key used to spend an output locked by a silent address script. + * @param inputs outpoints of the transaction sending to the silent payment address + * @param inputPublicKey public key owning the spent outpoint in the tx (may be sum of public keys) + * @param spendSecretKey private key of the silent payment address + * @param index index of the silent payment address in the transaction, default to 0 + * @returns 32 bytes key + */ + makeSigningKey(inputs, inputPublicKey, spendSecretKey, index = 0) { + const inputsHash = hashOutpoints(inputs); + const sharedSecret = this.makeSharedSecret( + inputsHash, + inputPublicKey, + spendSecretKey, + ); + return this.makeSecretKey(spendSecretKey, index, sharedSecret); } - // compute the ecdh shared secret from scan private keys + public tx data (outpoints & pubkeys) - // it may be useful to scan and spend coins owned by silent addresses. - makeSharedSecret(inputsOutpointsHash, inputPubKey, scanSecretKey) { + /** + * ECDH shared secret used to share outpoints hash of the transactions. + * @param secret hash of the outpoints of the transaction sending to the silent payment address + */ + makeSharedSecret(secret, pubkey, seckey) { const ecdhSharedSecretStep = Buffer.from( - this.ecc.privateMultiply(inputsOutpointsHash, scanSecretKey), + this.ecc.privateMultiply(secret, seckey), ); const ecdhSharedSecret = this.ecc.pointMultiply( - inputPubKey, + pubkey, ecdhSharedSecretStep, ); if (!ecdhSharedSecret) { @@ -202,6 +175,13 @@ class SilentPayment { } return Buffer.from(ecdhSharedSecret); } + /** + * Compute the output public key of a silent payment address. + * @param spendPubKey spend public key of the silent payment address + * @param index index of the silent payment address. + * @param ecdhSharedSecret ecdh shared secret identifying the transaction. + * @returns 33 bytes public key + */ makePublicKey(spendPubKey, index, ecdhSharedSecret) { const tn = (0, crypto_1.sha256)( Buffer.concat([ecdhSharedSecret, ser32(index)]), @@ -212,23 +192,35 @@ class SilentPayment { if (!pubkey) throw new Error('Invalid pubkey'); return Buffer.from(pubkey); } + /** + * Compute the secret key locking the funds sent to a silent payment address. + * @param spendPrivKey spend private key of the silent payment address + * @param index index of the silent payment address. + * @param ecdhSharedSecret ecdh shared secret identifying the transaction + * @returns 32 bytes key + */ makeSecretKey(spendPrivKey, index, ecdhSharedSecret) { const tn = (0, crypto_1.sha256)( Buffer.concat([ecdhSharedSecret, ser32(index)]), ); - const privkey = this.ecc.privateAdd(spendPrivKey, tn); + let privkey = this.ecc.privateAdd(spendPrivKey, tn); if (!privkey) throw new Error('Invalid privkey'); + if (this.ecc.pointFromScalar(privkey)?.[0] === 0x03) { + privkey = this.ecc.privateNegate(privkey); + } return Buffer.from(privkey); } } -exports.SilentPayment = SilentPayment; function ser32(i) { const returnValue = Buffer.allocUnsafe(4); returnValue.writeUInt32BE(i); return returnValue; } -exports.ser32 = ser32; -function outpointsHash(parameters) { +/** + * Sort outpoints and hash them + * @param parameters list of outpoints + */ +function hashOutpoints(parameters) { let bufferConcat = Buffer.alloc(0); const outpoints = []; for (const parameter of parameters) { @@ -245,4 +237,3 @@ function outpointsHash(parameters) { } return crypto.createHash('sha256').update(bufferConcat).digest(); } -exports.outpointsHash = outpointsHash; diff --git a/test/fixtures/silent_payments.json b/test/fixtures/silent_payments.json deleted file mode 100644 index 9cc10f9a9..000000000 --- a/test/fixtures/silent_payments.json +++ /dev/null @@ -1,823 +0,0 @@ -[ - { - "comment": "Simple send: two inputs", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", - 1 - ] - ] - } - }, - { - "comment": "Simple send: two inputs, regtest", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", - false - ] - ], - "recipients": [ - [ - "sprt1qqw4c54wvvwt6m38rg6vz3gs46ukhtrqr6qttd9akwaqjkx0znnme6qertwh7y9gq3zy4hhlk7tlgssyuvvmgl9stw8catdph0zc3wayd9y4rrztx", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "848b21b6ab50bf12ea5829955b9557cd6d7c3e0e2b7a5448d9e46a6a935b69a2", - 1 - ] - ] - } - }, - { - "comment": "Simple send: two inputs, order reversed", - "given": { - "outpoints": [ - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ], - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "39a1e5ff6206cd316151b9b34cee4f80bb48ce61adee0a12ce7ff05ea436a1d9", - 1 - ] - ] - } - }, - { - "comment": "Simple send: two inputs from the same transaction", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 3 - ], - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 7 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "162f2298705b3ddca01ce1d214eedff439df3927582938d08e29e464908db00b", - 1 - ] - ] - } - }, - { - "comment": "Simple send: two inputs from the same transaction, order reversed", - "given": { - "outpoints": [ - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 7 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 3 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "d9ede52f7e1e64e36ccf895ca0250daad96b174987079c903519b17852b21a3f", - 1 - ] - ] - } - }, - { - "comment": "Single recipient: multiple UTXOs from the same public key", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "0aafdcdb5893ae813299b16eea75f34ec16653ac39171da04d7c4e6d2e09ab8e", - 1 - ] - ] - } - }, - { - "comment": "Single recipient: taproot only inputs with even y-values", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - true - ], - [ - "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7", - true - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", - 1 - ] - ] - } - }, - { - "comment": "Single recipient: taproot only with mixed even/odd y-values", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - true - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - true - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "15d1dfe4403791509cf47f073be2eb3277decabe90da395e63b1f49a09fe965e", - 1 - ] - ] - } - }, - { - "comment": "Single recipient: taproot input with even y-value and non-taproot input", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - true - ], - [ - "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "2b4ff8e5bc608cbdd12117171e7d265b6882ad597559caf67b5ecfaf15301dd0", - 1 - ] - ] - } - }, - { - "comment": "Single recipient: taproot input with odd y-value and non-taproot input", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - true - ], - [ - "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "75f501f319db549aaa613717bd7af44da566d4d859b67fe436946564fafc47a3", - 1 - ] - ] - } - }, - { - "comment": "Multiple outputs: multiple outputs, same recipient", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 2 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 3 - ] - ] - }, - "expected": { - "outputs": [ - [ - "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", - 2 - ], - [ - "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", - 3 - ] - ] - } - }, - { - "comment": "Multiple outputs: multiple outputs, multiple recipients", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 2 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 3 - ], - [ - "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", - 4 - ], - [ - "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", - 5 - ] - ] - }, - "expected": { - "outputs": [ - [ - "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", - 2 - ], - [ - "0a48c6ccc1d516e8244dc0153dc88db45f8f264357667c2057a29ca3c2445d09", - 3 - ], - [ - "c58e121044b23cba9b4695052229a9fd9e044b579f92864eb886ae7c99b021c9", - 4 - ], - [ - "4b15b75f3f184328c4a2f7c79357481ed06cf3b6f95512d5ed946fdc0b60d62b", - 5 - ] - ] - } - }, - { - "comment": "Receiving with labels: label with even parity", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqhmem6grvs4nacsu0v5v5mjs934j7qfgkdkj8c95gyuru3tjpulvcwky2dz", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "2cbceeab2a4982841eb7dc34b8b4f19c04bf3bc083ebf984f5664366778eb50f", - 1 - ] - ] - } - }, - { - "comment": "Receiving with labels: label with odd parity", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqc389f45lq7jyqt8jxq6fkskfukr2tlruf6w8cpcx2krntwe4fr9ykagp3j", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "6b4455de119f51bf4d4a12dea555f14a5dc2c1369af5fba4871c5367264c028d", - 1 - ] - ] - } - }, - { - "comment": "Receiving with labels: large label integer", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq4umqa5feskydh9xadc9jlc22c89tu0apcv72u2vkuwtsrgzf0uesq45zq9", - 1 - ] - ] - }, - "expected": { - "outputs": [ - [ - "c3473bfcbe5e4d20d0790ae91f1b339bc15b46de64ca068d140118d0e325b849", - 1 - ] - ] - } - }, - { - "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", - 2 - ] - ] - }, - "expected": { - "outputs": [ - [ - "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", - 1 - ], - [ - "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", - 2 - ] - ] - } - }, - { - "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", - 3 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", - 4 - ] - ] - }, - "expected": { - "outputs": [ - [ - "8890c19f005d6f6add5fef92d37ac6b161b7fdd5c1aef6eed1d32be3f216ac4c", - 3 - ], - [ - "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", - 4 - ] - ] - } - }, - { - "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; multiple recipients", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 5 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqah4hxfsjdwyaeel4g8x2npkj7qlvf2692l5760z5ut0ggnlrhdzsy3cvsj", - 6 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj", - 7 - ], - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq562yg7htxyg8eq60rl37uul37jy62apnf5ru62uef0eajpdfrnp5cmqndj", - 8 - ] - ] - }, - "expected": { - "outputs": [ - [ - "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", - 5 - ], - [ - "7956317130124c32afd07b3f2432a3e92c1447cf58da95491a307ae3d564535e", - 6 - ], - [ - "1b90a42136fef9ff2ca192abffc7be4536dc83d4e61cf18ae078f7e92b297cce", - 7 - ], - [ - "87a82600c08a255bc97d172e10816e322967eed6a77c9f37dd926492d7fdc106", - 8 - ] - ] - } - }, - { - "comment": "Single recipient: use silent payments for sender change", - "given": { - "outpoints": [ - [ - "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", - 0 - ], - [ - "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", - 0 - ] - ], - "input_priv_keys": [ - [ - "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", - false - ], - [ - "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a", - false - ] - ], - "recipients": [ - [ - "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", - 1 - ], - [ - "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqll5497pp2gcr4cmq0v5nv07x8u5jswmf8ap2q0kxmx8628mkqanyu63ck8", - 2 - ] - ] - }, - "expected": { - "outputs": [ - [ - "64f1c7e8992352d18cdbca600b9e1c3a6025050d56a3e1cc833222e4f3b59e18", - 1 - ], - [ - "0050c52a32566c0dfb517e473c68fedce4bd4543d219348d3bbdceeeb5755e34", - 2 - ] - ] - } - } -] diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index aa7d693a7..7794d322d 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -12,25 +12,28 @@ import { Signer, Transaction, Updater, + bip341, networks, silentpayment, } from '../../ts_src'; import { createPayment, getInputData } from './utils'; -import { TinySecp256k1Interface } from '../../ts_src/silentpayment'; import { broadcast, signTransaction } from './_regtest'; import { ECPair } from '../ecc'; describe('Silent Payments', () => { - let ecc: TinySecp256k1Interface; + let ecc: bip341.BIP341Secp256k1Interface; + let sp: silentpayment.SilentPayment; + before(async () => { const { ecc: stepEcc } = await secp256k1(); - ecc = { + const ecc = { ...stepEcc, privateMultiply: stepEcc.privateMul, pointAdd: tinyecc.pointAdd, pointMultiply: (p: Uint8Array, tweak: Uint8Array) => tinyecc.pointMultiply(p, tweak), }; + sp = silentpayment.SPFactory(ecc); }); it('should send payment to silent address', async () => { @@ -60,40 +63,23 @@ describe('Silent Payments', () => { updater.addInUtxoRangeProof(0, aliceInputData.witnessUtxo.rangeProof); updater.addInSighashType(0, Transaction.SIGHASH_ALL); - // alice creates the taproot "silent payment" outputs associated to bob's silent address - const outpointsHash = silentpayment.outpointsHash( - inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), - ); - const sumPrivateKeys = new silentpayment.SilentPayment(ecc).sumSecretKeys([ - { - key: alice.keys[0].privateKey, - }, - ]); - const sendAmount = 1000; const fee = 400; const change = 1_0000_0000 - sendAmount - fee; - const outputs = new silentpayment.SilentPayment(ecc).pay( - outpointsHash, - sumPrivateKeys, - [ - { - silentPaymentAddress: bob, - asset: networks.regtest.assetHash, - value: sendAmount, - }, - ], + const script = sp.makeScriptPubKey( + inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), + alice.keys[0].privateKey!, + bob, ); - // alice adds the outputs - updater.addOutputs( - outputs.map((o) => ({ - amount: o.value, - asset: o.asset, - script: Buffer.from(o.scriptPubKey, 'hex'), - })), - ); + updater.addOutputs([ + { + amount: sendAmount, + asset: networks.regtest.assetHash, + script, + }, + ]); // add change & fee outputs updater.addOutputs([ @@ -139,41 +125,22 @@ describe('Silent Payments', () => { bobUpdater.addInWitnessUtxo(0, outputToSpend); bobUpdater.addInSighashType(0, Transaction.SIGHASH_DEFAULT); - // to sign the input, bob has to compute the right privKey - - const sp = new silentpayment.SilentPayment(ecc); - - // 1. sum the outpoints public keys - const inputPubKey = sp.sumPublicKeys([alice.keys[0].publicKey]); - - // 2. compute the tweak - const ecdhSharedSecret = sp.makeSharedSecret( - outpointsHash, - inputPubKey, + // bob can use its scan private key to check if the output can be unlocked by its spend key + const isBob = sp.isMine( + outputToSpend.script, + inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), + alice.keys[0].publicKey!, bobKeyPairScan.privateKey!, ); - // bob may recompute the pubkey to scan the chain - const pubkey = sp.makePublicKey( - bobKeyPairSpend.publicKey, - 0, - ecdhSharedSecret, - ); - assert.deepStrictEqual(pubkey.slice(1), outputToSpend.script.slice(2)); + assert.strictEqual(isBob, true, 'bob should be able to spend the output'); - // 3. compute the privKey - let privKey = sp.makeSecretKey( + // bob can spend the output by computing the right signing key from the spend key + const privKey = sp.makeSigningKey( + inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), + alice.keys[0].publicKey!, bobKeyPairSpend.privateKey!, - 0, - ecdhSharedSecret, ); - const pubeyFromPrv = Buffer.from(ecc.pointFromScalar(privKey)!); - assert.deepStrictEqual(pubeyFromPrv.slice(1), pubkey.slice(1)); - - // negate if necessary - if (ecc.pointFromScalar(privKey)?.at(0) === 0x03) { - privKey = Buffer.from(ecc.privateNegate(privKey)); - } const preimage = bobPset.getInputPreimage( 0, diff --git a/test/silent-payment.spec.ts b/test/silent-payment.spec.ts deleted file mode 100644 index a53ff59fb..000000000 --- a/test/silent-payment.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import secp256k1 from '@vulpemventures/secp256k1-zkp'; -import * as tinyecc from 'tiny-secp256k1'; -import * as assert from 'assert'; -import { ECPairFactory } from 'ecpair'; -import { networks, silentpayment } from '../ts_src'; -const { SilentPayment } = silentpayment; - -import jsonImput from './fixtures/silent_payments.json'; - -const ECPair = ECPairFactory(tinyecc); - -type TestCase = { - comment: string; - given: { - outpoints: [string, number][]; - input_priv_keys: [string, boolean][]; - recipients: [string, number][]; - }; - expected: { - outputs: [string, number][]; - }; -}; - -const tests = jsonImput as unknown as Array; - -describe('silentPayments', () => { - let ecc: any; - - before(async () => { - ecc = (await secp256k1()).ecc; - ecc = { - ...ecc, - privateMultiply: ecc.privateMul, - pointAdd: tinyecc.pointAdd, - pointMultiply: tinyecc.pointMultiply, - }; - }); - - /* Sending tests from the BIP352 test vectors */ - tests.forEach((testCase) => { - // Prepare the 'inputs' array - const inputs = testCase.given.outpoints.map((outpoint, idx) => ({ - txid: outpoint[0], - vout: outpoint[1], - WIF: ECPair.fromPrivateKey( - Buffer.from(testCase.given.input_priv_keys[idx][0], 'hex'), - ).toWIF(), - isTaproot: testCase.given.input_priv_keys[idx][1], - })); - - // Prepare the 'recipients' array - const recipients: silentpayment.Target[] = testCase.given.recipients.map( - (recipient) => ({ - silentPaymentAddress: recipient[0], - value: recipient[1], - asset: networks.regtest.assetHash, - }), - ); - - it(`Test Case: ${testCase.comment} works`, () => { - const sp = new SilentPayment(ecc); - const outpointsHash = silentpayment.outpointsHash(inputs); - const sumPrivateKeys = sp.sumSecretKeys(inputs.map(castWIF)); - - assert.deepStrictEqual( - sp.pay(outpointsHash, sumPrivateKeys, recipients), - testCase.expected.outputs.map((output) => { - const scriptPubKey = '5120' + output[0]; - const value = output[1]; - return { - scriptPubKey, - value, - asset: networks.regtest.assetHash, - }; - }), - ); - }); - }); - - it('silentpayment.outpointHash() works', () => { - assert.deepStrictEqual( - silentpayment - .outpointsHash([ - { - txid: 'a2365547d16b555593e3f58a2b67143fc8ab84e7e1257b1c13d2a9a2ec3a2efb', - vout: 0, - }, - ]) - .toString('hex'), - 'dc28dfeffd23899e1ec394a601ef543fa4f29c59e8548ceeca8f3b40fef5d041', - ); - - // multiple outpoints - - assert.deepStrictEqual( - silentpayment - .outpointsHash([ - { - txid: 'f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16', - vout: 0, - }, - { - txid: 'a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d', - vout: 0, - }, - ]) - .toString('hex'), - '210fef5d624db17c965c7597e2c6c9f60ef440c831d149c43567c50158557f12', - ); - }); -}); - -function castWIF( - obj: T, -): Omit & { key: Buffer } { - const { WIF, ...rest } = obj; - const keyPair = ECPair.fromWIF(WIF); - if (!keyPair.privateKey) throw new Error('WIF is not a private key'); - return { - ...rest, - key: keyPair.privateKey, - }; -} diff --git a/ts_src/silentpayment.ts b/ts_src/silentpayment.ts index 5ed6850b8..a2c6380b4 100644 --- a/ts_src/silentpayment.ts +++ b/ts_src/silentpayment.ts @@ -1,37 +1,13 @@ import * as crypto from 'crypto'; import { bech32m } from 'bech32'; -import { bip341 } from '.'; import { sha256 } from './crypto'; -export type Target = { - silentPaymentAddress: string; - value: number; - asset: string; +export type Outpoint = { + txid: string; + vout: number; }; -export type Output = { - scriptPubKey: string; - value: number; - asset: string; -}; - -// internal use only -type SilentPaymentGroup = { - scanPublicKey: Buffer; - targets: Array<{ - value: number; - address: SilentPaymentAddress; - asset: string; - }>; -}; - -const G = Buffer.from( - '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', - 'hex', -); - -export interface TinySecp256k1Interface - extends bip341.BIP341Secp256k1Interface { +export interface TinySecp256k1Interface { privateMultiply: (key: Uint8Array, tweak: Uint8Array) => Uint8Array; pointMultiply: (point: Uint8Array, tweak: Uint8Array) => Uint8Array | null; pointAdd: (point1: Uint8Array, point2: Uint8Array) => Uint8Array | null; @@ -40,6 +16,28 @@ export interface TinySecp256k1Interface privateNegate: (key: Uint8Array) => Uint8Array; } +export interface SilentPayment { + makeScriptPubKey( + inputs: Outpoint[], + inputPrivateKey: Buffer, + silentPaymentAddress: string, + index?: number, + ): Buffer; + isMine( + scriptPubKey: Buffer, + inputs: Outpoint[], + inputPublicKey: Buffer, + scanSecretKey: Buffer, + index?: number, + ): boolean; + makeSigningKey( + inputs: Outpoint[], + inputPublicKey: Buffer, + spendSecretKey: Buffer, + index?: number, + ): Buffer; +} + export class SilentPaymentAddress { constructor(readonly spendPublicKey: Buffer, readonly scanPublicKey: Buffer) { if (spendPublicKey.length !== 33 || scanPublicKey.length !== 33) { @@ -70,137 +68,133 @@ export class SilentPaymentAddress { } } -export class SilentPayment { +// inject ecc dependency, returns a SilentPayment interface +export function SPFactory(ecc: TinySecp256k1Interface): SilentPayment { + return new SilentPaymentImpl(ecc); +} + +const SEGWIT_V1_SCRIPT_PREFIX = Buffer.from([0x51, 0x20]); + +const G = Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', +); + +class SilentPaymentImpl implements SilentPayment { constructor(private ecc: TinySecp256k1Interface) {} /** - * create the transaction outputs sending outpoints identified by *outpointHash* to the *targets* - * @param inputsOutpointsHash hash of the input outpoints sent to the targets - * @param sumInputsPrivKeys sum of input private keys - * @param targets silent payment addresses receiving value/asset pair - * @returns a list of "silent-payment" taproot outputs + * Compute scriptPubKey used to send funds to a silent payment address + * @param inputs list of ALL outpoints of the transaction sending to the silent payment address + * @param inputPrivateKey private key owning the spent outpoint. Sum of all private keys if multiple inputs + * @param silentPaymentAddress target of the scriptPubKey + * @param index index of the silent payment address. Prevent address reuse if multiple silent addresses are in the same transaction. + * @returns the output scriptPubKey belonging to the silent payment address */ - pay( - inputsOutpointsHash: Buffer, - sumInputsPrivKeys: Buffer, - targets: Target[], - ): Output[] { - const silentPaymentGroups: Array = []; - for (const target of targets) { - const addr = SilentPaymentAddress.decode(target.silentPaymentAddress); - - // Addresses with the same Bscan key all belong to the same recipient - // *Liquid* also sort by asset - const recipient = silentPaymentGroups.find( - (group) => - Buffer.compare(group.scanPublicKey, addr.scanPublicKey) === 0, - ); - - const newTarget = { ...target, address: addr }; - - if (recipient) { - recipient.targets.push(newTarget); - } else { - silentPaymentGroups.push({ - scanPublicKey: addr.scanPublicKey, - targets: [newTarget], - }); - } - } + makeScriptPubKey( + inputs: Outpoint[], + inputPrivateKey: Buffer, + silentPaymentAddress: string, + index = 0, + ): Buffer { + const inputsHash = hashOutpoints(inputs); + const addr = SilentPaymentAddress.decode(silentPaymentAddress); - const outputs: Output[] = []; + const sharedSecret = this.makeSharedSecret( + inputsHash, + addr.scanPublicKey, + inputPrivateKey, + ); - // Generating Pmn for each Bm in the group - for (const group of silentPaymentGroups) { - // Bscan * a * outpoint_hash - const ecdhSharedSecretStep = Buffer.from( - this.ecc.privateMultiply(inputsOutpointsHash, sumInputsPrivKeys), - ); - const ecdhSharedSecret = this.ecc.pointMultiply( - group.scanPublicKey, - ecdhSharedSecretStep, - ); + const outputPublicKey = this.makePublicKey( + addr.spendPublicKey, + index, + sharedSecret, + ); - if (!ecdhSharedSecret) { - throw new Error('Invalid ecdh shared secret'); - } - - let n = 0; - for (const target of group.targets) { - const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(n)])); - - // Let Pmn = tn·G + Bm - const pubkey = Buffer.from( - this.ecc.pointAdd( - this.ecc.pointMultiply(G, tn)!, - target.address.spendPublicKey, - )!, - ); - - const output = { - // Encode as a BIP341 taproot output - scriptPubKey: Buffer.concat([ - Buffer.from([0x51, 0x20]), - pubkey.slice(1), - ]).toString('hex'), - value: target.value, - asset: target.asset, - }; - outputs.push(output); - n += 1; - } - } - return outputs; + return Buffer.concat([SEGWIT_V1_SCRIPT_PREFIX, outputPublicKey.slice(1)]); } - sumSecretKeys(outpointKeys: { key: Buffer; isTaproot?: boolean }[]): Buffer { - const keys: Array = []; - for (const { key, isTaproot } of outpointKeys) { - // If taproot, check if the seckey results in an odd y-value and negate if so - if (isTaproot && this.ecc.pointFromScalar(key)?.at(0) === 0x03) { - const negated = Buffer.from(this.ecc.privateNegate(key)); - keys.push(negated); - continue; - } - - keys.push(key); - } - - if (keys.length === 0) { - throw new Error('No UTXOs with private keys found'); - } + /** + * Check if a scriptPubKey belongs to a silent payment address + * @param scriptPubKey scriptPubKey to check + * @param inputs list of ALL outpoints of the transaction sending to the silent payment address + * @param inputPublicKey public key owning the spent outpoint. Sum of all public keys if multiple inputs + * @param scanSecretKey private key of the silent payment address + * @param index index of the silent payment address. + */ + isMine( + scriptPubKey: Buffer, + inputs: Outpoint[], + inputPublicKey: Buffer, + scanSecretKey: Buffer, + index = 0, + ): boolean { + const inputsHash = hashOutpoints(inputs); + + const sharedSecret = this.makeSharedSecret( + inputsHash, + inputPublicKey, + scanSecretKey, + ); - // summary of every item in array - const ret = keys.reduce((acc, key) => { - const sum = this.ecc.privateAdd(acc, key); - if (!sum) throw new Error('Invalid private key sum'); - return Buffer.from(sum); - }); + const outputPublicKey = this.makePublicKey( + inputPublicKey, + index, + sharedSecret, + ); - return ret; + console.info( + 'isMine', + scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length).toString('hex'), + outputPublicKey.slice(1).toString('hex'), + ); + return ( + Buffer.compare( + scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length), + outputPublicKey.slice(1), + ) === 0 + ); } - // sum of public keys - sumPublicKeys(keys: Buffer[]): Buffer { - return keys.reduce((acc, key) => { - const sum = this.ecc.pointAdd(acc, key); - if (!sum) throw new Error('Invalid public key sum'); - return Buffer.from(sum); - }); + /** + * Compute the secret key used to spend an output locked by a silent address script. + * @param inputs outpoints of the transaction sending to the silent payment address + * @param inputPublicKey public key owning the spent outpoint in the tx (may be sum of public keys) + * @param spendSecretKey private key of the silent payment address + * @param index index of the silent payment address in the transaction, default to 0 + * @returns 32 bytes key + */ + makeSigningKey( + inputs: Outpoint[], + inputPublicKey: Buffer, + spendSecretKey: Buffer, + index = 0, + ): Buffer { + const inputsHash = hashOutpoints(inputs); + const sharedSecret = this.makeSharedSecret( + inputsHash, + inputPublicKey, + spendSecretKey, + ); + + return this.makeSecretKey(spendSecretKey, index, sharedSecret); } - // compute the ecdh shared secret from scan private keys + public tx data (outpoints & pubkeys) - // it may be useful to scan and spend coins owned by silent addresses. - makeSharedSecret( - inputsOutpointsHash: Buffer, - inputPubKey: Buffer, - scanSecretKey: Buffer, + /** + * ECDH shared secret used to share outpoints hash of the transactions. + * @param secret hash of the outpoints of the transaction sending to the silent payment address + */ + private makeSharedSecret( + secret: Buffer, + pubkey: Buffer, + seckey: Buffer, ): Buffer { const ecdhSharedSecretStep = Buffer.from( - this.ecc.privateMultiply(inputsOutpointsHash, scanSecretKey), + this.ecc.privateMultiply(secret, seckey), ); const ecdhSharedSecret = this.ecc.pointMultiply( - inputPubKey, + pubkey, ecdhSharedSecretStep, ); @@ -211,7 +205,14 @@ export class SilentPayment { return Buffer.from(ecdhSharedSecret); } - makePublicKey( + /** + * Compute the output public key of a silent payment address. + * @param spendPubKey spend public key of the silent payment address + * @param index index of the silent payment address. + * @param ecdhSharedSecret ecdh shared secret identifying the transaction. + * @returns 33 bytes public key + */ + private makePublicKey( spendPubKey: Buffer, index: number, ecdhSharedSecret: Buffer, @@ -227,29 +228,42 @@ export class SilentPayment { return Buffer.from(pubkey); } - makeSecretKey( + /** + * Compute the secret key locking the funds sent to a silent payment address. + * @param spendPrivKey spend private key of the silent payment address + * @param index index of the silent payment address. + * @param ecdhSharedSecret ecdh shared secret identifying the transaction + * @returns 32 bytes key + */ + private makeSecretKey( spendPrivKey: Buffer, index: number, ecdhSharedSecret: Buffer, ): Buffer { const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(index)])); - const privkey = this.ecc.privateAdd(spendPrivKey, tn); + let privkey = this.ecc.privateAdd(spendPrivKey, tn); if (!privkey) throw new Error('Invalid privkey'); + if (this.ecc.pointFromScalar(privkey)?.[0] === 0x03) { + privkey = this.ecc.privateNegate(privkey); + } + return Buffer.from(privkey); } } -export function ser32(i: number): Buffer { +function ser32(i: number): Buffer { const returnValue = Buffer.allocUnsafe(4); returnValue.writeUInt32BE(i); return returnValue; } -export function outpointsHash( - parameters: { txid: string; vout: number }[], -): Buffer { +/** + * Sort outpoints and hash them + * @param parameters list of outpoints + */ +function hashOutpoints(parameters: Outpoint[]): Buffer { let bufferConcat = Buffer.alloc(0); const outpoints: Array = []; for (const parameter of parameters) { From 279467e1921605d38daa891a6c0b832a078311d9 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 21 Sep 2023 10:43:23 +0200 Subject: [PATCH 4/5] fix new API --- src/silentpayment.js | 5 +++++ test/integration/silentpayment.spec.ts | 11 +++++++---- ts_src/silentpayment.ts | 16 ++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/silentpayment.js b/src/silentpayment.js index 3f7057ba8..94f5003f8 100644 --- a/src/silentpayment.js +++ b/src/silentpayment.js @@ -134,6 +134,11 @@ class SilentPaymentImpl { index, sharedSecret, ); + console.info( + 'isMine', + scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length).toString('hex'), + outputPublicKey.slice(1).toString('hex'), + ); return ( Buffer.compare( scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length), diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index 7794d322d..a82c9c56c 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -21,12 +21,13 @@ import { broadcast, signTransaction } from './_regtest'; import { ECPair } from '../ecc'; describe('Silent Payments', () => { - let ecc: bip341.BIP341Secp256k1Interface; + let ecc: silentpayment.TinySecp256k1Interface & + bip341.BIP341Secp256k1Interface; let sp: silentpayment.SilentPayment; before(async () => { const { ecc: stepEcc } = await secp256k1(); - const ecc = { + ecc = { ...stepEcc, privateMultiply: stepEcc.privateMul, pointAdd: tinyecc.pointAdd, @@ -125,20 +126,22 @@ describe('Silent Payments', () => { bobUpdater.addInWitnessUtxo(0, outputToSpend); bobUpdater.addInSighashType(0, Transaction.SIGHASH_DEFAULT); - // bob can use its scan private key to check if the output can be unlocked by its spend key + // bob can use its scan private key and spend pubkey to check if the output can be unlocked by its spend key const isBob = sp.isMine( outputToSpend.script, inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), alice.keys[0].publicKey!, bobKeyPairScan.privateKey!, + bobKeyPairSpend.publicKey!, ); assert.strictEqual(isBob, true, 'bob should be able to spend the output'); - // bob can spend the output by computing the right signing key from the spend key + // bob can spend the output by computing the right signing key from the both silent address secret keys const privKey = sp.makeSigningKey( inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), alice.keys[0].publicKey!, + bobKeyPairScan.privateKey!, bobKeyPairSpend.privateKey!, ); diff --git a/ts_src/silentpayment.ts b/ts_src/silentpayment.ts index a2c6380b4..6a86317f8 100644 --- a/ts_src/silentpayment.ts +++ b/ts_src/silentpayment.ts @@ -28,11 +28,13 @@ export interface SilentPayment { inputs: Outpoint[], inputPublicKey: Buffer, scanSecretKey: Buffer, + spendPublicKey: Buffer, index?: number, ): boolean; makeSigningKey( inputs: Outpoint[], inputPublicKey: Buffer, + scanSecretKey: Buffer, spendSecretKey: Buffer, index?: number, ): Buffer; @@ -120,7 +122,8 @@ class SilentPaymentImpl implements SilentPayment { * @param scriptPubKey scriptPubKey to check * @param inputs list of ALL outpoints of the transaction sending to the silent payment address * @param inputPublicKey public key owning the spent outpoint. Sum of all public keys if multiple inputs - * @param scanSecretKey private key of the silent payment address + * @param scanSecretKey *scan* secret key of the silent payment address + * @param spendPublicKey *spend* public key of the silent payment address * @param index index of the silent payment address. */ isMine( @@ -128,6 +131,7 @@ class SilentPaymentImpl implements SilentPayment { inputs: Outpoint[], inputPublicKey: Buffer, scanSecretKey: Buffer, + spendPublicKey: Buffer, index = 0, ): boolean { const inputsHash = hashOutpoints(inputs); @@ -139,16 +143,11 @@ class SilentPaymentImpl implements SilentPayment { ); const outputPublicKey = this.makePublicKey( - inputPublicKey, + spendPublicKey, index, sharedSecret, ); - console.info( - 'isMine', - scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length).toString('hex'), - outputPublicKey.slice(1).toString('hex'), - ); return ( Buffer.compare( scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length), @@ -168,6 +167,7 @@ class SilentPaymentImpl implements SilentPayment { makeSigningKey( inputs: Outpoint[], inputPublicKey: Buffer, + scanSecretKey: Buffer, spendSecretKey: Buffer, index = 0, ): Buffer { @@ -175,7 +175,7 @@ class SilentPaymentImpl implements SilentPayment { const sharedSecret = this.makeSharedSecret( inputsHash, inputPublicKey, - spendSecretKey, + scanSecretKey, ); return this.makeSecretKey(spendSecretKey, index, sharedSecret); From 2529301642d968187d0911289f210b066c428f73 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 25 Sep 2023 16:08:47 +0200 Subject: [PATCH 5/5] remove wrappers, exposes only base methods --- src/crypto.d.ts | 8 ++ src/crypto.js | 27 ++++- src/silentpayment.d.ts | 7 +- src/silentpayment.js | 153 +++---------------------- test/integration/silentpayment.spec.ts | 40 ++++--- ts_src/crypto.ts | 29 +++++ ts_src/silentpayment.ts | 153 +++++-------------------- 7 files changed, 139 insertions(+), 278 deletions(-) diff --git a/src/crypto.d.ts b/src/crypto.d.ts index d97f4c62f..4c76964f9 100644 --- a/src/crypto.d.ts +++ b/src/crypto.d.ts @@ -7,4 +7,12 @@ export declare function hash256(buffer: Buffer): Buffer; declare const TAGS: readonly ["BIP0340/challenge", "BIP0340/aux", "BIP0340/nonce", "TapLeaf", "TapLeaf/elements", "TapBranch/elements", "TapSighash", "TapSighash/elements", "TapTweak", "TapTweak/elements", "KeyAgg list", "KeyAgg coefficient"]; export declare type TaggedHashPrefix = typeof TAGS[number]; export declare function taggedHash(prefix: TaggedHashPrefix, data: Buffer): Buffer; +/** + * Serialize outpoint as txid | vout, sort them and sha256 the concatenated result + * @param parameters list of outpoints (txid, vout) + */ +export declare function hashOutpoints(parameters: { + txid: string; + vout: number; +}[]): Buffer; export {}; diff --git a/src/crypto.js b/src/crypto.js index 1d833e28f..339d06683 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -5,7 +5,8 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, '__esModule', { value: true }); -exports.taggedHash = +exports.hashOutpoints = + exports.taggedHash = exports.hash256 = exports.hash160 = exports.sha256 = @@ -62,3 +63,27 @@ function taggedHash(prefix, data) { return sha256(Buffer.concat([TAGGED_HASH_PREFIXES[prefix], data])); } exports.taggedHash = taggedHash; +/** + * Serialize outpoint as txid | vout, sort them and sha256 the concatenated result + * @param parameters list of outpoints (txid, vout) + */ +function hashOutpoints(parameters) { + let bufferConcat = Buffer.alloc(0); + const outpoints = []; + for (const parameter of parameters) { + const voutBuffer = Buffer.allocUnsafe(4); + voutBuffer.writeUint32BE(parameter.vout, 0); + outpoints.push( + Buffer.concat([ + Buffer.from(parameter.txid, 'hex').reverse(), + voutBuffer.reverse(), + ]), + ); + } + outpoints.sort(Buffer.compare); + for (const outpoint of outpoints) { + bufferConcat = Buffer.concat([bufferConcat, outpoint]); + } + return sha256(bufferConcat); +} +exports.hashOutpoints = hashOutpoints; diff --git a/src/silentpayment.d.ts b/src/silentpayment.d.ts index 60d1c6d51..45334dd86 100644 --- a/src/silentpayment.d.ts +++ b/src/silentpayment.d.ts @@ -12,9 +12,10 @@ export interface TinySecp256k1Interface { privateNegate: (key: Uint8Array) => Uint8Array; } export interface SilentPayment { - makeScriptPubKey(inputs: Outpoint[], inputPrivateKey: Buffer, silentPaymentAddress: string, index?: number): Buffer; - isMine(scriptPubKey: Buffer, inputs: Outpoint[], inputPublicKey: Buffer, scanSecretKey: Buffer, index?: number): boolean; - makeSigningKey(inputs: Outpoint[], inputPublicKey: Buffer, spendSecretKey: Buffer, index?: number): Buffer; + scriptPubKey(inputs: Outpoint[], inputPrivateKey: Buffer, silentPaymentAddress: string, index?: number): Buffer; + ecdhSharedSecret(secret: Buffer, pubkey: Buffer, seckey: Buffer): Buffer; + publicKey(spendPubKey: Buffer, index: number, ecdhSharedSecret: Buffer): Buffer; + secretKey(spendPrivKey: Buffer, index: number, ecdhSharedSecret: Buffer): Buffer; } export declare class SilentPaymentAddress { readonly spendPublicKey: Buffer; diff --git a/src/silentpayment.js b/src/silentpayment.js index 94f5003f8..44284f3c4 100644 --- a/src/silentpayment.js +++ b/src/silentpayment.js @@ -1,51 +1,6 @@ 'use strict'; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if ( - !desc || - ('get' in desc ? !m.__esModule : desc.writable || desc.configurable) - ) { - desc = { - enumerable: true, - get: function () { - return m[k]; - }, - }; - } - Object.defineProperty(o, k2, desc); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, 'default', { enumerable: true, value: v }); - } - : function (o, v) { - o['default'] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) - __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; Object.defineProperty(exports, '__esModule', { value: true }); exports.SPFactory = exports.SilentPaymentAddress = void 0; -const crypto = __importStar(require('crypto')); const bech32_1 = require('bech32'); const crypto_1 = require('./crypto'); class SilentPaymentAddress { @@ -99,75 +54,26 @@ class SilentPaymentImpl { * @param index index of the silent payment address. Prevent address reuse if multiple silent addresses are in the same transaction. * @returns the output scriptPubKey belonging to the silent payment address */ - makeScriptPubKey(inputs, inputPrivateKey, silentPaymentAddress, index = 0) { - const inputsHash = hashOutpoints(inputs); + scriptPubKey(inputs, inputPrivateKey, silentPaymentAddress, index = 0) { + const inputsHash = (0, crypto_1.hashOutpoints)(inputs); const addr = SilentPaymentAddress.decode(silentPaymentAddress); - const sharedSecret = this.makeSharedSecret( + const sharedSecret = this.ecdhSharedSecret( inputsHash, addr.scanPublicKey, inputPrivateKey, ); - const outputPublicKey = this.makePublicKey( + const outputPublicKey = this.publicKey( addr.spendPublicKey, index, sharedSecret, ); return Buffer.concat([SEGWIT_V1_SCRIPT_PREFIX, outputPublicKey.slice(1)]); } - /** - * Check if a scriptPubKey belongs to a silent payment address - * @param scriptPubKey scriptPubKey to check - * @param inputs list of ALL outpoints of the transaction sending to the silent payment address - * @param inputPublicKey public key owning the spent outpoint. Sum of all public keys if multiple inputs - * @param scanSecretKey private key of the silent payment address - * @param index index of the silent payment address. - */ - isMine(scriptPubKey, inputs, inputPublicKey, scanSecretKey, index = 0) { - const inputsHash = hashOutpoints(inputs); - const sharedSecret = this.makeSharedSecret( - inputsHash, - inputPublicKey, - scanSecretKey, - ); - const outputPublicKey = this.makePublicKey( - inputPublicKey, - index, - sharedSecret, - ); - console.info( - 'isMine', - scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length).toString('hex'), - outputPublicKey.slice(1).toString('hex'), - ); - return ( - Buffer.compare( - scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length), - outputPublicKey.slice(1), - ) === 0 - ); - } - /** - * Compute the secret key used to spend an output locked by a silent address script. - * @param inputs outpoints of the transaction sending to the silent payment address - * @param inputPublicKey public key owning the spent outpoint in the tx (may be sum of public keys) - * @param spendSecretKey private key of the silent payment address - * @param index index of the silent payment address in the transaction, default to 0 - * @returns 32 bytes key - */ - makeSigningKey(inputs, inputPublicKey, spendSecretKey, index = 0) { - const inputsHash = hashOutpoints(inputs); - const sharedSecret = this.makeSharedSecret( - inputsHash, - inputPublicKey, - spendSecretKey, - ); - return this.makeSecretKey(spendSecretKey, index, sharedSecret); - } /** * ECDH shared secret used to share outpoints hash of the transactions. * @param secret hash of the outpoints of the transaction sending to the silent payment address */ - makeSharedSecret(secret, pubkey, seckey) { + ecdhSharedSecret(secret, pubkey, seckey) { const ecdhSharedSecretStep = Buffer.from( this.ecc.privateMultiply(secret, seckey), ); @@ -187,13 +93,11 @@ class SilentPaymentImpl { * @param ecdhSharedSecret ecdh shared secret identifying the transaction. * @returns 33 bytes public key */ - makePublicKey(spendPubKey, index, ecdhSharedSecret) { - const tn = (0, crypto_1.sha256)( - Buffer.concat([ecdhSharedSecret, ser32(index)]), - ); - const Tn = this.ecc.pointMultiply(G, tn); - if (!Tn) throw new Error('Invalid Tn'); - const pubkey = this.ecc.pointAdd(Tn, spendPubKey); + publicKey(spendPubKey, index, ecdhSharedSecret) { + const hash = hashSharedSecret(ecdhSharedSecret, index); + const asPoint = this.ecc.pointMultiply(G, hash); + if (!asPoint) throw new Error('Invalid Tn'); + const pubkey = this.ecc.pointAdd(asPoint, spendPubKey); if (!pubkey) throw new Error('Invalid pubkey'); return Buffer.from(pubkey); } @@ -204,11 +108,9 @@ class SilentPaymentImpl { * @param ecdhSharedSecret ecdh shared secret identifying the transaction * @returns 32 bytes key */ - makeSecretKey(spendPrivKey, index, ecdhSharedSecret) { - const tn = (0, crypto_1.sha256)( - Buffer.concat([ecdhSharedSecret, ser32(index)]), - ); - let privkey = this.ecc.privateAdd(spendPrivKey, tn); + secretKey(spendPrivKey, index, ecdhSharedSecret) { + const hash = hashSharedSecret(ecdhSharedSecret, index); + let privkey = this.ecc.privateAdd(spendPrivKey, hash); if (!privkey) throw new Error('Invalid privkey'); if (this.ecc.pointFromScalar(privkey)?.[0] === 0x03) { privkey = this.ecc.privateNegate(privkey); @@ -216,29 +118,8 @@ class SilentPaymentImpl { return Buffer.from(privkey); } } -function ser32(i) { - const returnValue = Buffer.allocUnsafe(4); - returnValue.writeUInt32BE(i); - return returnValue; -} -/** - * Sort outpoints and hash them - * @param parameters list of outpoints - */ -function hashOutpoints(parameters) { - let bufferConcat = Buffer.alloc(0); - const outpoints = []; - for (const parameter of parameters) { - outpoints.push( - Buffer.concat([ - Buffer.from(parameter.txid, 'hex').reverse(), - ser32(parameter.vout).reverse(), - ]), - ); - } - outpoints.sort(Buffer.compare); - for (const outpoint of outpoints) { - bufferConcat = Buffer.concat([bufferConcat, outpoint]); - } - return crypto.createHash('sha256').update(bufferConcat).digest(); +function hashSharedSecret(secret, index) { + const serializedIndex = Buffer.allocUnsafe(4); + serializedIndex.writeUint32BE(index, 0); + return (0, crypto_1.sha256)(Buffer.concat([secret, serializedIndex])); } diff --git a/test/integration/silentpayment.spec.ts b/test/integration/silentpayment.spec.ts index a82c9c56c..a81aa86a5 100644 --- a/test/integration/silentpayment.spec.ts +++ b/test/integration/silentpayment.spec.ts @@ -19,6 +19,7 @@ import { import { createPayment, getInputData } from './utils'; import { broadcast, signTransaction } from './_regtest'; import { ECPair } from '../ecc'; +import { hashOutpoints } from '../../ts_src/crypto'; describe('Silent Payments', () => { let ecc: silentpayment.TinySecp256k1Interface & @@ -68,7 +69,7 @@ describe('Silent Payments', () => { const fee = 400; const change = 1_0000_0000 - sendAmount - fee; - const script = sp.makeScriptPubKey( + const script = sp.scriptPubKey( inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), alice.keys[0].privateKey!, bob, @@ -126,25 +127,38 @@ describe('Silent Payments', () => { bobUpdater.addInWitnessUtxo(0, outputToSpend); bobUpdater.addInSighashType(0, Transaction.SIGHASH_DEFAULT); - // bob can use its scan private key and spend pubkey to check if the output can be unlocked by its spend key - const isBob = sp.isMine( - outputToSpend.script, - inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), + const outpoints = inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })); + const inputsHash = hashOutpoints(outpoints); + + const sharedSecret = sp.ecdhSharedSecret( + inputsHash, alice.keys[0].publicKey!, bobKeyPairScan.privateKey!, - bobKeyPairSpend.publicKey!, ); - assert.strictEqual(isBob, true, 'bob should be able to spend the output'); + const outputPublicKey = sp.publicKey( + bobKeyPairSpend.publicKey!, + 0, + sharedSecret, + ); - // bob can spend the output by computing the right signing key from the both silent address secret keys - const privKey = sp.makeSigningKey( - inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })), - alice.keys[0].publicKey!, - bobKeyPairScan.privateKey!, - bobKeyPairSpend.privateKey!, + const isBob = outputPublicKey + .subarray(1) + .equals(outputToSpend.script.subarray(2)); + + assert.strictEqual( + isBob, + true, + `outputPublicKey ${outputPublicKey.toString( + 'hex', + )} is not equal to outputToSpend.script ${outputToSpend.script + .subarray(2) + .toString('hex')}}`, ); + // then bob can use its private key (spend one) to recompute the signing key and spend the ouput + const privKey = sp.secretKey(bobKeyPairSpend.privateKey!, 0, sharedSecret); + const preimage = bobPset.getInputPreimage( 0, Transaction.SIGHASH_DEFAULT, diff --git a/ts_src/crypto.ts b/ts_src/crypto.ts index 28e223640..ef6cb753e 100644 --- a/ts_src/crypto.ts +++ b/ts_src/crypto.ts @@ -50,3 +50,32 @@ const TAGGED_HASH_PREFIXES = Object.fromEntries( export function taggedHash(prefix: TaggedHashPrefix, data: Buffer): Buffer { return sha256(Buffer.concat([TAGGED_HASH_PREFIXES[prefix], data])); } + +/** + * Serialize outpoint as txid | vout, sort them and sha256 the concatenated result + * @param parameters list of outpoints (txid, vout) + */ +export function hashOutpoints( + parameters: { txid: string; vout: number }[], +): Buffer { + let bufferConcat = Buffer.alloc(0); + const outpoints: Array = []; + for (const parameter of parameters) { + const voutBuffer = Buffer.allocUnsafe(4); + voutBuffer.writeUint32BE(parameter.vout, 0); + + outpoints.push( + Buffer.concat([ + Buffer.from(parameter.txid, 'hex').reverse(), + voutBuffer.reverse(), + ]), + ); + } + + outpoints.sort(Buffer.compare); + + for (const outpoint of outpoints) { + bufferConcat = Buffer.concat([bufferConcat, outpoint]); + } + return sha256(bufferConcat); +} diff --git a/ts_src/silentpayment.ts b/ts_src/silentpayment.ts index 6a86317f8..2b8bbe6d7 100644 --- a/ts_src/silentpayment.ts +++ b/ts_src/silentpayment.ts @@ -1,6 +1,5 @@ -import * as crypto from 'crypto'; import { bech32m } from 'bech32'; -import { sha256 } from './crypto'; +import { hashOutpoints, sha256 } from './crypto'; export type Outpoint = { txid: string; @@ -17,26 +16,22 @@ export interface TinySecp256k1Interface { } export interface SilentPayment { - makeScriptPubKey( + scriptPubKey( inputs: Outpoint[], inputPrivateKey: Buffer, silentPaymentAddress: string, index?: number, ): Buffer; - isMine( - scriptPubKey: Buffer, - inputs: Outpoint[], - inputPublicKey: Buffer, - scanSecretKey: Buffer, - spendPublicKey: Buffer, - index?: number, - ): boolean; - makeSigningKey( - inputs: Outpoint[], - inputPublicKey: Buffer, - scanSecretKey: Buffer, - spendSecretKey: Buffer, - index?: number, + ecdhSharedSecret(secret: Buffer, pubkey: Buffer, seckey: Buffer): Buffer; + publicKey( + spendPubKey: Buffer, + index: number, + ecdhSharedSecret: Buffer, + ): Buffer; + secretKey( + spendPrivKey: Buffer, + index: number, + ecdhSharedSecret: Buffer, ): Buffer; } @@ -93,7 +88,7 @@ class SilentPaymentImpl implements SilentPayment { * @param index index of the silent payment address. Prevent address reuse if multiple silent addresses are in the same transaction. * @returns the output scriptPubKey belonging to the silent payment address */ - makeScriptPubKey( + scriptPubKey( inputs: Outpoint[], inputPrivateKey: Buffer, silentPaymentAddress: string, @@ -102,13 +97,13 @@ class SilentPaymentImpl implements SilentPayment { const inputsHash = hashOutpoints(inputs); const addr = SilentPaymentAddress.decode(silentPaymentAddress); - const sharedSecret = this.makeSharedSecret( + const sharedSecret = this.ecdhSharedSecret( inputsHash, addr.scanPublicKey, inputPrivateKey, ); - const outputPublicKey = this.makePublicKey( + const outputPublicKey = this.publicKey( addr.spendPublicKey, index, sharedSecret, @@ -117,79 +112,11 @@ class SilentPaymentImpl implements SilentPayment { return Buffer.concat([SEGWIT_V1_SCRIPT_PREFIX, outputPublicKey.slice(1)]); } - /** - * Check if a scriptPubKey belongs to a silent payment address - * @param scriptPubKey scriptPubKey to check - * @param inputs list of ALL outpoints of the transaction sending to the silent payment address - * @param inputPublicKey public key owning the spent outpoint. Sum of all public keys if multiple inputs - * @param scanSecretKey *scan* secret key of the silent payment address - * @param spendPublicKey *spend* public key of the silent payment address - * @param index index of the silent payment address. - */ - isMine( - scriptPubKey: Buffer, - inputs: Outpoint[], - inputPublicKey: Buffer, - scanSecretKey: Buffer, - spendPublicKey: Buffer, - index = 0, - ): boolean { - const inputsHash = hashOutpoints(inputs); - - const sharedSecret = this.makeSharedSecret( - inputsHash, - inputPublicKey, - scanSecretKey, - ); - - const outputPublicKey = this.makePublicKey( - spendPublicKey, - index, - sharedSecret, - ); - - return ( - Buffer.compare( - scriptPubKey.slice(SEGWIT_V1_SCRIPT_PREFIX.length), - outputPublicKey.slice(1), - ) === 0 - ); - } - - /** - * Compute the secret key used to spend an output locked by a silent address script. - * @param inputs outpoints of the transaction sending to the silent payment address - * @param inputPublicKey public key owning the spent outpoint in the tx (may be sum of public keys) - * @param spendSecretKey private key of the silent payment address - * @param index index of the silent payment address in the transaction, default to 0 - * @returns 32 bytes key - */ - makeSigningKey( - inputs: Outpoint[], - inputPublicKey: Buffer, - scanSecretKey: Buffer, - spendSecretKey: Buffer, - index = 0, - ): Buffer { - const inputsHash = hashOutpoints(inputs); - const sharedSecret = this.makeSharedSecret( - inputsHash, - inputPublicKey, - scanSecretKey, - ); - - return this.makeSecretKey(spendSecretKey, index, sharedSecret); - } - /** * ECDH shared secret used to share outpoints hash of the transactions. * @param secret hash of the outpoints of the transaction sending to the silent payment address */ - private makeSharedSecret( - secret: Buffer, - pubkey: Buffer, - seckey: Buffer, - ): Buffer { + ecdhSharedSecret(secret: Buffer, pubkey: Buffer, seckey: Buffer): Buffer { const ecdhSharedSecretStep = Buffer.from( this.ecc.privateMultiply(secret, seckey), ); @@ -212,17 +139,16 @@ class SilentPaymentImpl implements SilentPayment { * @param ecdhSharedSecret ecdh shared secret identifying the transaction. * @returns 33 bytes public key */ - private makePublicKey( + publicKey( spendPubKey: Buffer, index: number, ecdhSharedSecret: Buffer, ): Buffer { - const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(index)])); - - const Tn = this.ecc.pointMultiply(G, tn); - if (!Tn) throw new Error('Invalid Tn'); + const hash = hashSharedSecret(ecdhSharedSecret, index); + const asPoint = this.ecc.pointMultiply(G, hash); + if (!asPoint) throw new Error('Invalid Tn'); - const pubkey = this.ecc.pointAdd(Tn, spendPubKey); + const pubkey = this.ecc.pointAdd(asPoint, spendPubKey); if (!pubkey) throw new Error('Invalid pubkey'); return Buffer.from(pubkey); @@ -235,14 +161,13 @@ class SilentPaymentImpl implements SilentPayment { * @param ecdhSharedSecret ecdh shared secret identifying the transaction * @returns 32 bytes key */ - private makeSecretKey( + secretKey( spendPrivKey: Buffer, index: number, ecdhSharedSecret: Buffer, ): Buffer { - const tn = sha256(Buffer.concat([ecdhSharedSecret, ser32(index)])); - - let privkey = this.ecc.privateAdd(spendPrivKey, tn); + const hash = hashSharedSecret(ecdhSharedSecret, index); + let privkey = this.ecc.privateAdd(spendPrivKey, hash); if (!privkey) throw new Error('Invalid privkey'); if (this.ecc.pointFromScalar(privkey)?.[0] === 0x03) { @@ -253,30 +178,8 @@ class SilentPaymentImpl implements SilentPayment { } } -function ser32(i: number): Buffer { - const returnValue = Buffer.allocUnsafe(4); - returnValue.writeUInt32BE(i); - return returnValue; -} - -/** - * Sort outpoints and hash them - * @param parameters list of outpoints - */ -function hashOutpoints(parameters: Outpoint[]): Buffer { - let bufferConcat = Buffer.alloc(0); - const outpoints: Array = []; - for (const parameter of parameters) { - outpoints.push( - Buffer.concat([ - Buffer.from(parameter.txid, 'hex').reverse(), - ser32(parameter.vout).reverse(), - ]), - ); - } - outpoints.sort(Buffer.compare); - for (const outpoint of outpoints) { - bufferConcat = Buffer.concat([bufferConcat, outpoint]); - } - return crypto.createHash('sha256').update(bufferConcat).digest(); +function hashSharedSecret(secret: Buffer, index: number): Buffer { + const serializedIndex = Buffer.allocUnsafe(4); + serializedIndex.writeUint32BE(index, 0); + return sha256(Buffer.concat([secret, serializedIndex])); }