Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af6609bcbf |
23
package.json
23
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
47
src/index.ts
47
src/index.ts
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
237
src/noble_ecc.ts
237
src/noble_ecc.ts
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user