Skip to main content

Multi-Language Integration

Voltaire combines four languages, each chosen for specific strengths.

Language Roles

LanguageRoleExamples
TypeScriptPublic API, type safetyBranded types, namespace exports
ZigCore implementation, cross-compilationPrimitives, precompiles
RustComplex crypto (arkworks)BLS12-381, BN254 curves
CVendored librariesblst, c-kzg-4844

Architecture

┌─────────────────────────────────────────────────┐
│                 TypeScript API                   │
│     (Branded Uint8Array, Namespace exports)      │
├─────────────────────────────────────────────────┤
│                                                  │
│  ┌─────────────┐         ┌─────────────────┐   │
│  │   Bun FFI   │         │   WASM Loader   │   │
│  │  (Native)   │         │   (Browser)     │   │
│  └──────┬──────┘         └────────┬────────┘   │
│         │                         │             │
├─────────┴─────────────────────────┴─────────────┤
│                    Zig Core                      │
│          (primitives, crypto, precompiles)       │
├─────────────────────────────────────────────────┤
│                                                  │
│  ┌─────────────┐         ┌─────────────────┐   │
│  │    Rust     │         │       C         │   │
│  │  (arkworks) │         │ (blst, c-kzg)   │   │
│  └─────────────┘         └─────────────────┘   │
│                                                  │
└─────────────────────────────────────────────────┘

Zig-to-TypeScript (Primary Path)

Native FFI (Bun)

Bun’s FFI provides near-native performance:
// native/index.ts
import { dlopen, FFIType, suffix } from "bun:ffi";

const lib = dlopen(`libprimitives_ts_native.${suffix}`, {
  keccak256: {
    args: [FFIType.ptr, FFIType.u32, FFIType.ptr],
    returns: FFIType.void,
  },
});

export function keccak256(data: Uint8Array): Uint8Array {
  const output = new Uint8Array(32);
  lib.symbols.keccak256(data, data.length, output);
  return output;
}

WASM (Browser/Node)

For non-Bun environments:
// wasm/Keccak256.ts
import { getWasm } from "../wasm-loader/loader.js";

export function hash(data: Uint8Array): Uint8Array {
  const wasm = getWasm();
  const inputPtr = wasm.alloc(data.length);
  const outputPtr = wasm.alloc(32);

  try {
    new Uint8Array(wasm.memory.buffer, inputPtr, data.length).set(data);
    wasm.keccak256(inputPtr, data.length, outputPtr);
    const result = new Uint8Array(32);
    result.set(new Uint8Array(wasm.memory.buffer, outputPtr, 32));
    return result;
  } finally {
    wasm.free(inputPtr);
    wasm.free(outputPtr);
  }
}

Exporting from Zig

// c_api.zig - exports for FFI/WASM
const std = @import("std");
const Keccak256 = @import("crypto").Keccak256;

export fn keccak256(input: [*]const u8, len: usize, output: [*]u8) void {
    const data = input[0..len];
    const hash = Keccak256.hash(data);
    @memcpy(output[0..32], &hash);
}

export fn alloc(len: usize) ?[*]u8 {
    const slice = std.heap.wasm_allocator.alloc(u8, len) catch return null;
    return slice.ptr;
}

export fn free(ptr: [*]u8, len: usize) void {
    std.heap.wasm_allocator.free(ptr[0..len]);
}

Zig-to-Rust

Rust is used for complex elliptic curve operations via arkworks.

Cargo Configuration

# Cargo.toml
[lib]
crate-type = ["staticlib"]
name = "crypto_wrappers"

[dependencies]
ark-bn254 = "0.4"
ark-bls12-381 = "0.4"
ark-ec = "0.4"
ark-ff = "0.4"

[features]
default = ["asm"]
asm = ["keccak-asm"]
portable = ["tiny-keccak"]  # For WASM

Rust FFI Functions

// src/rust/bn254.rs
use ark_bn254::{Fr, G1Affine, G1Projective};
use ark_ec::CurveGroup;
use ark_ff::PrimeField;

#[no_mangle]
pub extern "C" fn bn254_g1_add(
    ax: *const u8,
    ay: *const u8,
    bx: *const u8,
    by: *const u8,
    out_x: *mut u8,
    out_y: *mut u8,
) -> i32 {
    // Safety: caller ensures valid pointers
    unsafe {
        let a = match read_g1_point(ax, ay) {
            Some(p) => p,
            None => return -1,
        };
        let b = match read_g1_point(bx, by) {
            Some(p) => p,
            None => return -1,
        };

        let result = (a + b).into_affine();
        write_g1_point(&result, out_x, out_y);
        0
    }
}

unsafe fn read_g1_point(x: *const u8, y: *const u8) -> Option<G1Projective> {
    let x_bytes = std::slice::from_raw_parts(x, 32);
    let y_bytes = std::slice::from_raw_parts(y, 32);
    // ... parse and validate point
    Some(G1Affine::new(x_field, y_field).into())
}

Zig Bindings

// bn254_ffi.zig
const c = @cImport({
    @cInclude("crypto_wrappers.h");
});

pub const G1Point = struct {
    x: [32]u8,
    y: [32]u8,
};

pub fn g1Add(a: G1Point, b: G1Point) !G1Point {
    var result: G1Point = undefined;

    const status = c.bn254_g1_add(
        &a.x, &a.y,
        &b.x, &b.y,
        &result.x, &result.y,
    );

    if (status != 0) return error.InvalidPoint;
    return result;
}

