The JSI (React Native) crypto backend was missing the x25519ScalarMult
method. The native function existed on __SalviumCrypto (registered in
js_engine.dart) but JsiCryptoBackend never wrapped it.
Impact: carrotEcdhKeyExchange() calls x25519ScalarMult via the crypto
provider. On JSI, this called undefined(), silently failing the ECDH
key exchange. With a bad shared secret, every CARROT output failed the
view tag check and was rejected — resulting in zero balance for any
wallet syncing CARROT-era blocks through the Flutter/QuickJS path.
Also fix integration-sync.test.js unlocked balance calculation:
the manual balance path checked txType === 'miner' (string) but
wallet-sync stores TX_TYPE.MINER = 1 (number), so coinbase outputs
were never identified as coinbase — they used the 10-block unlock
window instead of the correct 60-block coinbase maturity. Now
delegates to WalletOutput.isUnlocked() which handles both forms.
─────────────────────
- Rebuild WASM binary via wasm-pack — adds batch exports that were missing
from stale binary (cn_subaddress_map_batch, carrot_subaddress_map_batch,
derive_carrot_keys_batch, compute_carrot_view_tag, decrypt_carrot_amount,
parse_extra, serialize_tx_extra, compute_tx_prefix_hash, and 8 more)
- Deprecate JS backend scalar/point ops — JS backend now provides hashing
only (keccak256, blake2b, sha256); all EC math requires WASM or FFI
- Add initCrypto() entry point that loads WASM automatically at startup
- Wire batch subaddress, CARROT key derivation, CARROT helpers, and
tx_extra parsing/serialization through all four backends (WASM/FFI/JSI/JS)
- Extend crypto provider with 15 new delegating functions for the
unified backend interface
FFI use-after-free fix (critical)
─────────────────────────────────
- Fix Bun segfault / SIGILL crash when using CRYPTO_BACKEND=ffi:
toArrayBuffer() from bun:ffi returns a zero-copy VIEW of Rust-owned
heap memory. Calling salvium_storage_free_buf() afterwards left the
Uint8Array pointing to freed memory — classic use-after-free.
With small buffers the freed memory wasn't reused (masking the bug);
at 50×200 subaddress scale (~400KB) the allocator reused immediately,
corrupting data and crashing with SIGILL at 1.11GB RSS.
- Fix: .slice() after toArrayBuffer() to copy into JS-owned memory
before freeing the Rust buffer. Applied to all 4 affected call sites:
cnSubaddressMapBatch, carrotSubaddressMapBatch, parseExtra,
serializeTxExtra.
Subaddress batch fallback hardening
────────────────────────────────────
- Log warnings (not silent catch) when batch WASM/FFI calls fail,
so the fallback to per-item JS loop is visible in console output
Rust crate (salvium-crypto)
───────────────────────────
- Add crates/salvium-crypto/src/subaddress.rs — batch CN + CARROT
subaddress map generation with #[wasm_bindgen] + #[no_mangle] FFI
- Add crates/salvium-crypto/src/carrot_keys.rs — batch CARROT key
derivation (full + view-only) from master secret
- Add crates/salvium-crypto/src/tx_format.rs — tx_extra parse/serialize
and tx prefix hash computation
- Extend ffi.rs with 15 new extern "C" entry points for the above
- Extend lib.rs with matching #[wasm_bindgen] entry points
Test suite updates
──────────────────
- Add initCrypto() to all test files that perform key derivation or
EC math (wallet-class, persistent-wallet, integration-sync, keys,
keyimage, subaddress, scanning, address, transaction, etc.)
- Fix integration-sync.test.js: always init WASM unless FFI explicitly
requested (JS backend can't do scalar/point ops post-deprecation)
- Rewrite test/full-testnet.js: replace hardcoded phase functions with
data-driven FORKS[] table covering all 10 hard forks (HF1–HF10),
WASM miner probes at every fork boundary, per-fork TX tests with
era-appropriate asset types and address formats, --resume-from and
--skip-mining CLI flags
README
──────
- Update JS backend description: "Hashing only (keccak, blake2b)"
- Add initCrypto() requirement to Quick Start and all code examples
- Add full-testnet commands to Testing section
The C++ store_carrot_ephemeral_pubkeys_to_extra() omits tag 0x01 and
writes only tag 0x04 when outputs have distinct per-output D_e values.
The JS serializer unconditionally wrote both tags, producing non-standard
extra fields that masked the multi-output scan regression on testnet.
Multi-output CARROT transactions store per-output D_e in additional_pubkeys
(tag 0x04) with no tx_pubkey (tag 0x01), so the single-D_e pre-computation
path was skipped entirely — every output hit the full _scanCarrotOutput path
(~40ms each via FFI). Added per-output X25519 + 3-byte view tag rejection
that restores post-fork sync from ~10 blk/s back to ~900+ blk/s.
Also fixed parseExtra to handle 0xDE (minergate tag) and gracefully skip
unknown tags via varint-length instead of aborting all remaining extra bytes.
WalletSync.rescan() cleared storage but never notified PersistentWallet,
so _balanceCache kept returning pre-rescan balances until new outputs
arrived. Emit storageCleared event immediately after clear() and flush
the cache on receipt.
Route x25519ScalarMult through the crypto provider backend instead of
using a pure-JS BigInt implementation inline in carrot-scanning.js.
Add a full 7-step CARROT scanner in Rust (carrot_scan.rs) with two FFI
entry points (standard X25519 ECDH and self-send paths), so wallet-sync
can execute the entire scan in a single native call when the FFI backend
is active, falling back to the existing JS pipeline otherwise.
The previous commit introduced `const h` for CARROT height detection in
phaseCN, shadowing the existing `let h` used throughout the function.
Changed to `let` so subsequent reassignments compile.
After HF10 legacy CryptoNote addresses cannot be used — CARROT outputs built
with legacy pubkeys are undetectable by the receiver's CARROT scanner because
the CN view key differs from the CARROT viewIncomingKey. transfer() and sweep()
now fail fast with a clear error when a SaLv... address is used post-HF10,
using the network-aware hard fork table (not hardcoded heights).
After removing SAL defaults from the wallet API, four test files were
broken. Add era-aware assetType (SAL pre-HF6, SAL1 post-HF6) to every
transfer, stake, burn, sweep, and getStorageBalance call. Update fmt()
helper to accept an asset label parameter. Add wallet-b.json fallback
in bidirectional-test.js.
encrypt/decrypt to Rust FFI for Dart cache pipeline
Exposes salvium_aes256gcm_encrypt and salvium_aes256gcm_decrypt via the
native shared library. Encrypt generates a random 12-byte nonce
internally; output is nonce||ciphertext||tag (input_len + 28 bytes).
Dart side: gzip → FFI encrypt → disk, disk → FFI decrypt → gunzip.
Unify the two incompatible sync systems so getBalance/getUTXOs read from
_storage (syncWithDaemon path) when available, falling back to legacy
_utxos. Fix Account class calling non-existent wallet methods, add
getTransactions() API, implement CARROT subaddress spending, generate
_dataKey for restored wallets, and bridge WalletSync events to
WalletListener.
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.
Salvium coinbase outputs set unlock_time=60 (bare constant), not
height+60. WalletOutput.isUnlocked() was treating this as "unlock at
block 60" which is always true, causing ~12 SAL overcounting vs C++
wallet. Now checks txType for miner/protocol and applies the 60-block
confirmation window from blockHeight.
recoveredSpendPubkeyHex was block-scoped inside the subaddress else-if
branch but referenced in the return statement, causing a ReferenceError
for outputs sent to the main CARROT address (K_s). The exception was
silently caught, making all main-address mining rewards invisible.
ensureBytes() silently converted BigInt mask scalars to all-zeros via
Uint8Array.set(BigInt), causing Bulletproofs+ proofs to use wrong
commitments. The daemon correctly rejected these transactions. Also parse
WASM proof results into full field objects and fix test expectations for
upper-median, RCT type constants, and Pedersen commitment properties.
wallet-encryption.js was importing argon2id directly from @noble/hashes,
bypassing the crypto provider entirely and always using the slow pure-JS
implementation. Now routes through the provider so WASM (Rust) and JSI
(native) backends are used when available, with Noble as a last-resort
JS fallback that emits a console.warn.
Yield to event loop every 5 blocks (was 20) to prevent UI freeze on
mobile during batch processing.
Preserve WalletSync instance across setDaemon() calls — update daemon
reference instead of destroying sync state, listeners, and progress.
Stop in-progress sync cleanly before switching.
Replace bytesToHex Array.from().map().join() with pre-computed lookup
table across all hot paths (address.js, backend-wasm.js, mining.js,
subaddress.js). Eliminates ~400k intermediate allocations per full sync.
Add binary Uint8Array comparison for main address (0,0) in both CN and
CARROT scanning — skips hex conversion for 99%+ of non-owned outputs.
Add duplicate listener check in WalletSync.on() to prevent callback
accumulation on sync restart.
CRITICAL: Fix RCT type constants (SalviumZero/One/FullProofs) and
checkHash PoW byte order (LE words matching C++ difficulty.cpp).
HIGH: Fix TXOUT_TYPE wire format (0x02/0x03/0x04), replace Math.random()
with CSPRNG in decoy selection and BP+ batch verification.
MEDIUM: Fix CryptoNote median, LWMA difficulty BigInt arithmetic,
transaction hash (3-part H(prefix||rctBase||rctPrunable)), and
fee estimation for Salvium TX types.
LOW: Fix error message in getNetworkConfig, remove dead yMN in BP+.
Verify CARROT D_e is X25519 Montgomery u-coordinate per C++ source
(carrot_enote_types.h, format_utils.cpp) — no conversion needed.
Update README with crypto backends, WASM performance, 25 test suites.
Move native code from native/ to Expo-compatible layout with android/
and ios/ at package root. Add Android JNI bridge (OnLoad.cpp,
ExpoSalviumCryptoModule.java, ExpoSalviumCryptoPackage.java).
Prebuilt binaries now go in prebuilt/ instead of native/lib/.
Test fixes:
- wallet-sync: update stale DEFAULT_BATCH_SIZE assertion (10 -> 100)
- bulletproofs+: fix hashToPoint -> hashToPointMonero rename in test,
fix serializeProof to include V array matching parseProof format
- transaction-builder: expect 2 outputs for exact-amount tx (zero-change
output is always added for privacy)
Replace serial per-block RPC calls with binary bulk fetch via
getBlocksByHeight (2 RPCs per batch regardless of size). Falls
back to parallel JSON fetch with bulk tx retrieval when binary
endpoint unavailable. Adaptive batch sizing tracks ms/block
trend across batches — doubles when fast, halves when slow,
nudges up when stable. Starts at 10 blocks, ramps to 500.
Drop static re-export of WasmCryptoBackend from crypto/index.js
(import.meta breaks React Native/Hermes) and replace dynamic
import() in setCryptoBackend('wasm') with a thrown error pointing
to direct import for Node/Bun environments.
- Share one 256MB RandomX cache across all worker threads via
SharedArrayBuffer (correct light mode — was N×256MB per worker)
- Workers receive shared WebAssembly.Memory + Modules via workerData,
each creates its own VM with private scratchpad
- Delete 11 broken pure JS RandomX VM files (vm.js, vm-jit.js,
vm-wasm.js, aes.js, randomx-worker-js.js, debug/benchmark scripts)
- Remove stale browser field from package.json (pointed to nonexistent file)
- Tested: 3/3 blocks accepted at difficulty 2000 (~5.2 H/s, 2 threads)
Note: WASM-JIT multi-thread scaling is limited by WebAssembly.Module
compilation serialization in V8/JSC (~10 H/s single-thread ceiling).
Use Rust miner for multi-threaded performance.
Remove hand-written JS RandomX VM (vm.js, vm-jit.js, vm-wasm.js, aes.js)
which never produced correct hashes — the vendored l1mey112/randomx.js
WASM-JIT was always the working implementation. Simplifies mining to
two backends: WASM-JIT and Rust.
- Delete 11 files: broken JS VM, its worker, debug/benchmark scripts
- Fix stale message race: add jobId filtering + handler lifecycle cleanup
so late worker responses don't corrupt the next block's submission
- Optimize WASM worker: precomputed target byte-array comparison instead
of per-hash BigInt multiplication, increase chunk size to 500
- Remove require('crypto') Node.js fallback from carrot.js, ed25519.js,
crypto/provider.js (fixes Metro bundler issues)
- Update README and index.js exports
- Fix little-endian difficulty check in randomx-worker.js (was big-endian,
causing all mined blocks to be rejected by daemon)
- Convert synchronous mine loop to async chunked with generation-based
cancellation (eliminates stale nonce race condition between blocks)
- Fix Rust miner light mode: workers now check for new jobs via try_recv
instead of looping through entire u32 nonce space before accepting next job
- Rewrite test/solo-miner.js with js/wasm/rust backends, light/full modes,
proper worker pool management, and Rust child process integration
- Add src/randomx/randomx-worker-js.js for multi-threaded pure JS mining
- Fix transfer/sweep/stake/burn/convert rejecting with invalid_input: outputs
were selected from SAL index space but TX declared SAL1, causing daemon to
resolve ring members in wrong LMDB table (16/16 mismatch → 0/16 match)
- Filter UTXO selection to exact asset type in all 5 spending functions
- Fix zeroCommit to use blinding factor=1 (matching C++ rct::zeroCommit) in
Rust WASM, JS backend, and serialization layer; rebuild WASM binary
- Override CARROT coinbase mask to scalar 1 so ring signatures use correct
blinding factor for mining outputs
- Add wallet dataKey with AES-256-GCM encrypted sync cache at rest
- Add areAssetTypesEquivalent/getSpendableAssetTypes consensus helpers
- Refactor all test files to use Wallet.fromJSON/setDaemon/syncWithDaemon/transfer/sweep
instead of manual MemoryStorage/createWalletSync/markOutputSpent wiring
- Extract shared test helpers (getHeight, waitForHeight, fmt, short, loadWalletFromFile)
into test/test-helpers.js
- Add ML-KEM-768 + Argon2id + AES-256-GCM hybrid post-quantum wallet encryption
(src/wallet-encryption.js, 47 unit tests)
- Fix sweep() not returning spentKeyImages, causing wallet._markSpent() to silently
skip marking spent outputs after sweeps (led to double_spend errors on next sweep)
- Fix megaSweep test helper to always wait for maturity between rounds
- Burn-in tested: 1295 TXs across CN and CARROT eras (1000 CARROT micro transfers,
stakes, burns, sweeps) with 98% success rate (failures all sweep-related pre-fix)
- Add CARROT output creation to buildTransaction for rctType >= 9 (X25519 ECDH,
viewTag, encryptedJanusAnchor)
- Fix scanner to use txPubKey from tx_extra as D_e instead of p_r (mask commitment)
- Fix scReduce32 → scReduce64 in carrot-output.js (WASM sc_reduce32 silently
truncated 64-byte blake2b outputs, producing wrong scalars)
- Thread viewSecretKey through buildConvertTransaction
- Remove 31 debug console.log statements across transaction.js, transfer.js,
wallet-sync.js, and carrot-scanning.js
- Verified on testnet: 16 transfers, stake, burn, and 36-input sweep all accepted
by daemon with correct TCLSAG signatures and RCT sum checks
- CLSAG signing mask: scSub(inputMask, pseudoMask) not reverse
- Wallet-sync: convert spendSecretKey to bytes for key image calc
- Stake: subtract amountBurnt from change amount
- Sweep: limit to 60 inputs to stay under tx weight limit
- Add spentKeyImages tracking to stake function
- Wire up full transaction lifecycle: UTXO selection → input prep → build → serialize → broadcast
- Integrate bulletproofPlusProve() into buildTransaction() (replaces placeholder)
- Fix BP+ serialization to match Salvium binary format (varint-prefixed vectors)
- Fix serializeTransaction() ordering to match Salvium consensus (prefix → RCT base → prunable)
- Create src/wallet/transfer.js with transfer(), sweep(), stake() high-level functions
- Fix getOutputIndexes to use portable storage binary format
- Fix prepareInputs to use getOutputDistribution for decoy selection
- Handle coinbase outputs with null mask (identity scalar + zeroCommit)
- Refactor Wallet class to always derive BOTH legacy CN and CARROT keys from seed
(matching Salvium C++ carrot_and_legacy_account::generate behavior)
- Add getLegacyAddress(), getCarrotAddress(), height-based auto-format in getAddress()
- Bump wallet JSON to v3 with both addresses and full CARROT key set
- Backward-compatible: old wallet files with seed upgrade seamlessly
- Race multiple RPC servers on connect, pick fastest responder
- Automatic failover to next-fastest on network errors
- Re-race when latency degrades beyond 2x baseline
- Ranked failover order maintained after each race
- Hardcoded Salvium seed nodes (seed01-03.salvium.io) per network
- Supports single URL (no overhead), multiple URLs, or network name
- Comma-separated DAEMON_URL env var for multi-server configs
- Backward compatible: existing single-URL usage unchanged
- Add setup.sh (Unix) and setup.bat (Windows) build scripts that detect
platform, check dependencies, and build all components (JS, WASM, miner)
- Add GitHub Actions CI workflow (test on push/PR)
- Add GitHub Actions release workflow that builds static miner binaries
for Linux x86_64/aarch64, macOS Intel/Apple Silicon, and Windows
- Switch miner from OpenSSL to rustls for zero runtime dependencies
- Fix wallet-sync batch fetching (remove broken .bin binary endpoint,
use sequential JSON-RPC get_block calls)
- Add portable storage binary format serializer/deserializer for future
batch RPC support
- Add postBinary() to RPC client for binary endpoint communication
- Update package.json: bun-only runtime, remove node/npm references
Native miner achieves ~360 H/s/thread (vs ~10 H/s in WASM) using
hardware AES-NI/AVX2 via direct RandomX C FFI. Includes multi-threaded
mining with shared 2GB dataset, JSON-lines IPC protocol for parent
process control, and JS-based mining scripts for testnet.
- transaction.test: getFeeMultiplier(0) expects 5n (priority 2/Normal)
matching C++ wallet2.cpp fee algorithm >= 2, not 1n (priority 1)
- transaction-parser.test: Add missing txType and rct_type bytes to
minimal tx test data (Salvium prefix is longer than Monero's)
- transaction-parser.test: Use txnFee (parser's field name) not fee
in summarizeTransaction mock data
- wallet-sync.test: Add getBlocksByHeight stub to MockDaemon so the
existing individual-fetch fallback path is exercised
Rewire signature.js to use provider functions (scCheck, scSub, scReduce32,
doubleScalarMultBase with compressed bytes) instead of ed25519.js internals.
Remove unused pointFromBytes/pointToBytes import from carrot-scanning.js.
No consumer files now import directly from ed25519/keyimage/keccak/blake2b