Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c01fb8b
feat: implement committee aggregation spec
Feb 7, 2026
3a1441f
revert mock network standalone gossip scheduler
Feb 7, 2026
a70286f
fix spectest and attestation aggregation regressions
Feb 7, 2026
198df09
fix flaky forkchoice test logger lifetime and map synchronization
Feb 7, 2026
c3ed098
fix aggregation ownership and subnet validation
Feb 8, 2026
5475ecb
Merge branch 'main' into gr/devnet3
GrapeBaBa Feb 9, 2026
45c79c9
fix subnet id error handling and aggregation cleanup
Feb 9, 2026
4ad9682
Merge remote-tracking branch 'origin/main' into gr/devnet3
Feb 10, 2026
c4fafc2
refactor: restore justifiable slot check style
Feb 10, 2026
941607d
Fix segfault on early error: initialize NodeOptions slices to empty
ch4r10t33r Feb 10, 2026
323e437
Revert "Fix segfault on early error: initialize NodeOptions slices to…
ch4r10t33r Feb 10, 2026
a93dfcd
Align subnet typing and aggregator config handling
GrapeBaBa Feb 11, 2026
487465a
Fix review follow-ups for spectest and aggregation ownership
Feb 11, 2026
8e5ec28
Merge remote-tracking branch 'origin/main' into gr/devnet3
Feb 11, 2026
6847d7a
Apply zig fmt for spectest runner
Feb 11, 2026
2010e40
fix: fix review comments
Feb 11, 2026
c81b4c2
Merge remote-tracking branch 'origin/main' into gr/devnet3
anshalshukla Feb 16, 2026
892c1ee
fix: 0.15.2 changes
anshalshukla Feb 16, 2026
abb20af
fix: mem leaks
anshalshukla Feb 18, 2026
83cc656
Merge remote-tracking branch 'origin' into gr/devnet3
anshalshukla Feb 19, 2026
fe9bfbb
Merge branch 'main' into gr/devnet3
ch4r10t33r Feb 19, 2026
1bf85bd
fix: minor defensive changes
anshalshukla Feb 19, 2026
9b8bf49
Merge branch 'main' into gr/devnet3
anshalshukla Feb 19, 2026
137fa07
Merge branch 'main' into gr/devnet3
ch4r10t33r Feb 19, 2026
388761d
Merge branch 'gr/devnet3' of https://github.com/blockblaz/zeam into g…
ch4r10t33r Feb 19, 2026
1562fae
revert: sync changes, merge: main
anshalshukla Feb 24, 2026
c984a63
merge 'main' into 'gr/devnet3'
anshalshukla Feb 24, 2026
3e355e0
fix: fix review comments
GrapeBaBa Feb 25, 2026
8a4b613
Merge origin/main into gr/devnet3
GrapeBaBa Feb 25, 2026
9b36bf4
fix: fix spec test
GrapeBaBa Feb 25, 2026
8907c39
Merge branch 'main' into gr/devnet3
GrapeBaBa Feb 26, 2026
2f6d497
fix: fix review comments
GrapeBaBa Feb 27, 2026
ac8610c
fix: fix the review comments
GrapeBaBa Feb 27, 2026
f2a590e
fix: fix comments
GrapeBaBa Feb 27, 2026
0cfc7d4
Merge branch 'main' into gr/devnet3
anshalshukla Mar 2, 2026
9f51288
cleanup
g11tech Mar 2, 2026
c2c2883
fix: fix review comments
GrapeBaBa Mar 3, 2026
205f6d9
Merge branch 'main' into gr/devnet3
GrapeBaBa Mar 3, 2026
5d475c7
node: apply block attestations to tracker
GrapeBaBa Mar 3, 2026
0323eba
refactor: extract generic deserializeGossipMessage helper (#605)
zclawz Mar 3, 2026
099453c
Merge branch 'main' into gr/devnet3
anshalshukla Mar 3, 2026
f9042ea
node: move aggregation interval guard to caller
GrapeBaBa Mar 3, 2026
f485d76
rm: extractAttestationsFromAggregatedPayloads
anshalshukla Mar 3, 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
2 changes: 1 addition & 1 deletion leanSpec
Submodule leanSpec updated 257 files
19 changes: 14 additions & 5 deletions pkgs/cli/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub const NodeCommand = struct {
@"network-dir": []const u8 = "./network",
@"data-dir": []const u8 = constants.DEFAULT_DATA_DIR,
@"checkpoint-sync-url": ?[]const u8 = null,
@"is-aggregator": bool = false,

pub const __shorts__ = .{
.help = .h,
Expand All @@ -88,6 +89,7 @@ pub const NodeCommand = struct {
.@"sig-keys-dir" = "Relative path of custom genesis to signature key directory",
.@"data-dir" = "Path to the data directory",
.@"checkpoint-sync-url" = "URL to fetch finalized checkpoint state from for checkpoint sync (e.g., http://localhost:5052/lean/v0/states/finalized)",
.@"is-aggregator" = "Enable aggregator mode for committee signature aggregation",
.help = "Show help information for the node command",
};
};
Expand All @@ -98,15 +100,15 @@ const BeamCmd = struct {
@"api-port": u16 = constants.DEFAULT_API_PORT,
@"metrics-port": u16 = constants.DEFAULT_METRICS_PORT,
data_dir: []const u8 = constants.DEFAULT_DATA_DIR,
@"is-aggregator": bool = true,

pub fn format(self: BeamCmd, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
_ = fmt;
_ = options;
try writer.print("BeamCmd{{ mockNetwork={}, api-port={d}, metrics-port={d}, data_dir=\"{s}\" }}", .{
pub fn format(self: BeamCmd, writer: anytype) !void {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The format function signature has been changed to remove the unused fmt and options parameters. However, this breaks the standard formatting interface contract in Zig where format functions should accept these parameters even if unused. This could cause issues if BeamCmd is used with format strings or custom format options.

Suggested change
pub fn format(self: BeamCmd, writer: anytype) !void {
pub fn format(self: BeamCmd, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
_ = fmt;
_ = options;

Copilot uses AI. Check for mistakes.
try writer.print("BeamCmd{{ mockNetwork={}, api-port={d}, metrics-port={d}, data_dir=\"{s}\", is-aggregator={} }}", .{
self.mockNetwork,
self.@"api-port",
self.@"metrics-port",
self.data_dir,
self.@"is-aggregator",
});
}
Comment on lines +105 to 113
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

This format method no longer matches Zig's expected formatting hook signature (format(self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype)). If BeamCmd is formatted via std.fmt (e.g., {} / {any}), this will stop compiling. Restore the standard signature (or rename this method and call it explicitly) to keep formatting working.

Copilot uses AI. Check for mistakes.
};
Expand Down Expand Up @@ -222,7 +224,7 @@ const ZeamArgs = struct {
.prometheus => |cmd| switch (cmd.__commands__) {
.genconfig => |genconfig| try writer.print("prometheus.genconfig(api_port={d}, filename=\"{s}\")", .{ genconfig.@"api-port", genconfig.filename }),
},
.node => |cmd| try writer.print("node(node-id=\"{s}\", custom_genesis=\"{s}\", validator_config=\"{s}\", data-dir=\"{s}\", api_port={d})", .{ cmd.@"node-id", cmd.custom_genesis, cmd.validator_config, cmd.@"data-dir", cmd.@"api-port" }),
.node => |cmd| try writer.print("node(node-id=\"{s}\", custom_genesis=\"{s}\", validator_config=\"{s}\", data-dir=\"{s}\", api_port={d}), is-aggregator={}", .{ cmd.@"node-id", cmd.custom_genesis, cmd.validator_config, cmd.@"data-dir", cmd.@"api-port", cmd.@"is-aggregator" }),
.testsig => |cmd| try writer.print("testsig(epoch={d}, slot={d})", .{ cmd.epoch, cmd.slot }),
}
try writer.writeAll(")");
Expand Down Expand Up @@ -502,6 +504,7 @@ fn mainInner() !void {
.listen_addresses = listen_addresses1,
.connect_peers = null,
.node_registry = test_registry1,
.attestation_committee_count = chain_config.spec.attestation_committee_count,
}, logger1_config.logger(.network));
backend1 = network1.getNetworkInterface();

Expand All @@ -525,6 +528,7 @@ fn mainInner() !void {
.listen_addresses = listen_addresses2,
.connect_peers = connect_peers,
.node_registry = test_registry2,
.attestation_committee_count = chain_config.spec.attestation_committee_count,
}, logger2_config.logger(.network));
backend2 = network2.getNetworkInterface();

Expand All @@ -547,6 +551,7 @@ fn mainInner() !void {
.listen_addresses = listen_addresses3,
.connect_peers = connect_peers3,
.node_registry = test_registry3,
.attestation_committee_count = chain_config.spec.attestation_committee_count,
}, logger3_config.logger(.network));
backend3 = network3.getNetworkInterface();
logger1_config.logger(null).debug("--- ethlibp2p gossip {any}", .{backend1.gossip});
Expand Down Expand Up @@ -592,6 +597,7 @@ fn mainInner() !void {
.db = db_1,
.logger_config = &logger1_config,
.node_registry = registry_1,
.is_aggregator = beamcmd.@"is-aggregator",
});

if (api_server_handle) |handle| {
Expand All @@ -611,6 +617,7 @@ fn mainInner() !void {
.db = db_2,
.logger_config = &logger2_config,
.node_registry = registry_2,
.is_aggregator = beamcmd.@"is-aggregator",
});

// Node 3 setup - delayed start for initial sync testing
Expand All @@ -628,6 +635,7 @@ fn mainInner() !void {
.db = db_3,
.logger_config = &logger3_config,
.node_registry = registry_3,
.is_aggregator = beamcmd.@"is-aggregator",
});

// Delayed runner - starts both network3 and node3 together
Expand Down Expand Up @@ -736,6 +744,7 @@ fn mainInner() !void {
.validator_config = leancmd.validator_config,
.node_key_index = undefined,
.metrics_enable = leancmd.metrics_enable,
.is_aggregator = leancmd.@"is-aggregator",
.api_port = leancmd.@"api-port",
.metrics_port = leancmd.@"metrics-port",
.bootnodes = &.{}, // Initialize to empty slice to avoid segfault in deinit
Expand Down
56 changes: 52 additions & 4 deletions pkgs/cli/src/node.zig
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub const NodeOptions = struct {
validator_assignments: []ValidatorAssignment,
genesis_spec: types.GenesisSpec,
metrics_enable: bool,
is_aggregator: bool,
api_port: u16,
metrics_port: u16,
local_priv_key: []const u8,
Expand Down Expand Up @@ -171,6 +172,7 @@ pub const Node = struct {
.connect_peers = addresses.connect_peers,
.local_private_key = options.local_priv_key,
.node_registry = options.node_registry,
.attestation_committee_count = chain_config.spec.attestation_committee_count,
}, options.logger_config.logger(.network));
errdefer self.network.deinit();
self.clock = try Clock.init(allocator, chain_config.genesis.genesis_time, &self.loop);
Expand Down Expand Up @@ -244,6 +246,7 @@ pub const Node = struct {
.db = db,
.logger_config = options.logger_config,
.node_registry = options.node_registry,
.is_aggregator = options.is_aggregator,
});
errdefer self.beam_node.deinit();

Expand Down Expand Up @@ -377,7 +380,12 @@ pub const Node = struct {
defer enr_fields.deinit(self.allocator);

// Construct ENR from fields and private key
self.enr = try constructENRFromFields(self.allocator, self.options.local_priv_key, enr_fields);
self.enr = try constructENRFromFields(
self.allocator,
self.options.local_priv_key,
enr_fields,
self.options.is_aggregator,
);
}

// Overriding the IP to 0.0.0.0 to listen on all interfaces
Expand Down Expand Up @@ -565,6 +573,7 @@ pub fn buildStartOptions(
opts.node_key_index = node_key_index;
opts.hash_sig_key_dir = hash_sig_key_dir;
opts.checkpoint_sync_url = node_cmd.@"checkpoint-sync-url";
opts.is_aggregator = node_cmd.@"is-aggregator";
}

/// Downloads finalized checkpoint state from the given URL and deserializes it
Expand Down Expand Up @@ -829,6 +838,30 @@ fn getPrivateKeyFromValidatorConfig(allocator: std.mem.Allocator, node_key: []co
return error.InvalidNodeKey;
}

fn getIsAggregatorFromValidatorConfig(node_key: []const u8, validator_config: Yaml) !bool {
for (validator_config.docs.items[0].map.get("validators").?.list) |entry| {
const name_value = entry.map.get("name").?;
if (name_value == .scalar and std.mem.eql(u8, name_value.scalar, node_key)) {
const value = entry.map.get("is_aggregator") orelse return false;
return switch (value) {
.boolean => |b| b,
.scalar => |s| blk: {
if (std.ascii.eqlIgnoreCase(s, "true")) break :blk true;
if (std.ascii.eqlIgnoreCase(s, "false")) break :blk false;
const i = std.fmt.parseInt(i64, s, 10) catch break :blk error.InvalidAggregatorFlag;
return switch (i) {
0 => false,
1 => true,
else => error.InvalidAggregatorFlag,
};
},
else => error.InvalidAggregatorFlag,
};
}
}
return error.InvalidNodeKey;
}

fn getEnrFieldsFromValidatorConfig(allocator: std.mem.Allocator, node_key: []const u8, validator_config: Yaml) !EnrFields {
for (validator_config.docs.items[0].map.get("validators").?.list) |entry| {
const name_value = entry.map.get("name").?;
Expand Down Expand Up @@ -913,7 +946,12 @@ fn getEnrFieldsFromValidatorConfig(allocator: std.mem.Allocator, node_key: []con
return error.InvalidNodeKey;
}

fn constructENRFromFields(allocator: std.mem.Allocator, private_key: []const u8, enr_fields: EnrFields) !ENR {
fn constructENRFromFields(
allocator: std.mem.Allocator,
private_key: []const u8,
enr_fields: EnrFields,
is_aggregator: bool,
) !ENR {
// Clean up private key (remove 0x prefix if present)
const secret_key_str = if (std.mem.startsWith(u8, private_key, "0x"))
private_key[2..]
Expand Down Expand Up @@ -987,6 +1025,13 @@ fn constructENRFromFields(allocator: std.mem.Allocator, private_key: []const u8,
};
}

// Advertise aggregator capability in ENR.
// 0x00 = false, 0x01 = true.
const is_aggregator_bytes = [_]u8{if (is_aggregator) 0x01 else 0x00};
signable_enr.set("is_aggregator", &is_aggregator_bytes) catch {
return error.ENRSetIsAggregatorFailed;
};

// Set custom fields
var custom_iterator = enr_fields.custom_fields.iterator();
while (custom_iterator.next()) |kv| {
Expand Down Expand Up @@ -1075,9 +1120,10 @@ pub fn populateNodeNameRegistry(
if (privkey_value == .scalar) {
const enr_fields_value = entry.map.get("enrFields");
if (enr_fields_value != null) {
const is_aggregator = getIsAggregatorFromValidatorConfig(node_name, parsed_validator_config) catch break :blk null;
var enr_fields = getEnrFieldsFromValidatorConfig(allocator, node_name, parsed_validator_config) catch break :blk null;
defer enr_fields.deinit(allocator);
var enr = constructENRFromFields(allocator, privkey_value.scalar, enr_fields) catch break :blk null;
var enr = constructENRFromFields(allocator, privkey_value.scalar, enr_fields, is_aggregator) catch break :blk null;
defer enr.deinit();
const pid = enr.peerId(allocator) catch break :blk null;
const pid_str_slice = pid.toBase58(&peer_id_buf) catch break :blk null;
Expand Down Expand Up @@ -1198,7 +1244,7 @@ test "ENR construction from fields" {
defer std.testing.allocator.free(private_key);

// Construct ENR from fields
const constructed_enr = try constructENRFromFields(std.testing.allocator, private_key, enr_fields);
const constructed_enr = try constructENRFromFields(std.testing.allocator, private_key, enr_fields, true);

// Verify the ENR was constructed successfully
// We can't easily verify the exact ENR content without knowing the exact signature,
Expand All @@ -1207,6 +1253,7 @@ test "ENR construction from fields" {
try std.testing.expect(constructed_enr.kvs.get("quic") != null);
try std.testing.expect(constructed_enr.kvs.get("tcp") != null);
try std.testing.expect(constructed_enr.kvs.get("seq") != null);
try std.testing.expect(constructed_enr.kvs.get("is_aggregator") != null);
}

test "compare roots from genGensisBlock and genGenesisState and genStateBlockHeader" {
Expand Down Expand Up @@ -1334,6 +1381,7 @@ test "NodeOptions checkpoint_sync_url field is optional" {
.validator_assignments = &[_]ValidatorAssignment{},
.genesis_spec = genesis_spec,
.metrics_enable = false,
.is_aggregator = false,
.api_port = 5052,
.metrics_port = 5053,
.local_priv_key = try allocator.dupe(u8, "test"),
Expand Down
2 changes: 1 addition & 1 deletion pkgs/cli/test/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fn getZeamExecutable() ![]const u8 {
/// Returns the process handle for cleanup, or error if startup fails
fn spinBeamSimNode(allocator: std.mem.Allocator, exe_path: []const u8) !*process.Child {
// Set up process with beam command and mock network
const args = [_][]const u8{ exe_path, "beam", "--mockNetwork", "true" };
const args = [_][]const u8{ exe_path, "beam", "--mockNetwork", "true", "--is-aggregator", "true" };
const cli_process = try allocator.create(process.Child);
cli_process.* = process.Child.init(&args, allocator);

Expand Down
1 change: 1 addition & 0 deletions pkgs/configs/src/configs/mainnet.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pub const mainnet = types.ChainSpec{
// 10 minutes slot for proving purposes
.preset = types.Preset.mainnet,
.name = "mainnet",
.attestation_committee_count = 1,
};
6 changes: 5 additions & 1 deletion pkgs/configs/src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ pub const ChainConfig = struct {
pub fn init(chainId: Chain, chainOptsOrNull: ?ChainOptions) !Self {
switch (chainId) {
.custom => {
if (chainOptsOrNull) |*chainOpts| {
if (chainOptsOrNull) |chain_opts_in| {
var chainOpts = chain_opts_in;
if (chainOpts.attestation_committee_count == null) {
chainOpts.attestation_committee_count = 1;
}
const genesis = utils.Cast(types.GenesisSpec, chainOpts);
// transfer ownership of any allocated memory in chainOpts to spec
const spec = utils.Cast(types.ChainSpec, chainOpts);
Comment on lines +27 to 34
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

This changes the .custom path from mutating the passed ChainOptions in-place (previously |*chainOpts|) to operating on a local copy. If utils.Cast(...) relies on in-place mutation to transfer ownership (as the comment suggests), copying can break the ownership-transfer contract and can lead to double-free or leaks if the original ChainOptions is later deinitialized by the caller. Prefer restoring pointer capture (|*chainOpts|) and setting the default on the original struct before casting, so any ownership transfer semantics remain correct.

Suggested change
if (chainOptsOrNull) |chain_opts_in| {
var chainOpts = chain_opts_in;
if (chainOpts.attestation_committee_count == null) {
chainOpts.attestation_committee_count = 1;
}
const genesis = utils.Cast(types.GenesisSpec, chainOpts);
// transfer ownership of any allocated memory in chainOpts to spec
const spec = utils.Cast(types.ChainSpec, chainOpts);
if (chainOptsOrNull) |*chainOpts| {
if (chainOpts.attestation_committee_count == null) {
chainOpts.attestation_committee_count = 1;
}
const genesis = utils.Cast(types.GenesisSpec, chainOpts.*);
// transfer ownership of any allocated memory in chainOpts to spec
const spec = utils.Cast(types.ChainSpec, chainOpts.*);

Copilot uses AI. Check for mistakes.
Expand Down
4 changes: 2 additions & 2 deletions pkgs/database/src/interface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ const zeam_utils = @import("@zeam/utils");

/// Helper function to format block keys consistently
pub fn formatBlockKey(allocator: Allocator, block_root: *const types.Root) ![]const u8 {
return std.fmt.allocPrint(allocator, "block:{x}", .{block_root});
return std.fmt.allocPrint(allocator, "block:{x}", .{block_root.*});
}

/// Helper function to format state keys consistently
pub fn formatStateKey(allocator: Allocator, state_root: *const types.Root) ![]const u8 {
return std.fmt.allocPrint(allocator, "state:{x}", .{state_root});
return std.fmt.allocPrint(allocator, "state:{x}", .{state_root.*});
}

/// Helper function to format finalized slot index keys
Expand Down
Loading