Skip to content

Commit 331e4c2

Browse files
authored
chore: Code cleanup (#95)
2 parents 0871b72 + 2780935 commit 331e4c2

File tree

10 files changed

+70
-171
lines changed

10 files changed

+70
-171
lines changed

benchmark/zig_benchmark/src/cross_lang_zig_tool.zig

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,12 @@ fn signCommand(allocator: Allocator, message: []const u8, epoch: u32, lifetime:
240240

241241
var scheme: *hash_zig.GeneralizedXMSSSignatureScheme = undefined;
242242
const keypair: hash_zig.GeneralizedXMSSSignatureScheme.KeyGenResult = blk: {
243+
// For 2^8 lifetime, always regenerate from seed to avoid epoch configuration issues
244+
// The keygen -> sign flow for 2^8 can have mismatched active epochs in the SSZ file
245+
const skip_ssz_for_2_8 = (lifetime == .lifetime_2_8);
246+
243247
// Try to load SSZ secret key first if use_ssz is true and file exists
244-
if (use_ssz) {
248+
if (use_ssz and !skip_ssz_for_2_8) {
245249
if (std.fs.cwd().readFileAlloc(allocator, "tmp/zig_sk.ssz", std.math.maxInt(usize))) |sk_ssz| {
246250
defer allocator.free(sk_ssz);
247251

@@ -327,7 +331,6 @@ fn signCommand(allocator: Allocator, message: []const u8, epoch: u32, lifetime:
327331
};
328332
defer seed_file.close();
329333

330-
// Read seed hex string
331334
var buf: [64]u8 = undefined;
332335
const read_len = try seed_file.readAll(&buf);
333336
const hex_slice = buf[0..read_len];
@@ -365,18 +368,13 @@ fn signCommand(allocator: Allocator, message: []const u8, epoch: u32, lifetime:
365368

366369
const sk_data = try hash_zig.serialization.deserializeSecretKeyData(allocator, sk_json);
367370

368-
// Use the original seed (not PRF key) to ensure RNG state matches original keygen
369-
// The PRF key was generated from the seed, so we need to start from the seed
370-
// and consume RNG state to match where we were after generating parameter and PRF key
371371
const seed_file = std.fs.cwd().openFile("tmp/zig_seed.hex", .{}) catch {
372-
// If seed file is missing, fall back to using PRF key as seed (may not match exactly)
373372
scheme = try hash_zig.GeneralizedXMSSSignatureScheme.initWithSeed(allocator, lifetime, sk_data.prf_key);
374373
const kp = try scheme.keyGenWithParameter(sk_data.activation_epoch, sk_data.num_active_epochs, sk_data.parameter, sk_data.prf_key, false);
375374
break :blk kp;
376375
};
377376
defer seed_file.close();
378377

379-
// Read seed hex string
380378
var seed_buf: [64]u8 = undefined;
381379
const seed_read_len = try seed_file.readAll(&seed_buf);
382380
const seed_hex_slice = seed_buf[0..seed_read_len];
@@ -387,47 +385,13 @@ fn signCommand(allocator: Allocator, message: []const u8, epoch: u32, lifetime:
387385
}
388386
_ = try std.fmt.hexToBytes(&seed, seed_hex_slice);
389387

390-
// Initialize with original seed to match RNG state from keygen
391388
scheme = try hash_zig.GeneralizedXMSSSignatureScheme.initWithSeed(allocator, lifetime, seed);
392389

393-
// CRITICAL: We need to match the RNG state exactly as it was when keyGenWithParameter
394-
// was called from keyGen(). In keyGen(), the flow is:
395-
// 1. generateRandomParameter() - peeks 20 bytes (doesn't consume)
396-
// 2. generateRandomPRFKey() - consumes 32 bytes
397-
// 3. keyGenWithParameter() - consumes another 32 bytes (to match state after step 2)
398-
//
399-
// But wait - that's wrong! When keyGenWithParameter is called from keyGen(), the RNG
400-
// state is already after consuming 32 bytes. So keyGenWithParameter shouldn't consume
401-
// another 32 bytes when called from keyGen(). But it does, which means it's consuming
402-
// 64 bytes total when called from keyGen().
403-
//
404-
// Actually, I think the issue is that keyGenWithParameter is designed to be called
405-
// directly (not from keyGen()), so it consumes 32 bytes to match the state after
406-
// parameter/PRF key generation. But when called from keyGen(), this causes double
407-
// consumption.
408-
//
409-
// For now, let's NOT consume here, because keyGenWithParameter will consume 32 bytes
410-
// internally. But we need to account for the peek (20 bytes) and PRF key (32 bytes).
411-
// Actually, the peek doesn't consume, so we just need to consume 32 bytes for the PRF key.
412-
// But keyGenWithParameter already does that, so we shouldn't consume here.
413-
//
414-
// Wait, let me re-read the code. keyGenWithParameter consumes 32 bytes to match the
415-
// state AFTER parameter/PRF key generation. So when we call it directly, we need to
416-
// have consumed 32 bytes already. But we're starting fresh, so we need to consume
417-
// 32 bytes to get to the state after PRF key generation.
418-
// CRITICAL: Simulate the exact RNG consumption from keyGen():
419-
// 1. generateRandomParameter() - peeks 20 bytes (doesn't consume RNG offset)
420-
// 2. generateRandomPRFKey() - consumes 32 bytes (advances RNG offset)
421-
//
422-
// Even though peek doesn't consume, we should call the actual function to ensure
423-
// the RNG state is in the exact same condition. The peek reads from the current
424-
// offset without advancing it, but we want to ensure we're reading from the same
425-
// position in the RNG stream.
426-
_ = try scheme.generateRandomParameter(); // Peek at 20 bytes (doesn't consume)
390+
// Simulate RNG consumption from keyGen: peek parameter, consume PRF key
391+
_ = try scheme.generateRandomParameter();
427392
var dummy_prf_key: [32]u8 = undefined;
428-
scheme.rng.fill(&dummy_prf_key); // Consume 32 bytes to match generateRandomPRFKey()
393+
scheme.rng.fill(&dummy_prf_key);
429394

430-
// We've already consumed 32 bytes to match PRF key generation, so pass true
431395
const kp = try scheme.keyGenWithParameter(sk_data.activation_epoch, sk_data.num_active_epochs, sk_data.parameter, sk_data.prf_key, true);
432396
break :blk kp;
433397
};
@@ -462,7 +426,6 @@ fn signCommand(allocator: Allocator, message: []const u8, epoch: u32, lifetime:
462426
}
463427

464428
if (use_ssz) {
465-
// IMPORTANT: Also update the public key SSZ to match the regenerated keypair.
466429
const pk_bytes = try keypair.public_key.toBytes(allocator);
467430
defer allocator.free(pk_bytes);
468431
var pk_file = try std.fs.cwd().createFile("tmp/zig_pk.ssz", .{});
@@ -478,9 +441,6 @@ fn signCommand(allocator: Allocator, message: []const u8, epoch: u32, lifetime:
478441
try sig_file.writeAll(sig_bytes);
479442
std.debug.print("✅ Signature saved to tmp/zig_sig.ssz ({} bytes)\n", .{sig_bytes.len});
480443
} else {
481-
// IMPORTANT: Also update the public key JSON to match the regenerated keypair.
482-
// This ensures that verification (in both Zig and Rust) uses a public key that
483-
// is consistent with the trees/roots used during signing.
484444
const pk_json = try hash_zig.serialization.serializePublicKey(allocator, &keypair.public_key);
485445
defer allocator.free(pk_json);
486446
var pk_file = try std.fs.cwd().createFile("tmp/zig_pk.json", .{});

benchmark/zig_benchmark/src/remote_hash_tool.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pub fn writeSignatureBincode(path: []const u8, signature: *const hash_zig.Genera
164164
}
165165

166166
// Write rho (7 u32 values in CANONICAL form, no length prefix for fixed array)
167-
// CRITICAL: Rust's bincode serializes field elements in CANONICAL form (matching path and hashes)
167+
// Rust's bincode serializes field elements in CANONICAL form
168168
// This must match Rust's FieldArray serialization which uses as_canonical_u32()
169169
const rho = signature.getRho();
170170
if (rand_len > rho.len) return BincodeError.InvalidRandLength;
@@ -217,15 +217,15 @@ pub fn readSignatureBincode(path: []const u8, allocator: std.mem.Allocator, rand
217217

218218
// Read path nodes (each has: HASH_LEN u32 values in CANONICAL form, NO length prefix for fixed arrays)
219219
// Rust's bincode serializes Vec<FieldArray<N>> as: Vec length + elements directly (no per-array length)
220-
// CRITICAL: Rust writes FieldArray<HASH_LEN> which serializes exactly HASH_LEN elements using as_canonical_u32()
220+
// Rust writes FieldArray<HASH_LEN> which serializes exactly HASH_LEN elements
221221
// For lifetime 2^8/2^32: HASH_LEN=8, Rust writes 8 elements
222222
// For lifetime 2^18: HASH_LEN=7, Rust writes 7 elements
223223
// We must read exactly hash_len elements to match Rust's serialization
224224
var path_nodes = try allocator.alloc([8]FieldElement, path_len);
225225
errdefer allocator.free(path_nodes);
226226
for (0..path_len) |i| {
227227
// Read array elements in canonical form (fixed-size array, no length prefix)
228-
// CRITICAL: Read exactly hash_len elements (matching Rust's FieldArray<HASH_LEN>)
228+
// Read exactly hash_len elements (matching Rust's FieldArray<HASH_LEN>)
229229
for (0..hash_len) |j| {
230230
const canonical = try reader.readInt(u32, .little);
231231
path_nodes[i][j] = FieldElement.fromCanonical(canonical);
@@ -241,7 +241,7 @@ pub fn readSignatureBincode(path: []const u8, allocator: std.mem.Allocator, rand
241241
allocator.free(path_nodes);
242242

243243
// Read rho (rand_len u32 values in CANONICAL form, no length prefix for fixed array)
244-
// CRITICAL: Rust's bincode serializes field elements in CANONICAL form (matching path and hashes)
244+
// Rust's bincode serializes field elements in CANONICAL form
245245
// This must match Rust's FieldArray serialization which uses as_canonical_u32()
246246
// For lifetime 2^8/2^32: rand_len=7, for lifetime 2^18: rand_len=6
247247
if (rand_len > 7) {
@@ -268,15 +268,15 @@ pub fn readSignatureBincode(path: []const u8, allocator: std.mem.Allocator, rand
268268

269269
// Read hashes (each has: HASH_LEN u32 values in CANONICAL form, NO length prefix for fixed arrays)
270270
// Rust's bincode serializes Vec<FieldArray<N>> as: Vec length + elements directly (no per-array length)
271-
// CRITICAL: Rust writes FieldArray<HASH_LEN> which serializes exactly HASH_LEN elements using as_canonical_u32()
271+
// Rust writes FieldArray<HASH_LEN> which serializes exactly HASH_LEN elements
272272
// For lifetime 2^8/2^32: HASH_LEN=8, Rust writes 8 elements
273273
// For lifetime 2^18: HASH_LEN=7, Rust writes 7 elements
274274
// We must read exactly hash_len elements to match Rust's serialization
275275
var hashes_tmp = try allocator.alloc([8]FieldElement, hashes_len);
276276
errdefer allocator.free(hashes_tmp);
277277
for (0..hashes_len) |i| {
278278
// Read array elements in canonical form (fixed-size array, no length prefix)
279-
// CRITICAL: Read exactly hash_len elements (matching Rust's FieldArray<HASH_LEN>)
279+
// Read exactly hash_len elements (matching Rust's FieldArray<HASH_LEN>)
280280
for (0..hash_len) |j| {
281281
const canonical = try reader.readInt(u32, .little);
282282
hashes_tmp[i][j] = FieldElement.fromCanonical(canonical);

investigations/test/rust_compatibility_test.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const std = @import("std");
55
const log = @import("hash-zig").utils.log;
66
const hash_zig = @import("hash-zig");
77

8-
// CRITICAL: Comprehensive Rust compatibility test
8+
// Comprehensive Rust compatibility test
99
test "rust compatibility: GeneralizedXMSS validation (CRITICAL)" {
1010
const allocator = std.testing.allocator;
1111

src/hash/poseidon2_hash_simd.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub const Poseidon2SIMD = struct {
4141
/// Input: packed_input is [element][lane] format (vertical packing)
4242
/// Output: packed_output is [element][lane] format
4343
///
44-
/// CRITICAL OPTIMIZATION: Writes to pre-allocated output buffer instead of allocating.
44+
/// Writes to pre-allocated output buffer instead of allocating.
4545
/// This matches Rust's approach of returning fixed-size stack arrays, eliminating
4646
/// 114,688 allocations in chain walking (64 chains × 7 steps × 256 batches).
4747
pub fn compress16SIMD(
@@ -187,7 +187,7 @@ pub const Poseidon2SIMD = struct {
187187
/// Input: packed_input is [element][lane] format (vertical packing)
188188
/// Output: packed_output is [element][lane] format
189189
///
190-
/// CRITICAL OPTIMIZATION: Writes to pre-allocated output buffer instead of allocating.
190+
/// Writes to pre-allocated output buffer instead of allocating.
191191
/// This matches Rust's approach of returning fixed-size stack arrays.
192192
pub fn compress24SIMD(
193193
self: *Poseidon2SIMD,

src/poseidon2/poseidon2.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ pub fn sbox(x: F) F {
142142
}
143143

144144
// Apply MDS matrix to 4 elements (exact Plonky3 logic from apply_mat4)
145-
// CRITICAL FIX: Rust uses .clone() to preserve original values, so we must store them first
145+
// Rust uses .clone() to preserve original values, so we must store them first
146146
// Matrix: [ 2 3 1 1 ]
147147
// [ 1 2 3 1 ]
148148
// [ 1 1 2 3 ]
@@ -520,7 +520,7 @@ pub fn poseidon2_24_plonky3(state: []F) void {
520520
// FIX: Rust applies MDS light BEFORE the first external round for 24-width!
521521
// This matches Rust's external_initial_permute_state which calls mds_light_permutation first.
522522
pub fn poseidon2_24_plonky3_with_mds_light(state: []F, apply_mds_light: bool) void {
523-
// CRITICAL FIX: Rust applies MDS light BEFORE the first external round for 24-width!
523+
// Rust applies MDS light BEFORE the first external round for 24-width
524524
// This matches Rust's external_initial_permute_state behavior.
525525
// For 24-width, we should ALWAYS apply MDS light first (matching Rust).
526526
// The apply_mds_light parameter is kept for backward compatibility but should be true for 24-width.
@@ -529,7 +529,7 @@ pub fn poseidon2_24_plonky3_with_mds_light(state: []F, apply_mds_light: bool) vo
529529
}
530530

531531
// Initial external rounds (4 rounds)
532-
// CRITICAL: Rust's external_terminal_permute_state applies MDS light INSIDE each round
532+
// Rust's external_terminal_permute_state applies MDS light INSIDE each round
533533
// So each external round does: add_rc_and_sbox, then mds_light_permutation
534534
// NOT: add_rc_and_sbox, then full MDS matrix
535535
for (0..4) |i| {
@@ -541,7 +541,7 @@ pub fn poseidon2_24_plonky3_with_mds_light(state: []F, apply_mds_light: bool) vo
541541
for (state) |*elem| {
542542
elem.* = sbox(elem.*);
543543
}
544-
// CRITICAL FIX: Apply MDS light (not full MDS matrix) - matching Rust's external_terminal_permute_state
544+
// Apply MDS light (not full MDS matrix) - matching Rust's external_terminal_permute_state
545545
mds_light_permutation_24(state);
546546
}
547547

@@ -551,7 +551,7 @@ pub fn poseidon2_24_plonky3_with_mds_light(state: []F, apply_mds_light: bool) vo
551551
}
552552

553553
// Final external rounds (4 rounds)
554-
// CRITICAL: Rust's external_terminal_permute_state applies MDS light INSIDE each round
554+
// Rust's external_terminal_permute_state applies MDS light INSIDE each round
555555
// So each external round does: add_rc_and_sbox, then mds_light_permutation
556556
// NOT: add_rc_and_sbox, then full MDS matrix
557557
for (0..4) |i| {
@@ -563,7 +563,7 @@ pub fn poseidon2_24_plonky3_with_mds_light(state: []F, apply_mds_light: bool) vo
563563
for (state) |*elem| {
564564
elem.* = sbox(elem.*);
565565
}
566-
// CRITICAL FIX: Apply MDS light (not full MDS matrix) - matching Rust's external_terminal_permute_state
566+
// Apply MDS light (not full MDS matrix) - matching Rust's external_terminal_permute_state
567567
mds_light_permutation_24(state);
568568
}
569569
}

src/prf/shake_prf_to_field.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const crypto = std.crypto;
66
const plonky3_field = @import("../poseidon2/plonky3_field.zig");
77

88
// Constants matching Rust implementation
9-
// CRITICAL: Rust uses 16 bytes per FE (reads as u128), not 8!
9+
// Rust uses 16 bytes per FE (reads as u128), not 8
1010
const PRF_BYTES_PER_FE: usize = 16;
1111
const KEY_LENGTH: usize = 32; // 32 bytes
1212
const MESSAGE_LENGTH: usize = 32; // From Rust hash-sig

0 commit comments

Comments
 (0)