Skip to content

Commit f14a7f3

Browse files
committed
Merge pull request #246 from ChainSafe/gr/feature/forkchoice-z
Merge latest PR head a97a220 and adapt it to Zig 0.16 plus current chain/state-transition interfaces.
2 parents 6b1f8ec + a97a220 commit f14a7f3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+4102
-1029
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! Benchmark for `computeDeltas` — the core weight-propagation function used by fork choice.
2+
//!
3+
//! Ported from the TypeScript benchmark in `packages/fork-choice/test/perf/computeDeltas.test.ts`.
4+
//! Measures performance across varying validator counts and inactive-validator percentages.
5+
6+
const std = @import("std");
7+
const builtin = @import("builtin");
8+
const zbench = @import("zbench");
9+
const fork_choice = @import("fork_choice");
10+
11+
const computeDeltas = fork_choice.computeDeltas;
12+
const DeltasCache = fork_choice.DeltasCache;
13+
const EquivocatingIndices = fork_choice.EquivocatingIndices;
14+
const VoteIndex = fork_choice.VoteIndex;
15+
const NULL_VOTE_INDEX = fork_choice.NULL_VOTE_INDEX;
16+
17+
const ComputeDeltasBench = struct {
18+
num_proto_nodes: u32,
19+
current_indices: []VoteIndex,
20+
next_indices: []VoteIndex,
21+
old_balances: []u16,
22+
new_balances: []u16,
23+
equivocating: *EquivocatingIndices,
24+
deltas_cache: *DeltasCache,
25+
/// Snapshot of initial current_indices for reset before each iteration.
26+
/// computeDeltas mutates current_indices (sets current = next after processing),
27+
/// so without reset, subsequent iterations hit the unchanged fast path (no-op).
28+
initial_current_indices: []const VoteIndex,
29+
30+
pub fn run(self: ComputeDeltasBench, allocator: std.mem.Allocator) void {
31+
// Reset current_indices to initial state (matching TS beforeEach).
32+
@memcpy(self.current_indices, self.initial_current_indices);
33+
34+
const result = computeDeltas(
35+
allocator,
36+
self.deltas_cache,
37+
self.num_proto_nodes,
38+
self.current_indices,
39+
self.next_indices,
40+
self.old_balances,
41+
self.new_balances,
42+
self.equivocating,
43+
) catch unreachable;
44+
_ = result;
45+
}
46+
47+
pub fn deinit(self: *ComputeDeltasBench, allocator: std.mem.Allocator) void {
48+
allocator.free(self.current_indices);
49+
allocator.free(self.next_indices);
50+
allocator.free(self.old_balances);
51+
allocator.free(self.new_balances);
52+
allocator.free(self.initial_current_indices);
53+
self.deltas_cache.deinit(allocator);
54+
}
55+
};
56+
57+
/// Build a `ComputeDeltasBench` for the given validator count and inactive percentage.
58+
///
59+
/// For active validators: current_index = num_proto_nodes / 2, next_index = num_proto_nodes / 2 + 1.
60+
/// For inactive validators (determined by modulo on `inactive_pct`): both indices are NULL_VOTE_INDEX.
61+
/// Balances are all set to 32 (one effective-balance increment).
62+
fn setupBench(
63+
allocator: std.mem.Allocator,
64+
num_validators: u32,
65+
num_proto_nodes: u32,
66+
inactive_pct: u32,
67+
equivocating: *EquivocatingIndices,
68+
deltas_cache: *DeltasCache,
69+
) !ComputeDeltasBench {
70+
const current_indices = try allocator.alloc(VoteIndex, num_validators);
71+
const next_indices = try allocator.alloc(VoteIndex, num_validators);
72+
const old_balances = try allocator.alloc(u16, num_validators);
73+
const new_balances = try allocator.alloc(u16, num_validators);
74+
75+
const active_current: VoteIndex = num_proto_nodes / 2; // 150
76+
const active_next: VoteIndex = num_proto_nodes / 2 + 1; // 151
77+
78+
// Divisor for modulo-based inactive selection. E.g. 10% -> every 10th, 50% -> every 2nd.
79+
const modulo: u32 = if (inactive_pct > 0) 100 / inactive_pct else 0;
80+
81+
for (0..num_validators) |i| {
82+
const is_inactive = modulo > 0 and (i % modulo == 0);
83+
if (is_inactive) {
84+
current_indices[i] = NULL_VOTE_INDEX;
85+
next_indices[i] = NULL_VOTE_INDEX;
86+
} else {
87+
current_indices[i] = active_current;
88+
next_indices[i] = active_next;
89+
}
90+
old_balances[i] = 32;
91+
new_balances[i] = 32;
92+
}
93+
94+
// Snapshot of initial current_indices for per-iteration reset.
95+
const initial_current_indices = try allocator.alloc(VoteIndex, num_validators);
96+
@memcpy(initial_current_indices, current_indices);
97+
98+
return .{
99+
.num_proto_nodes = num_proto_nodes,
100+
.current_indices = current_indices,
101+
.next_indices = next_indices,
102+
.old_balances = old_balances,
103+
.new_balances = new_balances,
104+
.equivocating = equivocating,
105+
.deltas_cache = deltas_cache,
106+
.initial_current_indices = initial_current_indices,
107+
};
108+
}
109+
110+
pub fn main() !void {
111+
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
112+
const allocator = if (builtin.mode == .Debug) debug_allocator.allocator() else std.heap.c_allocator;
113+
defer if (builtin.mode == .Debug) {
114+
std.debug.assert(debug_allocator.deinit() == .ok);
115+
};
116+
const stdout = std.io.getStdOut().writer();
117+
118+
var bench = zbench.Benchmark.init(allocator, .{});
119+
120+
const num_proto_nodes: u32 = 300;
121+
const inactive_pcts = [_]u32{ 0, 10, 20, 50 };
122+
const validator_counts = [_]u32{ 1_400_000, 2_100_000 };
123+
124+
// Shared equivocating indices: {1, 2, 3, 4, 5}
125+
const equivocating = try allocator.create(EquivocatingIndices);
126+
defer {
127+
equivocating.deinit(allocator);
128+
allocator.destroy(equivocating);
129+
}
130+
equivocating.* = .empty;
131+
for ([_]u64{ 1, 2, 3, 4, 5 }) |idx| {
132+
try equivocating.put(allocator, idx, {});
133+
}
134+
135+
// Track benchmark instances for cleanup after bench.run().
136+
const BENCH_COUNT = validator_counts.len * inactive_pcts.len;
137+
var delta_caches: [BENCH_COUNT]*DeltasCache = undefined;
138+
var bench_instances: [BENCH_COUNT]*ComputeDeltasBench = undefined;
139+
var bench_idx: usize = 0;
140+
141+
// Register each benchmark combination.
142+
inline for (validator_counts) |vc| {
143+
inline for (inactive_pcts) |pct| {
144+
const dc = try allocator.create(DeltasCache);
145+
dc.* = .empty;
146+
const b = try allocator.create(ComputeDeltasBench);
147+
b.* = try setupBench(allocator, vc, num_proto_nodes, pct, equivocating, dc);
148+
delta_caches[bench_idx] = dc;
149+
bench_instances[bench_idx] = b;
150+
bench_idx += 1;
151+
const b_const: *const ComputeDeltasBench = b;
152+
try bench.addParam(
153+
std.fmt.comptimePrint("deltas {d}k i{d}%", .{ vc / 1000, pct }),
154+
b_const,
155+
.{},
156+
);
157+
}
158+
}
159+
defer for (0..BENCH_COUNT) |i| {
160+
bench_instances[i].deinit(allocator);
161+
allocator.destroy(delta_caches[i]);
162+
allocator.destroy(bench_instances[i]);
163+
};
164+
165+
defer bench.deinit();
166+
try bench.run(stdout);
167+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
//! Benchmark for fork-choice `onAttestation` operation.
2+
//!
3+
//! Measures how fast the fork choice can process indexed attestations.
4+
//!
5+
//! Setup: 600K validators, 64-block linear chain, 0 equivocated.
6+
//! - Unaggregated: 3 committees x 135 validators = 405 single-validator attestations.
7+
//! - Aggregated: 64 committees x 11 aggregators x 135 validators = 704 aggregated attestations.
8+
//! - Total: 1109 attestations per iteration.
9+
//!
10+
//! - Computes SSZ hashTreeRoot per attestation inside the measured loop (matching TS).
11+
//! - 64 unique attestation data roots (one per committee index), so the
12+
//! validation cache hits ~1045 times and misses ~64 times per iteration.
13+
//! - Clears the validation cache at the start of each run() to match TS
14+
//! beforeEach behavior (fresh ForkChoice per iteration).
15+
16+
const std = @import("std");
17+
const builtin = @import("builtin");
18+
const zbench = @import("zbench");
19+
const fork_choice = @import("fork_choice");
20+
const consensus_types = @import("consensus_types");
21+
const fork_types = @import("fork_types");
22+
23+
const ForkChoice = fork_choice.ForkChoice;
24+
const AnyIndexedAttestation = fork_types.AnyIndexedAttestation;
25+
const Phase0IndexedAttestation = consensus_types.phase0.IndexedAttestation.Type;
26+
const AttestationData = consensus_types.phase0.AttestationData;
27+
28+
const util = @import("util.zig");
29+
30+
const ZERO_HASH = @import("constants").ZERO_HASH;
31+
32+
// ── Constants matching the TS benchmark ──
33+
34+
const UNAGG_COMMITTEES: u32 = 3;
35+
const VALIDATORS_PER_COMMITTEE: u32 = 135;
36+
const AGG_COMMITTEES: u32 = 64;
37+
const AGGREGATORS_PER_COMMITTEE: u32 = 11;
38+
39+
const UNAGG_COUNT: u32 = UNAGG_COMMITTEES * VALIDATORS_PER_COMMITTEE; // 405
40+
const AGG_COUNT: u32 = AGG_COMMITTEES * AGGREGATORS_PER_COMMITTEE; // 704
41+
const TOTAL_ATT_COUNT: u32 = UNAGG_COUNT + AGG_COUNT; // 1109
42+
43+
/// Benchmark struct for onAttestation.
44+
///
45+
/// Pre-builds all 1109 attestations during setup. Each run() iteration
46+
/// computes SSZ hashTreeRoot per attestation inside the loop (matching TS).
47+
const OnAttestationBench = struct {
48+
fc: *ForkChoice,
49+
phase0_atts: []Phase0IndexedAttestation,
50+
any_atts: []AnyIndexedAttestation,
51+
52+
pub fn run(self: OnAttestationBench, allocator: std.mem.Allocator) void {
53+
// Clear validation cache to match TS behavior (fresh ForkChoice per iteration).
54+
self.fc.validated_attestation_datas.clearRetainingCapacity();
55+
56+
for (self.any_atts, self.phase0_atts) |*att, *phase0_att| {
57+
// Compute hashTreeRoot of AttestationData inside the loop, matching TS behavior.
58+
var att_data_root: [32]u8 = undefined;
59+
AttestationData.hashTreeRoot(&phase0_att.data, &att_data_root) catch unreachable;
60+
self.fc.onAttestation(allocator, att, att_data_root, false) catch unreachable;
61+
}
62+
}
63+
64+
pub fn deinit(self: *OnAttestationBench, allocator: std.mem.Allocator) void {
65+
for (self.phase0_atts) |*att| {
66+
att.attesting_indices.deinit(allocator);
67+
}
68+
allocator.free(self.phase0_atts);
69+
allocator.free(self.any_atts);
70+
util.deinitForkChoice(allocator, self.fc);
71+
}
72+
};
73+
74+
/// Build the OnAttestationBench instance.
75+
///
76+
/// 1. Initialize a ForkChoice with 600K validators and 64 blocks.
77+
/// 2. Compute head via updateAndGetHead.
78+
/// 3. Advance current_slot to 64 so attestations at slot 63 are past-slot.
79+
/// 4. Pre-build 405 unaggregated + 704 aggregated attestations with per-committee roots.
80+
fn setupBench(allocator: std.mem.Allocator) !OnAttestationBench {
81+
const fc = try util.initializeForkChoice(allocator, .{
82+
.initial_block_count = 64,
83+
.initial_validator_count = 600_000,
84+
.initial_equivocated_count = 0,
85+
});
86+
87+
// Compute head so fc.head is populated.
88+
_ = fc.updateAndGetHead(allocator, .{ .get_canonical_head = {} }) catch unreachable;
89+
90+
// Advance store time so attestations at slot 63 are past-slot (immediate apply).
91+
fc.fc_store.current_slot = 64;
92+
93+
// Head block root is the target for all attestations.
94+
const head_root = fc.head.block_root;
95+
96+
// Attestation parameters:
97+
// att_slot = 63 (past slot relative to current_slot = 64)
98+
// target_epoch = floor(63 / 32) = 1
99+
// target_root = head block root
100+
// beacon_block_root = head block root
101+
const att_slot: u64 = 63;
102+
const target_epoch: u64 = 1; // floor(63 / SLOTS_PER_EPOCH=32)
103+
104+
// Allocate attestation storage.
105+
const phase0_atts = try allocator.alloc(Phase0IndexedAttestation, TOTAL_ATT_COUNT);
106+
const any_atts = try allocator.alloc(AnyIndexedAttestation, TOTAL_ATT_COUNT);
107+
108+
var att_idx: u32 = 0;
109+
110+
// ── Unaggregated attestations: 3 committees x 135 validators ──
111+
// Each attestation has exactly one attesting index.
112+
// TS: index = committeeIndex, so committees 0-2 have unique data.
113+
for (0..UNAGG_COMMITTEES) |c| {
114+
for (0..VALIDATORS_PER_COMMITTEE) |v| {
115+
const vi: u64 = @as(u64, c) * VALIDATORS_PER_COMMITTEE + @as(u64, v);
116+
117+
phase0_atts[att_idx] = .{
118+
.attesting_indices = .{},
119+
.data = .{
120+
.slot = att_slot,
121+
.index = @intCast(c),
122+
.beacon_block_root = head_root,
123+
.source = .{ .epoch = 0, .root = ZERO_HASH },
124+
.target = .{ .epoch = target_epoch, .root = head_root },
125+
},
126+
.signature = [_]u8{0} ** 96,
127+
};
128+
try phase0_atts[att_idx].attesting_indices.append(allocator, vi);
129+
any_atts[att_idx] = .{ .phase0 = &phase0_atts[att_idx] };
130+
att_idx += 1;
131+
}
132+
}
133+
134+
// ── Aggregated attestations: 64 committees x 11 aggregators x 135 validators ──
135+
// Each attestation has 135 attesting indices.
136+
// TS: index = committeeIndex, so committees 0-63 have unique data.
137+
// Committees 0-2 share roots with unaggregated attestations above.
138+
for (0..AGG_COMMITTEES) |c| {
139+
for (0..AGGREGATORS_PER_COMMITTEE) |a| {
140+
const start_index: u64 = @as(u64, c) * VALIDATORS_PER_COMMITTEE * AGGREGATORS_PER_COMMITTEE +
141+
@as(u64, a) * VALIDATORS_PER_COMMITTEE;
142+
143+
phase0_atts[att_idx] = .{
144+
.attesting_indices = .{},
145+
.data = .{
146+
.slot = att_slot,
147+
.index = @intCast(c),
148+
.beacon_block_root = head_root,
149+
.source = .{ .epoch = 0, .root = ZERO_HASH },
150+
.target = .{ .epoch = target_epoch, .root = head_root },
151+
},
152+
.signature = [_]u8{0} ** 96,
153+
};
154+
for (0..VALIDATORS_PER_COMMITTEE) |v| {
155+
try phase0_atts[att_idx].attesting_indices.append(allocator, start_index + @as(u64, v));
156+
}
157+
any_atts[att_idx] = .{ .phase0 = &phase0_atts[att_idx] };
158+
att_idx += 1;
159+
}
160+
}
161+
162+
std.debug.assert(att_idx == TOTAL_ATT_COUNT);
163+
164+
return .{
165+
.fc = fc,
166+
.phase0_atts = phase0_atts,
167+
.any_atts = any_atts,
168+
};
169+
}
170+
171+
fn deinitBench(allocator: std.mem.Allocator, b: OnAttestationBench) void {
172+
for (b.phase0_atts) |att| {
173+
var a = att.attesting_indices;
174+
a.deinit(allocator);
175+
}
176+
allocator.free(b.phase0_atts);
177+
allocator.free(b.any_atts);
178+
util.deinitForkChoice(allocator, b.fc);
179+
}
180+
181+
pub fn main() !void {
182+
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
183+
const allocator = if (builtin.mode == .Debug) debug_allocator.allocator() else std.heap.c_allocator;
184+
defer if (builtin.mode == .Debug) {
185+
std.debug.assert(debug_allocator.deinit() == .ok);
186+
};
187+
const stdout = std.io.getStdOut().writer();
188+
189+
var bench = zbench.Benchmark.init(allocator, .{});
190+
191+
const b = try setupBench(allocator);
192+
defer deinitBench(allocator, b);
193+
try bench.addParam("onAttestation 1109 attestations (vc=600000 bc=64)", &b, .{});
194+
195+
defer bench.deinit();
196+
try bench.run(stdout);
197+
}

0 commit comments

Comments
 (0)