Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
159 lines
4.3 KiB
JavaScript
159 lines
4.3 KiB
JavaScript
// SPDX-License-Identifier: MIT
|
|
//
|
|
// Self-contained CJS reimplementation of the t-of-n threshold-ECDSA reference
|
|
// (Feldman VSS DKG + demonstrator combine) for the Hardhat suite. It mirrors
|
|
// sdk-js/src/mpc (the canonical TS reference); having an INDEPENDENT second
|
|
// implementation agree with the contract is extra assurance the wire math is right.
|
|
//
|
|
// Honest note (same as the TS lib): the DKG is non-custodial, but combine() reconstructs
|
|
// the secret at the combiner -- fine for a test-vector generator, NOT production signing.
|
|
|
|
const { secp256k1 } = require("@noble/curves/secp256k1");
|
|
const { keccak_256 } = require("@noble/hashes/sha3");
|
|
const { mod, invert } = require("@noble/curves/abstract/modular");
|
|
|
|
const N = secp256k1.CURVE.n;
|
|
const N_HALF = N >> 1n;
|
|
const Point = secp256k1.ProjectivePoint;
|
|
|
|
const modN = (x) => mod(x, N);
|
|
const invModN = (x) => invert(mod(x, N), N);
|
|
|
|
function randScalar() {
|
|
return bytesToBigInt(secp256k1.utils.randomPrivateKey());
|
|
}
|
|
function bytesToBigInt(b) {
|
|
let x = 0n;
|
|
for (const byte of b) x = (x << 8n) | BigInt(byte);
|
|
return x;
|
|
}
|
|
function bigIntTo32(x) {
|
|
const out = new Uint8Array(32);
|
|
let v = mod(x, 1n << 256n);
|
|
for (let i = 31; i >= 0; i--) {
|
|
out[i] = Number(v & 0xffn);
|
|
v >>= 8n;
|
|
}
|
|
return out;
|
|
}
|
|
function hex(b) {
|
|
return "0x" + Buffer.from(b).toString("hex");
|
|
}
|
|
function pointToPubKey64(P) {
|
|
return P.toRawBytes(false).slice(1);
|
|
}
|
|
function addressFromPoint(P) {
|
|
const h = keccak_256(pointToPubKey64(P));
|
|
return "0x" + Buffer.from(h.slice(12)).toString("hex");
|
|
}
|
|
|
|
function evalPoly(coeffs, x) {
|
|
let acc = 0n;
|
|
let xp = 1n;
|
|
for (const a of coeffs) {
|
|
acc = modN(acc + a * xp);
|
|
xp = modN(xp * x);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
// Full n-party Feldman VSS DKG (simulated in-process). Returns group key + shares.
|
|
function runDkg(n, t) {
|
|
const polys = [];
|
|
const commits = [];
|
|
for (let i = 0; i < n; i++) {
|
|
const c = [];
|
|
for (let k = 0; k < t; k++) c.push(randScalar());
|
|
polys.push(c);
|
|
commits.push(c.map((a) => Point.BASE.multiply(a)));
|
|
}
|
|
// shares
|
|
const shares = [];
|
|
for (let j = 1; j <= n; j++) {
|
|
let xj = 0n;
|
|
for (let d = 0; d < n; d++) xj = modN(xj + evalPoly(polys[d], BigInt(j)));
|
|
shares.push({ index: j, share: xj });
|
|
}
|
|
// aggregate commitment C0 = group point
|
|
let group = null;
|
|
for (let i = 0; i < n; i++) group = group === null ? commits[i][0] : group.add(commits[i][0]);
|
|
return {
|
|
n,
|
|
t,
|
|
groupPoint: group,
|
|
groupPubKey64: "0x" + Buffer.from(pointToPubKey64(group)).toString("hex"),
|
|
groupAddress: addressFromPoint(group),
|
|
shares,
|
|
};
|
|
}
|
|
|
|
function lagrangeAtZero(indices, i) {
|
|
let num = 1n;
|
|
let den = 1n;
|
|
const xi = BigInt(i);
|
|
for (const j of indices) {
|
|
if (j === i) continue;
|
|
const xj = BigInt(j);
|
|
num = modN(num * xj);
|
|
den = modN(den * modN(xj - xi));
|
|
}
|
|
return modN(num * invModN(den));
|
|
}
|
|
|
|
function reconstruct(shares) {
|
|
const idx = shares.map((s) => s.index);
|
|
let x = 0n;
|
|
for (const s of shares) x = modN(x + modN(s.share * lagrangeAtZero(idx, s.index)));
|
|
return x;
|
|
}
|
|
|
|
// Standard ECDSA over a 32-byte digest by scalar priv -> Ethereum (r||s||v) 65 bytes, low-s.
|
|
function ecdsaSign(digest32, priv, kFixed) {
|
|
const z = modN(bytesToBigInt(digest32));
|
|
for (let attempt = 0; attempt < 64; attempt++) {
|
|
const k = kFixed !== undefined ? kFixed : randScalar();
|
|
const R = Point.BASE.multiply(k).toAffine();
|
|
const r = modN(R.x);
|
|
if (r === 0n) {
|
|
if (kFixed !== undefined) throw new Error("bad k");
|
|
continue;
|
|
}
|
|
let s = modN(invModN(k) * modN(z + r * modN(priv)));
|
|
if (s === 0n) {
|
|
if (kFixed !== undefined) throw new Error("bad k");
|
|
continue;
|
|
}
|
|
let recovery = Number(R.y & 1n) & 1;
|
|
if (s > N_HALF) {
|
|
s = N - s;
|
|
recovery ^= 1;
|
|
}
|
|
const sig = new Uint8Array(65);
|
|
sig.set(bigIntTo32(r), 0);
|
|
sig.set(bigIntTo32(s), 32);
|
|
sig[64] = 27 + recovery;
|
|
return "0x" + Buffer.from(sig).toString("hex");
|
|
}
|
|
throw new Error("ecdsa failed");
|
|
}
|
|
|
|
// Threshold combine: interpolate the provided shares, sign digest. >=t shares -> group key.
|
|
function thresholdSign(digest32, shares, kFixed) {
|
|
const x = reconstruct(shares);
|
|
return ecdsaSign(digest32, x, kFixed);
|
|
}
|
|
|
|
module.exports = {
|
|
N,
|
|
N_HALF,
|
|
Point,
|
|
runDkg,
|
|
thresholdSign,
|
|
reconstruct,
|
|
ecdsaSign,
|
|
lagrangeAtZero,
|
|
bigIntTo32,
|
|
hex,
|
|
addressFromPoint,
|
|
};
|