From 01939c173afb0f3993c4b8a75edb66f25c008d72 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Sun, 22 Feb 2026 11:48:46 +0000 Subject: [PATCH 01/12] feat: pre-generated test keys for faster CI - Add keygen command to zeam-tools for generating XMSS key pairs - Add test-keys submodule (anshalshuklabot/zeam-test-keys) with 32 pre-generated keys - Update getTestKeyManager to load pre-generated keys from disk (near-instant) - Falls back to runtime generation if test-keys submodule not initialized - build.zig: add xmss and types imports to tools target Each XMSS key takes ~1 min to generate at runtime. Loading 32 pre-generated keys from SSZ files is near-instant, significantly speeding up CI and tests. --- .gitmodules | 3 + build.zig | 4 + pkgs/key-manager/src/lib.zig | 69 +++++++++++ pkgs/tools/src/main.zig | 143 +++++++++++++++++++++ plans/feat-pregenerated-keys.md | 213 ++++++++++++++++++++++++++++++++ test-keys | 1 + 6 files changed, 433 insertions(+) create mode 100644 plans/feat-pregenerated-keys.md create mode 160000 test-keys diff --git a/.gitmodules b/.gitmodules index fbc272db1..a659cfd69 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "leanSpec"] path = leanSpec url = https://github.com/leanEthereum/leanSpec +[submodule "test-keys"] + path = test-keys + url = https://github.com/anshalshuklabot/zeam-test-keys.git diff --git a/build.zig b/build.zig index b6ed881e1..b6c06bc7e 100644 --- a/build.zig +++ b/build.zig @@ -419,6 +419,8 @@ pub fn build(b: *Builder) !void { tools_cli_exe.root_module.addImport("enr", enr); tools_cli_exe.root_module.addImport("build_options", build_options_module); tools_cli_exe.root_module.addImport("simargs", simargs); + tools_cli_exe.root_module.addImport("@zeam/xmss", zeam_xmss); + tools_cli_exe.root_module.addImport("@zeam/types", zeam_types); const install_tools_cli = b.addInstallArtifact(tools_cli_exe, .{}); tools_step.dependOn(&install_tools_cli.step); @@ -598,6 +600,8 @@ pub fn build(b: *Builder) !void { .root_module = tools_cli_exe.root_module, }); tools_cli_tests.root_module.addImport("enr", enr); + tools_cli_tests.root_module.addImport("@zeam/xmss", zeam_xmss); + tools_cli_tests.root_module.addImport("@zeam/types", zeam_types); const run_tools_cli_test = b.addRunArtifact(tools_cli_tests); setTestRunLabelFromCompile(b, run_tools_cli_test, tools_cli_tests); tools_test_step.dependOn(&run_tools_cli_test.step); diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index ce3e933d3..518f9bd12 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -162,11 +162,80 @@ pub const KeyManager = struct { } }; +/// Maximum size of a serialized XMSS private key (20MB). +const MAX_SK_SIZE = 1024 * 1024 * 20; + +/// Maximum size of a serialized XMSS public key (256 bytes). +const MAX_PK_SIZE = 256; + +/// Try to load pre-generated test keys from the test-keys submodule. +/// +/// Keys are stored as SSZ-encoded files at test-keys/hash-sig-keys/validator_{i}_sk.ssz +/// and validator_{i}_pk.ssz. Loading from disk is near-instant compared to generating +/// XMSS keys at runtime (which takes minutes for 32 validators). +fn loadPreGeneratedKeys( + allocator: Allocator, + num_validators: usize, +) !KeyManager { + var key_manager = KeyManager.init(allocator); + errdefer key_manager.deinit(); + + for (0..num_validators) |i| { + // Build file paths + var sk_path_buf: [256]u8 = undefined; + const sk_path = std.fmt.bufPrint(&sk_path_buf, "test-keys/hash-sig-keys/validator_{d}_sk.ssz", .{i}) catch unreachable; + + var pk_path_buf: [256]u8 = undefined; + const pk_path = std.fmt.bufPrint(&pk_path_buf, "test-keys/hash-sig-keys/validator_{d}_pk.ssz", .{i}) catch unreachable; + + // Read private key + var sk_file = std.fs.cwd().openFile(sk_path, .{}) catch { + return error.KeyGenerationFailed; + }; + defer sk_file.close(); + const sk_data = sk_file.readToEndAlloc(allocator, MAX_SK_SIZE) catch { + return error.KeyGenerationFailed; + }; + defer allocator.free(sk_data); + + // Read public key + var pk_file = std.fs.cwd().openFile(pk_path, .{}) catch { + return error.KeyGenerationFailed; + }; + defer pk_file.close(); + const pk_data = pk_file.readToEndAlloc(allocator, MAX_PK_SIZE) catch { + return error.KeyGenerationFailed; + }; + defer allocator.free(pk_data); + + // Reconstruct keypair from SSZ + var keypair = xmss.KeyPair.fromSsz(allocator, sk_data, pk_data) catch { + return error.KeyGenerationFailed; + }; + errdefer keypair.deinit(); + + try key_manager.addKeypair(i, keypair); + } + + std.debug.print("Loaded {d} pre-generated test keys from test-keys/\n", .{num_validators}); + return key_manager; +} + pub fn getTestKeyManager( allocator: Allocator, num_validators: usize, max_slot: usize, ) !KeyManager { + // Try loading pre-generated keys first (fast path). + // Falls back to runtime generation if files don't exist. + if (num_validators <= 32) { + if (loadPreGeneratedKeys(allocator, num_validators)) |km| { + return km; + } else |_| { + std.debug.print("Pre-generated keys not found, falling back to runtime generation\n", .{}); + } + } + var key_manager = KeyManager.init(allocator); key_manager.owns_keypairs = false; errdefer key_manager.deinit(); diff --git a/pkgs/tools/src/main.zig b/pkgs/tools/src/main.zig index 44d52fd66..43b9c25f9 100644 --- a/pkgs/tools/src/main.zig +++ b/pkgs/tools/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const enr = @import("enr"); const build_options = @import("build_options"); const simargs = @import("simargs"); +const xmss = @import("@zeam/xmss"); pub const max_enr_txt_size = enr.max_enr_txt_size; @@ -11,9 +12,11 @@ const ToolsArgs = struct { __commands__: union(enum) { enrgen: ENRGenCmd, + keygen: KeyGenCmd, pub const __messages__ = .{ .enrgen = "Generate a new ENR (Ethereum Node Record)", + .keygen = "Generate pre-computed XMSS test validator keys", }; }, @@ -50,6 +53,27 @@ const ToolsArgs = struct { .help = "Show help information for the enrgen command", }; }; + + const KeyGenCmd = struct { + @"num-validators": usize = 32, + @"num-active-epochs": usize = 1000, + @"output-dir": []const u8 = "test-keys", + help: bool = false, + + pub const __shorts__ = .{ + .@"num-validators" = .n, + .@"num-active-epochs" = .e, + .@"output-dir" = .o, + .help = .h, + }; + + pub const __messages__ = .{ + .@"num-validators" = "Number of validator key pairs to generate (default: 32)", + .@"num-active-epochs" = "Number of active epochs for each key (default: 1000)", + .@"output-dir" = "Output directory for generated keys (default: test-keys)", + .help = "Show help information for the keygen command", + }; + }; }; pub fn main() !void { @@ -86,6 +110,12 @@ pub fn main() !void { defer enr.deinitGlobalSecp256k1Ctx(); switch (opts.args.__commands__) { + .keygen => |cmd| { + handleKeyGen(allocator, cmd) catch |err| { + std.debug.print("Error generating keys: {}\n", .{err}); + std.process.exit(1); + }; + }, .enrgen => |cmd| { handleENRGen(cmd) catch |err| switch (err) { error.EmptySecretKey => { @@ -113,6 +143,119 @@ pub fn main() !void { } } +fn handleKeyGen(allocator: std.mem.Allocator, cmd: ToolsArgs.KeyGenCmd) !void { + const num_validators = cmd.@"num-validators"; + const num_active_epochs = cmd.@"num-active-epochs"; + const output_dir = cmd.@"output-dir"; + + std.debug.print("Generating {d} validator keys with {d} active epochs...\n", .{ num_validators, num_active_epochs }); + std.debug.print("Output directory: {s}\n", .{output_dir}); + + // Create output directories + const hash_sig_dir = try std.fmt.allocPrint(allocator, "{s}/hash-sig-keys", .{output_dir}); + defer allocator.free(hash_sig_dir); + + std.fs.cwd().makePath(hash_sig_dir) catch |err| { + std.debug.print("Error creating directory {s}: {}\n", .{ hash_sig_dir, err }); + return err; + }; + + // Allocate buffers for serialization + // Private keys can be very large (~5-10MB for XMSS) + const sk_buffer = try allocator.alloc(u8, 1024 * 1024 * 20); // 20MB + defer allocator.free(sk_buffer); + var pk_buffer: [256]u8 = undefined; + + // Open manifest file + const manifest_path = try std.fmt.allocPrint(allocator, "{s}/validator-keys-manifest.yaml", .{output_dir}); + defer allocator.free(manifest_path); + const manifest_file = try std.fs.cwd().createFile(manifest_path, .{}); + defer manifest_file.close(); + + var manifest_buf: [4096]u8 = undefined; + var manifest_writer = manifest_file.writer(&manifest_buf); + + // Write manifest header + try manifest_writer.interface.print( + \\key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8 + \\hash_function: Poseidon2 + \\encoding: TargetSum + \\lifetime: {d} + \\num_active_epochs: {d} + \\num_validators: {d} + \\validators: + \\ + , .{ num_active_epochs, num_active_epochs, num_validators }); + + for (0..num_validators) |i| { + std.debug.print(" Generating validator {d}/{d}...\n", .{ i + 1, num_validators }); + + // Generate keypair with deterministic seed + const seed = try std.fmt.allocPrint(allocator, "test_validator_{d}", .{i}); + defer allocator.free(seed); + + var keypair = try xmss.KeyPair.generate(allocator, seed, 0, num_active_epochs); + defer keypair.deinit(); + + // Serialize public key + const pk_len = try keypair.pubkeyToBytes(&pk_buffer); + + // Serialize private key + const sk_len = try keypair.privkeyToBytes(sk_buffer); + + std.debug.print(" PK size: {d} bytes, SK size: {d} bytes\n", .{ pk_len, sk_len }); + + // Write private key file + const sk_filename = try std.fmt.allocPrint(allocator, "validator_{d}_sk.ssz", .{i}); + defer allocator.free(sk_filename); + const sk_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ hash_sig_dir, sk_filename }); + defer allocator.free(sk_path); + + const sk_file = try std.fs.cwd().createFile(sk_path, .{}); + defer sk_file.close(); + var sk_write_buf: [65536]u8 = undefined; + var sk_writer = sk_file.writer(&sk_write_buf); + try sk_writer.interface.writeAll(sk_buffer[0..sk_len]); + try sk_writer.interface.flush(); + + // Write public key file + const pk_filename = try std.fmt.allocPrint(allocator, "validator_{d}_pk.ssz", .{i}); + defer allocator.free(pk_filename); + const pk_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ hash_sig_dir, pk_filename }); + defer allocator.free(pk_path); + + const pk_file = try std.fs.cwd().createFile(pk_path, .{}); + defer pk_file.close(); + var pk_write_buf: [4096]u8 = undefined; + var pk_writer = pk_file.writer(&pk_write_buf); + try pk_writer.interface.writeAll(pk_buffer[0..pk_len]); + try pk_writer.interface.flush(); + + // Write manifest entry with pubkey as hex + // Format pubkey bytes as hex string + var hex_buf: [512]u8 = undefined; + const hex_len = pk_len * 2; + for (pk_buffer[0..pk_len], 0..) |byte, j| { + const high = byte >> 4; + const low = byte & 0x0f; + hex_buf[j * 2] = if (high < 10) '0' + high else 'a' + high - 10; + hex_buf[j * 2 + 1] = if (low < 10) '0' + low else 'a' + low - 10; + } + + try manifest_writer.interface.print( + \\- index: {d} + \\ pubkey_hex: "0x{s}" + \\ privkey_file: {s} + \\ + , .{ i, hex_buf[0..hex_len], sk_filename }); + } + + try manifest_writer.interface.flush(); + + std.debug.print("\nDone! Generated {d} keys in {s}/\n", .{ num_validators, output_dir }); + std.debug.print("Manifest written to {s}\n", .{manifest_path}); +} + fn handleENRGen(cmd: ToolsArgs.ENRGenCmd) !void { if (cmd.sk.len == 0) { return error.EmptySecretKey; diff --git a/plans/feat-pregenerated-keys.md b/plans/feat-pregenerated-keys.md new file mode 100644 index 000000000..9c868dd96 --- /dev/null +++ b/plans/feat-pregenerated-keys.md @@ -0,0 +1,213 @@ +# Plan: Pre-generated Validator Keys for CI + +## Problem + +XMSS key generation via `hashsig_keypair_generate` (Rust FFI) is extremely slow. Every CI run and every `getTestKeyManager` call generates keys from scratch. For 3 validators with 1000 active epochs, this adds significant time to: + +1. **Unit tests** — any test touching key-manager or state-transition with signatures +2. **Simtests** — the `beam` CLI command generates 3 validators inline via `getTestKeyManager` +3. **Spec tests** — fixture runners that need signed blocks + +## Current Flow + +``` +CLI main.zig (beam command) + → getTestKeyManager(allocator, 3, 1000) + → for each validator: + → getOrCreateCachedKeyPair(i, 1000) + → KeyPair.generate(seed="test_validator_{i}", activation=0, epochs=1000) + → hashsig_keypair_generate() [Rust FFI — SLOW] +``` + +There IS an in-memory cache (`global_test_key_pair_cache`) but it only survives within a single process — doesn't help across test runs or CI. + +## Proposed Solution + +Pre-generate 32 validator key pairs as SSZ files, commit them to the repo, and load them instead of generating at runtime. + +### Key Format (matching leanSpec/ream structure) + +``` +resources/test-keys/ +├── validator-keys-manifest.yaml +└── hash-sig-keys/ + ├── validator_0_sk.ssz # Private key (SSZ-encoded) + ├── validator_0_pk.ssz # Public key (SSZ-encoded) + ├── validator_1_sk.ssz + ├── validator_1_pk.ssz + ├── ... + ├── validator_31_sk.ssz + └── validator_31_pk.ssz +``` + +### Manifest Format (compatible with leanSpec) + +```yaml +key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8 +hash_function: Poseidon2 +encoding: TargetSum +lifetime: 1000 +log_num_active_epochs: 10 +num_active_epochs: 1000 +num_validators: 32 +validators: + - index: 0 + pubkey_hex: "0x..." + privkey_file: validator_0_sk.ssz + - index: 1 + pubkey_hex: "0x..." + privkey_file: validator_1_sk.ssz + # ... up to 31 +``` + +## Implementation Steps + +### Step 1: Add `keygen` command to zeam-tools + +Add a `keygen` subcommand to `pkgs/tools/src/main.zig` that: + +1. Takes `--num-validators N`, `--num-active-epochs E`, `--output-dir DIR` +2. Generates N keypairs using deterministic seeds (`test_validator_{i}`) +3. Serializes each keypair to `{dir}/hash-sig-keys/validator_{i}_sk.ssz` and `_pk.ssz` +4. Writes `{dir}/validator-keys-manifest.yaml` with pubkey hex and metadata + +### Step 2: Generate and commit 32 keys + +```bash +zig build tools +./zig-out/bin/zeam-tools keygen --num-validators 32 --num-active-epochs 1000 --output-dir resources/test-keys +``` + +Commit `resources/test-keys/` to the repo. These are test keys with known seeds — no security concern. + +### Step 3: Update `getTestKeyManager` to load pre-generated keys + +Modify `pkgs/key-manager/src/lib.zig`: + +```zig +pub fn getTestKeyManager( + allocator: Allocator, + num_validators: usize, + max_slot: usize, +) !KeyManager { + // Try loading pre-generated keys first + if (num_validators <= 32) { + if (loadPreGeneratedKeys(allocator, num_validators)) |km| { + return km; + } else |_| { + // Fall through to runtime generation + } + } + // ... existing runtime generation as fallback +} + +fn loadPreGeneratedKeys(allocator: Allocator, num_validators: usize) !KeyManager { + var km = KeyManager.init(allocator); + errdefer km.deinit(); + + for (0..num_validators) |i| { + const sk_path = std.fmt.comptimePrint( + "resources/test-keys/hash-sig-keys/validator_{d}_sk.ssz", .{i} + ); + // ... read SSZ files and call KeyPair.fromSsz() + } + return km; +} +``` + +**Note**: Need to handle path resolution — keys are relative to repo root. Could use `@embedFile` to compile them in, or resolve at runtime relative to the executable. + +### Step 4: Option A — `@embedFile` (preferred for tests) + +Embed the SSZ key data at compile time so tests don't need to find files at runtime: + +```zig +const embedded_keys = struct { + const sk_0 = @embedFile("resources/test-keys/hash-sig-keys/validator_0_sk.ssz"); + const pk_0 = @embedFile("resources/test-keys/hash-sig-keys/validator_0_pk.ssz"); + // ... generate these with comptime +}; +``` + +But 32 keys × ~10MB per private key = ~320MB embedded — too large. + +### Step 4: Option B — Runtime file loading (preferred) + +Load from disk relative to a known path. The build system can pass the resource path as a build option: + +```zig +// In build.zig +const test_keys_path = b.option([]const u8, "test-keys-path", "Path to pre-generated test keys") + orelse "resources/test-keys"; +``` + +### Step 5: Update CI workflow + +No changes needed if keys are committed to the repo and `getTestKeyManager` loads them automatically. The existing CI steps will just be faster. + +### Step 6: Update `beam` CLI command + +The `beam` command in `main.zig` currently calls `getTestKeyManager(allocator, 3, 1000)`. After this change, it will automatically use pre-generated keys when available — no CLI changes needed. + +## Key Storage: Separate Repo + +Keys will be stored in a separate repo (like leanSpec's approach with `leanEthereum/leansig-test-keys`): + +- **Repo**: `blockblaz/zeam-test-keys` (or under leanEthereum org if shared across clients) +- **Added to zeam as a git submodule** at `test-keys/` +- **CI caches** the submodule to avoid cloning every time + +### Submodule Integration +```bash +git submodule add https://github.com/blockblaz/zeam-test-keys.git test-keys +``` + +CI workflow update: +```yaml +- uses: actions/checkout@v4 + with: + submodules: recursive # Already done for leanSpec +``` + +### `getTestKeyManager` loads from submodule: +```zig +fn loadPreGeneratedKeys(allocator: Allocator, num_validators: usize) !KeyManager { + var km = KeyManager.init(allocator); + for (0..num_validators) |i| { + // Path relative to repo root (zig build runs from there) + const sk = try loadFile(allocator, "test-keys/hash-sig-keys/validator_{d}_sk.ssz", i); + const pk = try loadFile(allocator, "test-keys/hash-sig-keys/validator_{d}_pk.ssz", i); + var keypair = try xmss.KeyPair.fromSsz(allocator, sk, pk); + try km.addKeypair(i, keypair); + } + return km; +} +``` + +## Callers of getTestKeyManager (all benefit) + +| Location | Validators | max_slot | +|----------|-----------|----------| +| `cli/src/main.zig` (beam cmd) | 3 | 1000 | +| `node/src/chain.zig` (test) | 4 | 3 | +| `node/src/node.zig` (test) | num_validators | 10 | +| `node/src/testing.zig` | configurable | configurable | +| `state-transition/src/mock.zig` | num_validators | numBlocks | +| `types/src/block_signatures_testing.zig` | num_validators | 10 | + +## Questions for Anshal + +1. **Repo org**: `blockblaz/zeam-test-keys` or `leanEthereum/zeam-test-keys`? Or reuse the existing `leanEthereum/leansig-test-keys`? + +2. **Seed determinism**: Currently using `"test_validator_{i}"` as seed. Keep this for reproducibility? + +3. **num_active_epochs**: 1000 matches the CLI default. Enough? + +4. **Manifest format**: Match leanSpec's `validator-keys-manifest.yaml` exactly for cross-client compatibility? + +## Impact + +- **CI speedup**: Key loading from SSZ is near-instant vs minutes for generation +- **Test reliability**: No more flaky timing issues from key generation +- **Compatibility**: Same key format as leanSpec/ream — keys can be shared across implementations +- **Repo size**: Zero impact on zeam repo (keys in separate repo as submodule) diff --git a/test-keys b/test-keys new file mode 160000 index 000000000..0b645ebd2 --- /dev/null +++ b/test-keys @@ -0,0 +1 @@ +Subproject commit 0b645ebd2302636330689de12afe3e4e8dfde3df From b049f8c23ba929c90dd3a2e220b6c5d7deac2278 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Sun, 22 Feb 2026 12:33:57 +0000 Subject: [PATCH 02/12] fix: improve pre-generated key loading - Search upward for test-keys/ directory (handles test runners in subdirs) - Load pre-generated keys for first 32 validators, generate rest at runtime - If 40 validators needed: loads 32 from disk + generates 8 at runtime - Fixes CI failure where keys weren't found --- pkgs/key-manager/src/lib.zig | 149 +++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 66 deletions(-) diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index 518f9bd12..c3ce6ae51 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -168,57 +168,58 @@ const MAX_SK_SIZE = 1024 * 1024 * 20; /// Maximum size of a serialized XMSS public key (256 bytes). const MAX_PK_SIZE = 256; -/// Try to load pre-generated test keys from the test-keys submodule. -/// -/// Keys are stored as SSZ-encoded files at test-keys/hash-sig-keys/validator_{i}_sk.ssz -/// and validator_{i}_pk.ssz. Loading from disk is near-instant compared to generating -/// XMSS keys at runtime (which takes minutes for 32 validators). -fn loadPreGeneratedKeys( - allocator: Allocator, - num_validators: usize, -) !KeyManager { - var key_manager = KeyManager.init(allocator); - errdefer key_manager.deinit(); - - for (0..num_validators) |i| { - // Build file paths - var sk_path_buf: [256]u8 = undefined; - const sk_path = std.fmt.bufPrint(&sk_path_buf, "test-keys/hash-sig-keys/validator_{d}_sk.ssz", .{i}) catch unreachable; - - var pk_path_buf: [256]u8 = undefined; - const pk_path = std.fmt.bufPrint(&pk_path_buf, "test-keys/hash-sig-keys/validator_{d}_pk.ssz", .{i}) catch unreachable; - - // Read private key - var sk_file = std.fs.cwd().openFile(sk_path, .{}) catch { - return error.KeyGenerationFailed; - }; - defer sk_file.close(); - const sk_data = sk_file.readToEndAlloc(allocator, MAX_SK_SIZE) catch { - return error.KeyGenerationFailed; - }; - defer allocator.free(sk_data); - - // Read public key - var pk_file = std.fs.cwd().openFile(pk_path, .{}) catch { - return error.KeyGenerationFailed; - }; - defer pk_file.close(); - const pk_data = pk_file.readToEndAlloc(allocator, MAX_PK_SIZE) catch { - return error.KeyGenerationFailed; - }; - defer allocator.free(pk_data); - - // Reconstruct keypair from SSZ - var keypair = xmss.KeyPair.fromSsz(allocator, sk_data, pk_data) catch { - return error.KeyGenerationFailed; - }; - errdefer keypair.deinit(); +/// Number of pre-generated test keys available in the test-keys submodule. +const NUM_PREGENERATED_KEYS: usize = 32; + +/// Known paths to search for the test-keys directory. +/// Zig test runners may execute from subdirectories, so we search upward. +const TEST_KEY_SEARCH_PATHS = [_][]const u8{ + "test-keys/hash-sig-keys", + "../test-keys/hash-sig-keys", + "../../test-keys/hash-sig-keys", + "../../../test-keys/hash-sig-keys", + "../../../../test-keys/hash-sig-keys", +}; - try key_manager.addKeypair(i, keypair); +/// Find the test-keys directory by searching known paths. +fn findTestKeysDir() ?[]const u8 { + for (TEST_KEY_SEARCH_PATHS) |path| { + if (std.fs.cwd().openDir(path, .{})) |dir| { + var d = dir; + d.close(); + return path; + } else |_| {} } + return null; +} - std.debug.print("Loaded {d} pre-generated test keys from test-keys/\n", .{num_validators}); - return key_manager; +/// Load a single pre-generated key pair from SSZ files on disk. +fn loadPreGeneratedKey( + allocator: Allocator, + keys_dir: []const u8, + index: usize, +) !xmss.KeyPair { + // Build file paths + var sk_path_buf: [512]u8 = undefined; + const sk_path = std.fmt.bufPrint(&sk_path_buf, "{s}/validator_{d}_sk.ssz", .{ keys_dir, index }) catch unreachable; + + var pk_path_buf: [512]u8 = undefined; + const pk_path = std.fmt.bufPrint(&pk_path_buf, "{s}/validator_{d}_pk.ssz", .{ keys_dir, index }) catch unreachable; + + // Read private key + var sk_file = try std.fs.cwd().openFile(sk_path, .{}); + defer sk_file.close(); + const sk_data = try sk_file.readToEndAlloc(allocator, MAX_SK_SIZE); + defer allocator.free(sk_data); + + // Read public key + var pk_file = try std.fs.cwd().openFile(pk_path, .{}); + defer pk_file.close(); + const pk_data = try pk_file.readToEndAlloc(allocator, MAX_PK_SIZE); + defer allocator.free(pk_data); + + // Reconstruct keypair from SSZ + return xmss.KeyPair.fromSsz(allocator, sk_data, pk_data); } pub fn getTestKeyManager( @@ -226,29 +227,45 @@ pub fn getTestKeyManager( num_validators: usize, max_slot: usize, ) !KeyManager { - // Try loading pre-generated keys first (fast path). - // Falls back to runtime generation if files don't exist. - if (num_validators <= 32) { - if (loadPreGeneratedKeys(allocator, num_validators)) |km| { - return km; - } else |_| { - std.debug.print("Pre-generated keys not found, falling back to runtime generation\n", .{}); + var key_manager = KeyManager.init(allocator); + errdefer key_manager.deinit(); + + // Determine how many keys we can load from pre-generated files + const keys_dir = findTestKeysDir(); + const num_preloaded = if (keys_dir != null) + @min(num_validators, NUM_PREGENERATED_KEYS) + else + 0; + + // Load pre-generated keys (fast path: near-instant from SSZ files) + if (keys_dir) |dir| { + for (0..num_preloaded) |i| { + const keypair = loadPreGeneratedKey(allocator, dir, i) catch |err| { + std.debug.print("Failed to load pre-generated key {d}: {}\n", .{ i, err }); + break; + }; + key_manager.addKeypair(i, keypair) catch |err| { + std.debug.print("Failed to add pre-generated key {d}: {}\n", .{ i, err }); + break; + }; } + std.debug.print("Loaded {d} pre-generated test keys from {s}\n", .{ num_preloaded, dir }); + } else { + std.debug.print("Pre-generated keys not found, generating all keys at runtime\n", .{}); } - var key_manager = KeyManager.init(allocator); - key_manager.owns_keypairs = false; - errdefer key_manager.deinit(); + // Generate remaining keys at runtime (for validators beyond the pre-generated set) + if (num_validators > num_preloaded) { + key_manager.owns_keypairs = false; // cached keypairs are not owned - var num_active_epochs = max_slot + 1; - // to reuse cached keypairs, gen for 10 since most tests ask for < 10 max slot including - // building mock chain for tests. otherwise getOrCreateCachedKeyPair might cleanup previous - // key generated for smaller life time - if (num_active_epochs < 10) num_active_epochs = 10; + var num_active_epochs = max_slot + 1; + if (num_active_epochs < 10) num_active_epochs = 10; - for (0..num_validators) |i| { - const keypair = try getOrCreateCachedKeyPair(i, num_active_epochs); - try key_manager.addKeypair(i, keypair); + for (num_preloaded..num_validators) |i| { + const keypair = try getOrCreateCachedKeyPair(i, num_active_epochs); + try key_manager.addKeypair(i, keypair); + } + std.debug.print("Generated {d} additional keys at runtime\n", .{num_validators - num_preloaded}); } return key_manager; From b9f87d0bc67a86263b06faee4003c8c19d6e0ce0 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Sun, 22 Feb 2026 13:07:19 +0000 Subject: [PATCH 03/12] ci: add submodules: recursive to all checkout steps Ensures test-keys submodule is available in every CI job. Fixes CACHE MISS in Dummy prove jobs where pre-generated keys weren't found. --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36dbfe175..983bfce2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Zig uses: mlugg/setup-zig@v2.0.5 @@ -82,6 +84,8 @@ jobs: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Zig uses: mlugg/setup-zig@v2.0.5 @@ -135,6 +139,8 @@ jobs: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Zig uses: mlugg/setup-zig@v2.0.5 @@ -261,6 +267,8 @@ jobs: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Zig uses: mlugg/setup-zig@v2.0.5 @@ -299,6 +307,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + submodules: recursive fetch-depth: 0 # Fetch full history to get git commit info - name: Set up Zig From bfa13c07d3d7ba2d78399932156bb23051b1ebaa Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Sun, 22 Feb 2026 13:51:47 +0000 Subject: [PATCH 04/12] fix: add Rust build dependency to tools target tools now imports @zeam/xmss which requires libhashsig_glue.a. Without this dependency, the tools binary and tests fail to link on CI where Rust libs aren't pre-built in the cache. --- build.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.zig b/build.zig index b6c06bc7e..1f3b1e821 100644 --- a/build.zig +++ b/build.zig @@ -421,6 +421,7 @@ pub fn build(b: *Builder) !void { tools_cli_exe.root_module.addImport("simargs", simargs); tools_cli_exe.root_module.addImport("@zeam/xmss", zeam_xmss); tools_cli_exe.root_module.addImport("@zeam/types", zeam_types); + tools_cli_exe.step.dependOn(&build_rust_lib_steps.step); const install_tools_cli = b.addInstallArtifact(tools_cli_exe, .{}); tools_step.dependOn(&install_tools_cli.step); @@ -602,6 +603,7 @@ pub fn build(b: *Builder) !void { tools_cli_tests.root_module.addImport("enr", enr); tools_cli_tests.root_module.addImport("@zeam/xmss", zeam_xmss); tools_cli_tests.root_module.addImport("@zeam/types", zeam_types); + tools_cli_tests.step.dependOn(&build_rust_lib_steps.step); const run_tools_cli_test = b.addRunArtifact(tools_cli_tests); setTestRunLabelFromCompile(b, run_tools_cli_test, tools_cli_tests); tools_test_step.dependOn(&run_tools_cli_test.step); From 9c665811f4f3bf5f1739a8deda8c897a93b3e2cf Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Sun, 22 Feb 2026 15:43:02 +0000 Subject: [PATCH 05/12] fix: use addRustGlueLib for tools target to link Rust static libs The tools executable was missing proper Rust library linking (libhashsig_glue.a, libmultisig_glue.a, liblibp2p_glue.a) and platform-specific frameworks (macOS). Replace manual step.dependOn with addRustGlueLib() which handles object files, linkLibC, linkSystemLibrary(unwind), and macOS frameworks. --- build.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 1f3b1e821..226c05bbe 100644 --- a/build.zig +++ b/build.zig @@ -421,7 +421,7 @@ pub fn build(b: *Builder) !void { tools_cli_exe.root_module.addImport("simargs", simargs); tools_cli_exe.root_module.addImport("@zeam/xmss", zeam_xmss); tools_cli_exe.root_module.addImport("@zeam/types", zeam_types); - tools_cli_exe.step.dependOn(&build_rust_lib_steps.step); + addRustGlueLib(b, tools_cli_exe, target, prover); const install_tools_cli = b.addInstallArtifact(tools_cli_exe, .{}); tools_step.dependOn(&install_tools_cli.step); @@ -603,7 +603,7 @@ pub fn build(b: *Builder) !void { tools_cli_tests.root_module.addImport("enr", enr); tools_cli_tests.root_module.addImport("@zeam/xmss", zeam_xmss); tools_cli_tests.root_module.addImport("@zeam/types", zeam_types); - tools_cli_tests.step.dependOn(&build_rust_lib_steps.step); + addRustGlueLib(b, tools_cli_tests, target, prover); const run_tools_cli_test = b.addRunArtifact(tools_cli_tests); setTestRunLabelFromCompile(b, run_tools_cli_test, tools_cli_tests); tools_test_step.dependOn(&run_tools_cli_test.step); From 50b2aea1ac9df70b9365008ec7211873a06bf19f Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Wed, 4 Mar 2026 12:14:44 +0000 Subject: [PATCH 06/12] fix: add Rust build step dependency for tools target addRustGlueLib only adds object files and link flags but not the step dependency that ensures Rust libs are built first. Both dependOn AND addRustGlueLib are needed, matching how all other targets are wired. --- build.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.zig b/build.zig index fef1e4665..9ca97dbf4 100644 --- a/build.zig +++ b/build.zig @@ -420,6 +420,7 @@ pub fn build(b: *Builder) !void { tools_cli_exe.root_module.addImport("simargs", simargs); tools_cli_exe.root_module.addImport("@zeam/xmss", zeam_xmss); tools_cli_exe.root_module.addImport("@zeam/types", zeam_types); + tools_cli_exe.step.dependOn(&build_rust_lib_steps.step); addRustGlueLib(b, tools_cli_exe, target, prover); const install_tools_cli = b.addInstallArtifact(tools_cli_exe, .{}); @@ -604,6 +605,7 @@ pub fn build(b: *Builder) !void { tools_cli_tests.root_module.addImport("enr", enr); tools_cli_tests.root_module.addImport("@zeam/xmss", zeam_xmss); tools_cli_tests.root_module.addImport("@zeam/types", zeam_types); + tools_cli_tests.step.dependOn(&build_rust_lib_steps.step); addRustGlueLib(b, tools_cli_tests, target, prover); const run_tools_cli_test = b.addRunArtifact(tools_cli_tests); setTestRunLabelFromCompile(b, run_tools_cli_test, tools_cli_tests); From 0f3b10a47d03e4d0f51ccb11a176a4dd481f123a Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Wed, 4 Mar 2026 13:22:32 +0000 Subject: [PATCH 07/12] fix: resolve test-keys path via build.zig absolute path The relative path search for test-keys/hash-sig-keys failed because Zig test binaries run from cache directories, not the repo root. Fix by injecting the absolute repo path as a build option (test_keys_path) from build.zig using pathFromRoot(), and add build_options import to key-manager module. All 5 build commands verified locally: - zig build all - zig build test - zig build simtest - zig build spectest:generate - zig build spectest --- build.zig | 3 +++ pkgs/key-manager/src/lib.zig | 29 ++++++++++++----------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index 9ca97dbf4..a34951c27 100644 --- a/build.zig +++ b/build.zig @@ -153,6 +153,8 @@ pub fn build(b: *Builder) !void { build_options.addOption(bool, "has_openvm", prover == .openvm or prover == .all); const use_poseidon = b.option(bool, "use_poseidon", "Use Poseidon SSZ hasher (default: false)") orelse false; build_options.addOption(bool, "use_poseidon", use_poseidon); + // Absolute path to test-keys for pre-generated validator keys + build_options.addOption([]const u8, "test_keys_path", b.pathFromRoot("test-keys/hash-sig-keys")); const build_options_module = build_options.createModule(); // add zeam-utils @@ -237,6 +239,7 @@ pub fn build(b: *Builder) !void { .target = target, .optimize = optimize, }); + zeam_key_manager.addImport("build_options", build_options_module); zeam_key_manager.addImport("@zeam/xmss", zeam_xmss); zeam_key_manager.addImport("@zeam/types", zeam_types); zeam_key_manager.addImport("@zeam/utils", zeam_utils); diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index c3ce6ae51..015e08215 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -171,25 +171,20 @@ const MAX_PK_SIZE = 256; /// Number of pre-generated test keys available in the test-keys submodule. const NUM_PREGENERATED_KEYS: usize = 32; -/// Known paths to search for the test-keys directory. -/// Zig test runners may execute from subdirectories, so we search upward. -const TEST_KEY_SEARCH_PATHS = [_][]const u8{ - "test-keys/hash-sig-keys", - "../test-keys/hash-sig-keys", - "../../test-keys/hash-sig-keys", - "../../../test-keys/hash-sig-keys", - "../../../../test-keys/hash-sig-keys", -}; +const build_options = @import("build_options"); -/// Find the test-keys directory by searching known paths. +/// Find the test-keys directory using the repo root path injected by build.zig. fn findTestKeysDir() ?[]const u8 { - for (TEST_KEY_SEARCH_PATHS) |path| { - if (std.fs.cwd().openDir(path, .{})) |dir| { - var d = dir; - d.close(); - return path; - } else |_| {} - } + const keys_path = build_options.test_keys_path; + if (keys_path.len == 0) return null; + + // Verify it actually exists at runtime + if (std.fs.cwd().openDir(keys_path, .{})) |dir| { + var d = dir; + d.close(); + return keys_path; + } else |_| {} + return null; } From dbbdb84b6b6133e44250e1e72404b325cafdc2f4 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Wed, 4 Mar 2026 18:02:56 +0000 Subject: [PATCH 08/12] fix: handle partial preload failures and fix keypair memory leak Two fixes: 1. Track actually_loaded count instead of assuming all num_preloaded keys were inserted. On partial failure (corrupt SSZ, addKeypair error), the fallback loop now starts from the correct index, so no validators are left without keys. 2. Replace boolean owns_keypairs with num_owned_keypairs counter. Pre- generated keys (index < num_owned_keypairs) are freed on deinit while cached runtime keys (index >= num_owned_keypairs) are skipped. This prevents leaking ~20MB private keys when the mixed ownership path is taken. --- pkgs/key-manager/src/lib.zig | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index 015e08215..512482a1e 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -55,7 +55,10 @@ fn getOrCreateCachedKeyPair( pub const KeyManager = struct { keys: std.AutoHashMap(usize, xmss.KeyPair), allocator: Allocator, - owns_keypairs: bool, + /// Number of keypairs owned by this manager (allocated with our allocator). +/// Keypairs with index < num_owned_keypairs will be freed on deinit. +/// Cached/runtime-generated keypairs (index >= num_owned_keypairs) are not owned. +num_owned_keypairs: usize, const Self = @This(); @@ -63,14 +66,14 @@ pub const KeyManager = struct { return Self{ .keys = std.AutoHashMap(usize, xmss.KeyPair).init(allocator), .allocator = allocator, - .owns_keypairs = true, + .num_owned_keypairs = 0, }; } pub fn deinit(self: *Self) void { - if (self.owns_keypairs) { - var it = self.keys.iterator(); - while (it.next()) |entry| { + var it = self.keys.iterator(); + while (it.next()) |entry| { + if (entry.key_ptr.* < self.num_owned_keypairs) { entry.value_ptr.deinit(); } } @@ -233,6 +236,7 @@ pub fn getTestKeyManager( 0; // Load pre-generated keys (fast path: near-instant from SSZ files) + var actually_loaded: usize = 0; if (keys_dir) |dir| { for (0..num_preloaded) |i| { const keypair = loadPreGeneratedKey(allocator, dir, i) catch |err| { @@ -243,24 +247,24 @@ pub fn getTestKeyManager( std.debug.print("Failed to add pre-generated key {d}: {}\n", .{ i, err }); break; }; + actually_loaded += 1; } - std.debug.print("Loaded {d} pre-generated test keys from {s}\n", .{ num_preloaded, dir }); + key_manager.num_owned_keypairs = actually_loaded; + std.debug.print("Loaded {d} pre-generated test keys from {s}\n", .{ actually_loaded, dir }); } else { std.debug.print("Pre-generated keys not found, generating all keys at runtime\n", .{}); } - // Generate remaining keys at runtime (for validators beyond the pre-generated set) - if (num_validators > num_preloaded) { - key_manager.owns_keypairs = false; // cached keypairs are not owned - + // Generate remaining keys at runtime (for validators beyond the loaded set) + if (num_validators > actually_loaded) { var num_active_epochs = max_slot + 1; if (num_active_epochs < 10) num_active_epochs = 10; - for (num_preloaded..num_validators) |i| { + for (actually_loaded..num_validators) |i| { const keypair = try getOrCreateCachedKeyPair(i, num_active_epochs); try key_manager.addKeypair(i, keypair); } - std.debug.print("Generated {d} additional keys at runtime\n", .{num_validators - num_preloaded}); + std.debug.print("Generated {d} additional keys at runtime\n", .{num_validators - actually_loaded}); } return key_manager; From 1b93bffa06b4f8a452ab841e931e5377b89b061d Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Wed, 4 Mar 2026 18:27:02 +0000 Subject: [PATCH 09/12] fix: track per-key ownership to prevent leaks in CLI and mixed paths Replace index-threshold ownership check with a per-key owned_keys hashmap. addKeypair() marks keys as owned (freed on deinit), addCachedKeypair() does not (for borrowed/cached runtime keys). This fixes the regression where CLI-loaded keys (arbitrary validator indices) were never freed because num_owned_keypairs stayed 0. Now any caller using addKeypair() gets correct cleanup regardless of index order. --- pkgs/key-manager/src/lib.zig | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index 512482a1e..3e13581d0 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -55,10 +55,8 @@ fn getOrCreateCachedKeyPair( pub const KeyManager = struct { keys: std.AutoHashMap(usize, xmss.KeyPair), allocator: Allocator, - /// Number of keypairs owned by this manager (allocated with our allocator). -/// Keypairs with index < num_owned_keypairs will be freed on deinit. -/// Cached/runtime-generated keypairs (index >= num_owned_keypairs) are not owned. -num_owned_keypairs: usize, + /// Tracks which keypairs are owned (allocated by us) vs borrowed (cached). +owned_keys: std.AutoHashMap(usize, void), const Self = @This(); @@ -66,22 +64,30 @@ num_owned_keypairs: usize, return Self{ .keys = std.AutoHashMap(usize, xmss.KeyPair).init(allocator), .allocator = allocator, - .num_owned_keypairs = 0, + .owned_keys = std.AutoHashMap(usize, void).init(allocator), }; } pub fn deinit(self: *Self) void { var it = self.keys.iterator(); while (it.next()) |entry| { - if (entry.key_ptr.* < self.num_owned_keypairs) { + if (self.owned_keys.contains(entry.key_ptr.*)) { entry.value_ptr.deinit(); } } self.keys.deinit(); + self.owned_keys.deinit(); } + /// Add an owned keypair that will be freed on deinit. pub fn addKeypair(self: *Self, validator_id: usize, keypair: xmss.KeyPair) !void { try self.keys.put(validator_id, keypair); + try self.owned_keys.put(validator_id, {}); + } + + /// Add a cached/borrowed keypair that will NOT be freed on deinit. + pub fn addCachedKeypair(self: *Self, validator_id: usize, keypair: xmss.KeyPair) !void { + try self.keys.put(validator_id, keypair); } pub fn loadFromKeypairDir(_: *Self, _: []const u8) !void { @@ -249,7 +255,6 @@ pub fn getTestKeyManager( }; actually_loaded += 1; } - key_manager.num_owned_keypairs = actually_loaded; std.debug.print("Loaded {d} pre-generated test keys from {s}\n", .{ actually_loaded, dir }); } else { std.debug.print("Pre-generated keys not found, generating all keys at runtime\n", .{}); @@ -262,7 +267,7 @@ pub fn getTestKeyManager( for (actually_loaded..num_validators) |i| { const keypair = try getOrCreateCachedKeyPair(i, num_active_epochs); - try key_manager.addKeypair(i, keypair); + try key_manager.addCachedKeypair(i, keypair); } std.debug.print("Generated {d} additional keys at runtime\n", .{num_validators - actually_loaded}); } From be94c008b44c8251548f36d1cf5e1deca538d6e4 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Wed, 4 Mar 2026 19:23:33 +0000 Subject: [PATCH 10/12] chore: remove plans folder --- plans/feat-pregenerated-keys.md | 213 -------------------------------- 1 file changed, 213 deletions(-) delete mode 100644 plans/feat-pregenerated-keys.md diff --git a/plans/feat-pregenerated-keys.md b/plans/feat-pregenerated-keys.md deleted file mode 100644 index 9c868dd96..000000000 --- a/plans/feat-pregenerated-keys.md +++ /dev/null @@ -1,213 +0,0 @@ -# Plan: Pre-generated Validator Keys for CI - -## Problem - -XMSS key generation via `hashsig_keypair_generate` (Rust FFI) is extremely slow. Every CI run and every `getTestKeyManager` call generates keys from scratch. For 3 validators with 1000 active epochs, this adds significant time to: - -1. **Unit tests** — any test touching key-manager or state-transition with signatures -2. **Simtests** — the `beam` CLI command generates 3 validators inline via `getTestKeyManager` -3. **Spec tests** — fixture runners that need signed blocks - -## Current Flow - -``` -CLI main.zig (beam command) - → getTestKeyManager(allocator, 3, 1000) - → for each validator: - → getOrCreateCachedKeyPair(i, 1000) - → KeyPair.generate(seed="test_validator_{i}", activation=0, epochs=1000) - → hashsig_keypair_generate() [Rust FFI — SLOW] -``` - -There IS an in-memory cache (`global_test_key_pair_cache`) but it only survives within a single process — doesn't help across test runs or CI. - -## Proposed Solution - -Pre-generate 32 validator key pairs as SSZ files, commit them to the repo, and load them instead of generating at runtime. - -### Key Format (matching leanSpec/ream structure) - -``` -resources/test-keys/ -├── validator-keys-manifest.yaml -└── hash-sig-keys/ - ├── validator_0_sk.ssz # Private key (SSZ-encoded) - ├── validator_0_pk.ssz # Public key (SSZ-encoded) - ├── validator_1_sk.ssz - ├── validator_1_pk.ssz - ├── ... - ├── validator_31_sk.ssz - └── validator_31_pk.ssz -``` - -### Manifest Format (compatible with leanSpec) - -```yaml -key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8 -hash_function: Poseidon2 -encoding: TargetSum -lifetime: 1000 -log_num_active_epochs: 10 -num_active_epochs: 1000 -num_validators: 32 -validators: - - index: 0 - pubkey_hex: "0x..." - privkey_file: validator_0_sk.ssz - - index: 1 - pubkey_hex: "0x..." - privkey_file: validator_1_sk.ssz - # ... up to 31 -``` - -## Implementation Steps - -### Step 1: Add `keygen` command to zeam-tools - -Add a `keygen` subcommand to `pkgs/tools/src/main.zig` that: - -1. Takes `--num-validators N`, `--num-active-epochs E`, `--output-dir DIR` -2. Generates N keypairs using deterministic seeds (`test_validator_{i}`) -3. Serializes each keypair to `{dir}/hash-sig-keys/validator_{i}_sk.ssz` and `_pk.ssz` -4. Writes `{dir}/validator-keys-manifest.yaml` with pubkey hex and metadata - -### Step 2: Generate and commit 32 keys - -```bash -zig build tools -./zig-out/bin/zeam-tools keygen --num-validators 32 --num-active-epochs 1000 --output-dir resources/test-keys -``` - -Commit `resources/test-keys/` to the repo. These are test keys with known seeds — no security concern. - -### Step 3: Update `getTestKeyManager` to load pre-generated keys - -Modify `pkgs/key-manager/src/lib.zig`: - -```zig -pub fn getTestKeyManager( - allocator: Allocator, - num_validators: usize, - max_slot: usize, -) !KeyManager { - // Try loading pre-generated keys first - if (num_validators <= 32) { - if (loadPreGeneratedKeys(allocator, num_validators)) |km| { - return km; - } else |_| { - // Fall through to runtime generation - } - } - // ... existing runtime generation as fallback -} - -fn loadPreGeneratedKeys(allocator: Allocator, num_validators: usize) !KeyManager { - var km = KeyManager.init(allocator); - errdefer km.deinit(); - - for (0..num_validators) |i| { - const sk_path = std.fmt.comptimePrint( - "resources/test-keys/hash-sig-keys/validator_{d}_sk.ssz", .{i} - ); - // ... read SSZ files and call KeyPair.fromSsz() - } - return km; -} -``` - -**Note**: Need to handle path resolution — keys are relative to repo root. Could use `@embedFile` to compile them in, or resolve at runtime relative to the executable. - -### Step 4: Option A — `@embedFile` (preferred for tests) - -Embed the SSZ key data at compile time so tests don't need to find files at runtime: - -```zig -const embedded_keys = struct { - const sk_0 = @embedFile("resources/test-keys/hash-sig-keys/validator_0_sk.ssz"); - const pk_0 = @embedFile("resources/test-keys/hash-sig-keys/validator_0_pk.ssz"); - // ... generate these with comptime -}; -``` - -But 32 keys × ~10MB per private key = ~320MB embedded — too large. - -### Step 4: Option B — Runtime file loading (preferred) - -Load from disk relative to a known path. The build system can pass the resource path as a build option: - -```zig -// In build.zig -const test_keys_path = b.option([]const u8, "test-keys-path", "Path to pre-generated test keys") - orelse "resources/test-keys"; -``` - -### Step 5: Update CI workflow - -No changes needed if keys are committed to the repo and `getTestKeyManager` loads them automatically. The existing CI steps will just be faster. - -### Step 6: Update `beam` CLI command - -The `beam` command in `main.zig` currently calls `getTestKeyManager(allocator, 3, 1000)`. After this change, it will automatically use pre-generated keys when available — no CLI changes needed. - -## Key Storage: Separate Repo - -Keys will be stored in a separate repo (like leanSpec's approach with `leanEthereum/leansig-test-keys`): - -- **Repo**: `blockblaz/zeam-test-keys` (or under leanEthereum org if shared across clients) -- **Added to zeam as a git submodule** at `test-keys/` -- **CI caches** the submodule to avoid cloning every time - -### Submodule Integration -```bash -git submodule add https://github.com/blockblaz/zeam-test-keys.git test-keys -``` - -CI workflow update: -```yaml -- uses: actions/checkout@v4 - with: - submodules: recursive # Already done for leanSpec -``` - -### `getTestKeyManager` loads from submodule: -```zig -fn loadPreGeneratedKeys(allocator: Allocator, num_validators: usize) !KeyManager { - var km = KeyManager.init(allocator); - for (0..num_validators) |i| { - // Path relative to repo root (zig build runs from there) - const sk = try loadFile(allocator, "test-keys/hash-sig-keys/validator_{d}_sk.ssz", i); - const pk = try loadFile(allocator, "test-keys/hash-sig-keys/validator_{d}_pk.ssz", i); - var keypair = try xmss.KeyPair.fromSsz(allocator, sk, pk); - try km.addKeypair(i, keypair); - } - return km; -} -``` - -## Callers of getTestKeyManager (all benefit) - -| Location | Validators | max_slot | -|----------|-----------|----------| -| `cli/src/main.zig` (beam cmd) | 3 | 1000 | -| `node/src/chain.zig` (test) | 4 | 3 | -| `node/src/node.zig` (test) | num_validators | 10 | -| `node/src/testing.zig` | configurable | configurable | -| `state-transition/src/mock.zig` | num_validators | numBlocks | -| `types/src/block_signatures_testing.zig` | num_validators | 10 | - -## Questions for Anshal - -1. **Repo org**: `blockblaz/zeam-test-keys` or `leanEthereum/zeam-test-keys`? Or reuse the existing `leanEthereum/leansig-test-keys`? - -2. **Seed determinism**: Currently using `"test_validator_{i}"` as seed. Keep this for reproducibility? - -3. **num_active_epochs**: 1000 matches the CLI default. Enough? - -4. **Manifest format**: Match leanSpec's `validator-keys-manifest.yaml` exactly for cross-client compatibility? - -## Impact - -- **CI speedup**: Key loading from SSZ is near-instant vs minutes for generation -- **Test reliability**: No more flaky timing issues from key generation -- **Compatibility**: Same key format as leanSpec/ream — keys can be shared across implementations -- **Repo size**: Zero impact on zeam repo (keys in separate repo as submodule) From 4513158a1d88cc4db95612bcf601e8585169a354 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Wed, 4 Mar 2026 19:34:52 +0000 Subject: [PATCH 11/12] style: zig fmt --- pkgs/key-manager/src/lib.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index 3e13581d0..33c7f326b 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -56,7 +56,7 @@ pub const KeyManager = struct { keys: std.AutoHashMap(usize, xmss.KeyPair), allocator: Allocator, /// Tracks which keypairs are owned (allocated by us) vs borrowed (cached). -owned_keys: std.AutoHashMap(usize, void), + owned_keys: std.AutoHashMap(usize, void), const Self = @This(); From 892443eaf0378fd4cefd0c718c4216fe717a34c1 Mon Sep 17 00:00:00 2001 From: anshalshuklabot Date: Thu, 5 Mar 2026 18:29:17 +0000 Subject: [PATCH 12/12] chore: update test-keys submodule URL to blockblaz org --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index a659cfd69..0c25c3085 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,4 @@ url = https://github.com/leanEthereum/leanSpec [submodule "test-keys"] path = test-keys - url = https://github.com/anshalshuklabot/zeam-test-keys.git + url = https://github.com/blockblaz/zeam-test-keys.git