Skip to content

Commit 08880db

Browse files
feat: tree view state (#168)
This PR implements a major architectural change to migrate the beacon state from a value-based representation to a tree view representation using persistent merkle trees. This enables efficient state management with structural sharing and copy-on-write semantics for Ethereum consensus state transitions. Changes: - Migrated BeaconStateAllForks to BeaconState using tree view representation with persistent merkle trees - Updated all state access patterns from direct field access to getter/setter methods - Refactored execution payload types to use values instead of pointers - Added Node.Pool initialization throughout the test suite for merkle tree node management --------- Signed-off-by: Chen Kai <281165273grape@gmail.com> Co-authored-by: Chen Kai <281165273grape@gmail.com>
1 parent f7f35ec commit 08880db

155 files changed

Lines changed: 4266 additions & 2552 deletions

File tree

Some content is hidden

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

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ test/spec/test_case/
55
*.ssz
66
test/spec/ssz/generic_tests.zig
77
test/spec/ssz/static_tests.zig
8-
*.era
8+
*.era
9+
10+
# perf
11+
flamegraph.html
12+
perf.data
13+
perf.data.old

bench/state_transition/process_block.zig

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55

66
const std = @import("std");
77
const zbench = @import("zbench");
8+
const Node = @import("persistent_merkle_tree").Node;
89
const state_transition = @import("state_transition");
910
const types = @import("consensus_types");
1011
const config = @import("config");
12+
const download_era_options = @import("download_era_options");
13+
const era = @import("era");
1114
const preset = state_transition.preset;
1215
const ForkSeq = config.ForkSeq;
13-
const CachedBeaconStateAllForks = state_transition.CachedBeaconStateAllForks;
14-
const BeaconStateAllForks = state_transition.BeaconStateAllForks;
16+
const CachedBeaconState = state_transition.CachedBeaconState;
17+
const BeaconState = state_transition.BeaconState;
1518
const SignedBlock = state_transition.SignedBlock;
1619
const SignedBeaconBlock = state_transition.SignedBeaconBlock;
1720
const Body = state_transition.Body;
@@ -29,11 +32,11 @@ const BenchOpts = struct {
2932
};
3033

3134
const ProcessBlockHeaderBench = struct {
32-
cached_state: *CachedBeaconStateAllForks,
35+
cached_state: *CachedBeaconState,
3336
signed_block: SignedBlock,
3437

3538
pub fn run(self: ProcessBlockHeaderBench, allocator: std.mem.Allocator) void {
36-
const cloned = self.cached_state.clone(allocator) catch unreachable;
39+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
3740
defer {
3841
cloned.deinit();
3942
allocator.destroy(cloned);
@@ -44,11 +47,11 @@ const ProcessBlockHeaderBench = struct {
4447
};
4548

4649
const ProcessWithdrawalsBench = struct {
47-
cached_state: *CachedBeaconStateAllForks,
50+
cached_state: *CachedBeaconState,
4851
signed_block: SignedBlock,
4952

5053
pub fn run(self: ProcessWithdrawalsBench, allocator: std.mem.Allocator) void {
51-
const cloned = self.cached_state.clone(allocator) catch unreachable;
54+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
5255
defer {
5356
cloned.deinit();
5457
allocator.destroy(cloned);
@@ -81,11 +84,11 @@ const ProcessWithdrawalsBench = struct {
8184
};
8285

8386
const ProcessExecutionPayloadBench = struct {
84-
cached_state: *CachedBeaconStateAllForks,
87+
cached_state: *CachedBeaconState,
8588
body: Body,
8689

8790
pub fn run(self: ProcessExecutionPayloadBench, allocator: std.mem.Allocator) void {
88-
const cloned = self.cached_state.clone(allocator) catch unreachable;
91+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
8992
defer {
9093
cloned.deinit();
9194
allocator.destroy(cloned);
@@ -97,11 +100,11 @@ const ProcessExecutionPayloadBench = struct {
97100

98101
fn ProcessRandaoBench(comptime opts: BenchOpts) type {
99102
return struct {
100-
cached_state: *CachedBeaconStateAllForks,
103+
cached_state: *CachedBeaconState,
101104
signed_block: SignedBlock,
102105

103106
pub fn run(self: @This(), allocator: std.mem.Allocator) void {
104-
const cloned = self.cached_state.clone(allocator) catch unreachable;
107+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
105108
defer {
106109
cloned.deinit();
107110
allocator.destroy(cloned);
@@ -114,11 +117,11 @@ fn ProcessRandaoBench(comptime opts: BenchOpts) type {
114117
}
115118

116119
const ProcessEth1DataBench = struct {
117-
cached_state: *CachedBeaconStateAllForks,
120+
cached_state: *CachedBeaconState,
118121
signed_block: SignedBlock,
119122

120123
pub fn run(self: ProcessEth1DataBench, allocator: std.mem.Allocator) void {
121-
const cloned = self.cached_state.clone(allocator) catch unreachable;
124+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
122125
defer {
123126
cloned.deinit();
124127
allocator.destroy(cloned);
@@ -131,11 +134,11 @@ const ProcessEth1DataBench = struct {
131134

132135
fn ProcessOperationsBench(comptime opts: BenchOpts) type {
133136
return struct {
134-
cached_state: *CachedBeaconStateAllForks,
137+
cached_state: *CachedBeaconState,
135138
signed_block: SignedBlock,
136139

137140
pub fn run(self: @This(), allocator: std.mem.Allocator) void {
138-
const cloned = self.cached_state.clone(allocator) catch unreachable;
141+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
139142
defer {
140143
cloned.deinit();
141144
allocator.destroy(cloned);
@@ -149,11 +152,11 @@ fn ProcessOperationsBench(comptime opts: BenchOpts) type {
149152

150153
fn ProcessSyncAggregateBench(comptime opts: BenchOpts) type {
151154
return struct {
152-
cached_state: *CachedBeaconStateAllForks,
155+
cached_state: *CachedBeaconState,
153156
signed_block: SignedBlock,
154157

155158
pub fn run(self: @This(), allocator: std.mem.Allocator) void {
156-
const cloned = self.cached_state.clone(allocator) catch unreachable;
159+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
157160
defer {
158161
cloned.deinit();
159162
allocator.destroy(cloned);
@@ -167,11 +170,11 @@ fn ProcessSyncAggregateBench(comptime opts: BenchOpts) type {
167170

168171
fn ProcessBlockBench(comptime opts: BenchOpts) type {
169172
return struct {
170-
cached_state: *CachedBeaconStateAllForks,
173+
cached_state: *CachedBeaconState,
171174
signed_block: SignedBlock,
172175

173176
pub fn run(self: @This(), allocator: std.mem.Allocator) void {
174-
const cloned = self.cached_state.clone(allocator) catch unreachable;
177+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
175178
defer {
176179
cloned.deinit();
177180
allocator.destroy(cloned);
@@ -237,12 +240,12 @@ fn printSegmentStats(stdout: anytype) !void {
237240
}
238241

239242
const ProcessBlockSegmentedBench = struct {
240-
cached_state: *CachedBeaconStateAllForks,
243+
cached_state: *CachedBeaconState,
241244
signed_block: SignedBlock,
242245
body: Body,
243246

244247
pub fn run(self: @This(), allocator: std.mem.Allocator) void {
245-
const cloned = self.cached_state.clone(allocator) catch unreachable;
248+
const cloned = self.cached_state.clone(allocator, .{}) catch unreachable;
246249
defer {
247250
cloned.deinit();
248251
allocator.destroy(cloned);
@@ -258,7 +261,7 @@ const ProcessBlockSegmentedBench = struct {
258261
state_transition.processBlockHeader(allocator, cloned, block) catch unreachable;
259262
recordSegment(.block_header, elapsedSince(header_start));
260263

261-
if (state.isPostCapella()) {
264+
if (state.forkSeq().gte(.capella)) {
262265
const withdrawals_start = std.time.nanoTimestamp();
263266
var withdrawals_result = WithdrawalsResult{
264267
.withdrawals = Withdrawals.initCapacity(allocator, preset.MAX_WITHDRAWALS_PER_PAYLOAD) catch unreachable,
@@ -280,7 +283,7 @@ const ProcessBlockSegmentedBench = struct {
280283
recordSegment(.withdrawals, elapsedSince(withdrawals_start));
281284
}
282285

283-
if (state.isPostBellatrix()) {
286+
if (state.forkSeq().gte(.bellatrix)) {
284287
const exec_start = std.time.nanoTimestamp();
285288
const external_data = BlockExternalData{ .execution_payload_status = .valid, .data_availability_status = .available };
286289
state_transition.processExecutionPayload(allocator, cloned, self.body, external_data) catch unreachable;
@@ -299,7 +302,7 @@ const ProcessBlockSegmentedBench = struct {
299302
state_transition.processOperations(allocator, cloned, beacon_body, .{ .verify_signature = true }) catch unreachable;
300303
recordSegment(.operations, elapsedSince(ops_start));
301304

302-
if (state.isPostAltair()) {
305+
if (state.forkSeq().gte(.altair)) {
303306
const sync_start = std.time.nanoTimestamp();
304307
state_transition.processSyncAggregate(allocator, cloned, beacon_body.syncAggregate(), true) catch unreachable;
305308
recordSegment(.sync_aggregate, elapsedSince(sync_start));
@@ -310,54 +313,77 @@ const ProcessBlockSegmentedBench = struct {
310313
};
311314

312315
pub fn main() !void {
313-
const allocator = std.heap.page_allocator;
316+
var gpa: std.heap.DebugAllocator(.{}) = .init;
317+
const allocator = gpa.allocator();
314318
const stdout = std.io.getStdOut().writer();
319+
var pool = try Node.Pool.init(allocator, 10_000_000);
320+
defer pool.deinit();
315321

316-
const args = try std.process.argsAlloc(allocator);
317-
defer std.process.argsFree(allocator, args);
318-
const state_path = if (args.len > 1) args[1] else "bench/state_transition/state.ssz";
319-
const block_path = if (args.len > 2) args[2] else "bench/state_transition/block.ssz";
322+
// Use download_era_options.era_files[0] for state
320323

321-
const state_file = try std.fs.cwd().openFile(state_path, .{});
322-
defer state_file.close();
323-
const state_bytes = try state_file.readToEndAlloc(allocator, 10_000_000_000);
324+
const era_path_0 = try std.fs.path.join(
325+
allocator,
326+
&[_][]const u8{ download_era_options.era_out_dir, download_era_options.era_files[0] },
327+
);
328+
defer allocator.free(era_path_0);
329+
330+
var era_reader_0 = try era.Reader.open(allocator, config.mainnet.config, era_path_0);
331+
defer era_reader_0.close(allocator);
332+
333+
const state_bytes = try era_reader_0.readSerializedState(allocator, null);
324334
defer allocator.free(state_bytes);
325335

326336
const chain_config = config.mainnet.chain_config;
327337
const slot = slotFromStateBytes(state_bytes);
328338
const detected_fork = config.mainnet.config.forkSeq(slot);
329339
try stdout.print("Benchmarking processBlock with state at fork: {s} (slot {})\n", .{ @tagName(detected_fork), slot });
330340

331-
const block_file = try std.fs.cwd().openFile(block_path, .{});
332-
defer block_file.close();
333-
const block_bytes = try block_file.readToEndAlloc(allocator, 100_000_000);
341+
// Use download_era_options.era_files[1] for state
342+
343+
const era_path_1 = try std.fs.path.join(
344+
allocator,
345+
&[_][]const u8{ download_era_options.era_out_dir, download_era_options.era_files[1] },
346+
);
347+
defer allocator.free(era_path_1);
348+
349+
var era_reader_1 = try era.Reader.open(allocator, config.mainnet.config, era_path_1);
350+
defer era_reader_1.close(allocator);
351+
352+
const block_slot = try era.era.computeStartBlockSlotFromEraNumber(era_reader_1.era_number) + 1;
353+
354+
const block_bytes = try era_reader_1.readSerializedBlock(allocator, block_slot) orelse return error.InvalidEraFile;
334355
defer allocator.free(block_bytes);
335356

336357
inline for (comptime std.enums.values(ForkSeq)) |fork| {
337-
if (detected_fork == fork) return runBenchmark(fork, allocator, stdout, state_bytes, block_bytes, chain_config);
358+
if (detected_fork == fork) return runBenchmark(fork, allocator, &pool, stdout, state_bytes, block_bytes, chain_config);
338359
}
339360
return error.NoBenchmarkRan;
340361
}
341362

342-
fn runBenchmark(comptime fork: ForkSeq, allocator: std.mem.Allocator, stdout: anytype, state_bytes: []const u8, block_bytes: []const u8, chain_config: config.ChainConfig) !void {
343-
const beacon_state = try loadState(fork, allocator, state_bytes);
363+
fn runBenchmark(comptime fork: ForkSeq, allocator: std.mem.Allocator, pool: *Node.Pool, stdout: anytype, state_bytes: []const u8, block_bytes: []const u8, chain_config: config.ChainConfig) !void {
364+
const beacon_state = try loadState(fork, allocator, pool, state_bytes);
344365
const signed_beacon_block = try loadBlock(fork, allocator, block_bytes);
345366
const block_slot = signed_beacon_block.beaconBlock().slot();
367+
try stdout.print("Block: slot: {}\n", .{block_slot});
346368

347-
const beacon_config = config.BeaconConfig.init(chain_config, beacon_state.genesisValidatorsRoot());
369+
const beacon_config = config.BeaconConfig.init(chain_config, (try beacon_state.genesisValidatorsRoot()).*);
348370
const pubkey_index_map = try PubkeyIndexMap.init(allocator);
349371
const index_pubkey_cache = try allocator.create(state_transition.Index2PubkeyCache);
350372
index_pubkey_cache.* = state_transition.Index2PubkeyCache.init(allocator);
351-
try state_transition.syncPubkeys(beacon_state.validators().items, pubkey_index_map, index_pubkey_cache);
373+
const validators = try beacon_state.validatorsSlice(allocator);
374+
defer allocator.free(validators);
375+
376+
try state_transition.syncPubkeys(validators, pubkey_index_map, index_pubkey_cache);
352377

353-
const cached_state = try CachedBeaconStateAllForks.createCachedBeaconState(allocator, beacon_state, .{
378+
const cached_state = try CachedBeaconState.createCachedBeaconState(allocator, beacon_state, .{
354379
.config = &beacon_config,
355380
.index_to_pubkey = index_pubkey_cache,
356381
.pubkey_to_index = pubkey_index_map,
357382
}, .{ .skip_sync_committee_cache = !comptime fork.gte(.altair), .skip_sync_pubkeys = false });
358383

359-
try state_transition.state_transition.processSlotsWithTransientCache(allocator, cached_state, block_slot, .{});
360-
try stdout.print("State: slot={}, validators={}\n", .{ cached_state.state.slot(), beacon_state.validators().items.len });
384+
try state_transition.state_transition.processSlots(allocator, cached_state, block_slot, .{});
385+
try cached_state.state.commit();
386+
try stdout.print("State: slot={}, validators={}\n", .{ try cached_state.state.slot(), try beacon_state.validatorsCount() });
361387

362388
const signed_block = SignedBlock{ .regular = signed_beacon_block };
363389
const body = Body{ .regular = signed_beacon_block.beaconBlock().beaconBlockBody() };
@@ -390,7 +416,7 @@ fn runBenchmark(comptime fork: ForkSeq, allocator: std.mem.Allocator, stdout: an
390416
try bench.addParam("process_block", &ProcessBlockBench(.{ .verify_signature = true }){ .cached_state = cached_state, .signed_block = signed_block }, .{});
391417
try bench.addParam("process_block_no_sig", &ProcessBlockBench(.{ .verify_signature = false }){ .cached_state = cached_state, .signed_block = signed_block }, .{});
392418

393-
// Segmented benchmark (step-by-step timing)
419+
// // Segmented benchmark (step-by-step timing)
394420
resetSegmentStats();
395421
try bench.addParam("block(segments)", &ProcessBlockSegmentedBench{ .cached_state = cached_state, .signed_block = signed_block, .body = body }, .{});
396422

0 commit comments

Comments
 (0)