Add Pedersen commitment functions to WASM crypto backend (Phase 4)

Implement pedersen_commit, zero_commit, and gen_commitment_mask in Rust
  with hardcoded H generator point. All 56 equivalence tests pass including
  homomorphic property verification: commit(a,m) - zeroCommit(a) = m*G.
This commit is contained in:
Matt Hess
2026-02-01 01:24:43 +00:00
parent 638171efe1
commit 0bf0c9e4b3
7 changed files with 775 additions and 0 deletions
+426
View File
@@ -0,0 +1,426 @@
/// Elligator 2 map: 32-byte hash -> Ed25519 point
/// Ported from Salvium C++ crypto-ops.c ge_fromfe_frombytes_vartime
///
/// This maps a field element (from a hash) to a curve point using the
/// Elligator 2 algorithm. The result is NOT cofactor-cleared — caller
/// must multiply by 8.
use curve25519_dalek::edwards::EdwardsPoint;
use curve25519_dalek::edwards::CompressedEdwardsY;
/// 256-bit unsigned integer for field arithmetic mod p = 2^255 - 19
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct U256([u64; 4]); // little-endian
impl U256 {
const ZERO: Self = U256([0, 0, 0, 0]);
const ONE: Self = U256([1, 0, 0, 0]);
const TWO: Self = U256([2, 0, 0, 0]);
// p = 2^255 - 19
const P: Self = U256([
0xFFFFFFFFFFFFFFED,
0xFFFFFFFFFFFFFFFF,
0xFFFFFFFFFFFFFFFF,
0x7FFFFFFFFFFFFFFF,
]);
fn from_bytes_le(bytes: &[u8; 32]) -> Self {
let mut limbs = [0u64; 4];
for i in 0..4 {
let offset = i * 8;
limbs[i] = u64::from_le_bytes([
bytes[offset], bytes[offset+1], bytes[offset+2], bytes[offset+3],
bytes[offset+4], bytes[offset+5], bytes[offset+6], bytes[offset+7],
]);
}
U256(limbs)
}
fn to_bytes_le(&self) -> [u8; 32] {
let mut out = [0u8; 32];
for i in 0..4 {
let bytes = self.0[i].to_le_bytes();
out[i*8..i*8+8].copy_from_slice(&bytes);
}
out
}
fn is_zero(&self) -> bool {
self.0[0] == 0 && self.0[1] == 0 && self.0[2] == 0 && self.0[3] == 0
}
/// Least significant bit
fn is_odd(&self) -> bool {
self.0[0] & 1 == 1
}
/// Compare: self >= other
fn ge(&self, other: &Self) -> bool {
for i in (0..4).rev() {
if self.0[i] > other.0[i] { return true; }
if self.0[i] < other.0[i] { return false; }
}
true // equal
}
/// self + other (with carry, no reduction)
fn add_raw(&self, other: &Self) -> (Self, bool) {
let mut result = [0u64; 4];
let mut carry = 0u64;
for i in 0..4 {
let sum = (self.0[i] as u128) + (other.0[i] as u128) + (carry as u128);
result[i] = sum as u64;
carry = (sum >> 64) as u64;
}
(U256(result), carry != 0)
}
/// self - other (with borrow, no reduction)
fn sub_raw(&self, other: &Self) -> (Self, bool) {
let mut result = [0u64; 4];
let mut borrow = 0i128;
for i in 0..4 {
let diff = (self.0[i] as i128) - (other.0[i] as i128) + borrow;
if diff < 0 {
result[i] = (diff + (1i128 << 64)) as u64;
borrow = -1;
} else {
result[i] = diff as u64;
borrow = 0;
}
}
(U256(result), borrow != 0)
}
/// Reduce mod p
fn reduce(&self) -> Self {
let mut r = *self;
while r.ge(&Self::P) {
let (sub, _) = r.sub_raw(&Self::P);
r = sub;
}
r
}
}
// Field operations mod p
fn fe_add(a: &U256, b: &U256) -> U256 {
let (sum, _carry) = a.add_raw(b);
sum.reduce()
}
fn fe_sub(a: &U256, b: &U256) -> U256 {
if a.ge(b) {
let (diff, _) = a.sub_raw(b);
diff
} else {
let (sum, _) = a.add_raw(&U256::P);
let (diff, _) = sum.sub_raw(b);
diff.reduce()
}
}
/// Multiplication mod p using schoolbook with u128 intermediates
fn fe_mul(a: &U256, b: &U256) -> U256 {
// Full 512-bit product, then reduce mod p
let mut prod = [0u128; 8];
for i in 0..4 {
let mut carry = 0u128;
for j in 0..4 {
let v = (a.0[i] as u128) * (b.0[j] as u128) + prod[i+j] + carry;
prod[i+j] = v & 0xFFFFFFFFFFFFFFFF;
carry = v >> 64;
}
prod[i+4] += carry;
}
// Convert to bytes and reduce mod p using Barrett reduction
// For simplicity, use repeated subtraction approach with 512-bit number
reduce_512(&prod)
}
/// Reduce a 512-bit number mod p = 2^255 - 19
fn reduce_512(prod: &[u128; 8]) -> U256 {
// Strategy: split into low 255 bits and high bits, use p = 2^255 - 19
// so 2^255 ≡ 19 (mod p), meaning high * 19 + low
// We need to handle up to 512 bits.
// Convert product limbs to a big number represented as bytes
let mut bytes = [0u8; 64];
for i in 0..8 {
let b = (prod[i] as u64).to_le_bytes();
bytes[i*8..i*8+8].copy_from_slice(&b);
}
// Use iterative reduction: take top bits, multiply by 19, add to bottom
// 64 bytes = 512 bits. We reduce to 256 bits then to < p.
let mut val = [0u128; 5]; // 5 x 64-bit limbs for 320 bits headroom
for i in 0..8 {
val[i.min(4)] += prod[i] & 0xFFFFFFFFFFFFFFFF;
}
// Actually, let's do this properly with a simple approach:
// Load as two U256 halves and use 2^256 ≡ 2*19 = 38 (mod p)
let lo = U256([prod[0] as u64, prod[1] as u64, prod[2] as u64, prod[3] as u64]);
let hi = U256([prod[4] as u64, prod[5] as u64, prod[6] as u64, prod[7] as u64]);
// result = lo + hi * 2^256 mod p
// 2^256 = 2 * 2^255 = 2 * (p + 19) = 2p + 38 ≡ 38 (mod p)
let hi_times_38 = mul_u256_small(&hi, 38);
let (sum, carry) = lo.add_raw(&hi_times_38);
let mut result = sum;
if carry {
// carry means we exceeded 2^256, which is another *38
let (r, _) = result.add_raw(&U256([38, 0, 0, 0]));
result = r;
}
result.reduce()
}
/// Multiply U256 by a small constant
fn mul_u256_small(a: &U256, b: u64) -> U256 {
let mut result = [0u64; 4];
let mut carry = 0u128;
for i in 0..4 {
let v = (a.0[i] as u128) * (b as u128) + carry;
result[i] = v as u64;
carry = v >> 64;
}
// If carry, we need to reduce: carry * 2^256 ≡ carry * 38 (mod p)
let mut r = U256(result);
if carry > 0 {
let extra = U256([carry as u64 * 38, 0, 0, 0]);
let (sum, _) = r.add_raw(&extra);
r = sum;
}
r.reduce()
}
fn fe_sq(a: &U256) -> U256 {
fe_mul(a, a)
}
fn fe_neg(a: &U256) -> U256 {
if a.is_zero() {
U256::ZERO
} else {
let (diff, _) = U256::P.sub_raw(a);
diff
}
}
/// Modular exponentiation: base^exp mod p
fn fe_pow(base: &U256, exp: &U256) -> U256 {
let mut result = U256::ONE;
let mut b = *base;
// Process all bits
for limb_idx in 0..4 {
let mut bits = exp.0[limb_idx];
for _ in 0..64 {
if bits & 1 == 1 {
result = fe_mul(&result, &b);
}
b = fe_sq(&b);
bits >>= 1;
}
}
result
}
/// Modular inverse: a^(p-2) mod p
fn fe_inv(a: &U256) -> U256 {
// p - 2
let exp = U256([
0xFFFFFFFFFFFFFFEB, // ...ED - 2
0xFFFFFFFFFFFFFFFF,
0xFFFFFFFFFFFFFFFF,
0x7FFFFFFFFFFFFFFF,
]);
fe_pow(a, &exp)
}
/// Compute x^((p-5)/8) mod p
/// (p-5)/8 = (2^255 - 24)/8 = 2^252 - 3
fn fe_pow_pm5d8(x: &U256) -> U256 {
let exp = U256([
0xFFFFFFFFFFFFFFFF - 2, // 2^252 - 3 in little-endian
0xFFFFFFFFFFFFFFFF,
0xFFFFFFFFFFFFFFFF,
0x0FFFFFFFFFFFFFFF,
]);
fe_pow(x, &exp)
}
/// fe_divpowm1: compute (u/v)^((p+3)/8) using the formula:
/// (u/v)^((p+3)/8) = u * v^3 * (u * v^7)^((p-5)/8)
fn fe_divpowm1(u: &U256, v: &U256) -> U256 {
let v2 = fe_sq(v);
let v3 = fe_mul(&v2, v);
let v4 = fe_sq(&v2);
let v7 = fe_mul(&v4, &v3);
let uv7 = fe_mul(u, &v7);
let uv7_pow = fe_pow_pm5d8(&uv7);
fe_mul(&fe_mul(u, &v3), &uv7_pow)
}
/// Square root mod p
fn fe_sqrt(a: &U256) -> Option<U256> {
if a.is_zero() { return Some(U256::ZERO); }
// candidate = a^((p+3)/8)
let exp = U256([
0xFFFFFFFFFFFFFFFE, // (2^255 - 19 + 3)/8 = 2^252 - 2
0xFFFFFFFFFFFFFFFF,
0xFFFFFFFFFFFFFFFF,
0x0FFFFFFFFFFFFFFF,
]);
let candidate = fe_pow(a, &exp);
if fe_sq(&candidate) == *a { return Some(candidate); }
// sqrt(-1) mod p
let sqrt_m1 = U256::from_bytes_le(&[
0xb0, 0xa0, 0x0e, 0x4a, 0x27, 0x1b, 0xee, 0xc4,
0x78, 0xe4, 0x2f, 0xad, 0x06, 0x18, 0x43, 0x2f,
0xa7, 0xd7, 0xfb, 0x3d, 0x99, 0x00, 0x4d, 0x2b,
0x0b, 0xdf, 0xc1, 0x4f, 0x80, 0x24, 0x83, 0x2b,
]);
let adjusted = fe_mul(&candidate, &sqrt_m1);
if fe_sq(&adjusted) == *a { return Some(adjusted); }
None
}
/// Precomputed constants
fn sqrt_m1() -> U256 {
U256::from_bytes_le(&[
0xb0, 0xa0, 0x0e, 0x4a, 0x27, 0x1b, 0xee, 0xc4,
0x78, 0xe4, 0x2f, 0xad, 0x06, 0x18, 0x43, 0x2f,
0xa7, 0xd7, 0xfb, 0x3d, 0x99, 0x00, 0x4d, 0x2b,
0x0b, 0xdf, 0xc1, 0x4f, 0x80, 0x24, 0x83, 0x2b,
])
}
const A_MONT: u64 = 486662;
/// Main Elligator 2 map: 32-byte hash -> EdwardsPoint (NOT cofactor-cleared)
/// Ported from Salvium C++ crypto-ops.c ge_fromfe_frombytes_vartime
pub fn ge_fromfe_frombytes_vartime(hash: &[u8; 32]) -> EdwardsPoint {
// Load hash as field element u, reduced mod p
// The C++ fe_frombytes loads all 256 bits and reduces via carry chain.
// We load the full value and reduce mod p to match.
let u = U256::from_bytes_le(hash).reduce();
let neg_a = fe_neg(&U256([A_MONT, 0, 0, 0]).reduce());
let neg_a_sq = fe_sq(&U256([A_MONT, 0, 0, 0]).reduce());
let neg_a_sq = fe_neg(&neg_a_sq);
// v = 2 * u^2
let u2 = fe_sq(&u);
let v = fe_add(&u2, &u2);
// w = 2*u^2 + 1
let w = fe_add(&v, &U256::ONE);
// x = w^2 - 2*A^2*u^2 = w^2 + 2*(-A^2)*u^2
let w2 = fe_sq(&w);
let term = fe_mul(&fe_add(&neg_a_sq, &neg_a_sq), &u2); // 2*(-A^2)*u^2
let mut x = fe_add(&w2, &term);
// r_X = (w/x)^((p+3)/8) via fe_divpowm1
let mut r_x = fe_divpowm1(&w, &x);
// y = r_X^2 * x
let mut y = fe_mul(&fe_sq(&r_x), &x);
let sqm1 = sqrt_m1();
let mut z = neg_a;
let sign;
// Check branches
let diff1 = fe_sub(&w, &y);
if diff1.is_zero() {
// y == w
// fffb2 = sqrt(2*A*(A+2))
let a_val = U256([A_MONT, 0, 0, 0]).reduce();
let a_plus_2 = fe_add(&a_val, &U256::TWO);
let two_a_ap2 = fe_mul(&fe_add(&a_val, &a_val), &a_plus_2);
if let Some(fffb2) = fe_sqrt(&two_a_ap2) {
r_x = fe_mul(&r_x, &fffb2);
}
r_x = fe_mul(&r_x, &u);
z = fe_mul(&z, &v);
sign = false;
} else {
let sum1 = fe_add(&w, &y);
if sum1.is_zero() {
// y == -w
// fffb1 = sqrt(-2*A*(A+2))
let a_val = U256([A_MONT, 0, 0, 0]).reduce();
let a_plus_2 = fe_add(&a_val, &U256::TWO);
let two_a_ap2 = fe_mul(&fe_add(&a_val, &a_val), &a_plus_2);
let neg_two_a_ap2 = fe_neg(&two_a_ap2);
if let Some(fffb1) = fe_sqrt(&neg_two_a_ap2) {
r_x = fe_mul(&r_x, &fffb1);
}
r_x = fe_mul(&r_x, &u);
z = fe_mul(&z, &v);
sign = false;
} else {
// Negative branch: multiply x by sqrt(-1)
x = fe_mul(&x, &sqm1);
y = fe_mul(&fe_sq(&r_x), &x);
let diff2 = fe_sub(&w, &y);
if diff2.is_zero() {
// fffb4 = sqrt(sqrt(-1)*A*(A+2))
let a_val = U256([A_MONT, 0, 0, 0]).reduce();
let a_plus_2 = fe_add(&a_val, &U256::TWO);
let sqm1_a_ap2 = fe_mul(&sqm1, &fe_mul(&a_val, &a_plus_2));
if let Some(fffb4) = fe_sqrt(&sqm1_a_ap2) {
r_x = fe_mul(&r_x, &fffb4);
}
} else {
// fffb3 = sqrt(-sqrt(-1)*A*(A+2))
let a_val = U256([A_MONT, 0, 0, 0]).reduce();
let a_plus_2 = fe_add(&a_val, &U256::TWO);
let neg_sqm1_a_ap2 = fe_neg(&fe_mul(&sqm1, &fe_mul(&a_val, &a_plus_2)));
if let Some(fffb3) = fe_sqrt(&neg_sqm1_a_ap2) {
r_x = fe_mul(&r_x, &fffb3);
}
}
// z remains as -A
sign = true;
}
}
// Adjust sign of r_X
if r_x.is_odd() != sign {
r_x = fe_neg(&r_x);
}
// Compute projective coordinates
// Z_coord = z + w
// Y_coord = z - w
// X_coord = r_X * Z_coord
let z_coord = fe_add(&z, &w);
let y_coord = fe_sub(&z, &w);
let x_coord = fe_mul(&r_x, &z_coord);
// Convert to affine: x = X/Z, y = Y/Z
let z_inv = fe_inv(&z_coord);
let affine_x = fe_mul(&x_coord, &z_inv);
let affine_y = fe_mul(&y_coord, &z_inv);
// Compress to Ed25519 format: y with sign bit of x in high bit
let mut compressed = affine_y.to_bytes_le();
if affine_x.is_odd() {
compressed[31] |= 0x80;
}
CompressedEdwardsY(compressed).decompress().expect("elligator2 produced invalid point")
}
+138
View File
@@ -5,6 +5,8 @@ use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
use curve25519_dalek::traits::VartimeMultiscalarMul;
mod elligator2;
/// Keccak-256 hash (CryptoNote variant with 0x01 padding, NOT SHA3)
/// Matches Salvium C++ cn_fast_hash / keccak()
#[wasm_bindgen]
@@ -168,3 +170,139 @@ pub fn double_scalar_mult_base(a: &[u8], p: &[u8], b: &[u8]) -> Vec<u8> {
&[pp, curve25519_dalek::constants::ED25519_BASEPOINT_POINT],
).compress().to_bytes().to_vec()
}
// ─── Phase 3: Hash-to-Point & Key Derivation ────────────────────────────────
fn keccak256_internal(data: &[u8]) -> [u8; 32] {
let mut keccak = Keccak::v256();
let mut output = [0u8; 32];
keccak.update(data);
keccak.finalize(&mut output);
output
}
fn encode_varint(mut val: u32, buf: &mut Vec<u8>) {
loop {
let byte = (val & 0x7f) as u8;
val >>= 7;
if val == 0 {
buf.push(byte);
break;
}
buf.push(byte | 0x80);
}
}
fn derivation_to_scalar(derivation: &[u8], output_index: u32) -> Scalar {
let mut buf = Vec::with_capacity(40);
buf.extend_from_slice(derivation);
encode_varint(output_index, &mut buf);
let hash = keccak256_internal(&buf);
Scalar::from_bytes_mod_order(hash)
}
/// Hash-to-point: H_p(data) = cofactor * elligator2(keccak256(data))
/// Matches Salvium C++ hash_to_ec / ge_fromfe_frombytes_vartime
#[wasm_bindgen]
pub fn hash_to_point(data: &[u8]) -> Vec<u8> {
let hash = keccak256_internal(data);
let point = elligator2::ge_fromfe_frombytes_vartime(&hash);
// Multiply by cofactor 8
let cofactored = point + point; // 2P
let cofactored = cofactored + cofactored; // 4P
let cofactored = cofactored + cofactored; // 8P
cofactored.compress().to_bytes().to_vec()
}
/// Generate key image: KI = sec * H_p(pub)
#[wasm_bindgen]
pub fn generate_key_image(pub_key: &[u8], sec_key: &[u8]) -> Vec<u8> {
let hash = keccak256_internal(pub_key);
let hp = elligator2::ge_fromfe_frombytes_vartime(&hash);
// Cofactor multiply
let hp8 = {
let t = hp + hp;
let t = t + t;
t + t
};
let scalar = Scalar::from_bytes_mod_order(to32(sec_key));
EdwardsPoint::vartime_multiscalar_mul(&[scalar], &[hp8])
.compress().to_bytes().to_vec()
}
/// Generate key derivation: D = 8 * (sec * pub)
#[wasm_bindgen]
pub fn generate_key_derivation(pub_key: &[u8], sec_key: &[u8]) -> Vec<u8> {
let point = CompressedEdwardsY(to32(pub_key)).decompress().expect("invalid pub key");
let scalar = Scalar::from_bytes_mod_order(to32(sec_key));
let shared = scalar * point;
// Cofactor multiply by 8
let result = {
let t = shared + shared;
let t = t + t;
t + t
};
result.compress().to_bytes().to_vec()
}
/// Derive public key: base + H(derivation || index) * G
#[wasm_bindgen]
pub fn derive_public_key(derivation: &[u8], output_index: u32, base_pub: &[u8]) -> Vec<u8> {
let scalar = derivation_to_scalar(derivation, output_index);
let base = CompressedEdwardsY(to32(base_pub)).decompress().expect("invalid base pub key");
let derived = ED25519_BASEPOINT_TABLE * &scalar + base;
derived.compress().to_bytes().to_vec()
}
/// Derive secret key: base + H(derivation || index) mod L
#[wasm_bindgen]
pub fn derive_secret_key(derivation: &[u8], output_index: u32, base_sec: &[u8]) -> Vec<u8> {
let scalar = derivation_to_scalar(derivation, output_index);
let base = Scalar::from_bytes_mod_order(to32(base_sec));
(base + scalar).to_bytes().to_vec()
}
// ─── Phase 4: Pedersen Commitments ──────────────────────────────────────────
/// H generator for Pedersen commitments: H = H_p(G)
/// Precomputed from Salvium/CryptoNote rctTypes.h
const H_POINT_BYTES: [u8; 32] = [
0x8b, 0x65, 0x59, 0x70, 0x15, 0x37, 0x99, 0xaf,
0x2a, 0xea, 0xdc, 0x9f, 0xf1, 0xad, 0xd0, 0xea,
0x6c, 0x72, 0x51, 0xd5, 0x41, 0x54, 0xcf, 0xa9,
0x2c, 0x17, 0x3a, 0x0d, 0xd3, 0x9c, 0x1f, 0x94,
];
/// Pedersen commitment: C = mask*G + amount*H
#[wasm_bindgen]
pub fn pedersen_commit(amount: &[u8], mask: &[u8]) -> Vec<u8> {
let amount_scalar = Scalar::from_bytes_mod_order(to32(amount));
let mask_scalar = Scalar::from_bytes_mod_order(to32(mask));
let h = CompressedEdwardsY(H_POINT_BYTES).decompress().expect("invalid H");
// mask*G + amount*H
EdwardsPoint::vartime_multiscalar_mul(
&[mask_scalar, amount_scalar],
&[curve25519_dalek::constants::ED25519_BASEPOINT_POINT, h],
).compress().to_bytes().to_vec()
}
/// Zero commitment: C = amount*H (no blinding factor)
#[wasm_bindgen]
pub fn zero_commit(amount: &[u8]) -> Vec<u8> {
let amount_scalar = Scalar::from_bytes_mod_order(to32(amount));
let h = CompressedEdwardsY(H_POINT_BYTES).decompress().expect("invalid H");
EdwardsPoint::vartime_multiscalar_mul(&[amount_scalar], &[h])
.compress().to_bytes().to_vec()
}
/// Generate commitment mask from shared secret
/// mask = scReduce32(keccak256("commitment_mask" || sharedSecret))
#[wasm_bindgen]
pub fn gen_commitment_mask(shared_secret: &[u8]) -> Vec<u8> {
let prefix = b"commitment_mask";
let mut data = Vec::with_capacity(prefix.len() + shared_secret.len());
data.extend_from_slice(prefix);
data.extend_from_slice(shared_secret);
let hash = keccak256_internal(&data);
Scalar::from_bytes_mod_order(hash).to_bytes().to_vec()
}
+15
View File
@@ -17,6 +17,9 @@ import {
scalarMultBase, scalarMultPoint, pointAddCompressed,
pointSubCompressed, pointNegate, doubleScalarMultBase
} from '../ed25519.js';
import { hashToPoint, generateKeyImage } from '../keyimage.js';
import { generateKeyDerivation, derivePublicKey, deriveSecretKey } from '../scanning.js';
import { commit, zeroCommit, genCommitmentMask } from '../transaction/serialization.js';
export class JsCryptoBackend {
constructor() {
@@ -60,4 +63,16 @@ export class JsCryptoBackend {
const bG = scalarMultBase(b);
return pointAddCompressed(aP, bG);
}
// Hash-to-point & key derivation
hashToPoint(data) { return hashToPoint(data); }
generateKeyImage(pubKey, secKey) { return generateKeyImage(pubKey, secKey); }
generateKeyDerivation(pubKey, secKey) { return generateKeyDerivation(pubKey, secKey); }
derivePublicKey(derivation, outputIndex, basePub) { return derivePublicKey(derivation, outputIndex, basePub); }
deriveSecretKey(derivation, outputIndex, baseSec) { return deriveSecretKey(derivation, outputIndex, baseSec); }
// Pedersen commitments
commit(amount, mask) { return commit(amount, mask); }
zeroCommit(amount) { return zeroCommit(amount); }
genCommitmentMask(sharedSecret) { return genCommitmentMask(sharedSecret); }
}
+48
View File
@@ -76,4 +76,52 @@ export class WasmCryptoBackend {
pointSubCompressed(p, q) { return this.wasm.point_sub_compressed(p, q); }
pointNegate(p) { return this.wasm.point_negate(p); }
doubleScalarMultBase(a, p, b) { return this.wasm.double_scalar_mult_base(a, p, b); }
// Hash-to-point & key derivation
hashToPoint(data) { return this.wasm.hash_to_point(data); }
generateKeyImage(pubKey, secKey) { return this.wasm.generate_key_image(pubKey, secKey); }
generateKeyDerivation(pubKey, secKey) { return this.wasm.generate_key_derivation(pubKey, secKey); }
derivePublicKey(derivation, outputIndex, basePub) { return this.wasm.derive_public_key(derivation, outputIndex, basePub); }
deriveSecretKey(derivation, outputIndex, baseSec) { return this.wasm.derive_secret_key(derivation, outputIndex, baseSec); }
// Pedersen commitments
commit(amount, mask) {
// Convert amount (BigInt/number) to 32-byte LE scalar
let amountBytes = amount;
if (typeof amount === 'bigint' || typeof amount === 'number') {
let n = BigInt(amount);
amountBytes = new Uint8Array(32);
for (let i = 0; i < 32 && n > 0n; i++) {
amountBytes[i] = Number(n & 0xffn);
n >>= 8n;
}
}
// Convert mask if hex string
if (typeof mask === 'string') {
const hex = mask;
mask = new Uint8Array(hex.length / 2);
for (let i = 0; i < mask.length; i++) mask[i] = parseInt(hex.substr(i*2, 2), 16);
}
return this.wasm.pedersen_commit(amountBytes, mask);
}
zeroCommit(amount) {
let amountBytes = amount;
if (typeof amount === 'bigint' || typeof amount === 'number') {
let n = BigInt(amount);
amountBytes = new Uint8Array(32);
for (let i = 0; i < 32 && n > 0n; i++) {
amountBytes[i] = Number(n & 0xffn);
n >>= 8n;
}
}
return this.wasm.zero_commit(amountBytes);
}
genCommitmentMask(sharedSecret) {
if (typeof sharedSecret === 'string') {
const hex = sharedSecret;
sharedSecret = new Uint8Array(hex.length / 2);
for (let i = 0; i < sharedSecret.length; i++) sharedSecret[i] = parseInt(hex.substr(i*2, 2), 16);
}
return this.wasm.gen_commitment_mask(sharedSecret);
}
}
+3
View File
@@ -18,6 +18,9 @@ export {
scReduce32, scReduce64, scInvert, scCheck, scIsZero,
scalarMultBase, scalarMultPoint, pointAddCompressed,
pointSubCompressed, pointNegate, doubleScalarMultBase,
hashToPoint, generateKeyImage, generateKeyDerivation,
derivePublicKey, deriveSecretKey,
commit, zeroCommit, genCommitmentMask,
} from './provider.js';
// Backends (for direct access / testing)
+12
View File
@@ -88,3 +88,15 @@ export function pointAddCompressed(p, q) { return getCryptoBackend().pointAddCom
export function pointSubCompressed(p, q) { return getCryptoBackend().pointSubCompressed(p, q); }
export function pointNegate(p) { return getCryptoBackend().pointNegate(p); }
export function doubleScalarMultBase(a, p, b) { return getCryptoBackend().doubleScalarMultBase(a, p, b); }
// Hash-to-point & key derivation
export function hashToPoint(data) { return getCryptoBackend().hashToPoint(data); }
export function generateKeyImage(pubKey, secKey) { return getCryptoBackend().generateKeyImage(pubKey, secKey); }
export function generateKeyDerivation(pubKey, secKey) { return getCryptoBackend().generateKeyDerivation(pubKey, secKey); }
export function derivePublicKey(derivation, outputIndex, basePub) { return getCryptoBackend().derivePublicKey(derivation, outputIndex, basePub); }
export function deriveSecretKey(derivation, outputIndex, baseSec) { return getCryptoBackend().deriveSecretKey(derivation, outputIndex, baseSec); }
// Pedersen commitments
export function commit(amount, mask) { return getCryptoBackend().commit(amount, mask); }
export function zeroCommit(amount) { return getCryptoBackend().zeroCommit(amount); }
export function genCommitmentMask(sharedSecret) { return getCryptoBackend().genCommitmentMask(sharedSecret); }
+133
View File
@@ -15,6 +15,9 @@ import {
scReduce32, scReduce64, scInvert, scCheck, scIsZero,
scalarMultBase, scalarMultPoint, pointAddCompressed,
pointSubCompressed, pointNegate, doubleScalarMultBase,
hashToPoint, generateKeyImage, generateKeyDerivation,
derivePublicKey, deriveSecretKey,
commit, zeroCommit, genCommitmentMask,
} from '../src/crypto/index.js';
import { JsCryptoBackend } from '../src/crypto/backend-js.js';
import { hexToBytes, bytesToHex } from '../src/index.js';
@@ -343,6 +346,133 @@ await asyncTest('doubleScalarMultBase equivalence', async () => {
assertEqual(jsResult, wasmResult, 'doubleScalarMultBase JS vs WASM');
});
// ─── Hash-to-point & key derivation equivalence ─────────────────────────────
console.log('\n=== Hash-to-Point & Key Derivation Equivalence ===\n');
// Generate a test key pair
const testSecKey = new Uint8Array(32);
testSecKey.set([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
testSecKey[31] &= 0x0f;
const testPubKey = getCryptoBackend().scalarMultBase(testSecKey);
await asyncTest('hashToPoint equivalence', async () => {
const js = new JsCryptoBackend();
const jsResult = js.hashToPoint(testPubKey);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().hashToPoint(testPubKey);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'hashToPoint JS vs WASM');
});
await asyncTest('hashToPoint equivalence (random key)', async () => {
const randomKey = getCryptoBackend().scalarMultBase(scalarA);
const js = new JsCryptoBackend();
const jsResult = js.hashToPoint(randomKey);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().hashToPoint(randomKey);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'hashToPoint random JS vs WASM');
});
await asyncTest('generateKeyImage equivalence', async () => {
const js = new JsCryptoBackend();
const jsResult = js.generateKeyImage(testPubKey, testSecKey);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().generateKeyImage(testPubKey, testSecKey);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'generateKeyImage JS vs WASM');
});
await asyncTest('generateKeyDerivation equivalence', async () => {
const js = new JsCryptoBackend();
const jsResult = js.generateKeyDerivation(testPubKey, testSecKey);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().generateKeyDerivation(testPubKey, testSecKey);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'generateKeyDerivation JS vs WASM');
});
await asyncTest('derivePublicKey equivalence', async () => {
const js = new JsCryptoBackend();
const derivation = js.generateKeyDerivation(testPubKey, testSecKey);
const jsResult = js.derivePublicKey(derivation, 0, testPubKey);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().derivePublicKey(derivation, 0, testPubKey);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'derivePublicKey JS vs WASM');
});
await asyncTest('deriveSecretKey equivalence', async () => {
const js = new JsCryptoBackend();
const derivation = js.generateKeyDerivation(testPubKey, testSecKey);
const jsResult = js.deriveSecretKey(derivation, 0, testSecKey);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().deriveSecretKey(derivation, 0, testSecKey);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'deriveSecretKey JS vs WASM');
});
await asyncTest('derivePublicKey matches scalarMultBase(deriveSecretKey)', async () => {
const js = new JsCryptoBackend();
const derivation = js.generateKeyDerivation(testPubKey, testSecKey);
const derivedSec = js.deriveSecretKey(derivation, 0, testSecKey);
const derivedPubFromSec = js.scalarMultBase(derivedSec);
const derivedPubDirect = js.derivePublicKey(derivation, 0, testPubKey);
assertEqual(derivedPubFromSec, derivedPubDirect, 'derivePublicKey consistency');
});
// ─── Pedersen commitment equivalence ─────────────────────────────────────────
console.log('\n=== Pedersen Commitment Equivalence ===\n');
const H_HEX = '8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94';
await asyncTest('zeroCommit(1) = H', async () => {
const js = new JsCryptoBackend();
const result = js.zeroCommit(1n);
assertEqual(result, hexToBytes(H_HEX), 'zeroCommit(1) should be H');
});
await asyncTest('commit equivalence (random)', async () => {
const js = new JsCryptoBackend();
const jsResult = js.commit(12345n, scalarA);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().commit(12345n, scalarA);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'commit JS vs WASM');
});
await asyncTest('zeroCommit equivalence', async () => {
const js = new JsCryptoBackend();
const jsResult = js.zeroCommit(99999n);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().zeroCommit(99999n);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'zeroCommit JS vs WASM');
});
await asyncTest('genCommitmentMask equivalence', async () => {
const js = new JsCryptoBackend();
const secret = crypto.getRandomValues(new Uint8Array(32));
const jsResult = js.genCommitmentMask(secret);
await setCryptoBackend('wasm');
const wasmResult = getCryptoBackend().genCommitmentMask(secret);
await setCryptoBackend('js');
assertEqual(jsResult, wasmResult, 'genCommitmentMask JS vs WASM');
});
await asyncTest('commit homomorphic: commit(a,m) - zeroCommit(a) = m*G', async () => {
const js = new JsCryptoBackend();
const amount = 42n;
const mask = scalarA;
const c = js.commit(amount, mask);
const z = js.zeroCommit(amount);
const diff = js.pointSubCompressed(c, z);
const mG = js.scalarMultBase(mask);
assertEqual(diff, mG, 'homomorphic property');
});
// ─── Benchmark ──────────────────────────────────────────────────────────────
console.log('\n=== Benchmark (10,000 iterations) ===\n');
@@ -393,6 +523,9 @@ const benchOps = [
['scalarMultBase', BENCH_PT, () => scalarMultBase(benchScalar)],
['scalarMultPoint', BENCH_PT, () => scalarMultPoint(benchScalar, benchPoint)],
['pointAddCompressed', BENCH_PT, () => pointAddCompressed(benchPoint, benchPoint)],
['hashToPoint', BENCH_PT, () => hashToPoint(benchPoint)],
['generateKeyImage', BENCH_PT, () => generateKeyImage(benchPoint, benchScalar)],
['generateKeyDerivation', BENCH_PT, () => generateKeyDerivation(benchPoint, benchScalar)],
];
for (const [name, iters, fn] of benchOps) {