When V8 hits its ceiling — zero-copy Buffer handoff via napi-rs, and WASI sandboxing for deterministic cryptographic operations.
Module 18 — Crossing the Boundary: Rust, N-API, and WASI for Cryptographic Throughput
What this module covers: At 50,000 transactions/second, every transaction requires elliptic curve signature verification (secp256k1 for Ethereum/Bitcoin, ed25519 for Supra/Solana). V8's JavaScript implementation of these algorithms cannot reach the throughput that a Rust native addon can achieve. This module covers N-API — the stable C ABI that connects Node.js to native code — and how to use napi-rs to write Rust addons that pass multi-megabyte Buffer payloads across the JavaScript/native boundary without copying, maintaining the zero-allocation principles from Module 5. WASI provides a sandboxed alternative for deterministic computation that does not require compilation per platform.
When V8 Hits Its Ceiling
JavaScript's secp256k1 implementation (via elliptic or @noble/secp256k1) processes ~10,000–25,000 verifications/second on a single thread. A Rust implementation of the same algorithm (via the secp256k1 crate) processes ~200,000–500,000 verifications/second. The difference: Rust compiles to optimized machine code with SIMD instructions, no GC pauses, and no JIT compilation overhead.
javascript// Benchmark: JS vs Rust secp256k1 verification // Node.js (noble/secp256k1): ~15,000 verifications/sec per thread // Rust N-API addon: ~380,000 verifications/sec per thread // At 50,000 tx/sec requiring verification: // JS: needs 50,000/15,000 = 3.3 threads minimum (4 workers) // Rust: needs 50,000/380,000 = 0.13 threads (fits in single thread pool)
The 25× throughput difference is not marginal — it changes whether you need 1 server or 5.
N-API: The Stable Node.js Native Addon Interface
N-API (Node-API) is a stable C ABI for creating native addons that work across Node.js versions without recompilation. It replaced the older NAN (Native Abstractions for Node.js) which broke on every major Node.js version.
N-API guarantees: an addon compiled for Node.js 18 runs on Node.js 22 without recompilation. The ABI is stable.
napi-rs: Rust Bindings for N-API
toml# Cargo.toml [package] name = "blockchain-crypto" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] # dynamic library for N-API [dependencies] napi = { version = "2", features = ["napi9"] } napi-derive = "2" secp256k1 = { version = "0.28", features = ["recovery"] }
rust// src/lib.rs use napi::bindgen_prelude::*; use napi_derive::napi; use secp256k1::{Secp256k1, Message, PublicKey, ecdsa::Signature}; #[napi] // Zero-copy: receives a Buffer reference, does not clone the data pub fn verify_secp256k1( message_hash: Buffer, // 32-byte hash — direct reference to JS Buffer memory signature: Buffer, // 64-byte signature — direct reference public_key: Buffer, // 33 or 65-byte public key — direct reference ) -> Result<bool> { let secp = Secp256k1::verification_only(); let msg = Message::from_digest_slice(&message_hash) .map_err(|e| Error::from_reason(e.to_string()))?; let sig = Signature::from_compact(&signature) .map_err(|e| Error::from_reason(e.to_string()))?; let pk = PublicKey::from_slice(&public_key) .map_err(|e| Error::from_reason(e.to_string()))?; Ok(secp.verify_ecdsa(&msg, &sig, &pk).is_ok()) } #[napi] // Batch verification: verify many signatures in one call // Amortizes the JNI crossing overhead pub fn verify_secp256k1_batch( transactions: Vec<TransactionData>, ) -> Result<Vec<bool>> { let secp = Secp256k1::verification_only(); Ok(transactions.iter().map(|tx| { let msg = Message::from_digest_slice(&tx.message_hash).ok()?; let sig = Signature::from_compact(&tx.signature).ok()?; let pk = PublicKey::from_slice(&tx.public_key).ok()?; Some(secp.verify_ecdsa(&msg, &sig, &pk).is_ok()) }).map(|r| r.unwrap_or(false)).collect()) }
bash# Build the Rust addon npm install -g @napi-rs/cli napi build --platform --release # Produces: blockchain-crypto.node (platform-specific binary)
Using the Addon in Node.js
javascript// Zero-copy Buffer pass-through import { verifySecp256k1, verifySecp256k1Batch } from './blockchain-crypto.node'; // Single verification — passes Buffer references, no copy function verifyTransaction(tx) { const messageHash = computeHash(tx.data); // Buffer return verifySecp256k1( messageHash, // passed as reference — zero copy tx.signature, // passed as reference — zero copy tx.senderPublicKey // passed as reference — zero copy ); } // Batch verification — most efficient pattern // Amortizes the JS→native boundary crossing cost across many verifications async function verifyTransactionBatch(transactions) { const batchData = transactions.map(tx => ({ message_hash: computeHash(tx.data), signature: tx.signature, public_key: tx.senderPublicKey, })); return verifySecp256k1Batch(batchData); // one crossing for N verifications }
Zero-Copy: The Key Principle
When Node.js passes a Buffer to a native function, napi-rs passes a reference to the underlying memory — no copy. The Rust function reads directly from the V8/off-heap memory that the Buffer points to.
This maintains the zero-allocation principle from Module 5: the signature verification workload processes data that was allocated once (when the transaction was received) and never copied again — from socket → Buffer → Rust secp256k1 → result.
WASI: Sandboxed Native Computation
WebAssembly System Interface (WASI) allows running compiled C/Rust code inside a sandboxed WebAssembly runtime. Unlike N-API (which runs with full native permissions), WASI modules are sandboxed — they cannot access the filesystem or network unless explicitly granted.
javascriptimport { WASI } from 'node:wasi'; import { readFileSync } from 'node:fs'; // Load a WASI module const wasi = new WASI({ version: 'preview1', // No filesystem access granted — fully sandboxed preopens: {}, }); const importObject = { wasi_snapshot_preview1: wasi.wasiImport, }; const wasmModule = new WebAssembly.Module( readFileSync('./crypto-verifier.wasm') ); const instance = new WebAssembly.Instance(wasmModule, importObject); wasi.initialize(instance); // Call WASI function const { verify_ed25519 } = instance.exports; // Pass data via shared memory const memory = instance.exports.memory; const messageOffset = 0; const signatureOffset = 32; const publicKeyOffset = 96; // Write to WASM memory new Uint8Array(memory.buffer, messageOffset, 32).set(messageHash); new Uint8Array(memory.buffer, signatureOffset, 64).set(signature); new Uint8Array(memory.buffer, publicKeyOffset, 32).set(publicKey); // Call const isValid = verify_ed25519(messageOffset, signatureOffset, publicKeyOffset);
N-API vs WASI: When to Choose
| N-API (Rust) | WASI | |
|---|---|---|
| Performance | Near-native | ~1.5–2x slower than native (JIT compilation overhead) |
| Security | Full native permissions | Sandboxed — no filesystem/network by default |
| Portability | Platform-specific binary (linux-x64, darwin-arm64, etc.) | Single .wasm file runs everywhere |
| Use case | Maximum throughput (secp256k1, ed25519) | Deterministic sandboxed computation |
| Cold start | Instant | < 1ms for small modules |
For blockchain signature verification: N-API for maximum throughput. napi-rs Rust is 25× faster than JavaScript and there is no sandboxing need.
For untrusted user-supplied computation: WASI. Users can upload a WASM module that runs with only the permissions you grant.
Summary
| Concept | Key Takeaway |
|---|---|
| V8 ceiling | secp256k1 in JS: ~15K/sec. Rust N-API: ~380K/sec. 25× difference matters at 50K tx/sec. |
| N-API | Stable C ABI, works across Node.js versions. napi-rs provides safe Rust bindings. |
| Zero-copy Buffer | Pass Buffer to native function by reference — no copy. Zero allocation overhead. |
| Batch calls | Amortize JNI crossing cost. One native call for 1000 verifications > 1000 calls. |
| WASI | WebAssembly sandbox. One .wasm runs everywhere. No native permissions by default. |
| N-API vs WASI | N-API for max throughput. WASI for portable sandboxed computation. |