test "G1 point addition" {
    const generator = G1Point{
        .x = [_]u8{1} ++ [_]u8{0} ** 31,
        .y = [_]u8{2} ++ [_]u8{0} ** 31,
    };

    const result = try g1Add(generator, generator);
    // Verify 2G
    try std.testing.expect(result.x[0] != 0);
}

Zig-to-C

C libraries are used for mature, audited implementations.

Vendored Libraries

lib/
├── blst/              # BLS12-381 signatures
│   ├── src/
│   └── bindings/
├── c-kzg-4844/        # KZG commitments
│   ├── src/
│   └── bindings/
└── libwally-core/     # Wallet utilities (git submodule)
    └── src/

Build Integration

// build.zig
const blst = b.addStaticLibrary(.{
    .name = "blst",
    .target = target,
    .optimize = optimize,
});
blst.addCSourceFiles(.{
    .files = &.{
        "lib/blst/src/server.c",
        // ... other files
    },
    .flags = &.{"-O3", "-fno-builtin"},
});
blst.linkLibC();

// Link to main library
lib.linkLibrary(blst);

Zig C Imports

// bls12381_ffi.zig
const c = @cImport({
    @cInclude("blst.h");
});

pub fn sign(secret_key: [32]u8, message: []const u8) [96]u8 {
    var sig: c.blst_p2_affine = undefined;
    var sk: c.blst_scalar = undefined;

    // Import secret key
    c.blst_scalar_from_bendian(&sk, &secret_key);

    // Sign
    var hash: c.blst_p2 = undefined;
    c.blst_hash_to_g2(
        &hash,
        message.ptr, message.len,
        "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_",
        43,
        null, 0,
    );

    var sig_point: c.blst_p2 = undefined;
    c.blst_sign_pk_in_g1(&sig_point, &hash, &sk);
    c.blst_p2_to_affine(&sig, &sig_point);

    // Serialize
    var output: [96]u8 = undefined;
    c.blst_p2_affine_compress(&output, &sig);
    return output;
}

C Header Generation

Auto-generate C headers from Zig:
zig build generate-header
Output: src/primitives.h
// primitives.h (auto-generated)
#ifndef PRIMITIVES_H
#define PRIMITIVES_H

#include <stdint.h>
#include <stddef.h>

void keccak256(const uint8_t* input, size_t len, uint8_t* output);
int secp256k1_sign(
    const uint8_t* private_key,
    const uint8_t* message_hash,
    uint8_t* signature,
    uint8_t* recovery_id
);
// ... more exports

#endif

Cross-Language Testing

Zig Tests with C

test "blst signature verification" {
    const secret_key = [_]u8{1} ** 32;
    const message = "test message";

    const signature = sign(secret_key, message);
    const public_key = derivePublicKey(secret_key);

    try std.testing.expect(verify(public_key, message, signature));
}

TypeScript Tests with Native

// Cross-validate native vs WASM
describe("native/wasm parity", () => {
  it("produces identical keccak256 hashes", () => {
    const data = new TextEncoder().encode("test");

    const native = nativeKeccak256(data);
    const wasm = wasmKeccak256(data);

    expect(native).toEqual(wasm);
  });
});

Fuzzing Across Languages

// Fuzz test cross-language consistency
describe("fuzz", () => {
  it("native matches wasm for random inputs", () => {
    for (let i = 0; i < 10000; i++) {
      const data = crypto.getRandomValues(
        new Uint8Array(Math.floor(Math.random() * 1000))
      );

      const native = nativeKeccak256(data);
      const wasm = wasmKeccak256(data);

      expect(native).toEqual(wasm);
    }
  });
});

Error Handling Across Languages

Zig Errors

pub const CryptoError = error{
    InvalidPoint,
    InvalidScalar,
    SignatureFailed,
    VerificationFailed,
};

pub fn verify(sig: Signature) CryptoError!bool {
    if (!isValidSignature(sig)) return error.InvalidSignature;
    // ...
}

C Return Codes

// Convention: 0 = success, negative = error
#define CRYPTO_SUCCESS 0
#define CRYPTO_ERROR_INVALID_INPUT -1
#define CRYPTO_ERROR_VERIFICATION_FAILED -2

Rust Result Types

#[repr(C)]
pub struct CryptoResult {
    success: bool,
    error_code: i32,
}

#[no_mangle]
pub extern "C" fn verify_signature(...) -> CryptoResult {
    match internal_verify(...) {
        Ok(valid) => CryptoResult { success: valid, error_code: 0 },
        Err(e) => CryptoResult { success: false, error_code: e.code() },
    }
}

TypeScript Error Translation

// errors.ts
export function translateError(code: number): Error {
  switch (code) {
    case -1:
      return new InvalidInputError();
    case -2:
      return new VerificationFailedError();
    default:
      return new CryptoError(`Unknown error: ${code}`);
  }
}

// Usage
const result = lib.symbols.verify_signature(...);
if (result.error_code !== 0) {
  throw translateError(result.error_code);
}

Build Dependencies

Full Build Chain

# 1. Rust builds first (static library)
cargo build --release
# Output: target/release/libcrypto_wrappers.a

# 2. C libraries built by Zig
zig build deps
# Output: zig-out/lib/libblst.a, libc_kzg.a

# 3. Main Zig build links everything
zig build
# Output: native libs, WASM

# 4. TypeScript bundles
bun run build:dist
# Output: dist/

Dependency Graph

Cargo.toml ──► libcrypto_wrappers.a ──┐

lib/blst ──────► libblst.a ───────────┼──► Zig Build ──► WASM/Native

lib/c-kzg ─────► libc_kzg.a ──────────┘


                               TypeScript Bundle