@@ -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" , .{});
0 commit comments