Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,22 @@ zig build

When contributing changes that may affect portability, ensure that `zig build` succeeds on your target platforms, and use the benchmark script on at least one platform to confirm cross-language compatibility.

## SSZ Integration

hash-zig provides an SSZ-compatible Poseidon2 hasher that matches the SHA256-style API expected by SSZ merkleization libraries.

```zig
const ssz = @import("ssz.zig");
const hash_zig = @import("hash-zig");

const PoseidonHasher = hash_zig.ssz.SszHasher;

var root: [32]u8 = undefined;
try ssz.hashTreeRoot(PoseidonHasher, MyType, value, &root, allocator);
```

The SSZ wrapper lives under `src/ssz/` and is exported as `hash_zig.ssz`.

### Repository Layout

```
Expand Down Expand Up @@ -400,4 +416,3 @@ Licensed under the Apache License 2.0 – see [LICENSE](LICENSE).
- [leanSig](https://github.com/leanEthereum/leanSig) — original Rust implementation and reference tests
- [zig-poseidon](https://github.com/blockblaz/zig-poseidon) — Poseidon2 over the KoalaBear field
- [Generalized XMSS (ePrint 2025/055)](https://eprint.iacr.org/2025/055.pdf) — scheme specification
- [Rust ↔ Zig compatibility investigation](analysis/rust_zig_compatibility_investigation.md)
1 change: 0 additions & 1 deletion benchmark/zig_benchmark/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,4 @@ pub fn build(b: *std.Build) void {

const run_step = b.step("run", "Run the benchmark");
run_step.dependOn(&run_cmd.step);

}
24 changes: 12 additions & 12 deletions benchmark/zig_benchmark/src/remote_hash_tool.zig
Original file line number Diff line number Diff line change
Expand Up @@ -132,28 +132,28 @@ fn readPublicKeyFromJson(path: []const u8, allocator: std.mem.Allocator) !hash_z
/// 5. hashes: for each hash:
/// - array_len: u64 = 8 (little endian)
/// - 8 u32 values in canonical form
///
///
/// This function writes the signature in bincode format matching Rust's serialization.
/// The output is then padded to exactly 3116 bytes to comply with leanSpec:
/// https://github.com/leanEthereum/leanSpec/blob/main/src/lean_spec/subspecs/containers/signature.py
///
///
/// The leanSpec requires:
/// 1. Signature container: exactly 3116 bytes (Bytes3116)
/// 2. Signature data: bincode format at the beginning
/// 3. Can be sliced to scheme.config.SIGNATURE_LEN_BYTES if needed
/// 4. Format: XmssSignature.from_bytes (bincode deserialization)
///
///
/// Note: Rust's bincode serializes field elements in canonical form, not Montgomery
pub fn writeSignatureBincode(path: []const u8, signature: *const hash_zig.GeneralizedXMSSSignature, rand_len: usize, hash_len: usize) !void {
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
defer file.close();

const writer = file.writer();
const path_nodes = signature.getPath().getNodes();

// Write path_len (u64) - Vec length
try writeLength(writer, path_nodes.len);

// Write path nodes (each as: 8 u32 values in canonical form, NO length prefix for fixed arrays)
// Rust's bincode serializes Vec<[T; N]> as: Vec length + elements directly (no per-array length)
for (path_nodes) |node| {
Expand Down Expand Up @@ -181,7 +181,7 @@ pub fn writeSignatureBincode(path: []const u8, signature: *const hash_zig.Genera
// Write hashes_len (u64) - Vec length
const hashes = signature.getHashes();
try writeLength(writer, hashes.len);

// Write hashes (each as: 8 u32 values in canonical form, NO length prefix for fixed arrays)
// Rust's bincode serializes Vec<[T; N]> as: Vec length + elements directly (no per-array length)
// NOTE: Rust's bincode serializes field elements in CANONICAL form
Expand Down Expand Up @@ -214,7 +214,7 @@ pub fn readSignatureBincode(path: []const u8, allocator: std.mem.Allocator, rand
// Read path_len (u64) - Vec length
const path_len = try readLength(reader);
if (path_len == 0 or path_len > max_path_len) return BincodeError.InvalidPathLength;

// Read path nodes (each has: HASH_LEN u32 values in CANONICAL form, NO length prefix for fixed arrays)
// Rust's bincode serializes Vec<FieldArray<N>> as: Vec length + elements directly (no per-array length)
// Rust writes FieldArray<HASH_LEN> which serializes exactly HASH_LEN elements
Expand Down Expand Up @@ -265,7 +265,7 @@ pub fn readSignatureBincode(path: []const u8, allocator: std.mem.Allocator, rand
path_ptr.deinit();
return BincodeError.InvalidHashesLength;
}

// Read hashes (each has: HASH_LEN u32 values in CANONICAL form, NO length prefix for fixed arrays)
// Rust's bincode serializes Vec<FieldArray<N>> as: Vec length + elements directly (no per-array length)
// Rust writes FieldArray<HASH_LEN> which serializes exactly HASH_LEN elements
Expand Down Expand Up @@ -300,15 +300,15 @@ pub fn readSignatureBincode(path: []const u8, allocator: std.mem.Allocator, rand
return err;
};
allocator.free(hashes_tmp);

// Debug: print rho values from signature after creation (for Zig→Zig debugging)
const rho_from_sig = signature_ptr.getRho();
stderr.print("ZIG_READ_DEBUG: rho from signature.getRho() (Montgomery): ", .{}) catch {};
for (0..rand_len) |i| {
stderr.print("0x{x:0>8} ", .{rho_from_sig[i].toMontgomery()}) catch {};
}
stderr.print("\n", .{}) catch {};

return signature_ptr;
}

Expand Down Expand Up @@ -390,15 +390,15 @@ fn verifyCommand(
defer signature_ptr.deinit();

const msg_bytes = messageToBytes(message);

// Debug: Print signature rho values
const rho = signature_ptr.getRho();
log.emit("ZIG_REMOTE_VERIFY_DEBUG: Signature rho (first {}): ", .{scheme.lifetime_params.rand_len_fe});
for (0..scheme.lifetime_params.rand_len_fe) |i| {
log.emit("0x{x:0>8} ", .{rho[i].toCanonical()});
}
log.emit("\n", .{});

const ok = try scheme.verify(&pk, epoch, msg_bytes, signature_ptr);
log.emit("VERIFY_RESULT:{}\n", .{ok});
}
Expand Down
6 changes: 3 additions & 3 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ pub fn build(b: *std.Build) void {
const enable_debug_logs = b.option(bool, "debug-logs", "Enable verbose std.debug logging") orelse false;
const enable_profile_keygen = b.option(bool, "enable-profile-keygen", "Enable detailed keygen profiling logs") orelse false;
const enable_sanitize = b.option(bool, "sanitize", "Enable AddressSanitizer (default: false)") orelse false;

// Auto-detect SIMD width based on target CPU features
// If user explicitly sets simd-width, use that; otherwise auto-detect
const explicit_simd_width = b.option(u32, "simd-width", "SIMD width (4 or 8, default: auto-detect)");
const simd_width: u32 = if (explicit_simd_width) |width| width else blk: {
// Auto-detect based on target architecture and CPU features
const target_info = target.result;

// Only x86_64 can support AVX-512 (8-wide SIMD)
if (target_info.cpu.arch == .x86_64) {
// Check if AVX-512F feature is enabled in the target
const avx512f_feature = @intFromEnum(std.Target.x86.Feature.avx512f);
const has_avx512_feature = target_info.cpu.features.isEnabled(avx512f_feature);

if (has_avx512_feature) {
std.debug.print("Build: Auto-detected AVX-512 support, using 8-wide SIMD\n", .{});
break :blk 8;
Expand Down
3 changes: 1 addition & 2 deletions scripts/benchmark_hash_function.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn main() !void {

// Benchmark: compress16SIMD with SIMD-packed input
var packed_input: [16]poseidon2_simd.PackedF = undefined;

// Initialize with test data
for (0..16) |i| {
var values: [SIMD_WIDTH]u32 = undefined;
Expand Down Expand Up @@ -77,4 +77,3 @@ pub fn main() !void {
std.debug.print(" Actual keygen time: ~868 seconds\n", .{});
std.debug.print(" Overhead factor: {d:.2}x\n\n", .{868.0 / estimated_time_s});
}

1 change: 0 additions & 1 deletion scripts/benchmark_verify.zig
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,3 @@ pub fn main() !void {
std.debug.print(" Throughput: {d:.0} verifications/sec\n", .{ops_per_sec});
std.debug.print(" Lifetime: 2^{d}\n", .{lifetime_power});
}

11 changes: 5 additions & 6 deletions scripts/profile_keygen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,22 @@ pub fn main() !void {
// Generate seed
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);

// Generate keypair with timing
print("\nGenerating keypair...\n", .{});
const keygen_start = timer.lap();

const keypair = try scheme.keyGen(seed);
defer keypair.deinit(allocator);

const keygen_total = timer.lap();
const keygen_time = @as(f64, @floatFromInt(keygen_total)) / 1_000_000_000.0;

print("\n✅ Key generation completed!\n", .{});
print("⏱️ Total Time: {d:.3}s\n\n", .{keygen_time});

print("Breakdown:\n", .{});
print(" - Initialization: {d:.3}s\n", .{@as(f64, @floatFromInt(init_time)) / 1_000_000_000.0});
print(" - Key Generation: {d:.3}s\n", .{keygen_time});
print(" - Total: {d:.3}s\n", .{(@as(f64, @floatFromInt(init_time)) + keygen_time) / 1_000_000_000.0});
}

23 changes: 8 additions & 15 deletions scripts/profile_keygen_detailed.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,25 @@ pub fn main() !void {
// Generate keypair with timing
print("\nGenerating keypair...\n", .{});
timer.reset();

const activation_epoch: usize = 0;
const keypair = try scheme.keyGen(activation_epoch, num_active_epochs);
// KeyGenResult contains public_key and secret_key which are managed by the scheme
_ = keypair;

const keygen_time = @as(f64, @floatFromInt(timer.read())) / 1_000_000_000.0;

print("\n✅ Key generation completed!\n", .{});
print("⏱️ Total Time: {d:.3}s\n\n", .{keygen_time});

const init_time_sec = @as(f64, @floatFromInt(init_time)) / 1_000_000_000.0;
const total_time = init_time_sec + keygen_time;

print("Performance Breakdown:\n", .{});
print(" - Initialization: {d:.3}s ({d:.1}%)\n", .{
init_time_sec,
if (total_time > 0) (init_time_sec / total_time) * 100.0 else 0.0
});
print(" - Key Generation: {d:.3}s ({d:.1}%)\n", .{
keygen_time,
if (total_time > 0) (keygen_time / total_time) * 100.0 else 0.0
});
print(" - Initialization: {d:.3}s ({d:.1}%)\n", .{ init_time_sec, if (total_time > 0) (init_time_sec / total_time) * 100.0 else 0.0 });
print(" - Key Generation: {d:.3}s ({d:.1}%)\n", .{ keygen_time, if (total_time > 0) (keygen_time / total_time) * 100.0 else 0.0 });
print(" - Total: {d:.3}s\n", .{total_time});

print("\nNote: For detailed breakdown of chain computation vs tree hashing,\n", .{});
print(" instrumentation needs to be added to the key generation code.\n", .{});
}

6 changes: 4 additions & 2 deletions src/poseidon2/plonky3_field.zig
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ pub const KoalaBearField = struct {

// Field inverse (exact from Plonky3)
pub fn inverse(self: KoalaBearField) KoalaBearField {
const inv = modInverse(self.value, KOALABEAR_PRIME);
return KoalaBearField{ .value = inv };
// Compute inverse in normal form, then convert back to Montgomery.
const normal = fromMonty(self.value);
const inv_normal = modInverse(normal, KOALABEAR_PRIME);
return KoalaBearField{ .value = toMonty(inv_normal) };
}

// Double operation (exact from Plonky3)
Expand Down
15 changes: 15 additions & 0 deletions src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub const merkle = @import("merkle/mod.zig");
pub const signature = @import("signature/mod.zig");
pub const utils = @import("utils/mod.zig");
pub const poseidon2 = @import("poseidon2/root.zig");
pub const ssz = @import("ssz/root.zig");

// Note: SIMD implementations (simd_signature, simd_winternitz, etc.) are available
// as separate modules in build.zig. Access them via:
Expand Down Expand Up @@ -64,3 +65,17 @@ test "hash-zig root loads" {
// Smoke test to ensure the root module compiles.
try @import("std").testing.expect(true);
}

// Import all sub-modules to run their tests
test {
_ = core;
_ = hash;
_ = prf;
_ = encoding;
_ = wots;
_ = merkle;
_ = signature;
_ = utils;
_ = poseidon2;
_ = ssz;
}
114 changes: 114 additions & 0 deletions src/ssz/poseidon_plonky3_validation.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Cross-validation: SSZ Poseidon2-24 wrapper vs Plonky3 reference outputs
//!
//! This test verifies that the SSZ Poseidon2 wrapper produces IDENTICAL outputs
//! to Plonky3's reference implementation for the same 64-byte inputs.

const std = @import("std");

test "SSZ Poseidon2-24 matches Plonky3 reference outputs" {
const poseidon_wrapper = @import("./poseidon_wrapper.zig");
const poseidon2 = @import("../poseidon2/root.zig");
const Poseidon2 = poseidon2.Poseidon2KoalaBear24Plonky3;
const Hasher = poseidon_wrapper.PoseidonHasher(Poseidon2);

// Test 1: All zeros (64 bytes)
{
var hasher = Hasher.init(.{});
const input = [_]u8{0x00} ** 64;
hasher.update(&input);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0xe4, 0xcb, 0xc9, 0x51, 0xcc, 0xd0, 0xf9, 0x07, 0xe1, 0xca, 0x89, 0x29, 0xc0, 0xa8, 0x70, 0x76, 0xf7, 0x8d, 0x75, 0x7a, 0xda, 0x87, 0xd4, 0x35, 0xd3, 0x86, 0xcc, 0x62, 0xd0, 0x64, 0x5a, 0x13 };
try std.testing.expectEqualSlices(u8, &expected, &output);
}

// Test 2: All 0x01 bytes
{
var hasher = Hasher.init(.{});
const input = [_]u8{0x01} ** 64;
hasher.update(&input);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0xb3, 0x16, 0xc9, 0x34, 0x81, 0x0a, 0x37, 0x73, 0x93, 0x89, 0x61, 0x7a, 0x5e, 0x9d, 0xc8, 0x6f, 0x75, 0x28, 0xd4, 0x27, 0x22, 0x8f, 0xf3, 0x57, 0x9d, 0xfb, 0xff, 0x5c, 0xef, 0x08, 0x1f, 0x00 };
try std.testing.expectEqualSlices(u8, &expected, &output);
}

// Test 3: All 0x42 bytes
{
var hasher = Hasher.init(.{});
const input = [_]u8{0x42} ** 64;
hasher.update(&input);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0x78, 0xae, 0xf5, 0x68, 0xa5, 0x4c, 0xf6, 0x59, 0x2f, 0x82, 0x6d, 0x1e, 0x5f, 0x8f, 0x5e, 0x68, 0x95, 0x94, 0xc6, 0x09, 0x25, 0x87, 0xce, 0x6d, 0x16, 0xd2, 0xb2, 0x21, 0xdb, 0x21, 0x3c, 0x1c };
try std.testing.expectEqualSlices(u8, &expected, &output);
}

// Test 4: Sequential bytes (0..63)
{
var hasher = Hasher.init(.{});
var input: [64]u8 = undefined;
for (0..64) |i| {
input[i] = @intCast(i);
}
hasher.update(&input);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0x29, 0x43, 0x5f, 0x44, 0xc0, 0xab, 0xbb, 0x1e, 0x3b, 0x42, 0x73, 0x2c, 0xfb, 0xac, 0x95, 0x67, 0xb1, 0xa6, 0x4b, 0x6d, 0xb9, 0x51, 0x6a, 0x23, 0xdd, 0x01, 0x03, 0x1d, 0x15, 0xf4, 0x3a, 0x63 };
try std.testing.expectEqualSlices(u8, &expected, &output);
}

// Test 5: SSZ pattern - hash two 32-byte nodes (0xAA || 0xBB)
{
var hasher = Hasher.init(.{});
const left_node = [_]u8{0xAA} ** 32;
const right_node = [_]u8{0xBB} ** 32;

hasher.update(&left_node);
hasher.update(&right_node);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0xec, 0x3e, 0x77, 0x40, 0x7c, 0x50, 0xf7, 0x7a, 0x63, 0x98, 0xdb, 0x56, 0x94, 0x82, 0x6e, 0x21, 0xfb, 0xb8, 0x7f, 0x29, 0x92, 0x59, 0x3e, 0x59, 0x6c, 0xc9, 0x37, 0x7a, 0x50, 0x54, 0xdf, 0x56 };
try std.testing.expectEqualSlices(u8, &expected, &output);
}

// Test 6: Last byte boundary (63 bytes 0xFF, 1 byte 0x01)
{
var hasher = Hasher.init(.{});
var input: [64]u8 = undefined;
@memset(input[0..63], 0xFF);
input[63] = 0x01;
hasher.update(&input);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0xd2, 0xe5, 0x8c, 0x51, 0x39, 0xb5, 0x91, 0x64, 0xd2, 0xdb, 0x26, 0x49, 0x32, 0x50, 0x7d, 0x4e, 0x6d, 0xac, 0xef, 0x30, 0x76, 0x83, 0x12, 0x67, 0x4a, 0x9c, 0x70, 0x35, 0x87, 0xdf, 0xa9, 0x64 };
try std.testing.expectEqualSlices(u8, &expected, &output);
}

// Test 7: Last byte boundary variant (63 bytes 0xFF, 1 byte 0x02)
{
var hasher = Hasher.init(.{});
var input: [64]u8 = undefined;
@memset(input[0..63], 0xFF);
input[63] = 0x02;
hasher.update(&input);

var output: [32]u8 = undefined;
hasher.final(&output);

const expected = [_]u8{ 0xc7, 0xed, 0x40, 0x1c, 0x2c, 0x03, 0x7e, 0x29, 0x3d, 0xb7, 0x76, 0x3f, 0xf2, 0xa7, 0x49, 0x39, 0xec, 0x47, 0x52, 0x3e, 0x5c, 0xeb, 0xad, 0x34, 0xe7, 0x4b, 0x00, 0x74, 0xf5, 0x01, 0xd4, 0x43 };
try std.testing.expectEqualSlices(u8, &expected, &output);
}
}
Loading
Loading