Skip to content

Commit

Permalink
refactor: replace ecc with noble ecc
Browse files Browse the repository at this point in the history
  • Loading branch information
Overtorment committed Jul 29, 2023
1 parent 473bc72 commit f1b8024
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 102 deletions.
23 changes: 11 additions & 12 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import ecc from "./noble_ecc";

const sec = require("bcrypto").secp256k1;
const crypto = require("node:crypto");
const crypto = require("crypto");
const ECPairFactory = require("ecpair").ECPairFactory;
const { bech32m } = require("bech32");
const ECPair = ECPairFactory(ecc);
Expand Down Expand Up @@ -75,17 +73,18 @@ export class SilentPayment {
// Generating Pmn for each Bm in the group
for (const group of silentPaymentGroups) {
// Bscan * a * outpoint_hash
const ecdh_shared_secret_step1 = sec.privateKeyTweakMul(outpoint_hash, a);
const ecdh_shared_secret = sec.publicKeyTweakMul(group.Bscan, ecdh_shared_secret_step1);
const ecdh_shared_secret_step1 = Buffer.from(ecc.privateMultiply(outpoint_hash, a) as Uint8Array);
const ecdh_shared_secret = ecc.pointMultiply(group.Bscan, ecdh_shared_secret_step1);

let n = 0;
for (const [Bm, amount] of group.BmValues) {
const tn = crypto
.createHash("sha256")
.update(Buffer.concat([ecdh_shared_secret, SilentPayment._ser32(n)]))
.update(Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(n)]))
.digest();

// Let Pmn = tn·G + Bm
const Pmn = sec.publicKeyCombine([sec.publicKeyTweakMul(G, tn), Bm]);
const Pmn = Buffer.from(ecc.pointAdd(ecc.pointMultiply(G, tn) as Uint8Array, Bm) as Uint8Array);

// Encode Pmn as a BIP341 taproot output
const address = Pmn.slice(1).toString("hex");
Expand Down Expand Up @@ -125,17 +124,17 @@ export class SilentPayment {
let ret = ECPair.fromWIF(utxos[0].WIF).privateKey;

// If taproot, check if the seckey results in an odd y-value and negate if so
if (utxos[0].is_taproot && sec.publicKeyCreate(ret)[0] === 0x03) {
ret = sec.privateKeyNegate(ret);
if (utxos[0].is_taproot && ecc.pointFromScalar(ret)![0] === 0x03) {
ret = Buffer.from(ecc.privateNegate(ret));
}
for (let c = 1; c < utxos.length; c++) {
let negated_key = ECPair.fromWIF(utxos[c].WIF).privateKey;

// If taproot, check if the seckey results in an odd y-value and negate if so
if (utxos[c].is_taproot && sec.publicKeyCreate(negated_key)[0] === 0x03) {
negated_key = sec.privateKeyNegate(negated_key);
if (utxos[c].is_taproot && ecc.pointFromScalar(negated_key)![0] === 0x03) {
negated_key = Buffer.from(ecc.privateNegate(negated_key));
}
ret = sec.privateKeyTweakAdd(ret, negated_key);
ret = Buffer.from(ecc.privateAdd(ret, negated_key) as Uint8Array);
}

return ret;
Expand Down
145 changes: 133 additions & 12 deletions noble_ecc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { XOnlyPointAddTweakResult } from "bitcoinjs-lib/src/types";

export interface TinySecp256k1InterfaceExtended {
pointMultiply(p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null;
privateMultiply(p: Uint8Array, tweak: Uint8Array): Uint8Array | null;
privateNegate(d: Uint8Array): Uint8Array;

pointAdd(pA: Uint8Array, pB: Uint8Array, compressed?: boolean): Uint8Array | null;

Expand All @@ -34,17 +36,6 @@ necc.utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]): Uint8A
return Uint8Array.from(hash.digest());
};

/* const normal = necc.utils._normalizePrivateKey;
type Hex = string | Uint8Array;
type PrivKey = Hex | bigint | number;
necc.utils.privateAdd = (privateKey: PrivKey, tweak: Hex) => {
console.log({ privateKey, tweak });
const p = normal(privateKey);
const t = normal(tweak);
return necc.utils.privateAdd(necc.utils.mod(p + t, necc.CURVE.n));
}; */

const defaultTrue = (param?: boolean): boolean => param !== false;

function throwToNull<Type>(fn: () => Type): Type | null {
Expand Down Expand Up @@ -116,7 +107,7 @@ const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256
return ret;
}),

// privateNegate: (d: Uint8Array): Uint8Array => necc.utils.privateNegate(d),
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 });
Expand All @@ -133,8 +124,138 @@ const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256
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;

// module.exports.ecc = 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;
}

// We can't do `instanceof Uint8Array` because it's unreliable between Web Workers etc
function isUint8a(bytes: Uint8Array | unknown): bytes is Uint8Array {
return bytes instanceof Uint8Array;
}

function isWithinCurveOrder(num: bigint): boolean {
return _0n < num && num < CURVE.n;
}

// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
const _1n = BigInt(1);
const _2n = BigInt(2);
const _3n = BigInt(3);
const _8n = BigInt(8);

// @ts-ignore
const POW_2_256 = _2n ** BigInt(256);

const CURVE = {
// Params: a, b
a: _0n,
b: BigInt(7),
// Field over which we'll do calculations
// @ts-ignore
P: POW_2_256 - _2n ** BigInt(32) - BigInt(977),
// Curve order, a number of valid points in the field
n: POW_2_256 - BigInt("432420386565659656852420866394968145599"),
// Cofactor. It's 1, so other subgroups don't exist, and default subgroup is prime-order
h: _1n,
// Base point (x, y) aka generator point
Gx: BigInt("55066263022277343669578718895168534326250603453777594175500187360389116729240"),
Gy: BigInt("32670510020758816978083085130507043184471273380659243275938904335757337482424"),
// For endomorphism, see below
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");
}

// Caching slows it down 2-3x
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;
}
Loading

0 comments on commit f1b8024

Please sign in to comment.