Fix invalid point panic spam, add Bun FFI crypto backend
Replace .expect() with match on all point decompression in lib.rs — returns empty Vec instead of panicking on invalid Ed25519 points. During sync, generate_key_derivation is called for every tx on chain and most have invalid pubkeys; the old code threw hundreds of thousands of WASM exceptions (potential QuickJS crash vector). Now returns null with zero exceptions in the hot path. Also adds native Rust FFI backend (bun:ffi) as a third crypto code path for debugging sync issues alongside WASM and JS backends.
This commit is contained in:
@@ -27,6 +27,8 @@ JavaScript/TypeScript library for Salvium cryptocurrency — wallet management,
|
||||
- **Cryptographic Primitives** - Blake2b, Keccak-256, Ed25519, X25519, Base58
|
||||
- **Multi-Network Support** - Mainnet, Testnet, Stagenet
|
||||
- **TypeScript Support** - Full type definitions included
|
||||
- **Oracle & Pricing** - Verify oracle signatures, fetch pricing records, calculate conversions
|
||||
- **Wallet Encryption** - ML-KEM-768 + Argon2id encrypted wallet storage
|
||||
- **Minimal Dependencies** - Only @noble/curves and @noble/hashes
|
||||
|
||||
## Crypto Backends
|
||||
@@ -35,7 +37,7 @@ salvium-js includes three interchangeable crypto backends behind a unified provi
|
||||
|
||||
| Backend | Environment | Performance | Size |
|
||||
|---------|-------------|-------------|------|
|
||||
| **WASM** (default) | Browser, Node, Bun | 8-26x faster than JS | 319KB |
|
||||
| **WASM** (default) | Browser, Node, Bun | 8-26x faster than JS | 336KB |
|
||||
| **JSI** | React Native (iOS/Android) | Native speed via FFI | Static lib |
|
||||
| **JS** | QuickJS, any JS runtime | Baseline (Noble curves) | Zero deps |
|
||||
|
||||
@@ -912,15 +914,56 @@ const b2keyed = blake2b(data, 32, key); // Keyed hash (MAC)
|
||||
| `createWallet(options?)` | Create new wallet with mnemonic |
|
||||
| `restoreWallet(mnemonic, options?)` | Restore wallet from mnemonic |
|
||||
| `createViewOnlyWallet(options)` | Create view-only wallet from keys |
|
||||
| `wallet.getAddress()` | Get main wallet address |
|
||||
| `wallet.getAddress(format?)` | Get main address (auto-detects CARROT vs Legacy) |
|
||||
| `wallet.getSubaddress(major, minor)` | Generate subaddress |
|
||||
| `wallet.getBalance()` | Get balance (total, unlocked, locked) |
|
||||
| `wallet.syncWithDaemon(daemonRpc)` | Sync wallet with daemon (CARROT-aware) |
|
||||
| `wallet.transfer(options)` | Send transaction via synced wallet |
|
||||
| `wallet.getStorageBalance()` | Get balance from stored outputs |
|
||||
| `wallet.getUTXOs(options?)` | Get unspent outputs (filters: unlockedOnly, accountIndex) |
|
||||
| `wallet.getAssetTypes()` | Get all asset types with balances |
|
||||
| `wallet.syncWithDaemon(daemon?)` | Sync wallet with daemon (CARROT-aware) |
|
||||
| `wallet.startSyncing(daemon, interval?)` | Start background sync loop |
|
||||
| `wallet.stopSyncing()` | Stop background sync |
|
||||
| `wallet.transfer(destinations, options?)` | Build, sign, and broadcast transfer |
|
||||
| `wallet.stake(amount, options?)` | Stake coins for yield |
|
||||
| `wallet.burn(amount, options?)` | Burn coins |
|
||||
| `wallet.convert(amount, src, dest, addr, options?)` | Convert between assets |
|
||||
| `wallet.sweepAll(address, options?)` | Sweep all funds to address |
|
||||
| `wallet.canSign()` | Check if wallet can sign transactions |
|
||||
| `wallet.canScan()` | Check if wallet can scan for outputs |
|
||||
| `wallet.toJSON(includeSecrets?)` | Serialize wallet to JSON |
|
||||
| `wallet.toEncryptedJSON(password)` | Encrypt wallet (ML-KEM-768 + Argon2id) |
|
||||
| `Wallet.fromJSON(json)` | Restore wallet from JSON |
|
||||
| `Wallet.fromEncryptedJSON(data, password)` | Restore from encrypted JSON |
|
||||
| `Wallet.fromMnemonic(mnemonic, options?)` | Restore from 25-word phrase |
|
||||
| `Wallet.fromSeed(seed, options?)` | Restore from 32-byte seed |
|
||||
| `Wallet.fromViewKey(viewSec, spendPub)` | Create view-only wallet |
|
||||
|
||||
### Wallet Sync Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `WalletSync` | Blockchain sync engine class |
|
||||
| `createWalletSync(options)` | Create sync engine with keys + daemon |
|
||||
| `sync.start(startHeight?)` | Start synchronization |
|
||||
| `sync.stop()` | Stop synchronization |
|
||||
| `sync.getProgress()` | Get sync status, height, percent |
|
||||
| `sync.rescan(fromHeight)` | Rescan from specific height |
|
||||
| `sync.scanMempool()` | Scan mempool for pending TXs |
|
||||
| `sync.on(event, handler)` | Listen for sync events |
|
||||
| `SYNC_STATUS` | Enum: IDLE, SYNCING, COMPLETE, ERROR |
|
||||
|
||||
### Oracle & Pricing Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `fetchPricingRecord(options)` | Fetch pricing from oracle server |
|
||||
| `verifyPricingRecordSignature(pr, pubkey)` | Verify oracle signature |
|
||||
| `getOraclePublicKey(network)` | Get oracle public key for network |
|
||||
| `getConversionRate(pr, fromAsset, toAsset)` | Get conversion rate between assets |
|
||||
| `calculateConversion(pr, src, dest, amount)` | Full conversion with slippage |
|
||||
| `getConvertedAmount(rate, amount)` | Calculate converted amount |
|
||||
| `calculateSlippage(amount)` | Calculate 3.125% slippage |
|
||||
| `validatePricingRecord(pr, options)` | Full pricing record validation |
|
||||
|
||||
### UTXO Selection Functions
|
||||
|
||||
@@ -965,6 +1008,9 @@ const b2keyed = blake2b(data, 32, key); // Keyed hash (MAC)
|
||||
| `keccak256(data)` | Keccak-256 hash, returns Uint8Array |
|
||||
| `keccak256Hex(data)` | Keccak-256 hash, returns hex string |
|
||||
| `blake2b(data, outlen, key?)` | Blake2b hash with optional key |
|
||||
| `sha256(data)` | SHA-256 hash |
|
||||
| `argon2id(password, salt, tCost, mCost, parallelism, outLen)` | Argon2id key derivation |
|
||||
| `x25519ScalarMult(scalar, uCoord)` | X25519 scalar multiplication (Salvium clamping) |
|
||||
|
||||
## Wallet Class
|
||||
|
||||
@@ -1004,6 +1050,154 @@ const json = wallet.toJSON();
|
||||
const loaded = Wallet.fromJSON(json);
|
||||
```
|
||||
|
||||
## Wallet Sync Engine
|
||||
|
||||
The `WalletSync` engine scans the blockchain for owned outputs using both CryptoNote and CARROT protocols. It handles binary block parsing, key image tracking, spent detection, coinbase unlock windows, chain reorganizations, and staking return output prediction.
|
||||
|
||||
```javascript
|
||||
import { createDaemonRPC } from 'salvium-js/rpc';
|
||||
import { Wallet, initCrypto } from 'salvium-js';
|
||||
|
||||
// Initialize WASM crypto backend
|
||||
await initCrypto();
|
||||
|
||||
// Restore wallet and sync
|
||||
const wallet = Wallet.fromMnemonic('abbey ability able about ...');
|
||||
const daemon = createDaemonRPC({ url: 'http://localhost:19081' });
|
||||
|
||||
// One-shot sync (scans from last saved height to chain tip)
|
||||
await wallet.syncWithDaemon(daemon);
|
||||
|
||||
// Check balance
|
||||
const { balance, unlockedBalance, lockedBalance } = await wallet.getStorageBalance();
|
||||
console.log(`Balance: ${Number(unlockedBalance) / 1e8} SAL`);
|
||||
console.log(`Locked: ${Number(lockedBalance) / 1e8} SAL`);
|
||||
|
||||
// Background sync (polls every 10 seconds)
|
||||
await wallet.startSyncing(daemon, 10000);
|
||||
wallet.stopSyncing();
|
||||
```
|
||||
|
||||
### Direct WalletSync Usage
|
||||
|
||||
For lower-level control, use `WalletSync` directly:
|
||||
|
||||
```javascript
|
||||
import { WalletSync, createWalletSync, SYNC_STATUS } from 'salvium-js';
|
||||
import { WalletStorage } from 'salvium-js';
|
||||
|
||||
const sync = createWalletSync({
|
||||
storage, // WalletStorage instance
|
||||
daemon, // DaemonRPC instance
|
||||
keys: {
|
||||
viewSecretKey,
|
||||
spendSecretKey, // null for view-only
|
||||
spendPublicKey,
|
||||
viewPublicKey
|
||||
},
|
||||
carrotKeys, // From deriveCarrotKeys() — enables CARROT scanning
|
||||
batchSize: 100 // Blocks per RPC request
|
||||
});
|
||||
|
||||
// Listen for events
|
||||
sync.on('outputReceived', ({ amount, txHash }) => {
|
||||
console.log(`Received ${Number(amount) / 1e8} SAL in ${txHash}`);
|
||||
});
|
||||
sync.on('outputSpent', ({ keyImage }) => {
|
||||
console.log(`Output spent: ${keyImage}`);
|
||||
});
|
||||
sync.on('syncProgress', ({ currentHeight, targetHeight, percentComplete }) => {
|
||||
console.log(`${percentComplete.toFixed(1)}% (${currentHeight}/${targetHeight})`);
|
||||
});
|
||||
|
||||
await sync.start();
|
||||
```
|
||||
|
||||
### Sync Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `syncStart` | `{ startHeight, targetHeight }` | Sync started |
|
||||
| `syncProgress` | `{ currentHeight, targetHeight, percentComplete }` | Progress update |
|
||||
| `syncComplete` | `{ height }` | Reached chain tip |
|
||||
| `outputReceived` | `{ amount, txHash, outputIndex }` | Owned output found |
|
||||
| `outputSpent` | `{ keyImage, txHash }` | Output marked spent |
|
||||
| `reorg` | `{ fromHeight, toHeight }` | Chain reorganization |
|
||||
| `syncError` | `{ error }` | Error during sync |
|
||||
|
||||
## Staking, Burns, and Conversions
|
||||
|
||||
Salvium supports staking for yield, burning tokens, and cross-asset conversions.
|
||||
|
||||
```javascript
|
||||
// Stake 1000 SAL for yield
|
||||
const stakeTx = await wallet.stake(100000000000n, { daemon });
|
||||
console.log('Stake TX:', stakeTx.txHash);
|
||||
|
||||
// Burn 10 SAL
|
||||
const burnTx = await wallet.burn(1000000000n, { daemon });
|
||||
|
||||
// Convert between assets (requires oracle pricing)
|
||||
const convertTx = await wallet.convert(
|
||||
1000000000n, // amount
|
||||
'SAL', // source asset
|
||||
'SAL1', // destination asset
|
||||
destAddress, // destination address
|
||||
{ daemon }
|
||||
);
|
||||
|
||||
// Check staking status
|
||||
const locked = wallet.getLockedCoins();
|
||||
const yield_ = wallet.getTotalYield();
|
||||
```
|
||||
|
||||
## Oracle & Pricing
|
||||
|
||||
Verify oracle signatures and calculate cross-asset conversion rates.
|
||||
|
||||
```javascript
|
||||
import {
|
||||
fetchPricingRecord,
|
||||
verifyPricingRecordSignature,
|
||||
getOraclePublicKey,
|
||||
getConversionRate,
|
||||
calculateConversion
|
||||
} from 'salvium-js';
|
||||
|
||||
// Fetch current pricing from oracle
|
||||
const pr = await fetchPricingRecord({ network: 'mainnet' });
|
||||
|
||||
// Verify oracle signature
|
||||
const pubkey = getOraclePublicKey('mainnet');
|
||||
const valid = await verifyPricingRecordSignature(pr, pubkey);
|
||||
|
||||
// Get conversion rate
|
||||
const rate = getConversionRate(pr, 'SAL', 'SAL1');
|
||||
|
||||
// Calculate full conversion with slippage
|
||||
const result = calculateConversion(pr, 'SAL', 'SAL1', 1000000000n);
|
||||
console.log('You receive:', result.destAmount);
|
||||
console.log('Slippage:', result.slippage);
|
||||
```
|
||||
|
||||
## Wallet Encryption
|
||||
|
||||
Encrypt wallet data with ML-KEM-768 (post-quantum) + Argon2id key derivation.
|
||||
|
||||
```javascript
|
||||
// Encrypt wallet for storage
|
||||
const encrypted = wallet.toEncryptedJSON('my-password');
|
||||
|
||||
// Restore from encrypted JSON
|
||||
const restored = Wallet.fromEncryptedJSON(encrypted, 'my-password');
|
||||
|
||||
// Change password
|
||||
const reEncrypted = Wallet.changePassword(encrypted, 'old-pass', 'new-pass');
|
||||
|
||||
// Check if JSON is encrypted
|
||||
Wallet.isEncrypted(data); // true or false
|
||||
```
|
||||
|
||||
## UTXO Selection
|
||||
|
||||
Multiple strategies for selecting transaction inputs.
|
||||
@@ -1123,7 +1317,7 @@ bun test/all.js --integration
|
||||
bun test/all.js --integration http://localhost:19081
|
||||
```
|
||||
|
||||
25 test suites, 899+ tests covering addresses, keys, mnemonics, scanning, CLSAG/TCLSAG signatures, Bulletproofs+ range proofs, consensus rules, wallet sync, encrypted storage, blockchain reorgs, and cross-backend WASM/JS interop.
|
||||
25 test suites covering addresses, keys, mnemonics, CryptoNote + CARROT scanning, CLSAG/TCLSAG signatures, Bulletproofs+ range proofs, consensus rules, wallet sync, encrypted storage, blockchain reorgs, oracle verification, and cross-backend WASM/JS interop.
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -1135,32 +1329,33 @@ salvium-js/
|
||||
blockchain.js # Chain state, reorg detection
|
||||
bulletproofs_plus.js # BP+ range proofs
|
||||
carrot.js # CARROT key derivation
|
||||
carrot-scanning.js # CARROT output scanning (X25519 ECDH)
|
||||
carrot-scanning.js # CARROT output scanning (X25519 ECDH, internal/return)
|
||||
consensus.js # Difficulty, rewards, fees, median
|
||||
keccak.js # Keccak-256 hashing
|
||||
keyimage.js # Key image generation
|
||||
mining.js # RandomX mining
|
||||
mnemonic.js # 25-word seed phrases
|
||||
oracle.js # Oracle pricing, signature verification, conversions
|
||||
scanning.js # CryptoNote output scanning
|
||||
signature.js # Message signature verification
|
||||
subaddress.js # Subaddress generation
|
||||
transaction.js # TX construction, CLSAG/TCLSAG, decoy selection
|
||||
validation.js # TX/block validation rules
|
||||
wallet.js # Wallet class
|
||||
wallet-sync.js # Daemon sync engine (CARROT-aware)
|
||||
wallet.js # Wallet class (sync, transfer, stake, burn, convert)
|
||||
wallet-sync.js # Daemon sync engine (CARROT-aware, binary blocks)
|
||||
crypto/
|
||||
provider.js # Backend-agnostic crypto API
|
||||
backend-js.js # Pure JS backend (Noble curves)
|
||||
backend-wasm.js # Rust/WASM backend (curve25519-dalek)
|
||||
backend-jsi.js # React Native JSI backend (FFI)
|
||||
wasm/ # Compiled WASM binary (319KB)
|
||||
wasm/ # Compiled WASM binary (336KB)
|
||||
rpc/
|
||||
daemon.js # Daemon RPC client
|
||||
wallet.js # Wallet RPC client
|
||||
transaction/ # TX parsing, serialization, constants
|
||||
wallet/ # Account, storage, encryption
|
||||
wallet/ # Account, storage, encryption (ML-KEM-768 + Argon2id)
|
||||
crates/
|
||||
salvium-crypto/ # Rust crate (CLSAG, TCLSAG, BP+, 28 primitives)
|
||||
salvium-crypto/ # Rust crate (CLSAG, TCLSAG, BP+, X25519, 30+ primitives)
|
||||
test/ # 25 test suites
|
||||
```
|
||||
|
||||
|
||||
+174
-114
@@ -11,6 +11,25 @@
|
||||
|
||||
use std::slice;
|
||||
use std::ptr;
|
||||
use std::panic;
|
||||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
|
||||
/// Helper: run a closure that may panic, returning -1 on panic instead of aborting.
|
||||
/// This is critical for extern "C" functions where unwinding is UB.
|
||||
/// We use AssertUnwindSafe because the closures capture raw pointers (which are
|
||||
/// !UnwindSafe) but we know we won't observe torn state through them on panic.
|
||||
unsafe fn catch_ffi<F: FnOnce() -> i32>(f: F) -> i32 {
|
||||
match panic::catch_unwind(panic::AssertUnwindSafe(f)) {
|
||||
Ok(rc) => rc,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that 32 bytes represent a valid compressed Edwards Y point.
|
||||
/// Returns false if the point cannot be decompressed (invalid curve point).
|
||||
fn is_valid_point(bytes: &[u8; 32]) -> bool {
|
||||
CompressedEdwardsY(*bytes).decompress().is_some()
|
||||
}
|
||||
|
||||
// ─── Hashing ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -192,6 +211,7 @@ pub unsafe extern "C" fn salvium_scalar_mult_point(
|
||||
) -> i32 {
|
||||
let s = slice::from_raw_parts(s, 32);
|
||||
let p = slice::from_raw_parts(p, 32);
|
||||
if !is_valid_point(&crate::to32(p)) { return -1; }
|
||||
let result = crate::scalar_mult_point(s, p);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -205,6 +225,7 @@ pub unsafe extern "C" fn salvium_point_add(
|
||||
) -> i32 {
|
||||
let p = slice::from_raw_parts(p, 32);
|
||||
let q = slice::from_raw_parts(q, 32);
|
||||
if !is_valid_point(&crate::to32(p)) || !is_valid_point(&crate::to32(q)) { return -1; }
|
||||
let result = crate::point_add_compressed(p, q);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -218,6 +239,7 @@ pub unsafe extern "C" fn salvium_point_sub(
|
||||
) -> i32 {
|
||||
let p = slice::from_raw_parts(p, 32);
|
||||
let q = slice::from_raw_parts(q, 32);
|
||||
if !is_valid_point(&crate::to32(p)) || !is_valid_point(&crate::to32(q)) { return -1; }
|
||||
let result = crate::point_sub_compressed(p, q);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -229,6 +251,7 @@ pub unsafe extern "C" fn salvium_point_negate(
|
||||
out: *mut u8,
|
||||
) -> i32 {
|
||||
let p = slice::from_raw_parts(p, 32);
|
||||
if !is_valid_point(&crate::to32(p)) { return -1; }
|
||||
let result = crate::point_negate(p);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -244,6 +267,7 @@ pub unsafe extern "C" fn salvium_double_scalar_mult_base(
|
||||
let a = slice::from_raw_parts(a, 32);
|
||||
let p = slice::from_raw_parts(p, 32);
|
||||
let b = slice::from_raw_parts(b, 32);
|
||||
if !is_valid_point(&crate::to32(p)) { return -1; }
|
||||
let result = crate::double_scalar_mult_base(a, p, b);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -271,6 +295,7 @@ pub unsafe extern "C" fn salvium_generate_key_derivation(
|
||||
) -> i32 {
|
||||
let pk = slice::from_raw_parts(pub_key, 32);
|
||||
let sk = slice::from_raw_parts(sec_key, 32);
|
||||
if !is_valid_point(&crate::to32(pk)) { return -1; }
|
||||
let result = crate::generate_key_derivation(pk, sk);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -284,6 +309,8 @@ pub unsafe extern "C" fn salvium_generate_key_image(
|
||||
) -> i32 {
|
||||
let pk = slice::from_raw_parts(pub_key, 32);
|
||||
let sk = slice::from_raw_parts(sec_key, 32);
|
||||
// hash_to_point always produces valid points, so no validation needed on pk here
|
||||
// (the lib function calls hash_to_point internally, not decompress)
|
||||
let result = crate::generate_key_image(pk, sk);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -298,6 +325,7 @@ pub unsafe extern "C" fn salvium_derive_public_key(
|
||||
) -> i32 {
|
||||
let deriv = slice::from_raw_parts(derivation, 32);
|
||||
let base = slice::from_raw_parts(base_pub, 32);
|
||||
if !is_valid_point(&crate::to32(base)) { return -1; }
|
||||
let result = crate::derive_public_key(deriv, output_index, base);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -312,6 +340,7 @@ pub unsafe extern "C" fn salvium_derive_secret_key(
|
||||
) -> i32 {
|
||||
let deriv = slice::from_raw_parts(derivation, 32);
|
||||
let base = slice::from_raw_parts(base_sec, 32);
|
||||
// derive_secret_key does scalar math only (no point decompression), safe to call directly
|
||||
let result = crate::derive_secret_key(deriv, output_index, base);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -327,6 +356,7 @@ pub unsafe extern "C" fn salvium_pedersen_commit(
|
||||
) -> i32 {
|
||||
let amount = slice::from_raw_parts(amount, 32);
|
||||
let mask = slice::from_raw_parts(mask, 32);
|
||||
// pedersen_commit decompresses constant H_POINT_BYTES — always valid
|
||||
let result = crate::pedersen_commit(amount, mask);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -338,6 +368,7 @@ pub unsafe extern "C" fn salvium_zero_commit(
|
||||
out: *mut u8,
|
||||
) -> i32 {
|
||||
let amount = slice::from_raw_parts(amount, 32);
|
||||
// zero_commit decompresses constant H_POINT_BYTES — always valid
|
||||
let result = crate::zero_commit(amount);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
@@ -457,29 +488,35 @@ pub unsafe extern "C" fn salvium_clsag_sign(
|
||||
out: *mut u8,
|
||||
) -> i32 {
|
||||
let n = ring_count as usize;
|
||||
let message = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let ring_flat = slice::from_raw_parts(ring, n * 32);
|
||||
let ring_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&ring_flat[i*32..(i+1)*32])).collect();
|
||||
let comms_flat = slice::from_raw_parts(commitments, n * 32);
|
||||
let comms_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&comms_flat[i*32..(i+1)*32])).collect();
|
||||
let msg = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let ring_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(ring, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let comms_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(commitments, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let sk = crate::to32(slice::from_raw_parts(secret_key, 32));
|
||||
let cm = crate::to32(slice::from_raw_parts(commitment_mask, 32));
|
||||
let po = crate::to32(slice::from_raw_parts(pseudo_output, 32));
|
||||
let si = secret_index as usize;
|
||||
let out_ptr = out;
|
||||
|
||||
let sig = crate::clsag::clsag_sign(&message, &ring_arr, &sk, &comms_arr, &cm, &po, secret_index as usize);
|
||||
|
||||
// Write output: s[0..n], c1, I, D
|
||||
let mut offset = 0;
|
||||
for s in &sig.s {
|
||||
ptr::copy_nonoverlapping(s.as_ptr(), out.add(offset), 32);
|
||||
catch_ffi(move || {
|
||||
let sig = crate::clsag::clsag_sign(&msg, &ring_arr, &sk, &comms_arr, &cm, &po, si);
|
||||
let mut offset = 0;
|
||||
for s in &sig.s {
|
||||
ptr::copy_nonoverlapping(s.as_ptr(), out_ptr.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
ptr::copy_nonoverlapping(sig.c1.as_ptr(), out_ptr.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
ptr::copy_nonoverlapping(sig.c1.as_ptr(), out.add(offset), 32);
|
||||
offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.key_image.as_ptr(), out.add(offset), 32);
|
||||
offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.commitment_image.as_ptr(), out.add(offset), 32);
|
||||
0
|
||||
ptr::copy_nonoverlapping(sig.key_image.as_ptr(), out_ptr.add(offset), 32);
|
||||
offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.commitment_image.as_ptr(), out_ptr.add(offset), 32);
|
||||
0
|
||||
})
|
||||
}
|
||||
|
||||
/// CLSAG verify.
|
||||
@@ -499,27 +536,32 @@ pub unsafe extern "C" fn salvium_clsag_verify(
|
||||
let expected = n * 32 + 96;
|
||||
if sig_len < expected { return 0; }
|
||||
|
||||
let message = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let sig_bytes = slice::from_raw_parts(sig, sig_len);
|
||||
let ring_flat = slice::from_raw_parts(ring, n * 32);
|
||||
let ring_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&ring_flat[i*32..(i+1)*32])).collect();
|
||||
let comms_flat = slice::from_raw_parts(commitments, n * 32);
|
||||
let comms_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&comms_flat[i*32..(i+1)*32])).collect();
|
||||
let msg = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let sig_data = slice::from_raw_parts(sig, sig_len).to_vec();
|
||||
let ring_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(ring, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let comms_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(commitments, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let po = crate::to32(slice::from_raw_parts(pseudo_output, 32));
|
||||
|
||||
// Parse sig: s[0..n], c1, I, D (no length prefix in FFI format)
|
||||
let mut s = Vec::with_capacity(n);
|
||||
let mut offset = 0;
|
||||
for _ in 0..n {
|
||||
s.push(crate::to32(&sig_bytes[offset..offset + 32]));
|
||||
offset += 32;
|
||||
}
|
||||
let c1 = crate::to32(&sig_bytes[offset..offset + 32]); offset += 32;
|
||||
let key_image = crate::to32(&sig_bytes[offset..offset + 32]); offset += 32;
|
||||
let commitment_image = crate::to32(&sig_bytes[offset..offset + 32]);
|
||||
catch_ffi(move || {
|
||||
let mut s = Vec::with_capacity(n);
|
||||
let mut offset = 0;
|
||||
for _ in 0..n {
|
||||
s.push(crate::to32(&sig_data[offset..offset + 32]));
|
||||
offset += 32;
|
||||
}
|
||||
let c1 = crate::to32(&sig_data[offset..offset + 32]); offset += 32;
|
||||
let key_image = crate::to32(&sig_data[offset..offset + 32]); offset += 32;
|
||||
let commitment_image = crate::to32(&sig_data[offset..offset + 32]);
|
||||
|
||||
let sig_struct = crate::clsag::ClsagSignature { s, c1, key_image, commitment_image };
|
||||
if crate::clsag::clsag_verify(&message, &sig_struct, &ring_arr, &comms_arr, &po) { 1 } else { 0 }
|
||||
let sig_struct = crate::clsag::ClsagSignature { s, c1, key_image, commitment_image };
|
||||
if crate::clsag::clsag_verify(&msg, &sig_struct, &ring_arr, &comms_arr, &po) { 1 } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
// ─── TCLSAG Ring Signatures ────────────────────────────────────────────────
|
||||
@@ -541,31 +583,38 @@ pub unsafe extern "C" fn salvium_tclsag_sign(
|
||||
out: *mut u8,
|
||||
) -> i32 {
|
||||
let n = ring_count as usize;
|
||||
let message = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let ring_flat = slice::from_raw_parts(ring, n * 32);
|
||||
let ring_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&ring_flat[i*32..(i+1)*32])).collect();
|
||||
let comms_flat = slice::from_raw_parts(commitments, n * 32);
|
||||
let comms_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&comms_flat[i*32..(i+1)*32])).collect();
|
||||
let msg = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let ring_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(ring, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let comms_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(commitments, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let skx = crate::to32(slice::from_raw_parts(secret_key_x, 32));
|
||||
let sky = crate::to32(slice::from_raw_parts(secret_key_y, 32));
|
||||
let cm = crate::to32(slice::from_raw_parts(commitment_mask, 32));
|
||||
let po = crate::to32(slice::from_raw_parts(pseudo_output, 32));
|
||||
let si = secret_index as usize;
|
||||
let out_ptr = out;
|
||||
|
||||
let sig = crate::tclsag::tclsag_sign(&message, &ring_arr, &skx, &sky, &comms_arr, &cm, &po, secret_index as usize);
|
||||
|
||||
let mut offset = 0;
|
||||
for s in &sig.sx {
|
||||
ptr::copy_nonoverlapping(s.as_ptr(), out.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
for s in &sig.sy {
|
||||
ptr::copy_nonoverlapping(s.as_ptr(), out.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
ptr::copy_nonoverlapping(sig.c1.as_ptr(), out.add(offset), 32); offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.key_image.as_ptr(), out.add(offset), 32); offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.commitment_image.as_ptr(), out.add(offset), 32);
|
||||
0
|
||||
catch_ffi(move || {
|
||||
let sig = crate::tclsag::tclsag_sign(&msg, &ring_arr, &skx, &sky, &comms_arr, &cm, &po, si);
|
||||
let mut offset = 0;
|
||||
for s in &sig.sx {
|
||||
ptr::copy_nonoverlapping(s.as_ptr(), out_ptr.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
for s in &sig.sy {
|
||||
ptr::copy_nonoverlapping(s.as_ptr(), out_ptr.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
ptr::copy_nonoverlapping(sig.c1.as_ptr(), out_ptr.add(offset), 32); offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.key_image.as_ptr(), out_ptr.add(offset), 32); offset += 32;
|
||||
ptr::copy_nonoverlapping(sig.commitment_image.as_ptr(), out_ptr.add(offset), 32);
|
||||
0
|
||||
})
|
||||
}
|
||||
|
||||
/// TCLSAG verify.
|
||||
@@ -585,25 +634,31 @@ pub unsafe extern "C" fn salvium_tclsag_verify(
|
||||
let expected = 2 * n * 32 + 96;
|
||||
if sig_len < expected { return 0; }
|
||||
|
||||
let message = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let sig_bytes = slice::from_raw_parts(sig, sig_len);
|
||||
let ring_flat = slice::from_raw_parts(ring, n * 32);
|
||||
let ring_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&ring_flat[i*32..(i+1)*32])).collect();
|
||||
let comms_flat = slice::from_raw_parts(commitments, n * 32);
|
||||
let comms_arr: Vec<[u8; 32]> = (0..n).map(|i| crate::to32(&comms_flat[i*32..(i+1)*32])).collect();
|
||||
let msg = crate::to32(slice::from_raw_parts(message, 32));
|
||||
let sig_data = slice::from_raw_parts(sig, sig_len).to_vec();
|
||||
let ring_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(ring, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let comms_arr: Vec<[u8; 32]> = {
|
||||
let flat = slice::from_raw_parts(commitments, n * 32);
|
||||
(0..n).map(|i| crate::to32(&flat[i*32..(i+1)*32])).collect()
|
||||
};
|
||||
let po = crate::to32(slice::from_raw_parts(pseudo_output, 32));
|
||||
|
||||
let mut offset = 0;
|
||||
let mut sx = Vec::with_capacity(n);
|
||||
for _ in 0..n { sx.push(crate::to32(&sig_bytes[offset..offset+32])); offset += 32; }
|
||||
let mut sy = Vec::with_capacity(n);
|
||||
for _ in 0..n { sy.push(crate::to32(&sig_bytes[offset..offset+32])); offset += 32; }
|
||||
let c1 = crate::to32(&sig_bytes[offset..offset+32]); offset += 32;
|
||||
let key_image = crate::to32(&sig_bytes[offset..offset+32]); offset += 32;
|
||||
let commitment_image = crate::to32(&sig_bytes[offset..offset+32]);
|
||||
catch_ffi(move || {
|
||||
let mut offset = 0;
|
||||
let mut sx = Vec::with_capacity(n);
|
||||
for _ in 0..n { sx.push(crate::to32(&sig_data[offset..offset+32])); offset += 32; }
|
||||
let mut sy = Vec::with_capacity(n);
|
||||
for _ in 0..n { sy.push(crate::to32(&sig_data[offset..offset+32])); offset += 32; }
|
||||
let c1 = crate::to32(&sig_data[offset..offset+32]); offset += 32;
|
||||
let key_image = crate::to32(&sig_data[offset..offset+32]); offset += 32;
|
||||
let commitment_image = crate::to32(&sig_data[offset..offset+32]);
|
||||
|
||||
let sig_struct = crate::tclsag::TclsagSignature { sx, sy, c1, key_image, commitment_image };
|
||||
if crate::tclsag::tclsag_verify(&message, &sig_struct, &ring_arr, &comms_arr, &po) { 1 } else { 0 }
|
||||
let sig_struct = crate::tclsag::TclsagSignature { sx, sy, c1, key_image, commitment_image };
|
||||
if crate::tclsag::tclsag_verify(&msg, &sig_struct, &ring_arr, &comms_arr, &po) { 1 } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Bulletproofs+ Range Proofs ─────────────────────────────────────────────
|
||||
@@ -626,40 +681,43 @@ pub unsafe extern "C" fn salvium_bulletproof_plus_prove(
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
|
||||
let n = count as usize;
|
||||
let amounts_slice = slice::from_raw_parts(amounts, n * 8);
|
||||
let masks_slice = slice::from_raw_parts(masks, n * 32);
|
||||
let amounts_data = slice::from_raw_parts(amounts, n * 8).to_vec();
|
||||
let masks_data = slice::from_raw_parts(masks, n * 32).to_vec();
|
||||
let out_ptr = out;
|
||||
let out_len_ptr = out_len;
|
||||
|
||||
let amounts_vec: Vec<u64> = (0..n).map(|i| {
|
||||
u64::from_le_bytes([
|
||||
amounts_slice[i*8], amounts_slice[i*8+1], amounts_slice[i*8+2], amounts_slice[i*8+3],
|
||||
amounts_slice[i*8+4], amounts_slice[i*8+5], amounts_slice[i*8+6], amounts_slice[i*8+7],
|
||||
])
|
||||
}).collect();
|
||||
let masks_vec: Vec<Scalar> = (0..n).map(|i| {
|
||||
Scalar::from_bytes_mod_order(crate::to32(&masks_slice[i*32..(i+1)*32]))
|
||||
}).collect();
|
||||
catch_ffi(move || {
|
||||
let amounts_vec: Vec<u64> = (0..n).map(|i| {
|
||||
u64::from_le_bytes([
|
||||
amounts_data[i*8], amounts_data[i*8+1], amounts_data[i*8+2], amounts_data[i*8+3],
|
||||
amounts_data[i*8+4], amounts_data[i*8+5], amounts_data[i*8+6], amounts_data[i*8+7],
|
||||
])
|
||||
}).collect();
|
||||
let masks_vec: Vec<Scalar> = (0..n).map(|i| {
|
||||
Scalar::from_bytes_mod_order(crate::to32(&masks_data[i*32..(i+1)*32]))
|
||||
}).collect();
|
||||
|
||||
let proof = crate::bulletproofs_plus::bulletproof_plus_prove(&amounts_vec, &masks_vec);
|
||||
let proof_bytes = crate::bulletproofs_plus::serialize_proof(&proof);
|
||||
let proof = crate::bulletproofs_plus::bulletproof_plus_prove(&amounts_vec, &masks_vec);
|
||||
let proof_bytes = crate::bulletproofs_plus::serialize_proof(&proof);
|
||||
|
||||
// Output: [v_count u32 LE][V_0..V_n 32 bytes each][proof_bytes]
|
||||
let total = 4 + proof.v.len() * 32 + proof_bytes.len();
|
||||
if total > out_max { return -1; }
|
||||
let total = 4 + proof.v.len() * 32 + proof_bytes.len();
|
||||
if total > out_max { return -1; }
|
||||
|
||||
let mut off = 0;
|
||||
let v_count = proof.v.len() as u32;
|
||||
ptr::copy_nonoverlapping(v_count.to_le_bytes().as_ptr(), out.add(off), 4);
|
||||
off += 4;
|
||||
for v in &proof.v {
|
||||
let bytes = v.compress().to_bytes();
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), out.add(off), 32);
|
||||
off += 32;
|
||||
}
|
||||
ptr::copy_nonoverlapping(proof_bytes.as_ptr(), out.add(off), proof_bytes.len());
|
||||
off += proof_bytes.len();
|
||||
let mut off = 0;
|
||||
let v_count = proof.v.len() as u32;
|
||||
ptr::copy_nonoverlapping(v_count.to_le_bytes().as_ptr(), out_ptr.add(off), 4);
|
||||
off += 4;
|
||||
for v in &proof.v {
|
||||
let bytes = v.compress().to_bytes();
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr.add(off), 32);
|
||||
off += 32;
|
||||
}
|
||||
ptr::copy_nonoverlapping(proof_bytes.as_ptr(), out_ptr.add(off), proof_bytes.len());
|
||||
off += proof_bytes.len();
|
||||
|
||||
*out_len = off;
|
||||
0
|
||||
*out_len_ptr = off;
|
||||
0
|
||||
})
|
||||
}
|
||||
|
||||
/// Bulletproof+ verify.
|
||||
@@ -675,22 +733,24 @@ pub unsafe extern "C" fn salvium_bulletproof_plus_verify(
|
||||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
|
||||
let n = commitment_count as usize;
|
||||
let proof_data = slice::from_raw_parts(proof_bytes, proof_len);
|
||||
let comms_flat = slice::from_raw_parts(commitments, n * 32);
|
||||
let proof_data = slice::from_raw_parts(proof_bytes, proof_len).to_vec();
|
||||
let comms_data = slice::from_raw_parts(commitments, n * 32).to_vec();
|
||||
|
||||
let v: Vec<_> = (0..n).map(|i| {
|
||||
match CompressedEdwardsY(crate::to32(&comms_flat[i*32..(i+1)*32])).decompress() {
|
||||
catch_ffi(move || {
|
||||
let v: Vec<_> = (0..n).map(|i| {
|
||||
match CompressedEdwardsY(crate::to32(&comms_data[i*32..(i+1)*32])).decompress() {
|
||||
Some(p) => p,
|
||||
None => curve25519_dalek::edwards::EdwardsPoint::default(),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let proof = match crate::bulletproofs_plus::parse_proof(&proof_data) {
|
||||
Some(p) => p,
|
||||
None => return curve25519_dalek::edwards::EdwardsPoint::default(),
|
||||
}
|
||||
}).collect();
|
||||
None => return 0,
|
||||
};
|
||||
|
||||
let proof = match crate::bulletproofs_plus::parse_proof(proof_data) {
|
||||
Some(p) => p,
|
||||
None => return 0,
|
||||
};
|
||||
|
||||
if crate::bulletproofs_plus::bulletproof_plus_verify(&v, &proof) { 1 } else { 0 }
|
||||
if crate::bulletproofs_plus::bulletproof_plus_verify(&v, &proof) { 1 } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -142,7 +142,10 @@ pub fn scalar_mult_base(s: &[u8]) -> Vec<u8> {
|
||||
#[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");
|
||||
let point = match CompressedEdwardsY(to32(p)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
// Use variable-time Straus/wNAF — much faster than constant-time mul
|
||||
EdwardsPoint::vartime_multiscalar_mul(&[scalar], &[point])
|
||||
.compress().to_bytes().to_vec()
|
||||
@@ -150,21 +153,36 @@ pub fn scalar_mult_point(s: &[u8], p: &[u8]) -> Vec<u8> {
|
||||
|
||||
#[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");
|
||||
let pp = match CompressedEdwardsY(to32(p)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let qq = match CompressedEdwardsY(to32(q)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
(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");
|
||||
let pp = match CompressedEdwardsY(to32(p)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let qq = match CompressedEdwardsY(to32(q)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
(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");
|
||||
let pp = match CompressedEdwardsY(to32(p)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
(-pp).compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
@@ -172,7 +190,10 @@ pub fn point_negate(p: &[u8]) -> Vec<u8> {
|
||||
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");
|
||||
let pp = match CompressedEdwardsY(to32(p)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
// Variable-time multi-scalar: a*P + b*G
|
||||
EdwardsPoint::vartime_multiscalar_mul(
|
||||
&[sa, sb],
|
||||
@@ -242,7 +263,10 @@ pub fn generate_key_image(pub_key: &[u8], sec_key: &[u8]) -> Vec<u8> {
|
||||
/// 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 point = match CompressedEdwardsY(to32(pub_key)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let scalar = Scalar::from_bytes_mod_order(to32(sec_key));
|
||||
let shared = scalar * point;
|
||||
// Cofactor multiply by 8
|
||||
@@ -258,7 +282,10 @@ pub fn generate_key_derivation(pub_key: &[u8], sec_key: &[u8]) -> Vec<u8> {
|
||||
#[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 base = match CompressedEdwardsY(to32(base_pub)).decompress() {
|
||||
Some(pt) => pt,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let derived = ED25519_BASEPOINT_TABLE * &scalar + base;
|
||||
derived.compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
/**
|
||||
* Bun FFI Crypto Backend
|
||||
*
|
||||
* Loads the native Rust shared library (libsalvium_crypto.so) via Bun's
|
||||
* built-in FFI and wraps all C functions behind the unified backend interface.
|
||||
*
|
||||
* Requires: Bun runtime with bun:ffi support.
|
||||
*
|
||||
* Library resolution order:
|
||||
* 1. SALVIUM_CRYPTO_LIB env var (absolute path)
|
||||
* 2. crates/salvium-crypto/target/release/libsalvium_crypto.so (relative to project root)
|
||||
*
|
||||
* @module crypto/backend-ffi
|
||||
*/
|
||||
|
||||
import { dlopen, FFIType } from 'bun:ffi';
|
||||
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
||||
|
||||
const { ptr, i32, u32, usize } = FFIType;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function hexToBytes(hex) {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const _hexLUT = new Array(256);
|
||||
for (let i = 0; i < 256; i++) _hexLUT[i] = i.toString(16).padStart(2, '0');
|
||||
function bytesToHex(bytes) {
|
||||
let hex = '';
|
||||
for (let i = 0; i < bytes.length; i++) hex += _hexLUT[bytes[i]];
|
||||
return hex;
|
||||
}
|
||||
|
||||
function ensureBuffer(v) {
|
||||
if (typeof v === 'string') return Buffer.from(hexToBytes(v));
|
||||
if (v instanceof Buffer) return v;
|
||||
return Buffer.from(v);
|
||||
}
|
||||
|
||||
function ensureBytes(v) {
|
||||
if (typeof v === 'string') return hexToBytes(v);
|
||||
if (typeof v === 'bigint') {
|
||||
const bytes = new Uint8Array(32);
|
||||
let n = v;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
bytes[i] = Number(n & 0xffn);
|
||||
n >>= 8n;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function flattenArrayOf32(arr) {
|
||||
const flat = new Uint8Array(arr.length * 32);
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = ensureBytes(arr[i]);
|
||||
flat.set(item, i * 32);
|
||||
}
|
||||
return flat;
|
||||
}
|
||||
|
||||
function serializeAmounts(amounts) {
|
||||
const buf = new Uint8Array(amounts.length * 8);
|
||||
for (let i = 0; i < amounts.length; i++) {
|
||||
let n = BigInt(amounts[i]);
|
||||
for (let j = 0; j < 8; j++) {
|
||||
buf[i * 8 + j] = Number(n & 0xffn);
|
||||
n >>= 8n;
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── Library path resolution ────────────────────────────────────────────────
|
||||
|
||||
function resolveLibPath() {
|
||||
if (process.env.SALVIUM_CRYPTO_LIB) {
|
||||
return process.env.SALVIUM_CRYPTO_LIB;
|
||||
}
|
||||
|
||||
const { fileURLToPath } = require('url');
|
||||
const { dirname, join } = require('path');
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
// Navigate from src/crypto/ up to project root
|
||||
const projectRoot = join(__dirname, '..', '..');
|
||||
return join(projectRoot, 'crates', 'salvium-crypto', 'target', 'release', 'libsalvium_crypto.so');
|
||||
}
|
||||
|
||||
// ─── FFI symbol definitions ─────────────────────────────────────────────────
|
||||
|
||||
const FFI_SYMBOLS = {
|
||||
// Hashing
|
||||
salvium_keccak256: { args: [ptr, usize, ptr], returns: i32 },
|
||||
salvium_blake2b: { args: [ptr, usize, usize, ptr], returns: i32 },
|
||||
salvium_blake2b_keyed: { args: [ptr, usize, usize, ptr, usize, ptr], returns: i32 },
|
||||
salvium_sha256: { args: [ptr, usize, ptr], returns: i32 },
|
||||
|
||||
// Scalars
|
||||
salvium_sc_add: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_sc_sub: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_sc_mul: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_sc_mul_add: { args: [ptr, ptr, ptr, ptr], returns: i32 },
|
||||
salvium_sc_mul_sub: { args: [ptr, ptr, ptr, ptr], returns: i32 },
|
||||
salvium_sc_reduce32: { args: [ptr, ptr], returns: i32 },
|
||||
salvium_sc_reduce64: { args: [ptr, ptr], returns: i32 },
|
||||
salvium_sc_invert: { args: [ptr, ptr], returns: i32 },
|
||||
salvium_sc_check: { args: [ptr], returns: i32 },
|
||||
salvium_sc_is_zero: { args: [ptr], returns: i32 },
|
||||
|
||||
// Points
|
||||
salvium_scalar_mult_base: { args: [ptr, ptr], returns: i32 },
|
||||
salvium_scalar_mult_point: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_point_add: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_point_sub: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_point_negate: { args: [ptr, ptr], returns: i32 },
|
||||
salvium_double_scalar_mult_base: { args: [ptr, ptr, ptr, ptr], returns: i32 },
|
||||
|
||||
// X25519
|
||||
salvium_x25519_scalar_mult: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
|
||||
// Hash-to-point & key derivation
|
||||
salvium_hash_to_point: { args: [ptr, usize, ptr], returns: i32 },
|
||||
salvium_generate_key_derivation: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_generate_key_image: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_derive_public_key: { args: [ptr, u32, ptr, ptr], returns: i32 },
|
||||
salvium_derive_secret_key: { args: [ptr, u32, ptr, ptr], returns: i32 },
|
||||
|
||||
// Pedersen commitments
|
||||
salvium_pedersen_commit: { args: [ptr, ptr, ptr], returns: i32 },
|
||||
salvium_zero_commit: { args: [ptr, ptr], returns: i32 },
|
||||
salvium_gen_commitment_mask: { args: [ptr, ptr], returns: i32 },
|
||||
|
||||
// Oracle signature verification
|
||||
salvium_verify_signature: { args: [ptr, usize, ptr, usize, ptr, usize], returns: i32 },
|
||||
|
||||
// Key derivation
|
||||
salvium_argon2id: { args: [ptr, usize, ptr, usize, u32, u32, u32, usize, ptr], returns: i32 },
|
||||
|
||||
// CLSAG
|
||||
salvium_clsag_sign: { args: [ptr, ptr, u32, ptr, ptr, ptr, ptr, u32, ptr], returns: i32 },
|
||||
salvium_clsag_verify: { args: [ptr, ptr, usize, ptr, u32, ptr, ptr], returns: i32 },
|
||||
|
||||
// TCLSAG
|
||||
salvium_tclsag_sign: { args: [ptr, ptr, u32, ptr, ptr, ptr, ptr, ptr, u32, ptr], returns: i32 },
|
||||
salvium_tclsag_verify: { args: [ptr, ptr, usize, ptr, u32, ptr, ptr], returns: i32 },
|
||||
|
||||
// BP+
|
||||
salvium_bulletproof_plus_prove: { args: [ptr, ptr, u32, ptr, usize, ptr], returns: i32 },
|
||||
salvium_bulletproof_plus_verify: { args: [ptr, usize, ptr, u32], returns: i32 },
|
||||
};
|
||||
|
||||
// ─── Backend class ──────────────────────────────────────────────────────────
|
||||
|
||||
export class FfiCryptoBackend {
|
||||
constructor() {
|
||||
this.name = 'ffi';
|
||||
this.lib = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const libPath = resolveLibPath();
|
||||
this.lib = dlopen(libPath, FFI_SYMBOLS);
|
||||
}
|
||||
|
||||
// ─── Hashing ──────────────────────────────────────────────────────────
|
||||
|
||||
keccak256(data) {
|
||||
const input = ensureBuffer(data);
|
||||
const out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_keccak256(input, input.length, out);
|
||||
if (rc !== 0) throw new Error('keccak256 failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
blake2b(data, outLen, key) {
|
||||
const input = ensureBuffer(data);
|
||||
const out = Buffer.alloc(outLen);
|
||||
let rc;
|
||||
if (key) {
|
||||
const keyBuf = ensureBuffer(key);
|
||||
rc = this.lib.symbols.salvium_blake2b_keyed(input, input.length, outLen, keyBuf, keyBuf.length, out);
|
||||
} else {
|
||||
rc = this.lib.symbols.salvium_blake2b(input, input.length, outLen, out);
|
||||
}
|
||||
if (rc !== 0) throw new Error('blake2b failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
sha256(data) {
|
||||
return nobleSha256(data);
|
||||
}
|
||||
|
||||
// ─── Scalar operations ────────────────────────────────────────────────
|
||||
|
||||
scAdd(a, b) {
|
||||
const ba = ensureBuffer(a), bb = ensureBuffer(b), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_add(ba, bb, out);
|
||||
if (rc !== 0) throw new Error('sc_add failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scSub(a, b) {
|
||||
const ba = ensureBuffer(a), bb = ensureBuffer(b), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_sub(ba, bb, out);
|
||||
if (rc !== 0) throw new Error('sc_sub failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scMul(a, b) {
|
||||
const ba = ensureBuffer(a), bb = ensureBuffer(b), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_mul(ba, bb, out);
|
||||
if (rc !== 0) throw new Error('sc_mul failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scMulAdd(a, b, c) {
|
||||
const ba = ensureBuffer(a), bb = ensureBuffer(b), bc = ensureBuffer(c), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_mul_add(ba, bb, bc, out);
|
||||
if (rc !== 0) throw new Error('sc_mul_add failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scMulSub(a, b, c) {
|
||||
const ba = ensureBuffer(a), bb = ensureBuffer(b), bc = ensureBuffer(c), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_mul_sub(ba, bb, bc, out);
|
||||
if (rc !== 0) throw new Error('sc_mul_sub failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scReduce32(s) {
|
||||
const bs = ensureBuffer(s), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_reduce32(bs, out);
|
||||
if (rc !== 0) throw new Error('sc_reduce32 failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scReduce64(s) {
|
||||
const bs = ensureBuffer(s), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_reduce64(bs, out);
|
||||
if (rc !== 0) throw new Error('sc_reduce64 failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scInvert(a) {
|
||||
const ba = ensureBuffer(a), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_sc_invert(ba, out);
|
||||
if (rc !== 0) throw new Error('sc_invert failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scCheck(s) {
|
||||
const bs = ensureBuffer(s);
|
||||
return this.lib.symbols.salvium_sc_check(bs) === 1;
|
||||
}
|
||||
|
||||
scIsZero(s) {
|
||||
const bs = ensureBuffer(s);
|
||||
return this.lib.symbols.salvium_sc_is_zero(bs) === 1;
|
||||
}
|
||||
|
||||
// ─── Point operations ─────────────────────────────────────────────────
|
||||
|
||||
scalarMultBase(s) {
|
||||
const bs = ensureBuffer(s), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_scalar_mult_base(bs, out);
|
||||
if (rc !== 0) throw new Error('scalar_mult_base failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
scalarMultPoint(s, p) {
|
||||
const bs = ensureBuffer(s), bp = ensureBuffer(p), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_scalar_mult_point(bs, bp, out);
|
||||
if (rc !== 0) return null; // invalid point
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
pointAddCompressed(p, q) {
|
||||
const bp = ensureBuffer(p), bq = ensureBuffer(q), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_point_add(bp, bq, out);
|
||||
if (rc !== 0) return null; // invalid point
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
pointSubCompressed(p, q) {
|
||||
const bp = ensureBuffer(p), bq = ensureBuffer(q), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_point_sub(bp, bq, out);
|
||||
if (rc !== 0) return null; // invalid point
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
pointNegate(p) {
|
||||
const bp = ensureBuffer(p), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_point_negate(bp, out);
|
||||
if (rc !== 0) return null; // invalid point
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
doubleScalarMultBase(a, p, b) {
|
||||
const ba = ensureBuffer(a), bp = ensureBuffer(p), bb = ensureBuffer(b), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_double_scalar_mult_base(ba, bp, bb, out);
|
||||
if (rc !== 0) return null; // invalid point
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
// ─── X25519 ───────────────────────────────────────────────────────────
|
||||
|
||||
x25519ScalarMult(scalar, uCoord) {
|
||||
const bs = ensureBuffer(scalar), bu = ensureBuffer(uCoord), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_x25519_scalar_mult(bs, bu, out);
|
||||
if (rc !== 0) throw new Error('x25519_scalar_mult failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
// ─── Hash-to-point & key derivation ───────────────────────────────────
|
||||
|
||||
hashToPoint(data) {
|
||||
const input = ensureBuffer(data);
|
||||
const out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_hash_to_point(input, input.length, out);
|
||||
if (rc !== 0) throw new Error('hash_to_point failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
generateKeyDerivation(pubKey, secKey) {
|
||||
if (typeof pubKey === 'string') pubKey = hexToBytes(pubKey);
|
||||
if (typeof secKey === 'string') secKey = hexToBytes(secKey);
|
||||
const bPub = ensureBuffer(pubKey), bSec = ensureBuffer(secKey), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_generate_key_derivation(bPub, bSec, out);
|
||||
if (rc !== 0) return null; // invalid pub key
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
generateKeyImage(pubKey, secKey) {
|
||||
if (typeof pubKey === 'string') pubKey = hexToBytes(pubKey);
|
||||
if (typeof secKey === 'string') secKey = hexToBytes(secKey);
|
||||
const bPub = ensureBuffer(pubKey), bSec = ensureBuffer(secKey), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_generate_key_image(bPub, bSec, out);
|
||||
if (rc !== 0) throw new Error('generate_key_image failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
derivePublicKey(derivation, outputIndex, basePub) {
|
||||
const bDeriv = ensureBuffer(derivation), bBase = ensureBuffer(basePub), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_derive_public_key(bDeriv, outputIndex, bBase, out);
|
||||
if (rc !== 0) return null; // invalid base pub key
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
deriveSecretKey(derivation, outputIndex, baseSec) {
|
||||
const bDeriv = ensureBuffer(derivation), bBase = ensureBuffer(baseSec), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_derive_secret_key(bDeriv, outputIndex, bBase, out);
|
||||
if (rc !== 0) throw new Error('derive_secret_key failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
// ─── Pedersen commitments ─────────────────────────────────────────────
|
||||
|
||||
commit(amount, mask) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (typeof mask === 'string') {
|
||||
mask = hexToBytes(mask);
|
||||
}
|
||||
const bAmt = ensureBuffer(amountBytes), bMask = ensureBuffer(mask), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_pedersen_commit(bAmt, bMask, out);
|
||||
if (rc !== 0) throw new Error('pedersen_commit failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
zeroCommit(amount) {
|
||||
// Salvium rct::zeroCommit uses blinding factor = 1 (not 0).
|
||||
// Match the WASM backend behavior.
|
||||
const scalarOne = new Uint8Array(32);
|
||||
scalarOne[0] = 1;
|
||||
return this.commit(amount, scalarOne);
|
||||
}
|
||||
|
||||
genCommitmentMask(sharedSecret) {
|
||||
if (typeof sharedSecret === 'string') {
|
||||
sharedSecret = hexToBytes(sharedSecret);
|
||||
}
|
||||
const bSecret = ensureBuffer(sharedSecret), out = Buffer.alloc(32);
|
||||
const rc = this.lib.symbols.salvium_gen_commitment_mask(bSecret, out);
|
||||
if (rc !== 0) throw new Error('gen_commitment_mask failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
// ─── Oracle signature verification ────────────────────────────────────
|
||||
|
||||
async verifySignature(message, signature, pubkeyDer) {
|
||||
const bMsg = ensureBuffer(message);
|
||||
const bSig = ensureBuffer(signature);
|
||||
const bKey = ensureBuffer(pubkeyDer);
|
||||
const rc = this.lib.symbols.salvium_verify_signature(
|
||||
bMsg, bMsg.length, bSig, bSig.length, bKey, bKey.length
|
||||
);
|
||||
return rc === 1;
|
||||
}
|
||||
|
||||
// ─── Key derivation ───────────────────────────────────────────────────
|
||||
|
||||
argon2id(password, salt, opts) {
|
||||
const bPass = ensureBuffer(password);
|
||||
const bSalt = ensureBuffer(salt);
|
||||
const outLen = opts.dkLen || 32;
|
||||
const out = Buffer.alloc(outLen);
|
||||
const rc = this.lib.symbols.salvium_argon2id(
|
||||
bPass, bPass.length,
|
||||
bSalt, bSalt.length,
|
||||
opts.t, opts.m, opts.p,
|
||||
outLen, out
|
||||
);
|
||||
if (rc !== 0) throw new Error('argon2id failed');
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
// ─── CLSAG Ring Signatures ────────────────────────────────────────────
|
||||
|
||||
clsagSign(message, ring, secretKey, commitments, commitmentMask, pseudoOutput, secretIndex) {
|
||||
const bMsg = ensureBuffer(message);
|
||||
const ringFlat = ensureBuffer(flattenArrayOf32(ring));
|
||||
const bSk = ensureBuffer(secretKey);
|
||||
const commFlat = ensureBuffer(flattenArrayOf32(commitments));
|
||||
const bCm = ensureBuffer(commitmentMask);
|
||||
const bPo = ensureBuffer(pseudoOutput);
|
||||
const n = ring.length;
|
||||
const outSize = n * 32 + 96;
|
||||
const out = Buffer.alloc(outSize);
|
||||
|
||||
const rc = this.lib.symbols.salvium_clsag_sign(
|
||||
bMsg, ringFlat, n, bSk, commFlat, bCm, bPo, secretIndex, out
|
||||
);
|
||||
if (rc !== 0) throw new Error('clsag_sign failed');
|
||||
|
||||
// Parse: s[0..n], c1, I, D
|
||||
let offset = 0;
|
||||
const s = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
s.push(bytesToHex(out.slice(offset, offset + 32)));
|
||||
offset += 32;
|
||||
}
|
||||
const c1 = bytesToHex(out.slice(offset, offset + 32)); offset += 32;
|
||||
const I = bytesToHex(out.slice(offset, offset + 32)); offset += 32;
|
||||
const D = bytesToHex(out.slice(offset, offset + 32));
|
||||
return { s, c1, I, D };
|
||||
}
|
||||
|
||||
clsagVerify(message, sig, ring, commitments, pseudoOutput) {
|
||||
const bMsg = ensureBuffer(message);
|
||||
const n = ring.length;
|
||||
const ringFlat = ensureBuffer(flattenArrayOf32(ring));
|
||||
const commFlat = ensureBuffer(flattenArrayOf32(commitments));
|
||||
const bPo = ensureBuffer(pseudoOutput);
|
||||
|
||||
// Serialize sig: s[0..n], c1, I, D (no length prefix for FFI)
|
||||
const sigSize = n * 32 + 96;
|
||||
const sigBuf = Buffer.alloc(sigSize);
|
||||
let offset = 0;
|
||||
for (const si of sig.s) {
|
||||
sigBuf.set(ensureBytes(si), offset);
|
||||
offset += 32;
|
||||
}
|
||||
sigBuf.set(ensureBytes(sig.c1), offset); offset += 32;
|
||||
sigBuf.set(ensureBytes(sig.I), offset); offset += 32;
|
||||
sigBuf.set(ensureBytes(sig.D), offset);
|
||||
|
||||
const rc = this.lib.symbols.salvium_clsag_verify(
|
||||
bMsg, sigBuf, sigSize, ringFlat, n, commFlat, bPo
|
||||
);
|
||||
return rc === 1;
|
||||
}
|
||||
|
||||
// ─── TCLSAG Ring Signatures ───────────────────────────────────────────
|
||||
|
||||
tclsagSign(message, ring, secretKeyX, secretKeyY, commitments, commitmentMask, pseudoOutput, secretIndex) {
|
||||
const bMsg = ensureBuffer(message);
|
||||
const ringFlat = ensureBuffer(flattenArrayOf32(ring));
|
||||
const bSkx = ensureBuffer(secretKeyX);
|
||||
const bSky = ensureBuffer(secretKeyY);
|
||||
const commFlat = ensureBuffer(flattenArrayOf32(commitments));
|
||||
const bCm = ensureBuffer(commitmentMask);
|
||||
const bPo = ensureBuffer(pseudoOutput);
|
||||
const n = ring.length;
|
||||
const outSize = 2 * n * 32 + 96;
|
||||
const out = Buffer.alloc(outSize);
|
||||
|
||||
const rc = this.lib.symbols.salvium_tclsag_sign(
|
||||
bMsg, ringFlat, n, bSkx, bSky, commFlat, bCm, bPo, secretIndex, out
|
||||
);
|
||||
if (rc !== 0) throw new Error('tclsag_sign failed');
|
||||
|
||||
// Parse: sx[0..n], sy[0..n], c1, I, D
|
||||
let offset = 0;
|
||||
const sx = [];
|
||||
for (let i = 0; i < n; i++) { sx.push(bytesToHex(out.slice(offset, offset + 32))); offset += 32; }
|
||||
const sy = [];
|
||||
for (let i = 0; i < n; i++) { sy.push(bytesToHex(out.slice(offset, offset + 32))); offset += 32; }
|
||||
const c1 = bytesToHex(out.slice(offset, offset + 32)); offset += 32;
|
||||
const I = bytesToHex(out.slice(offset, offset + 32)); offset += 32;
|
||||
const D = bytesToHex(out.slice(offset, offset + 32));
|
||||
return { sx, sy, c1, I, D };
|
||||
}
|
||||
|
||||
tclsagVerify(message, sig, ring, commitments, pseudoOutput) {
|
||||
const bMsg = ensureBuffer(message);
|
||||
const n = ring.length;
|
||||
const ringFlat = ensureBuffer(flattenArrayOf32(ring));
|
||||
const commFlat = ensureBuffer(flattenArrayOf32(commitments));
|
||||
const bPo = ensureBuffer(pseudoOutput);
|
||||
|
||||
// Serialize sig: sx[0..n], sy[0..n], c1, I, D
|
||||
const sigSize = 2 * n * 32 + 96;
|
||||
const sigBuf = Buffer.alloc(sigSize);
|
||||
let offset = 0;
|
||||
for (const s of sig.sx) { sigBuf.set(ensureBytes(s), offset); offset += 32; }
|
||||
for (const s of sig.sy) { sigBuf.set(ensureBytes(s), offset); offset += 32; }
|
||||
sigBuf.set(ensureBytes(sig.c1), offset); offset += 32;
|
||||
sigBuf.set(ensureBytes(sig.I), offset); offset += 32;
|
||||
sigBuf.set(ensureBytes(sig.D), offset);
|
||||
|
||||
const rc = this.lib.symbols.salvium_tclsag_verify(
|
||||
bMsg, sigBuf, sigSize, ringFlat, n, commFlat, bPo
|
||||
);
|
||||
return rc === 1;
|
||||
}
|
||||
|
||||
// ─── Bulletproofs+ Range Proofs ───────────────────────────────────────
|
||||
|
||||
bulletproofPlusProve(amounts, masks) {
|
||||
const amountBytes = ensureBuffer(serializeAmounts(amounts));
|
||||
const masksFlat = ensureBuffer(flattenArrayOf32(masks));
|
||||
const count = amounts.length;
|
||||
const outMax = 8192; // generous upper bound
|
||||
const out = Buffer.alloc(outMax);
|
||||
const outLenBuf = Buffer.alloc(8); // size_t = 8 bytes on 64-bit
|
||||
|
||||
const rc = this.lib.symbols.salvium_bulletproof_plus_prove(
|
||||
amountBytes, masksFlat, count, out, outMax, outLenBuf
|
||||
);
|
||||
if (rc !== 0) throw new Error('bulletproof_plus_prove failed');
|
||||
|
||||
// Read actual output length (native size_t, 8 bytes LE on 64-bit)
|
||||
const actualLen = Number(outLenBuf.readBigUInt64LE(0));
|
||||
const result = new Uint8Array(out.slice(0, actualLen));
|
||||
|
||||
// Parse: [v_count u32 LE][V_0..V_n 32B each][proof_bytes]
|
||||
const vCount = result[0] | (result[1] << 8) | (result[2] << 16) | (result[3] << 24);
|
||||
let off = 4;
|
||||
const V = [];
|
||||
for (let i = 0; i < vCount; i++) {
|
||||
V.push(result.slice(off, off + 32));
|
||||
off += 32;
|
||||
}
|
||||
const proofBytes = result.slice(off);
|
||||
return { V, proofBytes };
|
||||
}
|
||||
|
||||
bulletproofPlusVerify(commitmentBytes, proofBytes) {
|
||||
const commFlat = ensureBuffer(flattenArrayOf32(commitmentBytes));
|
||||
const bProof = ensureBuffer(proofBytes);
|
||||
const rc = this.lib.symbols.salvium_bulletproof_plus_verify(
|
||||
bProof, bProof.length, commFlat, commitmentBytes.length
|
||||
);
|
||||
return rc === 1;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
||||
|
||||
let wasmExports = null;
|
||||
|
||||
/** Convert empty Uint8Array (Rust returned Vec::new() for invalid point) to null */
|
||||
function nullIfEmpty(result) {
|
||||
return (result && result.length > 0) ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if running in a browser environment
|
||||
*/
|
||||
@@ -95,13 +100,13 @@ export class WasmCryptoBackend {
|
||||
scCheck(s) { return this.wasm.sc_check(s); }
|
||||
scIsZero(s) { return this.wasm.sc_is_zero(s); }
|
||||
|
||||
// Point ops
|
||||
// Point ops — return null on invalid points (empty vec from Rust)
|
||||
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); }
|
||||
scalarMultPoint(s, p) { return nullIfEmpty(this.wasm.scalar_mult_point(s, p)); }
|
||||
pointAddCompressed(p, q) { return nullIfEmpty(this.wasm.point_add_compressed(p, q)); }
|
||||
pointSubCompressed(p, q) { return nullIfEmpty(this.wasm.point_sub_compressed(p, q)); }
|
||||
pointNegate(p) { return nullIfEmpty(this.wasm.point_negate(p)); }
|
||||
doubleScalarMultBase(a, p, b) { return nullIfEmpty(this.wasm.double_scalar_mult_base(a, p, b)); }
|
||||
|
||||
// Hash-to-point & key derivation
|
||||
hashToPoint(data) { return this.wasm.hash_to_point(data); }
|
||||
@@ -123,9 +128,9 @@ export class WasmCryptoBackend {
|
||||
if (typeof secKey === 'string') {
|
||||
secKey = new Uint8Array(secKey.match(/.{1,2}/g).map(b => parseInt(b, 16)));
|
||||
}
|
||||
return this.wasm.generate_key_derivation(pubKey, secKey);
|
||||
return nullIfEmpty(this.wasm.generate_key_derivation(pubKey, secKey));
|
||||
}
|
||||
derivePublicKey(derivation, outputIndex, basePub) { return this.wasm.derive_public_key(derivation, outputIndex, basePub); }
|
||||
derivePublicKey(derivation, outputIndex, basePub) { return nullIfEmpty(this.wasm.derive_public_key(derivation, outputIndex, basePub)); }
|
||||
deriveSecretKey(derivation, outputIndex, baseSec) { return this.wasm.derive_secret_key(derivation, outputIndex, baseSec); }
|
||||
|
||||
// Pedersen commitments
|
||||
|
||||
@@ -21,7 +21,7 @@ let backendType = 'js';
|
||||
|
||||
/**
|
||||
* Set the active crypto backend
|
||||
* @param {'js'|'wasm'|'jsi'} type - Backend type
|
||||
* @param {'js'|'wasm'|'ffi'|'jsi'} type - Backend type
|
||||
*/
|
||||
export async function setCryptoBackend(type) {
|
||||
if (type === 'js') {
|
||||
@@ -31,6 +31,10 @@ export async function setCryptoBackend(type) {
|
||||
const { JsiCryptoBackend } = await import('./backend-jsi.js');
|
||||
currentBackend = new JsiCryptoBackend();
|
||||
await currentBackend.init();
|
||||
} else if (type === 'ffi') {
|
||||
const { FfiCryptoBackend } = await import('./backend-ffi.js');
|
||||
currentBackend = new FfiCryptoBackend();
|
||||
await currentBackend.init();
|
||||
} else if (type === 'wasm') {
|
||||
// Dynamic import() works in Node/Bun but not React Native/Hermes.
|
||||
// On Hermes, use 'jsi' backend instead.
|
||||
@@ -45,7 +49,7 @@ export async function setCryptoBackend(type) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown crypto backend: ${type}. Use 'js', 'wasm', or 'jsi'.`);
|
||||
throw new Error(`Unknown crypto backend: ${type}. Use 'js', 'wasm', 'ffi', or 'jsi'.`);
|
||||
}
|
||||
backendType = type;
|
||||
}
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# Ignore WASM build intermediates, keep distributable files
|
||||
package.json
|
||||
*.wasm.d.ts
|
||||
*
|
||||
Vendored
+13
@@ -115,6 +115,18 @@ export function tclsag_sign_wasm(message: Uint8Array, ring_flat: Uint8Array, sec
|
||||
|
||||
export function tclsag_verify_wasm(message: Uint8Array, sig_bytes: Uint8Array, ring_flat: Uint8Array, commitments_flat: Uint8Array, pseudo_output: Uint8Array): boolean;
|
||||
|
||||
/**
|
||||
* X25519 scalar multiplication with Salvium's non-standard clamping.
|
||||
*
|
||||
* Salvium clamping only clears bit 255 (scalar[31] &= 0x7F).
|
||||
* Unlike RFC 7748, bits 0-2 are NOT cleared and bit 254 is NOT set.
|
||||
*
|
||||
* Uses a Montgomery ladder on Curve25519 (a24 = 121666, p = 2^255 - 19).
|
||||
* scalar and u_coord must each be 32 bytes (little-endian).
|
||||
* Returns the 32-byte u-coordinate of the result point.
|
||||
*/
|
||||
export function x25519_scalar_mult(scalar: Uint8Array, u_coord: Uint8Array): Uint8Array;
|
||||
|
||||
/**
|
||||
* Zero commitment: C = 1*G + amount*H (blinding factor = 1)
|
||||
* Matches C++ rct::zeroCommit() used for coinbase outputs and fee commitments.
|
||||
@@ -159,6 +171,7 @@ export interface InitOutput {
|
||||
readonly sha256: (a: number, b: number) => [number, number];
|
||||
readonly tclsag_sign_wasm: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number) => [number, number];
|
||||
readonly tclsag_verify_wasm: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => number;
|
||||
readonly x25519_scalar_mult: (a: number, b: number, c: number, d: number) => [number, number];
|
||||
readonly zero_commit: (a: number, b: number) => [number, number];
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
|
||||
@@ -584,6 +584,30 @@ export function tclsag_verify_wasm(message, sig_bytes, ring_flat, commitments_fl
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* X25519 scalar multiplication with Salvium's non-standard clamping.
|
||||
*
|
||||
* Salvium clamping only clears bit 255 (scalar[31] &= 0x7F).
|
||||
* Unlike RFC 7748, bits 0-2 are NOT cleared and bit 254 is NOT set.
|
||||
*
|
||||
* Uses a Montgomery ladder on Curve25519 (a24 = 121666, p = 2^255 - 19).
|
||||
* scalar and u_coord must each be 32 bytes (little-endian).
|
||||
* Returns the 32-byte u-coordinate of the result point.
|
||||
* @param {Uint8Array} scalar
|
||||
* @param {Uint8Array} u_coord
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function x25519_scalar_mult(scalar, u_coord) {
|
||||
const ptr0 = passArray8ToWasm0(scalar, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passArray8ToWasm0(u_coord, wasm.__wbindgen_malloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.x25519_scalar_mult(ptr0, len0, ptr1, len1);
|
||||
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero commitment: C = 1*G + amount*H (blinding factor = 1)
|
||||
* Matches C++ rct::zeroCommit() used for coinbase outputs and fee commitments.
|
||||
|
||||
Binary file not shown.
@@ -19,6 +19,7 @@
|
||||
* START_HEIGHT - Block height to start sync from (default: 0)
|
||||
* MAX_BLOCKS - Maximum blocks to sync (default: sync to chain tip)
|
||||
* EXPECTED_BALANCE - Expected minimum balance to verify (optional)
|
||||
* CRYPTO_BACKEND - Crypto backend: 'wasm' (default) or 'js'
|
||||
*/
|
||||
|
||||
import { createDaemonRPC } from '../src/rpc/index.js';
|
||||
@@ -29,6 +30,7 @@ import { NETWORK, ADDRESS_FORMAT, ADDRESS_TYPE } from '../src/constants.js';
|
||||
import { MemoryStorage } from '../src/wallet-store.js';
|
||||
import { WalletSync, SYNC_STATUS } from '../src/wallet-sync.js';
|
||||
import { cnSubaddress, generateCNSubaddressMap, generateCarrotSubaddressMap, SUBADDRESS_LOOKAHEAD_MAJOR, SUBADDRESS_LOOKAHEAD_MINOR } from '../src/subaddress.js';
|
||||
import { initCrypto, getCryptoBackend, getCurrentBackendType, setCryptoBackend } from '../src/crypto/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
@@ -117,6 +119,15 @@ async function runIntegrationTest() {
|
||||
console.log('║ Salvium Wallet Sync Integration Test ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
// Initialize crypto backend (WASM by default, CRYPTO_BACKEND=js|ffi to override)
|
||||
const requestedBackend = process.env.CRYPTO_BACKEND || 'wasm';
|
||||
if (requestedBackend === 'ffi') {
|
||||
await setCryptoBackend('ffi');
|
||||
} else if (requestedBackend === 'wasm') {
|
||||
await initCrypto(); // Loads WASM, falls back to JS
|
||||
}
|
||||
console.log(`Crypto backend: ${getCurrentBackendType()}\n`);
|
||||
|
||||
// Get keys
|
||||
const keys = getWalletKeys();
|
||||
if (!keys) {
|
||||
|
||||
Reference in New Issue
Block a user