Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4924e83
feat: impl sig spec test
GrapeBaBa Dec 22, 2025
0233f7d
feat: add signature spec test
GrapeBaBa Dec 27, 2025
a27a789
fix: fix signature test
GrapeBaBa Jan 3, 2026
5e56317
fix: bootstrap spectest:run when missing generated index
Jan 14, 2026
ff26975
fix(spectest): support verify_signatures attestations + proofs
Jan 15, 2026
71c50b4
chore: fix zig fmt step and rust lint
Jan 15, 2026
164aca4
Apply suggestion from @Copilot
GrapeBaBa Jan 15, 2026
ffe61b6
fix(spectest): stop using ctx allocator in signature parsing
Jan 16, 2026
c45ec58
Merge branch 'main' of github.com:blockblaz/zeam into sig_spec_test
Jan 22, 2026
ebb40d1
refactor: align latest spec test
Jan 22, 2026
f6abf9c
Merge branch 'main' into sig_spec_test
GrapeBaBa Jan 22, 2026
9fe4b7b
refactor: hashsig test scheme
Jan 23, 2026
cf0df8f
fix: fix test
Jan 23, 2026
1a33798
fix: fix lint
Jan 23, 2026
9b24b2b
fix: fix review comments
Jan 23, 2026
961ee03
fix: fix review comments
Jan 23, 2026
fc8eb66
Merge remote-tracking branch 'origin/main' into sig_spec_test
Jan 23, 2026
4e7046f
Merge branch 'main' into sig_spec_test
GrapeBaBa Jan 23, 2026
51f2ff4
feat: add API endpoint versioning (#514)
ch4r10t33r Jan 24, 2026
074c1c6
refactor: rename justified endpoint to /lean/v0/checkpoints/justified…
ch4r10t33r Jan 24, 2026
bdb711a
fix snappy frame decoding boundaries for req/resp (#515)
shariqnaiyer Jan 25, 2026
5c6b6f1
add OCI labels for git commit and branch to docker images (#493)
KatyaRyazantseva Jan 25, 2026
97e2760
update to use SSZ hasher agnostic hashTreeRoot API (#474)
chetanyb Jan 25, 2026
28f4bca
Merge remote-tracking branch 'origin/main' into sig_spec_test
Jan 25, 2026
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
5 changes: 5 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ pub fn build(b: *Builder) !void {
zeam_spectests.addImport("build_options", build_options_module);
zeam_spectests.addImport("@zeam/state-transition", zeam_state_transition);
zeam_spectests.addImport("@zeam/node", zeam_beam_node);
zeam_spectests.addImport("@zeam/xmss", zeam_xmss);

// Add the cli executable
const cli_exe = b.addExecutable(.{
Expand Down Expand Up @@ -570,6 +571,10 @@ pub fn build(b: *Builder) !void {
spectests.root_module.addImport("@zeam/metrics", zeam_metrics);
spectests.root_module.addImport("@zeam/state-transition", zeam_state_transition);
spectests.root_module.addImport("ssz", ssz);
spectests.root_module.addImport("@zeam/xmss", zeam_xmss);

spectests.step.dependOn(&build_rust_lib_steps.step);
addRustGlueLib(b, spectests, target, prover);

manager_tests.step.dependOn(&build_rust_lib_steps.step);

Expand Down
2 changes: 1 addition & 1 deletion leanSpec
Submodule leanSpec updated 66 files
+3 −0 .gitignore
+1 −1 CONTRIBUTING.md
+2 −0 VERSIONS.md
+ lean_consensus.pdf
+5 −3 pyproject.toml
+238 −21 src/lean_spec/__main__.py
+3 −3 src/lean_spec/subspecs/api/client.py
+29 −0 src/lean_spec/subspecs/api/server.py
+0 −2 src/lean_spec/subspecs/chain/__init__.py
+1 −1 src/lean_spec/subspecs/chain/clock.py
+1 −31 src/lean_spec/subspecs/chain/config.py
+57 −6 src/lean_spec/subspecs/chain/service.py
+20 −12 src/lean_spec/subspecs/containers/state/state.py
+6 −6 src/lean_spec/subspecs/containers/state/types.py
+23 −5 src/lean_spec/subspecs/forkchoice/store.py
+54 −41 src/lean_spec/subspecs/genesis/config.py
+511 −58 src/lean_spec/subspecs/networking/client/event_source.py
+59 −8 src/lean_spec/subspecs/networking/client/reqresp_client.py
+15 −0 src/lean_spec/subspecs/networking/config.py
+64 −0 src/lean_spec/subspecs/networking/gossipsub/__init__.py
+875 −0 src/lean_spec/subspecs/networking/gossipsub/behavior.py
+2 −4 src/lean_spec/subspecs/networking/gossipsub/parameters.py
+720 −0 src/lean_spec/subspecs/networking/gossipsub/rpc.py
+1 −3 src/lean_spec/subspecs/networking/reqresp/codec.py
+73 −38 src/lean_spec/subspecs/networking/reqresp/handler.py
+5 −0 src/lean_spec/subspecs/networking/transport/connection/manager.py
+1 −2 src/lean_spec/subspecs/networking/transport/multistream/negotiation.py
+9 −5 src/lean_spec/subspecs/networking/transport/noise/payload.py
+4 −2 src/lean_spec/subspecs/networking/transport/peer_id.py
+22 −0 src/lean_spec/subspecs/networking/transport/quic/__init__.py
+696 −0 src/lean_spec/subspecs/networking/transport/quic/connection.py
+408 −0 src/lean_spec/subspecs/networking/transport/quic/tls.py
+2 −2 src/lean_spec/subspecs/networking/varint.py
+10 −2 src/lean_spec/subspecs/node/node.py
+3 −0 src/lean_spec/subspecs/ssz/constants.py
+17 −15 src/lean_spec/subspecs/ssz/hash.py
+42 −16 src/lean_spec/subspecs/ssz/merkleization.py
+7 −22 src/lean_spec/subspecs/ssz/pack.py
+13 −1 src/lean_spec/subspecs/sync/service.py
+147 −50 src/lean_spec/subspecs/validator/registry.py
+35 −4 src/lean_spec/subspecs/validator/service.py
+1 −2 src/lean_spec/types/__init__.py
+6 −10 src/lean_spec/types/bitfields.py
+0 −6 src/lean_spec/types/byte_arrays.py
+48 −86 src/lean_spec/types/collections.py
+32 −1 src/lean_spec/types/exceptions.py
+10 −44 src/lean_spec/types/union.py
+60 −0 tests/lean_spec/subspecs/api/test_server.py
+20 −0 tests/lean_spec/subspecs/chain/test_clock.py
+6 −4 tests/lean_spec/subspecs/chain/test_service.py
+6 −30 tests/lean_spec/subspecs/containers/test_state_justified_slots.py
+32 −64 tests/lean_spec/subspecs/forkchoice/test_time_management.py
+28 −28 tests/lean_spec/subspecs/genesis/test_config.py
+21 −25 tests/lean_spec/subspecs/networking/client/test_gossip_reception.py
+368 −0 tests/lean_spec/subspecs/networking/test_gossipsub.py
+5 −3 tests/lean_spec/subspecs/networking/test_reqresp.py
+2 −2 tests/lean_spec/subspecs/networking/transport/multistream/test_negotiation.py
+18 −14 tests/lean_spec/subspecs/networking/transport/noise/test_payload.py
+7 −7 tests/lean_spec/subspecs/networking/transport/test_peer_id.py
+78 −1 tests/lean_spec/subspecs/node/test_node.py
+388 −0 tests/lean_spec/subspecs/ssz/test_boundary_values.py
+457 −0 tests/lean_spec/subspecs/ssz/test_edge_cases.py
+369 −0 tests/lean_spec/subspecs/ssz/test_malformed_data.py
+402 −0 tests/lean_spec/subspecs/ssz/test_nested_structures.py
+84 −28 tests/lean_spec/subspecs/validator/test_registry.py
+98 −0 uv.lock
164 changes: 164 additions & 0 deletions pkgs/key-manager/src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,170 @@ const types = @import("@zeam/types");
const zeam_metrics = @import("@zeam/metrics");
const ssz = @import("ssz");
const Allocator = std.mem.Allocator;
const JsonValue = std.json.Value;

pub const XmssTestConfig = struct {
scheme: xmss.HashSigScheme,
signature_ssz_len: usize,
allow_placeholder_aggregated_proof: bool,

pub fn fromLeanEnv(lean_env: ?[]const u8) XmssTestConfig {
const scheme = schemeFromLeanEnv(lean_env);
return .{
.scheme = scheme,
.signature_ssz_len = xmss.signatureSszLenForScheme(scheme),
.allow_placeholder_aggregated_proof = scheme == .@"test",
};
}
};

pub const TestKeyManagerError = error{
DuplicateKeyIndex,
InvalidKeyFile,
InvalidKeyIndex,
InvalidPublicKey,
NoKeysFound,
PublicKeyNotFound,
};

pub const TestKeyManager = struct {
Comment thread
GrapeBaBa marked this conversation as resolved.
Outdated
allocator: Allocator,
config: XmssTestConfig,
pubkeys: std.AutoHashMap(usize, types.Bytes52),

const Self = @This();

pub fn init(allocator: Allocator, lean_env: ?[]const u8) Self {
return Self{
.allocator = allocator,
.config = XmssTestConfig.fromLeanEnv(lean_env),
.pubkeys = std.AutoHashMap(usize, types.Bytes52).init(allocator),
};
}

pub fn deinit(self: *Self) void {
self.pubkeys.deinit();
}

pub fn loadLeanSpecKeys(self: *Self, keys_root: []const u8) !void {
const scheme_dir_name = switch (self.config.scheme) {
.@"test" => "test_scheme",
.prod => "prod_scheme",
};
const scheme_dir_path = try std.fs.path.join(self.allocator, &.{ keys_root, scheme_dir_name });
defer self.allocator.free(scheme_dir_path);
try self.loadKeysFromDir(scheme_dir_path);
}

pub fn loadKeysFromDir(self: *Self, keys_dir_path: []const u8) !void {
var dir = try std.fs.cwd().openDir(keys_dir_path, .{ .iterate = true });
defer dir.close();

self.pubkeys.clearRetainingCapacity();

var it = dir.iterate();
while (try it.next()) |entry| {
if (entry.kind != .file) continue;
const index = parseKeyIndex(entry.name) catch continue;
const pubkey = try readPublicKeyFromJson(self.allocator, dir, entry.name);

const gop = try self.pubkeys.getOrPut(index);
if (gop.found_existing) {
return TestKeyManagerError.DuplicateKeyIndex;
}
gop.value_ptr.* = pubkey;
}

if (self.pubkeys.count() == 0) {
return TestKeyManagerError.NoKeysFound;
}
}

pub fn getPublicKeyBytes(self: *const Self, validator_index: usize) !types.Bytes52 {
return self.pubkeys.get(validator_index) orelse TestKeyManagerError.PublicKeyNotFound;
}

pub fn getAllPubkeys(
self: *const Self,
allocator: Allocator,
num_validators: usize,
) ![]types.Bytes52 {
const pubkeys = try allocator.alloc(types.Bytes52, num_validators);
errdefer allocator.free(pubkeys);

for (0..num_validators) |i| {
pubkeys[i] = try self.getPublicKeyBytes(i);
}

return pubkeys;
}

pub fn signatureSszLen(self: *const Self) usize {
return self.config.signature_ssz_len;
}

pub fn allowPlaceholderAggregatedProof(self: *const Self) bool {
return self.config.allow_placeholder_aggregated_proof;
}
};

fn schemeFromLeanEnv(lean_env: ?[]const u8) xmss.HashSigScheme {
const env = lean_env orelse return .prod;
if (std.ascii.eqlIgnoreCase(env, "test")) return .@"test";
return .prod;
}

fn parseKeyIndex(file_name: []const u8) !usize {
if (!std.mem.endsWith(u8, file_name, ".json")) {
return TestKeyManagerError.InvalidKeyIndex;
}
const stem = file_name[0 .. file_name.len - ".json".len];
if (stem.len == 0) {
return TestKeyManagerError.InvalidKeyIndex;
}
return std.fmt.parseInt(usize, stem, 10) catch TestKeyManagerError.InvalidKeyIndex;
}
Comment on lines +38 to +47

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function lacks documentation. Add a doc comment explaining that it extracts a validator index from a JSON filename (e.g., '5.json' returns 5).

Copilot uses AI. Check for mistakes.

fn readPublicKeyFromJson(
allocator: Allocator,
dir: std.fs.Dir,
file_name: []const u8,
) !types.Bytes52 {
const max_bytes: usize = 2 * 1024 * 1024;
Comment thread
GrapeBaBa marked this conversation as resolved.
Outdated
const payload = dir.readFileAlloc(allocator, file_name, max_bytes) catch {
return TestKeyManagerError.InvalidKeyFile;
};
defer allocator.free(payload);

var parsed = std.json.parseFromSlice(JsonValue, allocator, payload, .{ .ignore_unknown_fields = true }) catch {
return TestKeyManagerError.InvalidKeyFile;
};
defer parsed.deinit();

const obj = switch (parsed.value) {
.object => |map| map,
else => return TestKeyManagerError.InvalidKeyFile,
};
const pub_val = obj.get("public") orelse return TestKeyManagerError.InvalidKeyFile;
const pub_hex = switch (pub_val) {
.string => |s| s,
else => return TestKeyManagerError.InvalidKeyFile,
};

return parsePublicKeyHex(pub_hex);
}

fn parsePublicKeyHex(input: []const u8) !types.Bytes52 {
const hex_str = if (std.mem.startsWith(u8, input, "0x")) input[2..] else input;
if (hex_str.len != 104) {
Comment thread
GrapeBaBa marked this conversation as resolved.
Outdated
return TestKeyManagerError.InvalidPublicKey;
}
var bytes: types.Bytes52 = undefined;
_ = std.fmt.hexToBytes(&bytes, hex_str) catch {
return TestKeyManagerError.InvalidPublicKey;
};
return bytes;
}

const KeyManagerError = error{
ValidatorKeyNotFound,
Expand Down
5 changes: 4 additions & 1 deletion pkgs/spectest/src/fixture_kind.zig
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
pub const FixtureKind = enum {
state_transition,
fork_choice,
verify_signatures,

pub fn runnerModule(self: FixtureKind) []const u8 {
return switch (self) {
.state_transition => "state_transition",
.fork_choice => "fork_choice",
.verify_signatures => "verify_signatures",
};
}

pub fn handlerSubdir(self: FixtureKind) []const u8 {
return switch (self) {
.state_transition => "state_transition",
.fork_choice => "fc",
.verify_signatures => "verify_signatures",
};
}
};

pub const all = [_]FixtureKind{ .state_transition, .fork_choice };
pub const all = [_]FixtureKind{ .state_transition, .fork_choice, .verify_signatures };
17 changes: 17 additions & 0 deletions pkgs/spectest/src/json_expect.zig
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,23 @@ pub fn expectArrayValue(
};
}

pub fn expectArrayField(
comptime FixtureError: type,
obj: std.json.ObjectMap,
field_names: []const []const u8,
context: Context,
label: []const u8,
) FixtureError!std.json.Array {
const value = getField(obj, field_names) orelse {
std.debug.print(
"fixture {s} case {s}{}: missing field {s}\n",
.{ context.fixture_label, context.case_name, context.formatStep(), label },
);
return FixtureError.InvalidFixture;
};
return expectArrayValue(FixtureError, value, context, label);
}

pub fn appendBytesDataField(
comptime FixtureError: type,
comptime T: type,
Expand Down
5 changes: 4 additions & 1 deletion pkgs/spectest/src/runner/fork_choice_runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,10 @@ fn processBlockStep(
}
try types.sszClone(ctx.allocator, types.BeamState, parent_state_ptr.*, new_state_ptr);

state_transition.apply_transition(ctx.allocator, new_state_ptr, block, .{ .logger = ctx.fork_logger, .validateResult = false }) catch |err| {
state_transition.apply_transition(ctx.allocator, new_state_ptr, block, .{
.logger = ctx.fork_logger,
.validateResult = false,
}) catch |err| {
std.debug.print(
"fixture {s} case {s}{}: state transition failed {s}\n",
.{ fixture_path, case_name, formatStep(step_index), @errorName(err) },
Expand Down
6 changes: 5 additions & 1 deletion pkgs/spectest/src/runner/state_transition_runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,11 @@ fn runCase(
}
}

state_transition.apply_transition(allocator, &pre_state, block, .{ .logger = logger }) catch |err| {
const validate_result = expect_exception != null;
state_transition.apply_transition(allocator, &pre_state, block, .{
.logger = logger,
.validateResult = validate_result,
}) catch |err| {
encountered_error = true;
if (expect_exception == null) {
std.debug.print(
Expand Down
Loading
Loading