Add Ed25519 scalar and point operations to WASM crypto backend (Phase 2)
Adds 16 operations via curve25519-dalek: scAdd, scSub, scMul, scMulAdd, scMulSub, scReduce32, scReduce64, scInvert, scCheck, scIsZero, scalarMultBase, scalarMultPoint, pointAddCompressed, pointSubCompressed, pointNegate, doubleScalarMultBase. Uses variable-time Straus algorithm for point multiplication. All 44 equivalence tests pass byte-for-byte. Benchmarks: scMulAdd 3.5x, scalarMultBase 2.3x, pointAdd 16.7x faster.
This commit is contained in:
Generated
+75
@@ -43,12 +43,59 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -73,6 +120,15 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -84,10 +140,23 @@ name = "salvium-crypto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake2b_simd",
|
||||
"curve25519-dalek",
|
||||
"tiny-keccak",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
@@ -158,3 +227,9 @@ checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
@@ -11,6 +11,7 @@ crate-type = ["cdylib"]
|
||||
tiny-keccak = { version = "2.0", features = ["keccak"] }
|
||||
blake2b_simd = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
curve25519-dalek = { version = "4", features = ["alloc"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use tiny_keccak::{Hasher, Keccak};
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
|
||||
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
|
||||
use curve25519_dalek::traits::VartimeMultiscalarMul;
|
||||
|
||||
/// Keccak-256 hash (CryptoNote variant with 0x01 padding, NOT SHA3)
|
||||
/// Matches Salvium C++ cn_fast_hash / keccak()
|
||||
@@ -35,3 +39,132 @@ pub fn blake2b_keyed(data: &[u8], out_len: usize, key: &[u8]) -> Vec<u8> {
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn to32(s: &[u8]) -> [u8; 32] {
|
||||
let mut buf = [0u8; 32];
|
||||
let len = s.len().min(32);
|
||||
buf[..len].copy_from_slice(&s[..len]);
|
||||
buf
|
||||
}
|
||||
|
||||
fn to64(s: &[u8]) -> [u8; 64] {
|
||||
let mut buf = [0u8; 64];
|
||||
let len = s.len().min(64);
|
||||
buf[..len].copy_from_slice(&s[..len]);
|
||||
buf
|
||||
}
|
||||
|
||||
// ─── Scalar Operations (mod L) ─────────────────────────────────────────────
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_add(a: &[u8], b: &[u8]) -> Vec<u8> {
|
||||
let sa = Scalar::from_bytes_mod_order(to32(a));
|
||||
let sb = Scalar::from_bytes_mod_order(to32(b));
|
||||
(sa + sb).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_sub(a: &[u8], b: &[u8]) -> Vec<u8> {
|
||||
let sa = Scalar::from_bytes_mod_order(to32(a));
|
||||
let sb = Scalar::from_bytes_mod_order(to32(b));
|
||||
(sa - sb).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_mul(a: &[u8], b: &[u8]) -> Vec<u8> {
|
||||
let sa = Scalar::from_bytes_mod_order(to32(a));
|
||||
let sb = Scalar::from_bytes_mod_order(to32(b));
|
||||
(sa * sb).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_mul_add(a: &[u8], b: &[u8], c: &[u8]) -> Vec<u8> {
|
||||
let sa = Scalar::from_bytes_mod_order(to32(a));
|
||||
let sb = Scalar::from_bytes_mod_order(to32(b));
|
||||
let sc = Scalar::from_bytes_mod_order(to32(c));
|
||||
(sa * sb + sc).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_mul_sub(a: &[u8], b: &[u8], c: &[u8]) -> Vec<u8> {
|
||||
let sa = Scalar::from_bytes_mod_order(to32(a));
|
||||
let sb = Scalar::from_bytes_mod_order(to32(b));
|
||||
let sc = Scalar::from_bytes_mod_order(to32(c));
|
||||
(sc - sa * sb).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_reduce32(s: &[u8]) -> Vec<u8> {
|
||||
Scalar::from_bytes_mod_order(to32(s)).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_reduce64(s: &[u8]) -> Vec<u8> {
|
||||
Scalar::from_bytes_mod_order_wide(&to64(s)).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_invert(a: &[u8]) -> Vec<u8> {
|
||||
Scalar::from_bytes_mod_order(to32(a)).invert().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_check(s: &[u8]) -> bool {
|
||||
bool::from(Scalar::from_canonical_bytes(to32(s)).is_some())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn sc_is_zero(s: &[u8]) -> bool {
|
||||
Scalar::from_bytes_mod_order(to32(s)) == Scalar::ZERO
|
||||
}
|
||||
|
||||
// ─── Point Operations (compressed Edwards) ──────────────────────────────────
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn scalar_mult_base(s: &[u8]) -> Vec<u8> {
|
||||
let scalar = Scalar::from_bytes_mod_order(to32(s));
|
||||
(ED25519_BASEPOINT_TABLE * &scalar).compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn scalar_mult_point(s: &[u8], p: &[u8]) -> Vec<u8> {
|
||||
let scalar = Scalar::from_bytes_mod_order(to32(s));
|
||||
let point = CompressedEdwardsY(to32(p)).decompress().expect("invalid point");
|
||||
// Use variable-time Straus/wNAF — much faster than constant-time mul
|
||||
EdwardsPoint::vartime_multiscalar_mul(&[scalar], &[point])
|
||||
.compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn point_add_compressed(p: &[u8], q: &[u8]) -> Vec<u8> {
|
||||
let pp = CompressedEdwardsY(to32(p)).decompress().expect("invalid point p");
|
||||
let qq = CompressedEdwardsY(to32(q)).decompress().expect("invalid point q");
|
||||
(pp + qq).compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn point_sub_compressed(p: &[u8], q: &[u8]) -> Vec<u8> {
|
||||
let pp = CompressedEdwardsY(to32(p)).decompress().expect("invalid point p");
|
||||
let qq = CompressedEdwardsY(to32(q)).decompress().expect("invalid point q");
|
||||
(pp - qq).compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn point_negate(p: &[u8]) -> Vec<u8> {
|
||||
let pp = CompressedEdwardsY(to32(p)).decompress().expect("invalid point");
|
||||
(-pp).compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn double_scalar_mult_base(a: &[u8], p: &[u8], b: &[u8]) -> Vec<u8> {
|
||||
let sa = Scalar::from_bytes_mod_order(to32(a));
|
||||
let sb = Scalar::from_bytes_mod_order(to32(b));
|
||||
let pp = CompressedEdwardsY(to32(p)).decompress().expect("invalid point");
|
||||
// Variable-time multi-scalar: a*P + b*G
|
||||
EdwardsPoint::vartime_multiscalar_mul(
|
||||
&[sa, sb],
|
||||
&[pp, curve25519_dalek::constants::ED25519_BASEPOINT_POINT],
|
||||
).compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
|
||||
import { keccak256 as jsKeccak } from '../keccak.js';
|
||||
import { blake2b as jsBlake2b } from '../blake2b.js';
|
||||
import {
|
||||
scAdd, scSub, scMul, scMulAdd, scMulSub,
|
||||
scReduce32, scReduce64, scInvert, scCheck, scIsZero
|
||||
} from '../transaction/serialization.js';
|
||||
import {
|
||||
scalarMultBase, scalarMultPoint, pointAddCompressed,
|
||||
pointSubCompressed, pointNegate, doubleScalarMultBase
|
||||
} from '../ed25519.js';
|
||||
|
||||
export class JsCryptoBackend {
|
||||
constructor() {
|
||||
@@ -26,4 +34,30 @@ export class JsCryptoBackend {
|
||||
blake2b(data, outLen, key) {
|
||||
return jsBlake2b(data, outLen, key);
|
||||
}
|
||||
|
||||
// Scalar ops
|
||||
scAdd(a, b) { return scAdd(a, b); }
|
||||
scSub(a, b) { return scSub(a, b); }
|
||||
scMul(a, b) { return scMul(a, b); }
|
||||
scMulAdd(a, b, c) { return scMulAdd(a, b, c); }
|
||||
scMulSub(a, b, c) { return scMulSub(a, b, c); }
|
||||
scReduce32(s) { return scReduce32(s); }
|
||||
scReduce64(s) { return scReduce64(s); }
|
||||
scInvert(a) { return scInvert(a); }
|
||||
scCheck(s) { return scCheck(s); }
|
||||
scIsZero(s) { return scIsZero(s); }
|
||||
|
||||
// Point ops
|
||||
scalarMultBase(s) { return scalarMultBase(s); }
|
||||
scalarMultPoint(s, p) { return scalarMultPoint(s, p); }
|
||||
pointAddCompressed(p, q) { return pointAddCompressed(p, q); }
|
||||
pointSubCompressed(p, q) { return pointSubCompressed(p, q); }
|
||||
pointNegate(p) { return pointNegate(p); }
|
||||
doubleScalarMultBase(a, p, b) {
|
||||
// JS doubleScalarMultBase expects decompressed point object, not bytes.
|
||||
// Compose from primitives instead: a*P + b*G
|
||||
const aP = scalarMultPoint(a, p);
|
||||
const bG = scalarMultBase(b);
|
||||
return pointAddCompressed(aP, bG);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +56,24 @@ export class WasmCryptoBackend {
|
||||
}
|
||||
return this.wasm.blake2b_hash(data, outLen);
|
||||
}
|
||||
|
||||
// Scalar ops
|
||||
scAdd(a, b) { return this.wasm.sc_add(a, b); }
|
||||
scSub(a, b) { return this.wasm.sc_sub(a, b); }
|
||||
scMul(a, b) { return this.wasm.sc_mul(a, b); }
|
||||
scMulAdd(a, b, c) { return this.wasm.sc_mul_add(a, b, c); }
|
||||
scMulSub(a, b, c) { return this.wasm.sc_mul_sub(a, b, c); }
|
||||
scReduce32(s) { return this.wasm.sc_reduce32(s); }
|
||||
scReduce64(s) { return this.wasm.sc_reduce64(s); }
|
||||
scInvert(a) { return this.wasm.sc_invert(a); }
|
||||
scCheck(s) { return this.wasm.sc_check(s); }
|
||||
scIsZero(s) { return this.wasm.sc_is_zero(s); }
|
||||
|
||||
// Point ops
|
||||
scalarMultBase(s) { return this.wasm.scalar_mult_base(s); }
|
||||
scalarMultPoint(s, p) { return this.wasm.scalar_mult_point(s, p); }
|
||||
pointAddCompressed(p, q) { return this.wasm.point_add_compressed(p, q); }
|
||||
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); }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ export {
|
||||
getCurrentBackendType,
|
||||
keccak256,
|
||||
blake2b,
|
||||
scAdd, scSub, scMul, scMulAdd, scMulSub,
|
||||
scReduce32, scReduce64, scInvert, scCheck, scIsZero,
|
||||
scalarMultBase, scalarMultPoint, pointAddCompressed,
|
||||
pointSubCompressed, pointNegate, doubleScalarMultBase,
|
||||
} from './provider.js';
|
||||
|
||||
// Backends (for direct access / testing)
|
||||
|
||||
@@ -68,3 +68,23 @@ export function keccak256(data) {
|
||||
export function blake2b(data, outLen, key) {
|
||||
return getCryptoBackend().blake2b(data, outLen, key);
|
||||
}
|
||||
|
||||
// Scalar ops
|
||||
export function scAdd(a, b) { return getCryptoBackend().scAdd(a, b); }
|
||||
export function scSub(a, b) { return getCryptoBackend().scSub(a, b); }
|
||||
export function scMul(a, b) { return getCryptoBackend().scMul(a, b); }
|
||||
export function scMulAdd(a, b, c) { return getCryptoBackend().scMulAdd(a, b, c); }
|
||||
export function scMulSub(a, b, c) { return getCryptoBackend().scMulSub(a, b, c); }
|
||||
export function scReduce32(s) { return getCryptoBackend().scReduce32(s); }
|
||||
export function scReduce64(s) { return getCryptoBackend().scReduce64(s); }
|
||||
export function scInvert(a) { return getCryptoBackend().scInvert(a); }
|
||||
export function scCheck(s) { return getCryptoBackend().scCheck(s); }
|
||||
export function scIsZero(s) { return getCryptoBackend().scIsZero(s); }
|
||||
|
||||
// Point ops
|
||||
export function scalarMultBase(s) { return getCryptoBackend().scalarMultBase(s); }
|
||||
export function scalarMultPoint(s, p) { return getCryptoBackend().scalarMultPoint(s, p); }
|
||||
export function pointAddCompressed(p, q) { return getCryptoBackend().pointAddCompressed(p, q); }
|
||||
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); }
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
getCurrentBackendType,
|
||||
keccak256,
|
||||
blake2b,
|
||||
scAdd, scSub, scMul, scMulAdd, scMulSub,
|
||||
scReduce32, scReduce64, scInvert, scCheck, scIsZero,
|
||||
scalarMultBase, scalarMultPoint, pointAddCompressed,
|
||||
pointSubCompressed, pointNegate, doubleScalarMultBase,
|
||||
} from '../src/crypto/index.js';
|
||||
import { JsCryptoBackend } from '../src/crypto/backend-js.js';
|
||||
import { hexToBytes, bytesToHex } from '../src/index.js';
|
||||
@@ -158,6 +162,187 @@ for (let i = 0; i < testInputs.length; i++) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scalar equivalence ─────────────────────────────────────────────────────
|
||||
|
||||
console.log('\n=== Scalar Ops Equivalence ===\n');
|
||||
|
||||
const ZERO = new Uint8Array(32);
|
||||
const ONE = new Uint8Array(32); ONE[0] = 1;
|
||||
const scalarA = crypto.getRandomValues(new Uint8Array(32)); scalarA[31] &= 0x0f; // keep < L
|
||||
const scalarB = crypto.getRandomValues(new Uint8Array(32)); scalarB[31] &= 0x0f;
|
||||
|
||||
const scalarOps = [
|
||||
['scAdd', (b) => b.scAdd(scalarA, scalarB)],
|
||||
['scSub', (b) => b.scSub(scalarA, scalarB)],
|
||||
['scMul', (b) => b.scMul(scalarA, scalarB)],
|
||||
['scMulAdd', (b) => b.scMulAdd(scalarA, scalarB, ONE)],
|
||||
['scMulSub', (b) => b.scMulSub(scalarA, scalarB, ONE)],
|
||||
['scReduce32', (b) => b.scReduce32(new Uint8Array(32).fill(0xff))],
|
||||
['scInvert', (b) => b.scInvert(scalarA)],
|
||||
];
|
||||
|
||||
for (const [name, fn] of scalarOps) {
|
||||
await asyncTest(`${name} equivalence`, async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const jsResult = fn(js);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = fn(getCryptoBackend());
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, `${name} JS vs WASM`);
|
||||
});
|
||||
}
|
||||
|
||||
// scReduce64
|
||||
await asyncTest('scReduce64 equivalence', async () => {
|
||||
const input64 = crypto.getRandomValues(new Uint8Array(64));
|
||||
const js = new JsCryptoBackend();
|
||||
const jsResult = js.scReduce64(input64);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().scReduce64(input64);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'scReduce64 JS vs WASM');
|
||||
});
|
||||
|
||||
// scCheck
|
||||
await asyncTest('scCheck equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const jsOk = js.scCheck(ONE);
|
||||
const jsBad = js.scCheck(new Uint8Array(32).fill(0xff));
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmOk = getCryptoBackend().scCheck(ONE);
|
||||
const wasmBad = getCryptoBackend().scCheck(new Uint8Array(32).fill(0xff));
|
||||
await setCryptoBackend('js');
|
||||
if (jsOk !== wasmOk) throw new Error(`scCheck(1): JS=${jsOk} WASM=${wasmOk}`);
|
||||
if (jsBad !== wasmBad) throw new Error(`scCheck(ff): JS=${jsBad} WASM=${wasmBad}`);
|
||||
});
|
||||
|
||||
// scIsZero
|
||||
await asyncTest('scIsZero equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const jsZero = js.scIsZero(ZERO);
|
||||
const jsNonzero = js.scIsZero(ONE);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmZero = getCryptoBackend().scIsZero(ZERO);
|
||||
const wasmNonzero = getCryptoBackend().scIsZero(ONE);
|
||||
await setCryptoBackend('js');
|
||||
if (jsZero !== wasmZero) throw new Error(`scIsZero(0): JS=${jsZero} WASM=${wasmZero}`);
|
||||
if (jsNonzero !== wasmNonzero) throw new Error(`scIsZero(1): JS=${jsNonzero} WASM=${wasmNonzero}`);
|
||||
});
|
||||
|
||||
// Identity: scAdd(a, 0) = a reduced
|
||||
await asyncTest('scAdd identity: a + 0 = reduce(a)', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const result = js.scAdd(scalarA, ZERO);
|
||||
const reduced = js.scReduce32(scalarA);
|
||||
assertEqual(result, reduced, 'scAdd identity');
|
||||
});
|
||||
|
||||
// Identity: scInvert(a) * a = 1
|
||||
await asyncTest('scInvert * a = 1', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const inv = js.scInvert(scalarA);
|
||||
const product = js.scMul(inv, scalarA);
|
||||
assertEqual(product, ONE, 'inverse identity');
|
||||
});
|
||||
|
||||
// ─── Point equivalence ──────────────────────────────────────────────────────
|
||||
|
||||
console.log('\n=== Point Ops Equivalence ===\n');
|
||||
|
||||
// Ed25519 base point G (compressed)
|
||||
const G_HEX = '5866666666666666666666666666666666666666666666666666666666666666';
|
||||
const G = hexToBytes(G_HEX);
|
||||
|
||||
await asyncTest('scalarMultBase(1) = G', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const result = js.scalarMultBase(ONE);
|
||||
assertEqual(result, G, 'scalarMultBase(1) should be G');
|
||||
});
|
||||
|
||||
await asyncTest('scalarMultBase equivalence (random scalar)', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const jsResult = js.scalarMultBase(scalarA);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().scalarMultBase(scalarA);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'scalarMultBase JS vs WASM');
|
||||
});
|
||||
|
||||
await asyncTest('scalarMultPoint equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const P = js.scalarMultBase(scalarA);
|
||||
const jsResult = js.scalarMultPoint(scalarB, P);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().scalarMultPoint(scalarB, P);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'scalarMultPoint JS vs WASM');
|
||||
});
|
||||
|
||||
await asyncTest('scalarMultPoint(s, G) = scalarMultBase(s)', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const viaBase = js.scalarMultBase(scalarA);
|
||||
const viaPoint = js.scalarMultPoint(scalarA, G);
|
||||
assertEqual(viaBase, viaPoint, 'scalarMultPoint(s,G) vs scalarMultBase(s)');
|
||||
});
|
||||
|
||||
await asyncTest('pointAddCompressed equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const P = js.scalarMultBase(scalarA);
|
||||
const Q = js.scalarMultBase(scalarB);
|
||||
const jsResult = js.pointAddCompressed(P, Q);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().pointAddCompressed(P, Q);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'pointAdd JS vs WASM');
|
||||
});
|
||||
|
||||
await asyncTest('pointAdd(G, G) = scalarMultBase(2)', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const TWO = new Uint8Array(32); TWO[0] = 2;
|
||||
const sum = js.pointAddCompressed(G, G);
|
||||
const doubled = js.scalarMultBase(TWO);
|
||||
assertEqual(sum, doubled, 'G+G vs 2*G');
|
||||
});
|
||||
|
||||
await asyncTest('pointSubCompressed equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const P = js.scalarMultBase(scalarA);
|
||||
const Q = js.scalarMultBase(scalarB);
|
||||
const jsResult = js.pointSubCompressed(P, Q);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().pointSubCompressed(P, Q);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'pointSub JS vs WASM');
|
||||
});
|
||||
|
||||
await asyncTest('pointNegate equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const P = js.scalarMultBase(scalarA);
|
||||
const jsResult = js.pointNegate(P);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().pointNegate(P);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'pointNegate JS vs WASM');
|
||||
});
|
||||
|
||||
await asyncTest('pointNegate roundtrip: -(-P) = P', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const P = js.scalarMultBase(scalarA);
|
||||
const negP = js.pointNegate(P);
|
||||
const negNegP = js.pointNegate(negP);
|
||||
assertEqual(P, negNegP, 'double negate roundtrip');
|
||||
});
|
||||
|
||||
await asyncTest('doubleScalarMultBase equivalence', async () => {
|
||||
const js = new JsCryptoBackend();
|
||||
const P = js.scalarMultBase(scalarA);
|
||||
const jsResult = js.doubleScalarMultBase(scalarB, P, ONE);
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmResult = getCryptoBackend().doubleScalarMultBase(scalarB, P, ONE);
|
||||
await setCryptoBackend('js');
|
||||
assertEqual(jsResult, wasmResult, 'doubleScalarMultBase JS vs WASM');
|
||||
});
|
||||
|
||||
// ─── Benchmark ──────────────────────────────────────────────────────────────
|
||||
|
||||
console.log('\n=== Benchmark (10,000 iterations) ===\n');
|
||||
@@ -197,6 +382,35 @@ const ITERATIONS = 10_000;
|
||||
console.log(` blake2b: JS ${jsTime.toFixed(1)}ms WASM ${wasmTime.toFixed(1)}ms (${speedup}x)`);
|
||||
}
|
||||
|
||||
// Scalar and point benchmarks (1,000 iterations — point ops are slower)
|
||||
const BENCH_SC = 10_000;
|
||||
const BENCH_PT = 1_000;
|
||||
const benchScalar = new Uint8Array(32); benchScalar[0] = 42; benchScalar[31] &= 0x0f;
|
||||
const benchPoint = getCryptoBackend().scalarMultBase(benchScalar);
|
||||
|
||||
const benchOps = [
|
||||
['scMulAdd', BENCH_SC, () => scMulAdd(benchScalar, benchScalar, benchScalar)],
|
||||
['scalarMultBase', BENCH_PT, () => scalarMultBase(benchScalar)],
|
||||
['scalarMultPoint', BENCH_PT, () => scalarMultPoint(benchScalar, benchPoint)],
|
||||
['pointAddCompressed', BENCH_PT, () => pointAddCompressed(benchPoint, benchPoint)],
|
||||
];
|
||||
|
||||
for (const [name, iters, fn] of benchOps) {
|
||||
await setCryptoBackend('js');
|
||||
const jsStart = performance.now();
|
||||
for (let i = 0; i < iters; i++) fn();
|
||||
const jsTime = performance.now() - jsStart;
|
||||
|
||||
await setCryptoBackend('wasm');
|
||||
const wasmStart = performance.now();
|
||||
for (let i = 0; i < iters; i++) fn();
|
||||
const wasmTime = performance.now() - wasmStart;
|
||||
|
||||
const speedup = (jsTime / wasmTime).toFixed(2);
|
||||
const pad = name.padEnd(20);
|
||||
console.log(` ${pad} JS ${jsTime.toFixed(1)}ms WASM ${wasmTime.toFixed(1)}ms (${speedup}x) [${iters} iters]`);
|
||||
}
|
||||
|
||||
await setCryptoBackend('js'); // reset
|
||||
|
||||
// ─── Summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user