Skip to content

Silent payment basic scheme #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/bip341.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/crypto.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
27 changes: 26 additions & 1 deletion src/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
18 changes: 9 additions & 9 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
28 changes: 11 additions & 17 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions src/silentpayment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference types="node" />
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;
125 changes: 125 additions & 0 deletions src/silentpayment.js
Original file line number Diff line number Diff line change
@@ -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]));
}
41 changes: 41 additions & 0 deletions test/integration/_regtest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -101,3 +112,33 @@ export async function broadcast(
function sleep(ms: number): Promise<any> {
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);
}
Loading