Skip to main content

WASM

Voltaire compiles to WebAssembly for browser and non-Bun JavaScript runtimes.

Build Modes

ReleaseSmall (Default)

Size-optimized for production bundles:
zig build build-ts-wasm
  • 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:
zig build crypto-wasm
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

  1. TypeScript passes data to WASM memory
  2. WASM processes in its linear memory
  3. 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

Platform Detection

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 |

Performance Comparison

# Compare WASM modes
bun run scripts/compare-wasm-modes.ts
Typical results:
OperationReleaseSmallReleaseFastNative
Keccak2562.1μs1.8μs0.5μs
secp256k1 sign150μs120μs50μs
Address checksum1.5μs1.2μs0.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/