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/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/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..45334dd86
--- /dev/null
+++ b/src/silentpayment.d.ts
@@ -0,0 +1,27 @@
+///
+export declare type Outpoint = {
+ txid: string;
+ vout: number;
+};
+export interface TinySecp256k1Interface {
+ 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;
+}
+export interface SilentPayment {
+ 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;
+ readonly scanPublicKey: Buffer;
+ constructor(spendPublicKey: Buffer, scanPublicKey: Buffer);
+ static decode(str: string): SilentPaymentAddress;
+ encode(): string;
+}
+export declare function SPFactory(ecc: TinySecp256k1Interface): SilentPayment;
diff --git a/src/silentpayment.js b/src/silentpayment.js
new file mode 100644
index 000000000..44284f3c4
--- /dev/null
+++ b/src/silentpayment.js
@@ -0,0 +1,125 @@
+'use strict';
+Object.defineProperty(exports, '__esModule', { value: true });
+exports.SPFactory = exports.SilentPaymentAddress = void 0;
+const bech32_1 = require('bech32');
+const crypto_1 = require('./crypto');
+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;
+// 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;
+ }
+ /**
+ * 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
+ */
+ scriptPubKey(inputs, inputPrivateKey, silentPaymentAddress, index = 0) {
+ const inputsHash = (0, crypto_1.hashOutpoints)(inputs);
+ const addr = SilentPaymentAddress.decode(silentPaymentAddress);
+ const sharedSecret = this.ecdhSharedSecret(
+ inputsHash,
+ addr.scanPublicKey,
+ inputPrivateKey,
+ );
+ const outputPublicKey = this.publicKey(
+ addr.spendPublicKey,
+ index,
+ sharedSecret,
+ );
+ return Buffer.concat([SEGWIT_V1_SCRIPT_PREFIX, outputPublicKey.slice(1)]);
+ }
+ /**
+ * 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
+ */
+ ecdhSharedSecret(secret, pubkey, seckey) {
+ const ecdhSharedSecretStep = Buffer.from(
+ this.ecc.privateMultiply(secret, seckey),
+ );
+ const ecdhSharedSecret = this.ecc.pointMultiply(
+ pubkey,
+ ecdhSharedSecretStep,
+ );
+ if (!ecdhSharedSecret) {
+ throw new Error('Invalid ecdh shared secret');
+ }
+ 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
+ */
+ 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);
+ }
+ /**
+ * 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
+ */
+ 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);
+ }
+ return Buffer.from(privkey);
+ }
+}
+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/_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..a81aa86a5
--- /dev/null
+++ b/test/integration/silentpayment.spec.ts
@@ -0,0 +1,192 @@
+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,
+ bip341,
+ networks,
+ silentpayment,
+} from '../../ts_src';
+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 &
+ bip341.BIP341Secp256k1Interface;
+ let sp: silentpayment.SilentPayment;
+
+ before(async () => {
+ const { ecc: stepEcc } = await secp256k1();
+ 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 () => {
+ // 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);
+
+ const sendAmount = 1000;
+ const fee = 400;
+ const change = 1_0000_0000 - sendAmount - fee;
+
+ const script = sp.scriptPubKey(
+ inputs.map((i) => ({ txid: i.txid, vout: i.txIndex })),
+ alice.keys[0].privateKey!,
+ bob,
+ );
+
+ updater.addOutputs([
+ {
+ amount: sendAmount,
+ asset: networks.regtest.assetHash,
+ script,
+ },
+ ]);
+
+ // 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);
+
+ 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!,
+ );
+
+ const outputPublicKey = sp.publicKey(
+ bobKeyPairSpend.publicKey!,
+ 0,
+ sharedSecret,
+ );
+
+ 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,
+ 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/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/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..2b8bbe6d7
--- /dev/null
+++ b/ts_src/silentpayment.ts
@@ -0,0 +1,185 @@
+import { bech32m } from 'bech32';
+import { hashOutpoints, sha256 } from './crypto';
+
+export type Outpoint = {
+ txid: string;
+ vout: number;
+};
+
+export interface TinySecp256k1Interface {
+ 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;
+}
+
+export interface SilentPayment {
+ 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 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);
+ }
+}
+
+// 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) {}
+
+ /**
+ * 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
+ */
+ scriptPubKey(
+ inputs: Outpoint[],
+ inputPrivateKey: Buffer,
+ silentPaymentAddress: string,
+ index = 0,
+ ): Buffer {
+ const inputsHash = hashOutpoints(inputs);
+ const addr = SilentPaymentAddress.decode(silentPaymentAddress);
+
+ const sharedSecret = this.ecdhSharedSecret(
+ inputsHash,
+ addr.scanPublicKey,
+ inputPrivateKey,
+ );
+
+ const outputPublicKey = this.publicKey(
+ addr.spendPublicKey,
+ index,
+ sharedSecret,
+ );
+
+ return Buffer.concat([SEGWIT_V1_SCRIPT_PREFIX, outputPublicKey.slice(1)]);
+ }
+
+ /**
+ * 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
+ */
+ ecdhSharedSecret(secret: Buffer, pubkey: Buffer, seckey: Buffer): Buffer {
+ const ecdhSharedSecretStep = Buffer.from(
+ this.ecc.privateMultiply(secret, seckey),
+ );
+ const ecdhSharedSecret = this.ecc.pointMultiply(
+ pubkey,
+ ecdhSharedSecretStep,
+ );
+
+ if (!ecdhSharedSecret) {
+ throw new Error('Invalid ecdh shared secret');
+ }
+
+ 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
+ */
+ publicKey(
+ spendPubKey: Buffer,
+ index: number,
+ ecdhSharedSecret: Buffer,
+ ): Buffer {
+ 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);
+ }
+
+ /**
+ * 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
+ */
+ secretKey(
+ spendPrivKey: Buffer,
+ index: number,
+ ecdhSharedSecret: Buffer,
+ ): Buffer {
+ 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);
+ }
+
+ return Buffer.from(privkey);
+ }
+}
+
+function hashSharedSecret(secret: Buffer, index: number): Buffer {
+ const serializedIndex = Buffer.allocUnsafe(4);
+ serializedIndex.writeUint32BE(index, 0);
+ return sha256(Buffer.concat([secret, serializedIndex]));
+}
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"