Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

Adding Crypto Functions

This guide covers adding new cryptographic functions. Crypto code requires extra care for security and cross-language implementation.

Prerequisites

Decision Tree

Before implementing, decide:
  1. Pure Zig? Simple operations (hashing, encoding)
  2. Rust FFI? Complex curves (arkworks ecosystem)
  3. C library? Performance-critical with existing impl (blst, c-kzg)

Example: Adding a Hash Function

We’ll add a hypothetical Whirlpool hash function.

Step 1: Create Directory

mkdir -p src/crypto/Whirlpool
src/crypto/Whirlpool/
├── whirlpool.zig           # Core implementation
├── Whirlpool.js            # TypeScript wrapper
├── Whirlpool.test.ts       # Tests
├── Whirlpool.wasm.ts       # WASM variant
├── Whirlpool.wasm.test.ts  # WASM tests
├── index.ts                # Exports
└── whirlpool.mdx           # Documentation

Step 2: Implement in Zig

// whirlpool.zig
const std = @import("std");

pub const Whirlpool = struct {
    const DIGEST_SIZE = 64;
    const BLOCK_SIZE = 64;

    state: [8]u64,
    buffer: [BLOCK_SIZE]u8,
    buffer_len: usize,
    total_len: u64,

    pub fn init() Whirlpool {
        return Whirlpool{
            .state = [_]u64{0} ** 8,
            .buffer = [_]u8{0} ** BLOCK_SIZE,
            .buffer_len = 0,
            .total_len = 0,
        };
    }

    pub fn update(self: *Whirlpool, data: []const u8) void {
        var i: usize = 0;

        // Fill buffer first
        if (self.buffer_len > 0) {
            const space = BLOCK_SIZE - self.buffer_len;
            const copy_len = @min(space, data.len);
            @memcpy(self.buffer[self.buffer_len..][0..copy_len], data[0..copy_len]);
            self.buffer_len += copy_len;
            i = copy_len;

            if (self.buffer_len == BLOCK_SIZE) {
                self.processBlock(&self.buffer);
                self.buffer_len = 0;
            }
        }

        // Process full blocks
        while (i + BLOCK_SIZE <= data.len) : (i += BLOCK_SIZE) {
            self.processBlock(data[i..][0..BLOCK_SIZE]);
        }

        // Store remainder
        if (i < data.len) {
            const remainder = data.len - i;
            @memcpy(self.buffer[0..remainder], data[i..]);
            self.buffer_len = remainder;
        }

        self.total_len += data.len;
    }

    pub fn final(self: *Whirlpool) [DIGEST_SIZE]u8 {
        // Padding
        self.buffer[self.buffer_len] = 0x80;
        self.buffer_len += 1;

        if (self.buffer_len > 32) {
            @memset(self.buffer[self.buffer_len..], 0);
            self.processBlock(&self.buffer);
            self.buffer_len = 0;
        }

        @memset(self.buffer[self.buffer_len..], 0);

        // Length in bits
        const bit_len = self.total_len * 8;
        std.mem.writeInt(u64, self.buffer[56..64], bit_len, .big);

        self.processBlock(&self.buffer);

        // Output
        var digest: [DIGEST_SIZE]u8 = undefined;
        for (self.state, 0..) |word, j| {
            std.mem.writeInt(u64, digest[j * 8 ..][0..8], word, .big);
        }

        return digest;
    }

    fn processBlock(self: *Whirlpool, block: *const [BLOCK_SIZE]u8) void {
        // Actual Whirlpool compression function
        _ = self;
        _ = block;
        // ... implementation details ...
    }

    /// One-shot hash function
    pub fn hash(data: []const u8) [DIGEST_SIZE]u8 {
        var h = Whirlpool.init();
        h.update(data);
        return h.final();
    }
};

// Tests
test "Whirlpool empty string" {
    const digest = Whirlpool.hash("");
    // Compare against known test vector
    const expected = [_]u8{ 0x19, 0xFA, ... }; // Full vector
    try std.testing.expectEqualSlices(u8, &expected, &digest);
}

