|
| 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