feat: 添加 Lighter 适配器及相关功能,支持 trailing stops 和新的交易逻辑

This commit is contained in:
discountry
2025-09-30 22:08:05 +08:00
parent e83bdfccf9
commit c8b0ab1c8e
101 changed files with 90621 additions and 3 deletions
+144
View File
@@ -0,0 +1,144 @@
import { Fp } from "./goldilocks";
import {
Fp5,
FP5_ZERO,
FP5_ONE,
FP5_TWO,
FP5_W,
fp5Add,
fp5Mul,
fp5ScalarMul,
fp5Double,
fp5InverseOrZero,
} from "./goldilocks-fp5";
import { Scalar } from "./scalar";
const B1 = 263n;
const B = Fp5.fromUint64Array([0n, B1, 0n, 0n, 0n]);
const B_MUL2 = Fp5.fromUint64Array([0n, 2n * B1, 0n, 0n, 0n]);
const B_MUL4 = Fp5.fromUint64Array([0n, 4n * B1, 0n, 0n, 0n]);
const B_MUL16 = Fp5.fromUint64Array([0n, 16n * B1, 0n, 0n, 0n]);
const A = Fp5.fromUint64Array([2n, 0n, 0n, 0n, 0n]);
const FOUR_CONST = Fp5.fromUint64Array([4n, 0n, 0n, 0n, 0n]);
export class ECPoint {
constructor(
readonly x: Fp5,
readonly z: Fp5,
readonly u: Fp5,
readonly t: Fp5,
) {}
clone(): ECPoint {
return new ECPoint(this.x.clone(), this.z.clone(), this.u.clone(), this.t.clone());
}
static neutral(): ECPoint {
return new ECPoint(FP5_ZERO, FP5_ONE, FP5_ZERO, FP5_ONE);
}
static generator(): ECPoint {
return new ECPoint(
Fp5.fromUint64Array([
12883135586176881569n,
4356519642755055268n,
5248930565894896907n,
2165973894480315022n,
2448410071095648785n,
]),
FP5_ONE,
FP5_ONE,
Fp5.fromUint64Array([4n, 0n, 0n, 0n, 0n]),
);
}
isNeutral(): boolean {
return this.u.equals(FP5_ZERO);
}
encode(): Fp5 {
return fp5Mul(this.t, fp5InverseOrZero(this.u));
}
add(other: ECPoint): ECPoint {
if (this.isNeutral()) return other.clone();
if (other.isNeutral()) return this.clone();
const x1 = this.x;
const z1 = this.z;
const u1 = this.u;
const t1 = this.t;
const x2 = other.x;
const z2 = other.z;
const u2 = other.u;
const t2 = other.t;
const t1_ = fp5Mul(x1, x2);
const t2_ = fp5Mul(z1, z2);
const t3 = fp5Mul(u1, u2);
const t4 = fp5Mul(t1, t2);
const t5 = fp5SubMul(fp5Add(x1, z1), fp5Add(x2, z2), fp5Add(t1_, t2_));
const t6 = fp5SubMul(fp5Add(u1, t1), fp5Add(u2, t2), fp5Add(t3, t4));
const t7 = fp5Add(t1_, fp5Mul(t2_, B));
const t8 = fp5Mul(t4, t7);
const t9 = fp5Mul(t3, fp5Add(fp5Mul(t5, B_MUL2), fp5Double(t7)));
const t10 = fp5Mul(fp5Add(t4, fp5Double(t3)), fp5Add(t5, t7));
const xNew = fp5Mul(fp5Sub(t10, t8), B);
const zNew = fp5Sub(t8, t9);
const uNew = fp5Mul(t6, fp5Sub(fp5Mul(t2_, B), t1_));
const tNew = fp5Add(t8, t9);
return new ECPoint(xNew, zNew, uNew, tNew);
}
double(): ECPoint {
if (this.isNeutral()) return this.clone();
const x = this.x;
const z = this.z;
const u = this.u;
const t = this.t;
const t1 = fp5Mul(z, t);
const t2 = fp5Mul(t1, t);
const x1 = fp5Mul(t2, t2);
const z1 = fp5Mul(t1, u);
const t3 = fp5Mul(u, u);
const w1 = fp5Sub(t2, fp5Mul(fp5Double(fp5Add(x, z)), t3));
const t4 = fp5Mul(z1, z1);
const xNew = fp5Mul(t4, B_MUL4);
const zNew = fp5Mul(w1, w1);
const uNew = fp5Sub(fp5Mul(fp5Add(w1, z1), fp5Add(w1, z1)), fp5Add(t4, zNew));
const tNew = fp5Sub(
fp5Double(x1),
fp5Add(fp5Mul(t4, FOUR_CONST), zNew),
);
return new ECPoint(xNew, zNew, uNew, tNew);
}
mul(scalar: Scalar): ECPoint {
let result = ECPoint.neutral();
let addend = this.clone();
let k = scalar.toBigInt();
while (k > 0n) {
if (k & 1n) {
result = result.add(addend);
}
addend = addend.double();
k >>= 1n;
}
return result;
}
}
function fp5Sub(a: Fp5, b: Fp5): Fp5 {
return a.sub(b);
}
function fp5SubMul(a: Fp5, b: Fp5, subtract: Fp5): Fp5 {
return fp5Mul(a, b).sub(subtract);
}
@@ -0,0 +1,460 @@
import { Fp, GOLDILOCKS_MODULUS, addMany, powers as fpPowers } from "./goldilocks";
export type Fp5Tuple = [Fp, Fp, Fp, Fp, Fp];
function expectLength(bytes: Uint8Array, expected: number): void {
if (bytes.length !== expected) {
throw new Error(`Expected ${expected} bytes, got ${bytes.length}`);
}
}
const BYTE_LENGTH = 5 * 8;
export const FP5_W = new Fp(3n);
export const FP5_DTH_ROOT = new Fp(1041288259238279555n);
const THREE = new Fp(3n);
export class Fp5 {
readonly c0: Fp;
readonly c1: Fp;
readonly c2: Fp;
readonly c3: Fp;
readonly c4: Fp;
constructor(coeffs: Fp5Tuple) {
[this.c0, this.c1, this.c2, this.c3, this.c4] = coeffs;
}
static ZERO = new Fp5([Fp.ZERO, Fp.ZERO, Fp.ZERO, Fp.ZERO, Fp.ZERO]);
static ONE = new Fp5([Fp.ONE, Fp.ZERO, Fp.ZERO, Fp.ZERO, Fp.ZERO]);
static fromFp(value: Fp): Fp5 {
return new Fp5([value.clone(), Fp.ZERO, Fp.ZERO, Fp.ZERO, Fp.ZERO]);
}
static fromUint64Array(values: ArrayLike<bigint | number>): Fp5 {
if (values.length !== 5) {
throw new Error("Fp5.fromUint64Array expects 5 values");
}
const get = (index: number): bigint | number => {
const value = values[index];
if (value === undefined) {
throw new Error("Fp5.fromUint64Array missing value");
}
return value;
};
const arr: [bigint | number, bigint | number, bigint | number, bigint | number, bigint | number] = [
get(0),
get(1),
get(2),
get(3),
get(4),
];
return new Fp5([
new Fp(arr[0]),
new Fp(arr[1]),
new Fp(arr[2]),
new Fp(arr[3]),
new Fp(arr[4]),
]);
}
toTuple(): Fp5Tuple {
return [this.c0, this.c1, this.c2, this.c3, this.c4];
}
toBytes(): Uint8Array {
const out = new Uint8Array(BYTE_LENGTH);
out.set(this.c0.toBytesLE(), 0);
out.set(this.c1.toBytesLE(), 8);
out.set(this.c2.toBytesLE(), 16);
out.set(this.c3.toBytesLE(), 24);
out.set(this.c4.toBytesLE(), 32);
return out;
}
static fromBytes(bytes: Uint8Array): Fp5 {
expectLength(bytes, BYTE_LENGTH);
return new Fp5([
Fp.fromBytesLE(bytes.subarray(0, 8)),
Fp.fromBytesLE(bytes.subarray(8, 16)),
Fp.fromBytesLE(bytes.subarray(16, 24)),
Fp.fromBytesLE(bytes.subarray(24, 32)),
Fp.fromBytesLE(bytes.subarray(32, 40)),
]);
}
equals(other: Fp5): boolean {
return (
this.c0.equals(other.c0) &&
this.c1.equals(other.c1) &&
this.c2.equals(other.c2) &&
this.c3.equals(other.c3) &&
this.c4.equals(other.c4)
);
}
isZero(): boolean {
return this.equals(Fp5.ZERO);
}
clone(): Fp5 {
return new Fp5([
this.c0.clone(),
this.c1.clone(),
this.c2.clone(),
this.c3.clone(),
this.c4.clone(),
]);
}
add(other: Fp5): Fp5 {
return new Fp5([
this.c0.add(other.c0),
this.c1.add(other.c1),
this.c2.add(other.c2),
this.c3.add(other.c3),
this.c4.add(other.c4),
]);
}
sub(other: Fp5): Fp5 {
return new Fp5([
this.c0.sub(other.c0),
this.c1.sub(other.c1),
this.c2.sub(other.c2),
this.c3.sub(other.c3),
this.c4.sub(other.c4),
]);
}
neg(): Fp5 {
return new Fp5([
this.c0.neg(),
this.c1.neg(),
this.c2.neg(),
this.c3.neg(),
this.c4.neg(),
]);
}
mul(other: Fp5): Fp5 {
const w = FP5_W;
const a0b0 = this.c0.mul(other.c0);
const a1b4 = this.c1.mul(other.c4);
const a2b3 = this.c2.mul(other.c3);
const a3b2 = this.c3.mul(other.c2);
const a4b1 = this.c4.mul(other.c1);
const added0 = addMany([a1b4, a2b3, a3b2, a4b1]);
const muld0 = added0.mul(w);
const c0 = a0b0.add(muld0);
const a0b1 = this.c0.mul(other.c1);
const a1b0 = this.c1.mul(other.c0);
const a2b4 = this.c2.mul(other.c4);
const a3b3 = this.c3.mul(other.c3);
const a4b2 = this.c4.mul(other.c2);
const added1 = addMany([a2b4, a3b3, a4b2]);
const muld1 = added1.mul(w);
const c1 = addMany([a0b1, a1b0, muld1]);
const a0b2 = this.c0.mul(other.c2);
const a1b1 = this.c1.mul(other.c1);
const a2b0 = this.c2.mul(other.c0);
const a3b4 = this.c3.mul(other.c4);
const a4b3 = this.c4.mul(other.c3);
const added2 = addMany([a3b4, a4b3]);
const muld2 = added2.mul(w);
const c2 = addMany([a0b2, a1b1, a2b0, muld2]);
const a0b3 = this.c0.mul(other.c3);
const a1b2 = this.c1.mul(other.c2);
const a2b1 = this.c2.mul(other.c1);
const a3b0 = this.c3.mul(other.c0);
const a4b4 = this.c4.mul(other.c4);
const muld3 = a4b4.mul(w);
const c3 = addMany([a0b3, a1b2, a2b1, a3b0, muld3]);
const a0b4 = this.c0.mul(other.c4);
const a1b3 = this.c1.mul(other.c3);
const a2b2 = this.c2.mul(other.c2);
const a3b1 = this.c3.mul(other.c1);
const a4b0 = this.c4.mul(other.c0);
const c4 = addMany([a0b4, a1b3, a2b2, a3b1, a4b0]);
return new Fp5([c0, c1, c2, c3, c4]);
}
square(): Fp5 {
const w = FP5_W;
const doubleW = FP5_W.add(FP5_W);
const a0s = this.c0.mul(this.c0);
const a1a4 = this.c1.mul(this.c4);
const a2a3 = this.c2.mul(this.c3);
const added0 = addMany([a1a4, a2a3]);
const muld0 = added0.mul(doubleW);
const c0 = a0s.add(muld0);
const a0Double = this.c0.add(this.c0);
const a0Doublea1 = a0Double.mul(this.c1);
const a2a4DoubleW = this.c2.mul(this.c4).mul(doubleW);
const a3a3w = this.c3.mul(this.c3).mul(w);
const c1 = addMany([a0Doublea1, a2a4DoubleW, a3a3w]);
const a0Doublea2 = a0Double.mul(this.c2);
const a1Square = this.c1.mul(this.c1);
const a4a3DoubleW = this.c4.mul(this.c3).mul(doubleW);
const c2 = addMany([a0Doublea2, a1Square, a4a3DoubleW]);
const a1Double = this.c1.add(this.c1);
const a0Doublea3 = a0Double.mul(this.c3);
const a1Doublea2 = a1Double.mul(this.c2);
const a4SquareW = this.c4.mul(this.c4).mul(w);
const c3 = addMany([a0Doublea3, a1Doublea2, a4SquareW]);
const a0Doublea4 = a0Double.mul(this.c4);
const a1Doublea3 = a1Double.mul(this.c3);
const a2Square = this.c2.mul(this.c2);
const c4 = addMany([a0Doublea4, a1Doublea3, a2Square]);
return new Fp5([c0, c1, c2, c3, c4]);
}
expPowerOf2(power: number): Fp5 {
let result = this.clone();
for (let i = 0; i < power; i++) {
result = result.square();
}
return result;
}
double(): Fp5 {
return this.add(this);
}
triple(): Fp5 {
return new Fp5([
this.c0.mul(THREE),
this.c1.mul(THREE),
this.c2.mul(THREE),
this.c3.mul(THREE),
this.c4.mul(THREE),
]);
}
scalarMul(scalar: Fp): Fp5 {
return new Fp5([
this.c0.mul(scalar),
this.c1.mul(scalar),
this.c2.mul(scalar),
this.c3.mul(scalar),
this.c4.mul(scalar),
]);
}
inverseOrZero(): Fp5 {
if (this.isZero()) return Fp5.ZERO;
const d = this.frobenius();
const e = this.mul(d.frobenius());
const f = e.mul(e.repeatedFrobenius(2));
const a0b0 = this.c0.mul(f.c0);
const a1b4 = this.c1.mul(f.c4);
const a2b3 = this.c2.mul(f.c3);
const a3b2 = this.c3.mul(f.c2);
const a4b1 = this.c4.mul(f.c1);
const added = addMany([a1b4, a2b3, a3b2, a4b1]);
const muld = added.mul(FP5_W);
const g = a0b0.add(muld);
const gInv = g.inverse();
return f.scalarMul(gInv);
}
div(other: Fp5): Fp5 {
const inv = other.inverseOrZero();
if (inv.isZero()) {
throw new Error("Division by zero in Fp5");
}
return this.mul(inv);
}
frobenius(): Fp5 {
return this.repeatedFrobenius(1);
}
repeatedFrobenius(count: number): Fp5 {
if (count === 0) return this;
const reduced = count % 5;
if (reduced === 0) return this;
let z0 = FP5_DTH_ROOT;
for (let i = 1; i < reduced; i++) {
z0 = z0.mul(FP5_DTH_ROOT);
}
const powerArray = fpPowers(z0, 5) as [Fp, Fp, Fp, Fp, Fp];
const [p0, p1, p2, p3, p4] = powerArray;
return new Fp5([
this.c0.mul(p0),
this.c1.mul(p1),
this.c2.mul(p2),
this.c3.mul(p3),
this.c4.mul(p4),
]);
}
legendre(): Fp {
const frob1 = this.frobenius();
const frob2 = frob1.frobenius();
const frob1TimesFrob2 = frob1.mul(frob2);
const frob2Frob1TimesFrob2 = frob1TimesFrob2.repeatedFrobenius(2);
const xrExt = this.mul(frob1TimesFrob2).mul(frob2Frob1TimesFrob2);
const xr = new Fp(xrExt.c0.toBigInt());
const xr31 = xr.pow(1n << 31n);
const xr31Inv = xr31.isZero() ? Fp.ZERO : xr31.inverse();
const xr63 = xr31.pow(1n << 32n);
return xr63.mul(xr31Inv);
}
sqrt(): { value: Fp5; exists: boolean } {
const v = this.expPowerOf2(31);
const d = this.mul(v.expPowerOf2(32)).mul(v.inverseOrZero());
const e = d.mul(d.repeatedFrobenius(2)).frobenius();
const f = e.square();
const x1f4 = this.c1.mul(f.c4);
const x2f3 = this.c2.mul(f.c3);
const x3f2 = this.c3.mul(f.c2);
const x4f1 = this.c4.mul(f.c1);
const added = addMany([x1f4, x2f3, x3f2, x4f1]);
const muld = added.mul(THREE);
const x0f0 = this.c0.mul(f.c0);
const g = x0f0.add(muld);
const s = sqrtFp(g);
if (!s) {
return { value: Fp5.ZERO, exists: false };
}
const eInv = e.inverseOrZero();
const sFp5 = Fp5.fromFp(s);
return { value: sFp5.mul(eInv), exists: true };
}
canonicalSqrt(): { value: Fp5; exists: boolean } {
const { value, exists } = this.sqrt();
if (!exists) return { value: Fp5.ZERO, exists: false };
return { value: sgn0(value) ? value.neg() : value, exists: true };
}
}
function sgn0(x: Fp5): boolean {
let sign = false;
let zero = true;
for (const limb of [x.c0, x.c1, x.c2, x.c3, x.c4]) {
const limbSign = (limb.toBigInt() & 1n) === 0n;
const limbZero = limb.isZero();
sign = sign || (zero && limbSign);
zero = zero && limbZero;
}
return sign;
}
function sqrtFp(value: Fp): Fp | null {
if (value.isZero()) return Fp.ZERO;
const p = GOLDILOCKS_MODULUS;
const leg = value.pow((p - 1n) / 2n);
if (leg.isZero()) return Fp.ZERO;
const legVal = leg.toBigInt();
if (legVal === p - 1n) return null;
let q = p - 1n;
let s = 0n;
while ((q & 1n) === 0n) {
q >>= 1n;
s += 1n;
}
let z = 2n;
while (true) {
const zLeg = new Fp(z).pow((p - 1n) / 2n).toBigInt();
if (zLeg === p - 1n) break;
z += 1n;
}
let c = new Fp(z).pow(q);
let x = value.pow((q + 1n) >> 1n);
let t = value.pow(q);
let m = s;
while (t.toBigInt() !== 1n) {
let i = 1n;
let t2i = t.mul(t);
while (t2i.toBigInt() !== 1n) {
t2i = t2i.mul(t2i);
i += 1n;
if (i === m) return null;
}
const b = c.pow(1n << (m - i - 1n));
x = x.mul(b);
c = b.mul(b);
t = t.mul(c);
m = i;
}
return x;
}
export const FP5_ZERO = Fp5.ZERO;
export const FP5_ONE = Fp5.ONE;
export const FP5_TWO = Fp5.fromFp(new Fp(2n));
export function fp5Add(a: Fp5, ...others: Fp5[]): Fp5 {
let acc = a;
for (const other of others) {
acc = acc.add(other);
}
return acc;
}
export function fp5Sub(a: Fp5, b: Fp5): Fp5 {
return a.sub(b);
}
export function fp5Mul(a: Fp5, b: Fp5): Fp5 {
return a.mul(b);
}
export function fp5Square(a: Fp5): Fp5 {
return a.square();
}
export function fp5Double(a: Fp5): Fp5 {
return a.double();
}
export function fp5ScalarMul(a: Fp5, scalar: Fp): Fp5 {
return a.scalarMul(scalar);
}
export function fp5InverseOrZero(a: Fp5): Fp5 {
return a.inverseOrZero();
}
export function fp5Frobenius(a: Fp5): Fp5 {
return a.frobenius();
}
export function fp5RepeatedFrobenius(a: Fp5, count: number): Fp5 {
return a.repeatedFrobenius(count);
}
export function fp5Legendre(a: Fp5): Fp {
return a.legendre();
}
export function fp5CanonicalSqrt(a: Fp5): { value: Fp5; exists: boolean } {
return a.canonicalSqrt();
}
+179
View File
@@ -0,0 +1,179 @@
import { randomBytes } from "crypto";
const TWO_POW_32 = 1n << 32n;
const TWO_POW_64 = 1n << 64n;
export const GOLDILOCKS_MODULUS = TWO_POW_64 - TWO_POW_32 + 1n;
const BYTE_LENGTH = 8;
function mod(value: bigint): bigint {
let v = value % GOLDILOCKS_MODULUS;
if (v < 0n) v += GOLDILOCKS_MODULUS;
return v;
}
export class Fp {
readonly value: bigint;
constructor(value: bigint | number) {
this.value = mod(typeof value === "number" ? BigInt(value) : value);
}
static readonly ZERO = new Fp(0n);
static readonly ONE = new Fp(1n);
static fromBytesLE(bytes: Uint8Array): Fp {
if (bytes.length !== BYTE_LENGTH) {
throw new Error(`Goldilocks element expects 8 bytes, got ${bytes.length}`);
}
let acc = 0n;
for (let i = 0; i < BYTE_LENGTH; i++) {
const byte = bytes[i];
if (byte === undefined) throw new Error("Unexpected undefined byte when reading Goldilocks element");
acc |= BigInt(byte) << BigInt(8 * i);
}
return new Fp(acc);
}
toBytesLE(): Uint8Array {
let v = this.value;
const out = new Uint8Array(BYTE_LENGTH);
for (let i = 0; i < BYTE_LENGTH; i++) {
out[i] = Number(v & 0xffn);
v >>= 8n;
}
return out;
}
toBigInt(): bigint {
return this.value;
}
toNumber(): number {
return Number(this.value);
}
add(...others: Fp[]): Fp {
let acc = this.value;
for (const other of others) acc += other.value;
return new Fp(acc);
}
sub(other: Fp): Fp {
return new Fp(this.value - other.value);
}
neg(): Fp {
return new Fp(this.value === 0n ? 0n : GOLDILOCKS_MODULUS - this.value);
}
mul(...others: Fp[]): Fp {
let acc = this.value;
for (const other of others) acc = mod(acc * other.value);
return new Fp(acc);
}
square(): Fp {
return new Fp(mod(this.value * this.value));
}
double(): Fp {
return new Fp(this.value << 1n);
}
inverse(): Fp {
if (this.isZero()) {
throw new Error("Cannot invert zero in Goldilocks field");
}
return this.pow(GOLDILOCKS_MODULUS - 2n);
}
pow(exponent: bigint): Fp {
let result = 1n;
let base = this.value;
let exp = exponent;
while (exp > 0n) {
if (exp & 1n) result = mod(result * base);
base = mod(base * base);
exp >>= 1n;
}
return new Fp(result);
}
isZero(): boolean {
return this.value === 0n;
}
isOne(): boolean {
return this.value === 1n;
}
equals(other: Fp): boolean {
return this.value === other.value;
}
clone(): Fp {
return new Fp(this.value);
}
static random(): Fp {
while (true) {
const buf = randomBytes(BYTE_LENGTH);
let acc = 0n;
for (let i = 0; i < BYTE_LENGTH; i++) {
const byte = buf[i];
if (byte === undefined) throw new Error("Unexpected undefined byte when sampling Goldilocks element");
acc |= BigInt(byte) << BigInt(8 * i);
}
if (acc < GOLDILOCKS_MODULUS) {
return new Fp(acc);
}
}
}
static fromUint32(value: number): Fp {
return new Fp(BigInt(value >>> 0));
}
static fromUint64(value: bigint | number): Fp {
return new Fp(value);
}
}
export function addMany(elements: readonly Fp[]): Fp {
let acc = 0n;
for (const el of elements) acc += el.value;
return new Fp(acc);
}
export function arrayToBytesLE(elements: readonly Fp[]): Uint8Array {
const out = new Uint8Array(elements.length * BYTE_LENGTH);
elements.forEach((elem, idx) => {
out.set(elem.toBytesLE(), idx * BYTE_LENGTH);
});
return out;
}
export function arrayFromBytesLE(bytes: Uint8Array): Fp[] {
if (bytes.length % BYTE_LENGTH !== 0) {
throw new Error("Goldilocks array bytes length must be multiple of 8");
}
const out: Fp[] = [];
for (let i = 0; i < bytes.length; i += BYTE_LENGTH) {
out.push(Fp.fromBytesLE(bytes.subarray(i, i + BYTE_LENGTH)));
}
return out;
}
export function powers(base: Fp, count: number): Fp[] {
if (count <= 0) return [];
const result = new Array<Fp>(count);
result[0] = Fp.ONE;
for (let i = 1; i < count; i++) {
const prev = result[i - 1];
if (!prev) throw new Error("unexpected undefined in goldilocks powers");
result[i] = prev.mul(base);
}
return result;
}
export const GOLDILOCKS_BYTE_LENGTH = BYTE_LENGTH;
+320
View File
@@ -0,0 +1,320 @@
import { Fp } from "./goldilocks";
import { Fp5 } from "./goldilocks-fp5";
const WIDTH = 12;
const RATE = 8;
const ROUNDS_F = 8;
const ROUNDS_F_HALF = 4;
const ROUNDS_P = 22;
const EXTERNAL_CONSTANTS: Fp[][] = [
[
new Fp(15492826721047263190n),
new Fp(11728330187201910315n),
new Fp(8836021247773420868n),
new Fp(16777404051263952451n),
new Fp(5510875212538051896n),
new Fp(6173089941271892285n),
new Fp(2927757366422211339n),
new Fp(10340958981325008808n),
new Fp(8541987352684552425n),
new Fp(9739599543776434497n),
new Fp(15073950188101532019n),
new Fp(12084856431752384512n),
],
[
new Fp(4584713381960671270n),
new Fp(8807052963476652830n),
new Fp(54136601502601741n),
new Fp(4872702333905478703n),
new Fp(5551030319979516287n),
new Fp(12889366755535460989n),
new Fp(16329242193178844328n),
new Fp(412018088475211848n),
new Fp(10505784623379650541n),
new Fp(9758812378619434837n),
new Fp(7421979329386275117n),
new Fp(375240370024755551n),
],
[
new Fp(3331431125640721931n),
new Fp(15684937309956309981n),
new Fp(578521833432107983n),
new Fp(14379242000670861838n),
new Fp(17922409828154900976n),
new Fp(8153494278429192257n),
new Fp(15904673920630731971n),
new Fp(11217863998460634216n),
new Fp(3301540195510742136n),
new Fp(9937973023749922003n),
new Fp(3059102938155026419n),
new Fp(1895288289490976132n),
],
[
new Fp(5580912693628927540n),
new Fp(10064804080494788323n),
new Fp(9582481583369602410n),
new Fp(10186259561546797986n),
new Fp(247426333829703916n),
new Fp(13193193905461376067n),
new Fp(6386232593701758044n),
new Fp(17954717245501896472n),
new Fp(1531720443376282699n),
new Fp(2455761864255501970n),
new Fp(11234429217864304495n),
new Fp(4746959618548874102n),
],
[
new Fp(13571697342473846203n),
new Fp(17477857865056504753n),
new Fp(15963032953523553760n),
new Fp(16033593225279635898n),
new Fp(14252634232868282405n),
new Fp(8219748254835277737n),
new Fp(7459165569491914711n),
new Fp(15855939513193752003n),
new Fp(16788866461340278896n),
new Fp(7102224659693946577n),
new Fp(3024718005636976471n),
new Fp(13695468978618890430n),
],
[
new Fp(8214202050877825436n),
new Fp(2670727992739346204n),
new Fp(16259532062589659211n),
new Fp(11869922396257088411n),
new Fp(3179482916972760137n),
new Fp(13525476046633427808n),
new Fp(3217337278042947412n),
new Fp(14494689598654046340n),
new Fp(15837379330312175383n),
new Fp(8029037639801151344n),
new Fp(2153456285263517937n),
new Fp(8301106462311849241n),
],
[
new Fp(13294194396455217955n),
new Fp(17394768489610594315n),
new Fp(12847609130464867455n),
new Fp(14015739446356528640n),
new Fp(5879251655839607853n),
new Fp(9747000124977436185n),
new Fp(8950393546890284269n),
new Fp(10765765936405694368n),
new Fp(14695323910334139959n),
new Fp(16366254691123000864n),
new Fp(15292774414889043182n),
new Fp(10910394433429313384n),
],
[
new Fp(17253424460214596184n),
new Fp(3442854447664030446n),
new Fp(3005570425335613727n),
new Fp(10859158614900201063n),
new Fp(9763230642109343539n),
new Fp(6647722546511515039n),
new Fp(909012944955815706n),
new Fp(18101204076790399111n),
new Fp(11588128829349125809n),
new Fp(15863878496612806566n),
new Fp(5201119062417750399n),
new Fp(176665553780565743n),
],
];
const INTERNAL_CONSTANTS: Fp[] = [
new Fp(11921381764981422944n),
new Fp(10318423381711320787n),
new Fp(8291411502347000766n),
new Fp(229948027109387563n),
new Fp(9152521390190983261n),
new Fp(7129306032690285515n),
new Fp(15395989607365232011n),
new Fp(8641397269074305925n),
new Fp(17256848792241043600n),
new Fp(6046475228902245682n),
new Fp(12041608676381094092n),
new Fp(12785542378683951657n),
new Fp(14546032085337914034n),
new Fp(3304199118235116851n),
new Fp(16499627707072547655n),
new Fp(10386478025625759321n),
new Fp(13475579315436919170n),
new Fp(16042710511297532028n),
new Fp(1411266850385657080n),
new Fp(9024840976168649958n),
new Fp(14047056970978379368n),
new Fp(838728605080212101n),
];
const MATRIX_DIAG: Fp[] = [
new Fp(0xc3b6c08e23ba9300n),
new Fp(0xd84b5de94a324fb6n),
new Fp(0x0d0c371c5b35b84fn),
new Fp(0x7964f570e7188037n),
new Fp(0x5daf18bbd996604bn),
new Fp(0x6743bc47b9595257n),
new Fp(0x5528b9362c59bb70n),
new Fp(0xac45e25b7127b68bn),
new Fp(0xa2077d7dfbb606b5n),
new Fp(0xf3faac6faee378aen),
new Fp(0x0c6388b51545e883n),
new Fp(0xd27dbb6944917b60n),
];
export function hashToFp5(values: Fp[]): Fp5 {
const result = hashNToMNoPad(values, 5);
const [c0, c1, c2, c3, c4] = result as [Fp, Fp, Fp, Fp, Fp];
return new Fp5([c0, c1, c2, c3, c4]);
}
export function hashTwoToOne(a: Fp[], b: Fp[]): Fp[] {
return hashNToMNoPad([...a, ...b], 4);
}
export function hashNToMNoPad(input: Fp[], outputCount: number): Fp[] {
const state: Fp[] = Array.from({ length: WIDTH }, () => Fp.ZERO);
for (let offset = 0; offset < input.length; offset += RATE) {
for (let j = 0; j < RATE && offset + j < input.length; j++) {
const current = state[j]!;
const value = input[offset + j];
if (value === undefined) throw new Error("poseidon input missing element");
state[j] = current.add(value);
}
permute(state);
}
const outputs: Fp[] = [];
while (outputs.length < outputCount) {
for (let i = 0; i < RATE && outputs.length < outputCount; i++) {
outputs.push(state[i]!);
}
if (outputs.length < outputCount) {
permute(state);
}
}
return outputs;
}
function permute(state: Fp[]): void {
externalLinearLayer(state);
fullRounds(state, 0);
partialRounds(state);
fullRounds(state, ROUNDS_F_HALF);
}
function fullRounds(state: Fp[], startRound: number): void {
for (let r = startRound; r < startRound + ROUNDS_F_HALF; r++) {
addRoundConstants(state, r);
sbox(state);
externalLinearLayer(state);
}
}
function partialRounds(state: Fp[]): void {
for (let r = 0; r < ROUNDS_P; r++) {
addInternalConstant(state, r);
sboxP(state, 0);
internalLinearLayer(state);
}
}
function addRoundConstants(state: Fp[], round: number): void {
const constants = EXTERNAL_CONSTANTS[round];
if (!constants) {
throw new Error(`poseidon round constant missing for round ${round}`);
}
for (let i = 0; i < WIDTH; i++) {
state[i] = state[i]!.add(constants[i]!);
}
}
function addInternalConstant(state: Fp[], round: number): void {
const constant = INTERNAL_CONSTANTS[round];
if (!constant) {
throw new Error(`poseidon internal constant missing for round ${round}`);
}
state[0] = state[0]!.add(constant);
}
function sbox(state: Fp[]): void {
for (let i = 0; i < WIDTH; i++) {
sboxP(state, i);
}
}
function sboxP(state: Fp[], index: number): void {
const x = state[index];
if (!x) throw new Error(`poseidon state missing at index ${index}`);
const x2 = x.square();
const x3 = x2.mul(x);
const x6 = x3.square();
state[index] = x6.mul(x);
}
function externalLinearLayer(state: Fp[]): void {
for (let block = 0; block < 3; block++) {
const base = block * 4;
const s0 = state[base]!;
const s1 = state[base + 1]!;
const s2 = state[base + 2]!;
const s3 = state[base + 3]!;
const t0 = s0.add(s1);
const t1 = s2.add(s3);
const t2 = t0.add(t1);
const t3 = t2.add(s1);
const t4 = t2.add(s3);
const t5 = s0.double();
const t6 = s2.double();
state[base] = t3.add(t0);
state[base + 1] = t6.add(t3);
state[base + 2] = t1.add(t4);
state[base + 3] = t5.add(t4);
}
const sums: Fp[] = [Fp.ZERO, Fp.ZERO, Fp.ZERO, Fp.ZERO];
for (let k = 0; k < 4; k++) {
for (let j = 0; j < WIDTH; j += 4) {
const currentSum = sums[k];
if (currentSum === undefined) throw new Error("poseidon sums missing value");
sums[k] = currentSum.add(state[j + k]!);
}
}
for (let i = 0; i < WIDTH; i++) {
state[i] = state[i]!.add(sums[i % 4]!);
}
}
function internalLinearLayer(state: Fp[]): void {
let sum = state[0]!;
for (let i = 1; i < WIDTH; i++) {
sum = sum.add(state[i]!);
}
for (let i = 0; i < WIDTH; i++) {
state[i] = state[i]!.mul(MATRIX_DIAG[i]!).add(sum);
}
}
export function hashToQuinticExtension(preimage: Fp[]): Fp5 {
return hashToFp5(preimage);
}
export function hashFp5Pair(a: Fp5, b: Fp5): Fp5 {
const inputs: Fp[] = [...a.toTuple(), ...b.toTuple()];
return hashToQuinticExtension(inputs);
}
export function mergeFp5WithHash(fp5Values: Fp5[]): Fp5 {
if (fp5Values.length === 0) return Fp5.ZERO;
const first = fp5Values[0];
if (!first) throw new Error("mergeFp5WithHash received empty array");
let acc = first;
for (let i = 1; i < fp5Values.length; i++) {
const next = fp5Values[i];
if (!next) throw new Error("mergeFp5WithHash encountered undefined value");
acc = hashFp5Pair(acc, next);
}
return acc;
}
+159
View File
@@ -0,0 +1,159 @@
import { randomBytes } from "crypto";
import { Fp } from "./goldilocks";
import { Fp5 } from "./goldilocks-fp5";
const ORDER = BigInt("1067993516717146951041484916571792702745057740581727230159139685185762082554198619328292418486241");
const BYTE_LENGTH = 40;
const FOUR_BIT_LIMBS = 80;
const BIT_LENGTH = 319;
function modOrder(value: bigint): bigint {
let v = value % ORDER;
if (v < 0n) v += ORDER;
return v;
}
export class Scalar {
readonly value: bigint;
constructor(value: bigint | number) {
this.value = modOrder(typeof value === "number" ? BigInt(value) : value);
}
static readonly ZERO = new Scalar(0n);
static readonly ONE = new Scalar(1n);
static fromBytesLE(bytes: Uint8Array): Scalar {
if (bytes.length !== BYTE_LENGTH) {
throw new Error(`Scalar expects ${BYTE_LENGTH} bytes, got ${bytes.length}`);
}
let acc = 0n;
for (let i = 0; i < BYTE_LENGTH; i++) {
const byte = bytes[i];
if (byte === undefined) throw new Error("Unexpected undefined byte when reading scalar");
acc |= BigInt(byte) << BigInt(8 * i);
}
return new Scalar(acc);
}
toBytesLE(): Uint8Array {
let v = this.value;
const out = new Uint8Array(BYTE_LENGTH);
for (let i = 0; i < BYTE_LENGTH; i++) {
out[i] = Number(v & 0xffn);
v >>= 8n;
}
return out;
}
toBigInt(): bigint {
return this.value;
}
add(other: Scalar): Scalar {
return new Scalar(this.value + other.value);
}
sub(other: Scalar): Scalar {
return new Scalar(this.value - other.value);
}
neg(): Scalar {
return new Scalar(this.value === 0n ? 0n : ORDER - this.value);
}
mul(other: Scalar): Scalar {
return new Scalar(this.value * other.value);
}
square(): Scalar {
return this.mul(this);
}
pow(exponent: bigint): Scalar {
let result = 1n;
let base = this.value;
let exp = exponent;
while (exp > 0n) {
if (exp & 1n) result = modOrder(result * base);
base = modOrder(base * base);
exp >>= 1n;
}
return new Scalar(result);
}
isZero(): boolean {
return this.value === 0n;
}
equals(other: Scalar): boolean {
return this.value === other.value;
}
clone(): Scalar {
return new Scalar(this.value);
}
splitTo4BitLimbs(): Uint8Array {
const limbs = new Uint8Array(FOUR_BIT_LIMBS);
let tmp = this.value;
for (let i = 0; i < FOUR_BIT_LIMBS; i++) {
limbs[i] = Number(tmp & 0xfn);
tmp >>= 4n;
}
return limbs;
}
recodeSigned(window: number): Int32Array {
const length = Math.ceil(BIT_LENGTH / window);
const digits = new Int32Array(length);
const twoPowW = 1n << BigInt(window);
const twoPowWMinus1 = 1n << BigInt(window - 1);
let k = this.value;
let i = 0;
while (k > 0n && i < length) {
if (k & 1n) {
let remainder = Number(k % twoPowW);
if (remainder >= Number(twoPowWMinus1)) {
remainder -= Number(twoPowW);
}
digits[i] = remainder;
k -= BigInt(remainder);
}
k >>= 1n;
i++;
}
// remaining digits already zero
return digits;
}
static random(): Scalar {
while (true) {
const buf = randomBytes(BYTE_LENGTH);
let acc = 0n;
for (let i = 0; i < BYTE_LENGTH; i++) {
const byte = buf[i];
if (byte === undefined) throw new Error("Unexpected undefined byte when sampling scalar");
acc |= BigInt(byte) << BigInt(8 * i);
}
if (acc < ORDER) {
return new Scalar(acc);
}
}
}
static fromFp5(element: Fp5): Scalar {
const coeffs = element.toTuple();
const entries = [coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4]];
let acc = 0n;
for (let i = entries.length - 1; i >= 0; i--) {
const limb = entries[i];
if (!limb) throw new Error("Fp5 tuple missing limb");
acc <<= 64n;
acc |= limb.toBigInt();
}
return new Scalar(acc);
}
}
export const SCALAR_ORDER = ORDER;
+64
View File
@@ -0,0 +1,64 @@
import { randomBytes } from "crypto";
import { Fp } from "./goldilocks";
import { Fp5 } from "./goldilocks-fp5";
import { hashToQuinticExtension } from "./poseidon2";
import { Scalar } from "./scalar";
import { ECPoint } from "./curve";
export interface SchnorrSignature {
s: Scalar;
e: Scalar;
}
export class LighterPrivateKey {
readonly scalar: Scalar;
constructor(scalar: Scalar) {
this.scalar = scalar;
}
static fromBytes(bytes: Uint8Array): LighterPrivateKey {
if (bytes.length !== 40) {
throw new Error("Lighter private key must be 40 bytes");
}
return new LighterPrivateKey(Scalar.fromBytesLE(bytes));
}
static fromHex(hex: string): LighterPrivateKey {
const normalized = hex.startsWith("0x") ? hex.slice(2) : hex;
if (normalized.length !== 80) {
throw new Error("Lighter private key hex must encode 40 bytes");
}
const bytes = Buffer.from(normalized, "hex");
return LighterPrivateKey.fromBytes(bytes);
}
toBytes(): Uint8Array {
return this.scalar.toBytesLE();
}
publicKey(): Fp5 {
const point = ECPoint.generator().mul(this.scalar);
return point.encode();
}
signHashedMessage(hashed: Fp5): SchnorrSignature {
const k = Scalar.random();
const rPoint = ECPoint.generator().mul(k);
const rEncoded = rPoint.encode();
const preimage: Fp[] = [...rEncoded.toTuple(), ...hashed.toTuple()];
const hash = hashToQuinticExtension(preimage);
const e = Scalar.fromFp5(hash);
const s = k.sub(e.mul(this.scalar));
return { s, e };
}
}
export function signatureToBytes(sig: SchnorrSignature): Uint8Array {
const sBytes = sig.s.toBytesLE();
const eBytes = sig.e.toBytesLE();
const out = new Uint8Array(80);
out.set(sBytes, 0);
out.set(eBytes, 40);
return out;
}