aere-contracts/test/helpers/tss.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

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,
};