Files
salvium-rs/src/ed25519.js
T
Matt Hess aa716113f5 Add STAKE transaction creation and fix CLSAG verification
STAKE transactions:
  - Add buildStakeTransaction() for creating stake transactions
  - Add createStakeTransaction() wallet API for staking SAL/SAL1
  - Extend serializeTxPrefix() with Salvium-specific fields (txType,
    amount_burnt, asset types, return address, protocol_tx_data)
  - Support STAKE_LOCK_PERIOD from network config (21600 mainnet, 20 testnet)

  CLSAG fixes:
  - Fix challenge hash to include full ring data matching C++ implementation
  - Update both signing and verification to use consistent hash format
2026-01-22 03:10:40 +00:00

845 lines
22 KiB
JavaScript

/**
* Ed25519 Elliptic Curve Operations
*
* Uses @noble/ed25519 for optimized scalar multiplication (hot path).
* Falls back to BigInt implementation for specialized operations.
*/
import { Point as NoblePoint } from '@noble/ed25519';
// Prime field: p = 2^255 - 19
const P = (1n << 255n) - 19n;
// Curve constant d = -121665/121666 mod p
const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
// 2*d
const D2 = (2n * D) % P;
// sqrt(-1) mod p
const I = 19681161376707505956807079304988542015446066515923890162744021073123829784752n;
// Base point G (Ed25519 standard base point)
// GY = 4/5 mod P, GX is the positive (even) square root satisfying the curve equation
const GX = 15112221349535400772501151409588531511454012693041857206046113283949847762202n;
const GY = 46316835694926478169428394003475163141307993866256225615783033603165251855960n;
// Generator T for CARROT/FCMP++ (from Salvium generators.cpp)
// T = H_p(keccak("Monero Generator T"))
// Encoded as: 966fc66b82cd56cf85eaec801c42845f5f408878d1561e00d3d7ded2794d094f
const T_BYTES = new Uint8Array([
0x96, 0x6f, 0xc6, 0x6b, 0x82, 0xcd, 0x56, 0xcf,
0x85, 0xea, 0xec, 0x80, 0x1c, 0x42, 0x84, 0x5f,
0x5f, 0x40, 0x88, 0x78, 0xd1, 0x56, 0x1e, 0x00,
0xd3, 0xd7, 0xde, 0xd2, 0x79, 0x4d, 0x09, 0x4f
]);
// Group order L
const L = (1n << 252n) + 27742317777372353535851937790883648493n;
// Crypto random (for browser & Node.js compatibility)
function getRandomBytes(n) {
if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
const buf = new Uint8Array(n);
globalThis.crypto.getRandomValues(buf);
return buf;
}
// Node.js fallback
const { randomBytes } = require('crypto');
return new Uint8Array(randomBytes(n));
}
// Field operations
function feAdd(a, b) {
return (a + b) % P;
}
function feSub(a, b) {
return (a - b + P) % P;
}
function feMul(a, b) {
return (a * b) % P;
}
function feSq(a) {
return (a * a) % P;
}
function feNeg(a) {
return (P - a) % P;
}
// Modular exponentiation
function fePow(base, exp) {
let result = 1n;
base = base % P;
while (exp > 0n) {
if (exp & 1n) result = (result * base) % P;
exp = exp >> 1n;
base = (base * base) % P;
}
return result;
}
// Modular inverse using Fermat's little theorem: a^(-1) = a^(p-2) mod p
function feInv(a) {
return fePow(a, P - 2n);
}
// Square root mod p (for point decompression)
// p ≡ 5 (mod 8), so sqrt(a) = a^((p+3)/8) or i*a^((p+3)/8)
function feSqrt(a) {
const exp = (P + 3n) / 8n;
let r = fePow(a, exp);
// Check if r^2 = a
if (feSq(r) === a) return r;
// Try i*r
r = feMul(r, I);
if (feSq(r) === a) return r;
return null; // No square root exists
}
// Point in extended coordinates: (X:Y:Z:T) where x=X/Z, y=Y/Z, xy=T/Z
// Represented as {X, Y, Z, T} with BigInt values
function pointZero() {
return { X: 0n, Y: 1n, Z: 1n, T: 0n };
}
function pointFromXY(x, y) {
return { X: x, Y: y, Z: 1n, T: feMul(x, y) };
}
function pointCopy(p) {
return { X: p.X, Y: p.Y, Z: p.Z, T: p.T };
}
// Point addition using unified formula (works for doubling too)
// Based on https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
function pointAdd(p, q) {
const A = feMul(feSub(p.Y, p.X), feSub(q.Y, q.X));
const B = feMul(feAdd(p.Y, p.X), feAdd(q.Y, q.X));
const C = feMul(feMul(p.T, q.T), D2);
const D_ = feMul(p.Z, q.Z);
const D2_ = feAdd(D_, D_);
const E = feSub(B, A);
const F = feSub(D2_, C);
const G = feAdd(D2_, C);
const H = feAdd(B, A);
return {
X: feMul(E, F),
Y: feMul(G, H),
Z: feMul(F, G),
T: feMul(E, H)
};
}
// Point doubling using ge_p2_dbl formula from Salvium ref10
// This matches ge_p2_dbl.h exactly
function pointDouble(p) {
// Matching ge_p2_dbl.h exactly:
// XX = X^2
const XX = feSq(p.X);
// YY = Y^2
const YY = feSq(p.Y);
// B = 2*Z^2 (fe_sq2 computes 2*a^2)
const B = feMul(2n, feSq(p.Z));
// A = X + Y
const A = feAdd(p.X, p.Y);
// AA = A^2 = (X+Y)^2
const AA = feSq(A);
// Y3 = YY + XX
const Y3_p1p1 = feAdd(YY, XX);
// Z3 = YY - XX
const Z3_p1p1 = feSub(YY, XX);
// X3 = AA - Y3 = (X+Y)^2 - (YY+XX) = 2XY
const X3_p1p1 = feSub(AA, Y3_p1p1);
// T3 = B - Z3 = 2Z^2 - (YY-XX)
const T3_p1p1 = feSub(B, Z3_p1p1);
// Now convert from p1p1 to p3 (extended) using ge_p1p1_to_p3
// r->X = p->X * p->T
// r->Y = p->Y * p->Z
// r->Z = p->Z * p->T
// r->T = p->X * p->Y
return {
X: feMul(X3_p1p1, T3_p1p1),
Y: feMul(Y3_p1p1, Z3_p1p1),
Z: feMul(Z3_p1p1, T3_p1p1),
T: feMul(X3_p1p1, Y3_p1p1)
};
}
// Unified add formula for comparison
function pointAddUnified(p, q) {
const A = feMul(feSub(p.Y, p.X), feSub(q.Y, q.X));
const B = feMul(feAdd(p.Y, p.X), feAdd(q.Y, q.X));
const C = feMul(feMul(p.T, q.T), D2);
const D_ = feMul(p.Z, q.Z);
const D2_ = feAdd(D_, D_);
const E = feSub(B, A);
const F = feSub(D2_, C);
const G = feAdd(D2_, C);
const H = feAdd(B, A);
return {
X: feMul(E, F),
Y: feMul(G, H),
Z: feMul(F, G),
T: feMul(E, H)
};
}
// Scalar multiplication using double-and-add
function scalarMult(p, s) {
let result = pointZero();
let base = pointCopy(p);
// Process scalar bits from LSB to MSB
while (s > 0n) {
if (s & 1n) {
result = pointAdd(result, base);
}
base = pointDouble(base);
s = s >> 1n;
}
return result;
}
// Compress point to 32 bytes
function pointToBytes(p) {
// Convert to affine: x = X/Z, y = Y/Z
const zi = feInv(p.Z);
const x = feMul(p.X, zi);
const y = feMul(p.Y, zi);
// Encode y with sign of x in high bit
const bytes = new Uint8Array(32);
let yy = y;
for (let i = 0; i < 32; i++) {
bytes[i] = Number(yy & 0xffn);
yy = yy >> 8n;
}
// Set high bit if x is "negative" (odd)
if (x & 1n) {
bytes[31] |= 0x80;
}
return bytes;
}
// Decompress 32 bytes to point
function pointFromBytes(bytes) {
// Extract y (clear high bit)
let y = 0n;
for (let i = 31; i >= 0; i--) {
y = (y << 8n) | BigInt(bytes[i]);
}
const xSign = (y >> 255n) & 1n;
y = y & ((1n << 255n) - 1n);
if (y >= P) return null;
// Recover x from curve equation: x^2 = (y^2 - 1) / (d*y^2 + 1)
const y2 = feSq(y);
const num = feSub(y2, 1n);
const den = feAdd(feMul(D, y2), 1n);
const denInv = feInv(den);
const x2 = feMul(num, denInv);
let x = feSqrt(x2);
if (x === null) return null;
// Adjust sign
if ((x & 1n) !== xSign) {
x = feNeg(x);
}
return pointFromXY(x, y);
}
// Convert scalar bytes (little-endian) to BigInt
function scalarFromBytes(bytes) {
let s = 0n;
for (let i = bytes.length - 1; i >= 0; i--) {
s = (s << 8n) | BigInt(bytes[i]);
}
return s;
}
// Public API
/**
* Check if scalar is valid (< L)
* @param {Uint8Array} s - 32-byte scalar
* @returns {boolean} true if valid
*/
export function scalarCheck(s) {
if (s.length !== 32) return false;
const scalar = scalarFromBytes(s);
return scalar < L;
}
/**
* Check if scalar is non-zero
* @param {Uint8Array} s - 32-byte scalar
* @returns {boolean} true if non-zero
*/
export function scalarIsNonzero(s) {
const scalar = scalarFromBytes(s);
return scalar !== 0n;
}
/**
* Scalar subtraction: r = a - b (mod L)
* @param {Uint8Array} r - 32-byte output
* @param {Uint8Array} a - 32-byte input
* @param {Uint8Array} b - 32-byte input
*/
export function scalarSub(r, a, b) {
const aVal = scalarFromBytes(a);
const bVal = scalarFromBytes(b);
let result = (aVal - bVal) % L;
if (result < 0n) result += L;
// Convert back to bytes (little-endian)
for (let i = 0; i < 32; i++) {
r[i] = Number(result & 0xffn);
result = result >> 8n;
}
}
/**
* Scalar multiplication with base point: s * G
* Uses @noble/ed25519 for optimized performance.
* @param {Uint8Array} s - 32-byte scalar
* @returns {Uint8Array} 32-byte compressed public key
*/
export function scalarMultBase(s) {
try {
// Convert scalar to BigInt (little-endian)
let scalar = 0n;
for (let i = 0; i < 32; i++) {
scalar |= BigInt(s[i]) << BigInt(i * 8);
}
// Use @noble for fast base point multiplication
const result = NoblePoint.BASE.multiply(scalar);
return result.toBytes();
} catch (e) {
// Fallback to original implementation
const scalarBig = scalarFromBytes(s);
const G = pointFromXY(GX, GY);
const result = scalarMult(G, scalarBig);
return pointToBytes(result);
}
}
/**
* Scalar multiplication with arbitrary point: s * P
* Uses @noble/ed25519 for optimized performance.
* @param {Uint8Array} s - 32-byte scalar
* @param {Uint8Array} P - 32-byte compressed point
* @returns {Uint8Array|null} 32-byte compressed result, or null if P is invalid
*/
export function scalarMultPoint(s, P) {
try {
// Convert scalar to BigInt (little-endian)
let scalar = 0n;
for (let i = 0; i < 32; i++) {
scalar |= BigInt(s[i]) << BigInt(i * 8);
}
// Use @noble for fast scalar multiplication
const point = NoblePoint.fromBytes(P);
const result = point.multiply(scalar);
return result.toBytes();
} catch (e) {
return null;
}
}
/**
* Get the T generator point (used in CARROT)
* @returns {Uint8Array} 32-byte compressed T point
*/
export function getGeneratorT() {
return new Uint8Array(T_BYTES);
}
/**
* Get the G generator point (standard Ed25519 base point)
* @returns {Uint8Array} 32-byte compressed G point
*/
export function getGeneratorG() {
const G = pointFromXY(GX, GY);
return pointToBytes(G);
}
/**
* Point addition: P + Q
* @param {Uint8Array} P - 32-byte compressed point
* @param {Uint8Array} Q - 32-byte compressed point
* @returns {Uint8Array|null} 32-byte compressed result, or null if invalid
*/
export function pointAddCompressed(P, Q) {
const p = pointFromBytes(P);
const q = pointFromBytes(Q);
if (!p || !q) return null;
const result = pointAdd(p, q);
return pointToBytes(result);
}
/**
* Compute CARROT account spend public key: K_s = k_gi * G + k_ps * T
* @param {Uint8Array} k_gi - 32-byte generate-image key
* @param {Uint8Array} k_ps - 32-byte prove-spend key
* @returns {Uint8Array} 32-byte compressed public key
*/
export function computeCarrotSpendPubkey(k_gi, k_ps) {
const giScalar = scalarFromBytes(k_gi);
const psScalar = scalarFromBytes(k_ps);
// Get generators
const G = pointFromXY(GX, GY);
const T = pointFromBytes(T_BYTES);
if (!T) throw new Error('Failed to decode T generator');
// K_s = k_gi * G + k_ps * T
const giG = scalarMult(G, giScalar);
const psT = scalarMult(T, psScalar);
const result = pointAdd(giG, psT);
return pointToBytes(result);
}
/**
* Compute CARROT account view public key: K_v = k_vi * K_s
* Used for subaddress derivation, NOT for main address
* @param {Uint8Array} k_vi - 32-byte view-incoming key
* @param {Uint8Array} K_s - 32-byte spend public key
* @returns {Uint8Array|null} 32-byte compressed view public key, or null if K_s invalid
*/
export function computeCarrotAccountViewPubkey(k_vi, K_s) {
return scalarMultPoint(k_vi, K_s);
}
/**
* Compute CARROT main address view public key: K_v = k_vi * G
* This is what goes into the main address (j=0)
* @param {Uint8Array} k_vi - 32-byte view-incoming key
* @returns {Uint8Array} 32-byte compressed view public key
*/
export function computeCarrotMainAddressViewPubkey(k_vi) {
return scalarMultBase(k_vi);
}
/**
* Decompress a public key (ge_frombytes_vartime equivalent)
* @param {Uint8Array} pk - 32-byte compressed public key
* @returns {Object|null} Point or null if invalid
*/
export { pointFromBytes };
/**
* Compress a point to bytes (ge_tobytes equivalent)
* @param {Object} p - Point {X, Y, Z, T}
* @returns {Uint8Array} 32-byte compressed point
*/
export { pointToBytes };
/**
* Double scalar multiplication: aP + bG
* @param {Uint8Array} a - 32-byte scalar
* @param {Object} P - Point
* @param {Uint8Array} b - 32-byte scalar
* @returns {Uint8Array} 32-byte compressed result
*/
export function doubleScalarMultBase(a, P, b) {
const aScalar = scalarFromBytes(a);
const bScalar = scalarFromBytes(b);
const G = pointFromXY(GX, GY);
// Compute a*P + b*G
const aP = scalarMult(P, aScalar);
const bG = scalarMult(G, bScalar);
const result = pointAdd(aP, bG);
return pointToBytes(result);
}
/**
* Check if a point is the identity (neutral element)
* @param {Uint8Array} p - 32-byte compressed point
* @returns {boolean} true if identity
*/
export function isIdentity(p) {
// Identity point encodes as y=1, x=0 -> bytes = [1, 0, 0, ..., 0]
if (p[0] !== 1) return false;
for (let i = 1; i < 32; i++) {
if (p[i] !== 0) return false;
}
return true;
}
// Debug functions
export function testDouble() {
const G = pointFromXY(GX, GY);
const G2 = pointAdd(G, G);
return pointToBytes(G2);
}
export function getBasePoint() {
const G = pointFromXY(GX, GY);
return pointToBytes(G);
}
export function test2G() {
const scalar = new Uint8Array(32);
scalar[0] = 2;
return scalarMultBase(scalar);
}
// Debug: test G + identity = G
export function testIdentity() {
const G = pointFromXY(GX, GY);
const identity = pointZero();
const result = pointAdd(G, identity);
return pointToBytes(result);
}
// Debug: get affine coordinates of 2G
export function get2GAffine() {
const G = pointFromXY(GX, GY);
const G2 = pointAdd(G, G);
const zi = feInv(G2.Z);
const x = feMul(G2.X, zi);
const y = feMul(G2.Y, zi);
return { x: x.toString(), y: y.toString() };
}
// Debug: check if point (x,y) is on the curve: -x² + y² = 1 + d*x²*y²
export function isOnCurve(x, y) {
const x2 = feSq(x);
const y2 = feSq(y);
const lhs = feSub(y2, x2); // -x² + y² = y² - x²
const rhs = feAdd(1n, feMul(D, feMul(x2, y2))); // 1 + d*x²*y²
return lhs === rhs;
}
// Debug: show curve equation values for G
export function debugCurveEquation() {
const x2 = feSq(GX);
const y2 = feSq(GY);
const lhs = feSub(y2, x2);
const dxy2 = feMul(D, feMul(x2, y2));
const rhs = feAdd(1n, dxy2);
return {
GX: GX.toString(),
GY: GY.toString(),
D: D.toString(),
P: P.toString(),
x2: x2.toString(),
y2: y2.toString(),
lhs: lhs.toString(), // y² - x²
dxy2: dxy2.toString(), // d*x²*y²
rhs: rhs.toString(), // 1 + d*x²*y²
match: lhs === rhs,
diff: (lhs > rhs ? lhs - rhs : rhs - lhs).toString()
};
}
// Debug: verify G is on curve
export function checkG() {
return isOnCurve(GX, GY);
}
// Debug: verify 2G is on curve
export function check2G() {
const G = pointFromXY(GX, GY);
const G2 = pointAdd(G, G);
const zi = feInv(G2.Z);
const x = feMul(G2.X, zi);
const y = feMul(G2.Y, zi);
return isOnCurve(x, y);
}
// Debug: compare doubling methods
export function compare2GMethods() {
const G = pointFromXY(GX, GY);
// Method 1: Using pointDouble (Salvium ge_p2_dbl)
const G2_double = pointDouble(G);
const zi1 = feInv(G2_double.Z);
const x1 = feMul(G2_double.X, zi1);
const y1 = feMul(G2_double.Y, zi1);
// Method 2: Using unified add formula
const G2_add = pointAddUnified(G, G);
const zi2 = feInv(G2_add.Z);
const x2 = feMul(G2_add.X, zi2);
const y2 = feMul(G2_add.Y, zi2);
return {
doubleX: x1.toString(),
doubleY: y1.toString(),
addX: x2.toString(),
addY: y2.toString(),
match: x1 === x2 && y1 === y2,
onCurve1: isOnCurve(x1, y1),
onCurve2: isOnCurve(x2, y2)
};
}
// Debug: decode expected 2G from hex and show coordinates
export function decodeExpected2G() {
// Expected encoding: c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022
const hex = 'c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022';
const bytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
// Decode using our pointFromBytes
const point = pointFromBytes(bytes);
if (!point) return { error: 'Failed to decode point' };
const zi = feInv(point.Z);
const x = feMul(point.X, zi);
const y = feMul(point.Y, zi);
return {
x: x.toString(),
y: y.toString(),
onCurve: isOnCurve(x, y)
};
}
// Debug: verify field operations are correct
export function testFieldOps() {
// Test that (a + b) mod p works
const a = P - 1n; // largest value
const b = 2n;
const sum = feAdd(a, b); // should be 1
// Test subtraction
const diff = feSub(2n, 5n); // should be p - 3
// Test multiplication
const prod = feMul(GX, GY);
// Test identity point (0, 1) - must be on ANY Edwards curve
const identityOnCurve = isOnCurve(0n, 1n);
// Verify y = 4/5 mod p for base point
// 5 * GY mod P should equal 4
const fiveTimesY = feMul(5n, GY);
const yIsFourFifths = fiveTimesY === 4n;
return {
addTest: sum === 1n,
subTest: diff === P - 3n,
prodNonZero: prod > 0n && prod < P,
gxLessThanP: GX < P,
gyLessThanP: GY < P,
dLessThanP: D < P,
identityOnCurve: identityOnCurve,
yIsFourFifths: yIsFourFifths,
fiveTimesY: fiveTimesY.toString()
};
}
// Convert 10-limb ref10 representation to BigInt
// Limbs alternate between 26 and 25 bits: 26,25,26,25,26,25,26,25,26,25
function limbsToFe(limbs) {
const shifts = [0n, 26n, 51n, 77n, 102n, 128n, 153n, 179n, 204n, 230n];
let result = 0n;
for (let i = 0; i < 10; i++) {
result += BigInt(limbs[i]) << shifts[i];
}
// Reduce mod P and handle negatives
result = ((result % P) + P) % P;
return result;
}
// Debug: verify our D constant matches Salvium's d.h
export function verifyDConstant() {
// From Salvium d.h:
const dLimbs = [-10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116];
const dFromLimbs = limbsToFe(dLimbs);
// From Salvium d2.h (2*d):
const d2Limbs = [-21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199];
const d2FromLimbs = limbsToFe(d2Limbs);
return {
ourD: D.toString(),
salviumD: dFromLimbs.toString(),
dMatch: D === dFromLimbs,
ourD2: D2.toString(),
salviumD2: d2FromLimbs.toString(),
d2Match: D2 === d2FromLimbs
};
}
// Compute x from y for twisted Edwards curve: -x² + y² = 1 + d*x²*y²
// x² = (y² - 1) / (1 + d*y²)
export function computeXFromY(y) {
const y2 = feSq(y);
const num = feSub(y2, 1n); // y² - 1
const den = feAdd(1n, feMul(D, y2)); // 1 + d*y²
const x2 = feMul(num, feInv(den)); // (y² - 1) / (1 + d*y²)
// Get square root
const x = feSqrt(x2);
const xNeg = x ? feNeg(x) : null;
// Verify x² = x2
const xSquaredCheck = x ? feSq(x) === x2 : null;
// Check if computed point is on curve
const computedOnCurve = x ? isOnCurve(x, y) : null;
const computedNegOnCurve = xNeg ? isOnCurve(xNeg, y) : null;
// Check if our GX satisfies x² = x2
const gxSquared = feSq(GX);
const gxSatisfiesX2 = gxSquared === x2;
return {
x2: x2.toString(),
x: x ? x.toString() : 'no sqrt exists',
xNeg: xNeg ? xNeg.toString() : null,
xSquaredCheck: xSquaredCheck,
computedOnCurve: computedOnCurve,
computedNegOnCurve: computedNegOnCurve,
ourGX: GX.toString(),
gxSquared: gxSquared.toString(),
gxSatisfiesX2: gxSatisfiesX2,
xMatchesGX: x === GX,
xNegMatchesGX: xNeg === GX
};
}
/**
* Generate a random scalar in range [1, L-1]
* @returns {Uint8Array} 32-byte random scalar
*/
export function randomScalar() {
// Generate 64 bytes and reduce mod L for uniform distribution
const bytes64 = getRandomBytes(64);
let val = 0n;
for (let i = bytes64.length - 1; i >= 0; i--) {
val = (val << 8n) | BigInt(bytes64[i]);
}
// Reduce mod L and ensure non-zero
val = val % L;
if (val === 0n) val = 1n;
// Convert back to 32 bytes
const result = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
result[i] = Number(val & 0xffn);
val = val >> 8n;
}
return result;
}
/**
* Generate a random point on the curve: r*G where r is random
* @returns {Uint8Array} 32-byte compressed random point
*/
export function randomPoint() {
const scalar = randomScalar();
return scalarMultBase(scalar);
}
/**
* Negate a point: -P
* @param {Uint8Array} P - 32-byte compressed point
* @returns {Uint8Array|null} 32-byte compressed negated point, or null if P invalid
*/
export function pointNegate(P) {
const point = pointFromBytes(P);
if (!point) return null;
// Negate: (X, Y, Z, T) -> (-X, Y, Z, -T)
const negated = {
X: feNeg(point.X),
Y: point.Y,
Z: point.Z,
T: feNeg(point.T)
};
return pointToBytes(negated);
}
/**
* Point subtraction: P - Q = P + (-Q)
* @param {Uint8Array} P - 32-byte compressed point
* @param {Uint8Array} Q - 32-byte compressed point
* @returns {Uint8Array|null} 32-byte compressed result, or null if invalid
*/
export function pointSubCompressed(P, Q) {
const negQ = pointNegate(Q);
if (!negQ) return null;
return pointAddCompressed(P, negQ);
}
/**
* Check if a point is valid (on the curve and in the prime-order subgroup)
* @param {Uint8Array} P - 32-byte compressed point
* @returns {boolean} true if valid
*/
export function isValidPoint(P) {
const point = pointFromBytes(P);
if (!point) return false;
// Check if on curve
const zi = feInv(point.Z);
const x = feMul(point.X, zi);
const y = feMul(point.Y, zi);
if (!isOnCurve(x, y)) return false;
// Check if in prime-order subgroup (L*P = identity)
const lP = scalarMult(point, L);
const lPz = feInv(lP.Z);
const lPx = feMul(lP.X, lPz);
const lPy = feMul(lP.Y, lPz);
return lPx === 0n && lPy === 1n;
}
export default {
scalarCheck,
scalarIsNonzero,
scalarSub,
scalarMultBase,
scalarMultPoint,
pointFromBytes,
pointToBytes,
pointAddCompressed,
pointSubCompressed,
pointNegate,
randomScalar,
randomPoint,
isValidPoint,
doubleScalarMultBase,
isIdentity,
getGeneratorG,
getGeneratorT,
computeCarrotSpendPubkey,
computeCarrotAccountViewPubkey,
computeCarrotMainAddressViewPubkey,
testDouble,
getBasePoint,
test2G,
testIdentity,
get2GAffine
};