Module A-19·16 min read

When V8 hits its ceiling — zero-copy Buffer handoff via napi-rs, and WASI sandboxing for deterministic cryptographic operations.

Written by Jatin Jain Saraf · Architect of SupraScan

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.

// 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 vs Neon: Two Ways to Bridge Rust to N-API

napi-rs (used throughout this module) is one of two mainstream frameworks for writing N-API addons in Rust. The other is Neon. Both sit on top of the same underlying N-API C ABI and solve the same core problem — safely converting Rust types (structs, Vec<T>, String, integers) into JavaScript values and back without hand-rolling raw N-API pointer calls. The practical differences are mostly about ergonomics and defaults: napi-rs leans on procedural macros (#[napi], #[napi(object)]) to generate TypeScript type definitions alongside the compiled binary and has first-class ThreadsafeFunction support for async callbacks; Neon has historically emphasized a more manual, explicit binding style and its own async task abstraction. For a new blockchain indexer addon, either is a reasonable choice — this module standardizes on napi-rs for its TypeScript generation and batch-call ergonomics, which matter once you're verifying thousands of transactions per call.

napi-rs: Rust Bindings for N-API

# 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"] }
// 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())
}
 
// A single incoming transaction's verification material — mirrors the shape
// the JS side builds up before calling into the batch function.
#[napi(object)]
pub struct TransactionData {
  pub tx_hash: Buffer,       // 32-byte transaction hash, used for correlating results back to callers
  pub message_hash: Buffer,  // 32-byte hash of the signed message
  pub signature: Buffer,     // 64-byte compact ECDSA signature
  pub public_key: Buffer,    // 33 or 65-byte public key
}
 
#[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())
}
# 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

// 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

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.