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