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:
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user