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
- Understand Zig Patterns
- Understand Multi-Language Integration
- Know constant-time programming basics
Decision Tree
Before implementing, decide:- Pure Zig? Simple operations (hashing, encoding)
- Rust FFI? Complex curves (arkworks ecosystem)
- C library? Performance-critical with existing impl (blst, c-kzg)
Example: Adding a Hash Function
We’ll add a hypotheticalWhirlpool 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
| Input | Output (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

