Compare commits

...

1 Commits
master ... bump

Author SHA1 Message Date
Ivan Vershigora
af6609bcbf
chore: bump deps and switch ECC/hash utilities 2026-01-16 23:31:30 +00:00
4 changed files with 53 additions and 261 deletions

View File

@ -1,6 +1,6 @@
{
"name": "silent-payments",
"version": "2.2.1",
"version": "2.2.2",
"description": "SilentPayments (BIP-352) in pure typescript",
"main": "src/index.ts",
"scripts": {
@ -10,18 +10,17 @@
"author": "overtorment",
"license": "MIT",
"dependencies": {
"@noble/secp256k1": "1.6.3",
"bip32": "5.0.0",
"bip39": "3.1.0",
"bitcoinjs-lib": "7.0.0",
"create-hash": "1.2.0",
"ecpair": "3.0.0"
"@bitcoinerlab/secp256k1": "^1.2.0",
"@noble/hashes": "^1.7.1",
"bip32": "^5.0.0",
"bip39": "^3.1.0",
"bitcoinjs-lib": "^7.0.1",
"ecpair": "^3.0.0"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.2",
"@types/create-hash": "^1.2.2",
"prettier": "2.8.8",
"typescript": "^5.1.6",
"vitest": "^3.0.8"
"@tsconfig/recommended": "^1.0.13",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"vitest": "^4.0.17"
}
}

View File

@ -1,10 +1,10 @@
import * as crypto from "crypto";
import { ECPairFactory } from "ecpair";
import { sha256 } from "@noble/hashes/sha2";
import { bech32m } from "bech32";
import { BIP32Factory } from "bip32";
import * as bip39 from "bip39";
import * as bitcoin from "bitcoinjs-lib";
import { Stack, Transaction, script, address, networks } from "bitcoinjs-lib";
import { BIP32Factory } from 'bip32';
import * as bip39 from 'bip39';
import { Stack, Transaction, script } from "bitcoinjs-lib";
import { ECPairFactory } from "ecpair";
import ecc from "./noble_ecc";
import { compareUint8Arrays, concatUint8Arrays, hexToUint8Array, uint8ArrayToHex } from "./uint8array-extras";
@ -105,10 +105,9 @@ export class SilentPayment {
}
static taggedHash(tag: "BIP0352/Inputs" | "BIP0352/SharedSecret", data: Uint8Array): Uint8Array {
const hash = crypto.createHash("sha256");
const tagHash = new Uint8Array(hash.update(tag, "utf-8").digest());
const tagHash = sha256(new TextEncoder().encode(tag));
const ss = concatUint8Arrays([tagHash, tagHash, data]);
return new Uint8Array(crypto.createHash("sha256").update(ss).digest());
return sha256(ss);
}
static _outpointsHash(parameters: UTXO[], A: Uint8Array): Uint8Array {
@ -320,11 +319,10 @@ export class SilentPayment {
return result;
}
/**
* takes BIP-39 mnemonic seed and returns shareable static payment code; also: Bscan, bscan, Bspend, bspend
*/
static seedToCode(bip39seed: string, accountNum = 0, passphrase = ''): { address: string; Bscan: Uint8Array; bscan: Uint8Array; Bspend: Uint8Array, bspend: Uint8Array } {
static seedToCode(bip39seed: string, accountNum = 0, passphrase = ""): { address: string; Bscan: Uint8Array; bscan: Uint8Array; Bspend: Uint8Array; bspend: Uint8Array } {
const root = bip32.fromSeed(new Uint8Array(bip39.mnemonicToSeedSync(bip39seed, passphrase)));
const scanXprv = root.derivePath(`m/352'/0'/${accountNum}'/1'/0`);
const spendXprv = root.derivePath(`m/352'/0'/${accountNum}'/0'/0`);
@ -333,12 +331,12 @@ export class SilentPayment {
const Bspend = spendXprv.publicKey;
const bspend = spendXprv.privateKey;
assert(bscan, 'could not derive bscan from seed');
assert(bspend, 'could not derive bspend from seed');
assert(bscan, "could not derive bscan from seed");
assert(bspend, "could not derive bspend from seed");
const bech32Version = 0;
const words = [bech32Version].concat(bech32m.toWords(concatUint8Arrays([Bscan, Bspend])));
const address = bech32m.encode('sp', words, 1023);
const address = bech32m.encode("sp", words, 1023);
return { address, Bscan, bscan, Bspend, bspend };
}
@ -359,9 +357,9 @@ export class SilentPayment {
// Compute the expected output pubkey
const tkG = ecc.pointMultiply(G, t_k);
assert(tkG, 'Failed to compute tkG');
assert(tkG, "Failed to compute tkG");
const P_k = ecc.pointAdd(tkG, code.Bspend);
assert(P_k, 'Failed to compute output pubkey');
assert(P_k, "Failed to compute output pubkey");
let pubkeyHex = uint8ArrayToHex(P_k);
if (pubkeyHex.startsWith("02") || pubkeyHex.startsWith("03")) pubkeyHex = pubkeyHex.substring(2);
@ -386,8 +384,8 @@ export class SilentPayment {
txid: tx.getId(),
vout,
wif,
utxoType: "p2tr"
}
utxoType: "p2tr",
};
ret.push(u);
}
@ -398,7 +396,7 @@ export class SilentPayment {
}
static detectOurUtxosUsingTweakbscanBspend(tx: Transaction, tweakHex: string, bscan: string, Bspend: string) {
const ret: Omit<UTXO, 'wif'>[] = [];
const ret: Omit<UTXO, "wif">[] = [];
const sharedSecret = ecc.getSharedSecret(hexToUint8Array(bscan), hexToUint8Array(tweakHex));
// todo: iterate k (aka label), cause it might be non-zero
@ -407,9 +405,9 @@ export class SilentPayment {
// Compute the expected output pubkey
const tkG = ecc.pointMultiply(G, t_k);
assert(tkG, 'Failed to compute tkG');
assert(tkG, "Failed to compute tkG");
const P_k = ecc.pointAdd(tkG, hexToUint8Array(Bspend));
assert(P_k, 'Failed to compute output pubkey');
assert(P_k, "Failed to compute output pubkey");
let pubkeyHex = uint8ArrayToHex(P_k);
if (pubkeyHex.startsWith("02") || pubkeyHex.startsWith("03")) pubkeyHex = pubkeyHex.substring(2);
@ -421,11 +419,11 @@ export class SilentPayment {
// alternatively, could compare addresses: SilentPayment.pubkeyToAddress(pubkeyHex) === SilentPayment.pubkeyToAddress(o.script)
// deriving spending privkey for this utxo: d = b_spend + t_k (mod n)
const u: Omit<UTXO, 'wif'> = {
const u: Omit<UTXO, "wif"> = {
txid: tx.getId(),
vout,
utxoType: "p2tr"
}
utxoType: "p2tr",
};
ret.push(u);
}
@ -436,7 +434,6 @@ export class SilentPayment {
}
}
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message);
}
}

View File

@ -1,231 +1,28 @@
/**
* adapted from https://github.com/BitGo/BitGoJS/blob/bitcoinjs_lib_6_sync/modules/utxo-lib/src/noble_ecc.ts
* license: Apache License
* ECC implementation using @bitcoinerlab/secp256k1
* Extended with privateMultiply and getSharedSecret for Silent Payments (BIP-352).
*
* some pieces are ported from:
* https://github.com/paulmillr/noble-secp256k1
* https://github.com/bitcoinerlab/secp256k1
*
* @see https://github.com/bitcoinjs/tiny-secp256k1/issues/84#issuecomment-1185682315
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/1781
* @see https://github.com/bitcoinerlab/secp256k1
*/
import createHash from "create-hash";
import { createHmac } from "crypto";
import * as necc from "@noble/secp256k1";
import * as ecc from "@bitcoinerlab/secp256k1";
import { mod } from "@noble/curves/abstract/modular";
import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToNumberBE, numberToBytesBE } from "@noble/curves/utils";
necc.utils.sha256Sync = (...messages: Uint8Array[]): Uint8Array => {
const sha256 = createHash("sha256");
for (const message of messages) sha256.update(message);
return new Uint8Array(sha256.digest());
};
necc.utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]): Uint8Array => {
const hash = createHmac("sha256", key);
messages.forEach((m) => hash.update(m));
return Uint8Array.from(hash.digest());
};
const defaultTrue = (param?: boolean): boolean => param !== false;
function throwToNull<Type>(fn: () => Type): Type | null {
function privateMultiply(d: Uint8Array, tweak: Uint8Array): Uint8Array | null {
if (!ecc.isPrivate(d)) throw new Error("Expected Private");
try {
return fn();
} catch (e) {
const result = numberToBytesBE(mod(bytesToNumberBE(d) * bytesToNumberBE(tweak), secp256k1.CURVE.n), 32);
return secp256k1.utils.isValidSecretKey(result) ? result : null;
} catch {
return null;
}
}
function isPoint(p: Uint8Array, xOnly: boolean): boolean {
if ((p.length === 32) !== xOnly) return false;
try {
return !!necc.Point.fromHex(p);
} catch (e) {
return false;
}
}
const ecc = {
isPoint: (p: Uint8Array): boolean => isPoint(p, false),
isPrivate: (d: Uint8Array): boolean => {
return necc.utils.isValidPrivateKey(d);
},
isXOnlyPoint: (p: Uint8Array): boolean => isPoint(p, true),
xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array): { parity: 0 | 1; xOnlyPubkey: Uint8Array } | null =>
throwToNull(() => {
const P = necc.utils.pointAddScalar(p, tweak, true);
const parity = P[0] % 2 === 1 ? 1 : 0;
return { parity, xOnlyPubkey: P.slice(1) };
}),
getSharedSecret: (sk: Uint8Array, pk: Uint8Array, compressed?: boolean): Uint8Array => {
return necc.getSharedSecret(sk, pk, defaultTrue(compressed));
},
pointFromScalar: (sk: Uint8Array, compressed?: boolean): Uint8Array | null => throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))),
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => {
return necc.Point.fromHex(p).toRawBytes(defaultTrue(compressed));
},
pointMultiply: (a: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null => throwToNull(() => necc.utils.pointMultiply(a, tweak, defaultTrue(compressed))),
pointAdd: (a: Uint8Array, b: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => {
const A = necc.Point.fromHex(a);
const B = necc.Point.fromHex(b);
return A.add(B).toRawBytes(defaultTrue(compressed));
}),
pointAddScalar: (p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null => throwToNull(() => necc.utils.pointAddScalar(p, tweak, defaultTrue(compressed))),
privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null =>
throwToNull(() => {
if (d.join("") === "00000000000000000000000000000001" && tweak.join("") === "00000000000000000000000000000000") {
// dirty hack to make testEcc in ecpair lib pass
return new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]);
}
const ret = necc.utils.privateAdd(d, tweak);
if (ret.join("") === "00000000000000000000000000000000") {
return null;
}
return ret;
}),
privateNegate: (d: Uint8Array): Uint8Array => necc.utils.privateNegate(d),
sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
return necc.signSync(h, d, { der: false, extraEntropy: e });
},
signSchnorr: (h: Uint8Array, d: Uint8Array, e: Uint8Array = new Uint8Array(32).fill(0x00)): Uint8Array => {
return necc.schnorr.signSync(h, d, e);
},
verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean => {
return necc.verify(signature, h, Q, { strict });
},
verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
return necc.schnorr.verifySync(signature, h, Q);
},
privateMultiply: (d: Uint8Array, tweak: Uint8Array) => {
if (ecc.isPrivate(d) === false) {
throw new Error("Expected Private");
}
const _privateMultiply = (privateKey: Uint8Array, tweak: Uint8Array) => {
const p = normalizePrivateKey(privateKey);
const t = normalizeScalar(tweak);
const mul = _bigintTo32Bytes(necc.utils.mod(p * t, necc.CURVE.n));
if (necc.utils.isValidPrivateKey(mul)) return mul;
else return null;
};
return throwToNull(() => _privateMultiply(d, tweak));
export default {
...ecc,
privateMultiply,
getSharedSecret: (sk: Uint8Array, pk: Uint8Array, compressed = true): Uint8Array => {
return secp256k1.getSharedSecret(sk, pk, compressed);
},
};
export default ecc;
function normalizeScalar(scalar: any) {
let num;
if (typeof scalar === "bigint") {
num = scalar;
} else if (typeof scalar === "number" && Number.isSafeInteger(scalar) && scalar >= 0) {
num = BigInt(scalar);
} else if (typeof scalar === "string") {
if (scalar.length !== 64) throw new Error("Expected 32 bytes of private scalar");
num = hexToNumber(scalar);
} else if (scalar instanceof Uint8Array) {
if (scalar.length !== 32) throw new Error("Expected 32 bytes of private scalar");
num = bytesToNumber(scalar);
} else {
throw new TypeError("Expected valid private scalar");
}
if (num < 0) throw new Error("Expected private scalar >= 0");
return num;
}
function hexToNumber(hex: string) {
return BigInt(`0x${hex}`);
}
function bytesToNumber(bytes: Uint8Array) {
return hexToNumber(necc.utils.bytesToHex(bytes));
}
type Hex = Uint8Array | string;
type PrivKey = Hex | bigint | number;
function normalizePrivateKey(key: PrivKey): bigint {
let num: bigint;
if (typeof key === "bigint") {
num = key;
} else if (typeof key === "number" && Number.isSafeInteger(key) && key > 0) {
num = BigInt(key);
} else if (typeof key === "string") {
if (key.length !== 64) throw new Error("Expected 32 bytes of private key");
num = hexToNumber(key);
} else if (isUint8a(key)) {
if (key.length !== 32) throw new Error("Expected 32 bytes of private key");
num = bytesToNumber(key);
} else {
throw new TypeError("Expected valid private key");
}
if (!isWithinCurveOrder(num)) throw new Error("Expected private key: 0 < key < n");
return num;
}
function isUint8a(bytes: Uint8Array | unknown): bytes is Uint8Array {
return bytes instanceof Uint8Array;
}
function isWithinCurveOrder(num: bigint): boolean {
return _0n < num && num < CURVE.n;
}
const _0n = BigInt(0);
const _1n = BigInt(1);
const _2n = BigInt(2);
const POW_2_256 = _2n ** BigInt(256);
const CURVE = {
a: _0n,
b: BigInt(7),
P: POW_2_256 - _2n ** BigInt(32) - BigInt(977),
n: POW_2_256 - BigInt("432420386565659656852420866394968145599"),
h: _1n,
Gx: BigInt("55066263022277343669578718895168534326250603453777594175500187360389116729240"),
Gy: BigInt("32670510020758816978083085130507043184471273380659243275938904335757337482424"),
beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),
};
function _bigintTo32Bytes(num: bigint): Uint8Array {
const b = hexToBytes(numTo32bStr(num));
if (b.length !== 32) throw new Error("Error: expected 32 bytes");
return b;
}
function numTo32bStr(num: bigint): string {
if (typeof num !== "bigint") throw new Error("Expected bigint");
if (!(_0n <= num && num < POW_2_256)) throw new Error("Expected number 0 <= n < 2^256");
return num.toString(16).padStart(64, "0");
}
function hexToBytes(hex: string): Uint8Array {
if (typeof hex !== "string") {
throw new TypeError("hexToBytes: expected string, got " + typeof hex);
}
if (hex.length % 2) throw new Error("hexToBytes: received invalid unpadded hex" + hex.length);
const array = new Uint8Array(hex.length / 2);
for (let i = 0; i < array.length; i++) {
const j = i * 2;
const hexByte = hex.slice(j, j + 2);
const byte = Number.parseInt(hexByte, 16);
if (Number.isNaN(byte) || byte < 0) throw new Error("Invalid byte sequence");
array[i] = byte;
}
return array;
}

View File

@ -1,5 +1,6 @@
import { UTXOType } from "../src";
import * as crypto from 'crypto';
import { sha256 } from "@noble/hashes/sha2";
import { ripemd160 } from "@noble/hashes/ripemd160";
import { areUint8ArraysEqual, hexToUint8Array, readUInt16, readUInt32 } from "../src/uint8array-extras";
// The following utilities are provided to determine the UTXOType of a transaction input.
@ -84,9 +85,7 @@ export type Vin = {
};
function hash160(s: Uint8Array): Uint8Array {
const sha256Digest = new Uint8Array(crypto.createHash('sha256').update(s).digest());
const ripemd160Digest = crypto.createHash('ripemd160').update(sha256Digest).digest();
return new Uint8Array(ripemd160Digest);
return ripemd160(sha256(s));
}
function isP2tr(spk: Uint8Array): boolean {