test "Whirlpool 'abc'" {
    const digest = Whirlpool.hash("abc");
    const expected = [_]u8{ ... }; // Test vector for "abc"
    try std.testing.expectEqualSlices(u8, &expected, &digest);
}

test "Whirlpool incremental equals one-shot" {
    const data = "The quick brown fox jumps over the lazy dog";

    const one_shot = Whirlpool.hash(data);

    var incremental = Whirlpool.init();
    incremental.update(data[0..10]);
    incremental.update(data[10..]);
    const inc_result = incremental.final();

    try std.testing.expectEqualSlices(u8, &one_shot, &inc_result);
}

Step 3: TypeScript Wrapper

// Whirlpool.js
import { getWasm } from "../wasm-loader/loader.js";

/**
 * Compute Whirlpool hash of data
 * @param {Uint8Array | string} data
 * @returns {Uint8Array}
 */
export function hash(data) {
  const input = typeof data === "string"
    ? new TextEncoder().encode(data)
    : data;

  const wasm = getWasm();
  return wasm.whirlpool_hash(input);
}

/**
 * Compute Whirlpool hash and return as hex
 * @param {Uint8Array | string} data
 * @returns {string}
 */
export function hashHex(data) {
  const digest = hash(data);
  return "0x" + [...digest].map(b => b.toString(16).padStart(2, "0")).join("");
}
// index.ts
export { hash } from "./Whirlpool.js";
export { hashHex } from "./Whirlpool.js";

// Re-export for namespace usage
import { hash, hashHex } from "./Whirlpool.js";
export const Whirlpool = { hash, hashHex };

Step 4: Register in Module

// src/crypto/root.zig
pub const Keccak256 = @import("Keccak256/keccak256.zig").Keccak256;
pub const Whirlpool = @import("Whirlpool/whirlpool.zig").Whirlpool;  // Add

Step 5: Tests

// Whirlpool.test.ts
import { describe, it, expect } from "vitest";
import * as Whirlpool from "./index.js";

describe("Whirlpool", () => {
  // Test vectors from specification
  const vectors = [
    { input: "", expected: "19fa61d75522a466..." },
    { input: "abc", expected: "..." },
    { input: "The quick brown fox...", expected: "..." },
  ];

  describe("hash", () => {
    it.each(vectors)("hashes '$input' correctly", ({ input, expected }) => {
      const result = Whirlpool.hashHex(input);
      expect(result).toBe(expected);
    });

    it("handles Uint8Array input", () => {
      const input = new Uint8Array([0x61, 0x62, 0x63]); // "abc"
      const result = Whirlpool.hash(input);
      expect(result).toBeInstanceOf(Uint8Array);
      expect(result.length).toBe(64);
    });
  });
});

Adding Curve Operations (Rust FFI)

For elliptic curve operations, use Rust with arkworks.

Step 1: Add Rust Dependency

# Cargo.toml
[dependencies]
ark-ff = "0.4"
ark-ec = "0.4"
ark-whirlpool = "0.4"  # Hypothetical

Step 2: Create Rust Wrapper

// src/rust/whirlpool.rs
use ark_whirlpool::{Curve, Point};

#[no_mangle]
pub extern "C" fn whirlpool_point_add(
    ax: *const u8, ay: *const u8,
    bx: *const u8, by: *const u8,
    out_x: *mut u8, out_y: *mut u8,
) -> i32 {
    // Implementation
}

Step 3: Zig FFI Bindings

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

pub fn pointAdd(a: Point, b: Point) !Point {
    var result_x: [32]u8 = undefined;
    var result_y: [32]u8 = undefined;

    const status = c.whirlpool_point_add(
        &a.x, &a.y,
        &b.x, &b.y,
        &result_x, &result_y,
    );

    if (status != 0) return error.CurveError;

    return Point{ .x = result_x, .y = result_y };
}

Security Requirements

Constant-Time Operations

