WASM
Voltaire compiles to WebAssembly for browser and non-Bun JavaScript runtimes.
Runtime support: In Node.js, use the regular TypeScript API or WASM. Native FFI is currently Bun-only.
Build Modes
ReleaseSmall (Default)
Size-optimized for production bundles:
- Output:
wasm/primitives.wasm (~385KB)
- Optimized for bundle size
- Suitable for browser deployment
ReleaseFast
Performance-optimized for benchmarking:
zig build build-ts-wasm-fast
- Output:
wasm/primitives-fast.wasm (~500KB)
- Maximum performance
- Use for performance-critical applications
Individual Crypto Modules
Tree-shakeable individual modules:
Output in wasm/crypto/:
keccak256.wasm (~50KB)
secp256k1.wasm (~80KB)
blake2.wasm (~40KB)
ripemd160.wasm (~30KB)
bn254.wasm (~100KB)
WASM Loader
The loader handles instantiation, memory management, and error translation.
Location
src/wasm-loader/
├── loader.ts # Main loader
├── memory.ts # Memory management
├── errors.ts # Error translation
└── types.ts # TypeScript types
Usage
import { loadWasm, getWasm } from "@voltaire/wasm-loader";
// Initialize (async, once at startup)
await loadWasm();
// Get WASM instance (sync, after initialization)
const wasm = getWasm();
// Use WASM functions
const hash = wasm.keccak256(data);
Automatic Loading
Most functions auto-load WASM on first use:
import * as Keccak256 from "@voltaire/crypto/Keccak256";
// First call loads WASM automatically
const hash = await Keccak256.hash("hello");
// Subsequent calls are sync
const hash2 = Keccak256.hash("world");
Memory Management
How It Works
- TypeScript passes data to WASM memory
- WASM processes in its linear memory
- Results copied back to JavaScript
// Internal flow (simplified)
function wasmHash(data: Uint8Array): Uint8Array {
// Allocate WASM memory
const inputPtr = wasm.alloc(data.length);
const outputPtr = wasm.alloc(32);
// Copy input to WASM
new Uint8Array(wasm.memory.buffer, inputPtr, data.length).set(data);
// Call WASM function
wasm.keccak256(inputPtr, data.length, outputPtr);
// Copy result from WASM
const result = new Uint8Array(32);
result.set(new Uint8Array(wasm.memory.buffer, outputPtr, 32));
// Free WASM memory
wasm.free(inputPtr);
wasm.free(outputPtr);
return result;
}
Memory Limits
Default WASM memory: 256 pages (16MB)
For large operations, memory grows automatically:
// Handles large blobs transparently
const bigData = new Uint8Array(10_000_000);
const hash = Keccak256.hash(bigData); // Memory grows as needed
The library automatically selects the best implementation:
// Internal detection
function getImplementation() {
if (typeof Bun !== "undefined") {
return "native"; // Bun FFI
}
if (typeof window !== "undefined" || typeof self !== "undefined") {
return "wasm"; // Browser
}
if (typeof process !== "undefined") {
return "wasm"; // Node.js
}
return "wasm"; // Fallback
}
Forcing WASM
import * as Keccak256 from "@voltaire/crypto/Keccak256/wasm";
// Always uses WASM, even in Bun
const hash = Keccak256.hash("hello");
Limitations
KZG Not Supported
KZG operations require the trusted setup and are too large for WASM:
import * as Kzg from "@voltaire/crypto/Kzg";
// In WASM environment
try {
const commitment = Kzg.blobToCommitment(blob);
} catch (e) {
// Error: KZG not supported in WASM
}
For KZG in browsers, use a server-side proxy or the c-kzg-4844 JS library.
No Assembly Optimization
WASM can’t use platform-specific assembly. Rust crypto uses portable feature:
# Cargo.toml
[features]
default = ["asm"] # Native: keccak-asm
portable = ["tiny-keccak"] # WASM: pure Rust
Performance difference:
- Native (asm): ~500ns per Keccak256
- WASM: ~2μs per Keccak256
Browser Integration
Bundler Setup
Vite
// vite.config.ts
export default {
optimizeDeps: {
exclude: ["@voltaire/primitives"]
},
build: {
target: "esnext"
}
};
Webpack
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true
},
module: {
rules: [
{
test: /\.wasm$/,
type: "webassembly/async"
}
]
}
};
CDN Usage
<script type="module">
import { loadWasm } from "https://esm.sh/@voltaire/primitives";
import * as Address from "https://esm.sh/@voltaire/primitives/Address";
await loadWasm();
const addr = Address.fromHex("0x742d35Cc6634C0532925a3b844Bc9e7595f251e3");
console.log(Address.toChecksummed(addr));
</script>
Testing WASM
Separate Test Files
// Keccak256.wasm.test.ts
import { describe, it, expect, beforeAll } from "vitest";
import { loadWasm } from "../wasm-loader/loader.js";
import * as Keccak256 from "./Keccak256.wasm.js";
describe("Keccak256 WASM", () => {
beforeAll(async () => {
await loadWasm();
});
it("hashes correctly", () => {
const result = Keccak256.hash("hello");
expect(result.length).toBe(32);
});
});
Running WASM Tests
# All WASM tests
bun run test:wasm
# Specific module
bun run test:wasm -- Keccak256
Cross-Validation
describe("WASM matches native", () => {
it("produces identical results", () => {
const data = "test data";
const wasmResult = Keccak256Wasm.hash(data);
const nativeResult = Keccak256Native.hash(data);
expect(wasmResult).toEqual(nativeResult);
});
});
Bundle Size Analysis
# Analyze bundle sizes
bun run size
Output in BUNDLE-SIZES.md:
| Module | Size | Gzipped |
|--------|------|---------|
| primitives.wasm | 385KB | 120KB |
| crypto/keccak256.wasm | 50KB | 18KB |
| crypto/secp256k1.wasm | 80KB | 28KB |
# Compare WASM modes
bun run scripts/compare-wasm-modes.ts
Typical results:
| Operation | ReleaseSmall | ReleaseFast | Native |
|---|
| Keccak256 | 2.1μs | 1.8μs | 0.5μs |
| secp256k1 sign | 150μs | 120μs | 50μs |
| Address checksum | 1.5μs | 1.2μs | 0.3μs |
Troubleshooting
WASM Not Loading
// Check if WASM is available
import { isWasmLoaded, loadWasm } from "@voltaire/wasm-loader";
if (!isWasmLoaded()) {
try {
await loadWasm();
} catch (e) {
console.error("WASM failed to load:", e);
// Fallback to JS implementation
}
}
Memory Errors
// For very large operations
import { setMemoryLimit } from "@voltaire/wasm-loader";
// Increase to 64MB (4096 pages)
setMemoryLimit(4096);
// Now process large data
const hugeBlob = new Uint8Array(50_000_000);
const hash = Keccak256.hash(hugeBlob);
Build Issues
# Verify WASI support
zig targets | grep wasm32-wasi
# Clean rebuild
zig build clean
zig build build-ts-wasm
# Check output
ls -la wasm/