UNPKG

@symbioticfi/relay-stats-ts

Version:

TypeScript library for deriving validator sets from Symbiotic network contracts

172 lines (139 loc) 4.85 kB
import type { Hex } from 'viem'; import { keccak256 } from 'viem'; // Minimal BN254 (altbn128) helpers for G1 in affine coordinates. // This is a lightweight implementation sufficient for building extraData only. const FP_MODULUS = BigInt('0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47'); // (p+1)/4 used for Tonelli-Shanks on BN254 (matches Go implementation) const SQRT_EXPONENT = BigInt('0xc19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52'); function mod(a: bigint, m: bigint = FP_MODULUS): bigint { const r = a % m; return r >= 0n ? r : r + m; } // reserved helpers (kept simple for clarity) function modMul(a: bigint, b: bigint): bigint { return mod(a * b); } function modPow(base: bigint, exp: bigint): bigint { let result = 1n; let b = mod(base); let e = exp; while (e > 0n) { if (e & 1n) result = modMul(result, b); b = modMul(b, b); e >>= 1n; } return result; } function modInv(a: bigint): bigint { // a^(p-2) mod p for prime field return modPow(mod(a), FP_MODULUS - 2n); } function fromHex(hex: Hex): Uint8Array { const h = hex.startsWith('0x') ? hex.slice(2) : hex; if (h.length % 2 !== 0) throw new Error('Invalid hex'); return new Uint8Array(Buffer.from(h, 'hex')); } function toHex(bytes: Uint8Array): Hex { return ('0x' + Buffer.from(bytes).toString('hex')) as Hex; } function bytesToBigIntBE(bytes: Uint8Array): bigint { // Preserve leading zeros by padding to the correct length const hex = Buffer.from(bytes).toString('hex'); return BigInt('0x' + hex); } function bigIntToBytes32BE(x: bigint): Uint8Array { const out = new Uint8Array(32); const hex = x.toString(16).padStart(64, '0'); out.set(Buffer.from(hex, 'hex')); return out; } export type G1 = { x: bigint; y: bigint } | null; // null represents infinity function isInfinity(p: G1): p is null { return p === null; } // equality not required by current usage export function findYFromX(x: bigint): bigint { // Go: Calculate beta = x^3 + 3 mod p const beta = mod(modPow(mod(x), 3n) + 3n); // Go: Calculate y = beta^((p+1)/4) mod p return modPow(beta, SQRT_EXPONENT); } export function compressG1FromXY(x: bigint, y: bigint): Hex { const xMod = mod(x); const yMod = mod(y); const derivedY = findYFromX(xMod); const flag = yMod === derivedY ? 0n : 1n; const compressed = 2n * xMod + flag; return toHex(bigIntToBytes32BE(compressed)); } export function parseG1Uncompressed(raw: Hex): G1 { const bytes = fromHex(raw); if (bytes.length < 64) throw new Error('Expected 64-byte uncompressed G1'); // Extract X and Y coordinates directly from hex string to preserve leading zeros const hex = raw.startsWith('0x') ? raw.slice(2) : raw; const xHex = hex.slice(0, 64); const yHex = hex.slice(64, 128); const x = BigInt('0x' + xHex); const y = BigInt('0x' + yHex.padStart(64, '0')); return { x: mod(x), y: mod(y) }; } // negation not required by current usage function pointDouble(p: G1): G1 { if (isInfinity(p)) return p; if (p.y === 0n) return null; const lambda = modMul(3n * modMul(p.x, p.x), modInv(2n * p.y)); const xr = mod(lambda * lambda - 2n * p.x); const yr = mod(lambda * (p.x - xr) - p.y); return { x: xr, y: yr }; } export function pointAdd(a: G1, b: G1): G1 { if (isInfinity(a)) return b; if (isInfinity(b)) return a; if (a.x === b.x) { if (mod(a.y + b.y) === 0n) return null; return pointDouble(a); } const lambda = modMul(b.y - a.y, modInv(b.x - a.x)); const xr = mod(lambda * lambda - a.x - b.x); const yr = mod(lambda * (a.x - xr) - a.y); return { x: xr, y: yr }; } export function aggregateG1(keys: Hex[]): G1 { let acc: G1 = null; for (const k of keys) { const p = parseKeyToPoint(k); acc = pointAdd(acc, p); } return acc; } export function compressAggregatedG1(keys: Hex[]): Hex { const p = aggregateG1(keys); if (p === null) return ('0x' + '00'.repeat(32)) as Hex; return compressG1FromXY(p.x, p.y); } export function compressRawG1(raw: Hex): Hex { const p = parseKeyToPoint(raw); if (p === null) return ('0x' + '00'.repeat(32)) as Hex; return compressG1FromXY(p.x, p.y); } export function keccak(bytes: Uint8Array): Hex { return keccak256(toHex(bytes)); } function parseCompressedToPoint(raw: Hex): G1 { const bytes = fromHex(raw); if (bytes.length !== 32) throw new Error('Expected 32-byte compressed G1'); const v = bytesToBigIntBE(bytes); const x = v >> 1n; const flag = v & 1n; let y = findYFromX(x); if (flag === 1n) { y = mod(-y); } return { x: mod(x), y }; } export function parseKeyToPoint(raw: Hex): G1 { const bytes = fromHex(raw); if (bytes.length >= 64) return parseG1Uncompressed(raw); if (bytes.length === 32) return parseCompressedToPoint(raw); throw new Error('Unsupported G1 key length'); }