All crypto code must be constant-time to prevent timing attacks.
// ✅ Constant time comparison
pub fn secureEquals(a: []const u8, b: []const u8) bool {
    if (a.len != b.len) return false;

    var result: u8 = 0;
    for (a, b) |x, y| {
        result |= x ^ y;
    }
    return result == 0;
}

// ❌ Timing leak
pub fn insecureEquals(a: []const u8, b: []const u8) bool {
    for (a, b) |x, y| {
        if (x != y) return false;  // Early return leaks timing
    }
    return true;
}

Memory Clearing

Clear sensitive data after use:
pub fn signMessage(private_key: [32]u8, message: []const u8) ![64]u8 {
    var key_copy = private_key;
    defer std.crypto.utils.secureZero(u8, &key_copy);  // Clear on exit

    // ... signing logic ...

    return signature;
}

Input Validation

Always validate inputs before processing:
pub fn verifySignature(sig: []const u8, msg: []const u8, pubkey: []const u8) !bool {
    // Validate lengths
    if (sig.len != 64) return error.InvalidSignatureLength;
    if (pubkey.len != 33 and pubkey.len != 65) return error.InvalidPublicKeyLength;

    // Validate signature components (r, s in valid range)
    const r = sig[0..32];
    const s = sig[32..64];
    if (!isValidScalar(r) or !isValidScalar(s)) return error.InvalidSignature;

    // ... verification logic ...
}

Test Vectors

Every crypto function needs test vectors from official sources:
test "Whirlpool official test vectors" {
    // From ISO/IEC 10118-3:2004
    const vectors = .{
        .{ .input = "", .expected = "19FA61D75522A466..." },
        .{ .input = "a", .expected = "8ACA2602792AEC6F..." },
        .{ .input = "abc", .expected = "..." },
        // ... more vectors
    };

    for (vectors) |v| {
        const result = Whirlpool.hash(v.input);
        const expected = try hexToBytes(v.expected);
        try std.testing.expectEqualSlices(u8, &expected, &result);
    }
}

Cross-Validation

Test against reference implementations:
// Whirlpool.test.ts
import { whirlpool as nobleWhirlpool } from "@noble/hashes/whirlpool";

describe("cross-validation", () => {
  it("matches noble implementation", () => {
    const data = "test data";
    const ours = Whirlpool.hash(data);
    const noble = nobleWhirlpool(data);
    expect(ours).toEqual(noble);
  });

  // Fuzz test
  it("matches noble for random inputs", () => {
    for (let i = 0; i < 1000; i++) {
      const data = crypto.getRandomValues(new Uint8Array(Math.random() * 1000));
      const ours = Whirlpool.hash(data);
      const noble = nobleWhirlpool(data);
      expect(ours).toEqual(noble);
    }
  });
});

Documentation

// docs/crypto/whirlpool/index.mdx
---
title: Whirlpool
description: Whirlpool cryptographic hash function
---

# Whirlpool

Whirlpool is a cryptographic hash function that produces a 512-bit digest.

<Warning>
Whirlpool is not commonly used in Ethereum. Consider using [Keccak256](/crypto/keccak256) for Ethereum operations.
</Warning>

## Usage

```typescript
import * as Whirlpool from "@voltaire/crypto/Whirlpool";

// Hash a string
const hash = Whirlpool.hash("hello world");

// Hash as hex
const hex = Whirlpool.hashHex("hello world");

Security

  • 512-bit output
  • Designed by Vincent Rijmen and Paulo Barreto
  • Standardized in ISO/IEC 10118-3:2004

Test Vectors

InputOutput (truncated)
""0x19FA61D75522A466...
"abc"0x...

## Checklist

Before submitting crypto code:

- [ ] Zig implementation with inline tests
- [ ] All operations constant-time where needed
- [ ] Input validation on all public functions
- [ ] Test vectors from official specification
- [ ] Cross-validation against reference implementation
- [ ] TypeScript wrapper with types
- [ ] WASM variant working
- [ ] Memory cleared after sensitive operations
- [ ] Documentation with security notes
- [ ] Registered in `crypto/root.zig`
- [ ] All tests passing