From cf11f58d479901102ea31da04ab39a05478cdea1 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Sun, 28 Dec 2025 20:23:11 +0530 Subject: [PATCH 01/14] add: granular metrics for block processing breakdown --- pkgs/metrics/src/lib.zig | 129 +++++++++++++++++++++++++++++++++++ pkgs/node/src/chain.zig | 15 ++++ pkgs/node/src/forkchoice.zig | 6 ++ 3 files changed, 150 insertions(+) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 83526a1d2..14abe6b26 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -46,6 +46,16 @@ const Metrics = struct { lean_attestation_validation_time_seconds: ForkChoiceAttestationValidationTimeHistogram, lean_pq_signature_attestation_signing_time_seconds: PQSignatureSigningHistogram, lean_pq_signature_attestation_verification_time_seconds: PQSignatureVerificationHistogram, + // Granular metrics for block processing breakdown + lean_fork_choice_updatehead_time_seconds: ForkChoiceUpdateHeadHistogram, + lean_chain_database_write_time_seconds: ChainDatabaseWriteHistogram, + lean_chain_attestation_loop_time_seconds: ChainAttestationLoopHistogram, + lean_chain_state_clone_time_seconds: ChainStateCloneHistogram, + lean_chain_onblockfollowup_time_seconds: ChainOnBlockFollowupHistogram, + lean_fork_choice_computedeltas_time_seconds: ForkChoiceComputeDeltasHistogram, + lean_fork_choice_applydeltas_time_seconds: ForkChoiceApplyDeltasHistogram, + lean_chain_signature_verification_time_seconds: ChainSignatureVerificationHistogram, + lean_chain_proposer_attestation_time_seconds: ChainProposerAttestationHistogram, const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); @@ -55,6 +65,16 @@ const Metrics = struct { const AttestationsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const PQSignatureSigningHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const PQSignatureVerificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + // Granular histogram types + const ForkChoiceUpdateHeadHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1 }); + const ChainDatabaseWriteHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1 }); + const ChainAttestationLoopHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const ChainStateCloneHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const ChainOnBlockFollowupHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1 }); + const ForkChoiceComputeDeltasHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const ForkChoiceApplyDeltasHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const ChainSignatureVerificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5 }); + const ChainProposerAttestationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const LeanHeadSlotGauge = metrics_lib.Gauge(u64); const LeanLatestJustifiedSlotGauge = metrics_lib.Gauge(u64); const LeanLatestFinalizedSlotGauge = metrics_lib.Gauge(u64); @@ -181,6 +201,60 @@ fn observePQSignatureAttestationVerification(ctx: ?*anyopaque, value: f32) void histogram.observe(value); } +fn observeForkChoiceUpdateHead(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ForkChoiceUpdateHeadHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeChainDatabaseWrite(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ChainDatabaseWriteHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeChainAttestationLoop(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ChainAttestationLoopHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeChainStateClone(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ChainStateCloneHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeChainOnBlockFollowup(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ChainOnBlockFollowupHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeForkChoiceComputeDeltas(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ForkChoiceComputeDeltasHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeForkChoiceApplyDeltas(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ForkChoiceApplyDeltasHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeChainSignatureVerification(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ChainSignatureVerificationHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeChainProposerAttestation(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.ChainProposerAttestationHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + /// The public variables the application interacts with. /// Calling `.start()` on these will start a new timer. pub var chain_onblock_duration_seconds: Histogram = .{ @@ -225,6 +299,43 @@ pub var lean_pq_signature_attestation_verification_time_seconds: Histogram = .{ .observe = &observePQSignatureAttestationVerification, }; +pub var lean_fork_choice_updatehead_time_seconds: Histogram = .{ + .context = null, + .observe = &observeForkChoiceUpdateHead, +}; +pub var lean_chain_database_write_time_seconds: Histogram = .{ + .context = null, + .observe = &observeChainDatabaseWrite, +}; +pub var lean_chain_attestation_loop_time_seconds: Histogram = .{ + .context = null, + .observe = &observeChainAttestationLoop, +}; +pub var lean_chain_state_clone_time_seconds: Histogram = .{ + .context = null, + .observe = &observeChainStateClone, +}; +pub var lean_chain_onblockfollowup_time_seconds: Histogram = .{ + .context = null, + .observe = &observeChainOnBlockFollowup, +}; +pub var lean_fork_choice_computedeltas_time_seconds: Histogram = .{ + .context = null, + .observe = &observeForkChoiceComputeDeltas, +}; +pub var lean_fork_choice_applydeltas_time_seconds: Histogram = .{ + .context = null, + .observe = &observeForkChoiceApplyDeltas, +}; +pub var lean_chain_signature_verification_time_seconds: Histogram = .{ + .context = null, + .observe = &observeChainSignatureVerification, +}; +pub var lean_chain_proposer_attestation_time_seconds: Histogram = .{ + .context = null, + .observe = &observeChainProposerAttestation, +}; + /// Initializes the metrics system. Must be called once at startup. pub fn init(allocator: std.mem.Allocator) !void { if (g_initialized) return; @@ -255,6 +366,15 @@ pub fn init(allocator: std.mem.Allocator) !void { .lean_attestation_validation_time_seconds = Metrics.ForkChoiceAttestationValidationTimeHistogram.init("lean_attestation_validation_time_seconds", .{ .help = "Time taken to validate attestation." }, .{}), .lean_pq_signature_attestation_signing_time_seconds = Metrics.PQSignatureSigningHistogram.init("lean_pq_signature_attestation_signing_time_seconds", .{ .help = "Time taken to sign an attestation." }, .{}), .lean_pq_signature_attestation_verification_time_seconds = Metrics.PQSignatureVerificationHistogram.init("lean_pq_signature_attestation_verification_time_seconds", .{ .help = "Time taken to verify an attestation signature." }, .{}), + .lean_fork_choice_updatehead_time_seconds = Metrics.ForkChoiceUpdateHeadHistogram.init("lean_fork_choice_updatehead_time_seconds", .{ .help = "Fork choice head computation." }, .{}), + .lean_chain_database_write_time_seconds = Metrics.ChainDatabaseWriteHistogram.init("lean_chain_database_write_time_seconds", .{ .help = "Block and state database writes." }, .{}), + .lean_chain_attestation_loop_time_seconds = Metrics.ChainAttestationLoopHistogram.init("lean_chain_attestation_loop_time_seconds", .{ .help = "Attestation validation in block processing." }, .{}), + .lean_chain_state_clone_time_seconds = Metrics.ChainStateCloneHistogram.init("lean_chain_state_clone_time_seconds", .{ .help = "SSZ state cloning." }, .{}), + .lean_chain_onblockfollowup_time_seconds = Metrics.ChainOnBlockFollowupHistogram.init("lean_chain_onblockfollowup_time_seconds", .{ .help = "Event emission and finalization checks." }, .{}), + .lean_fork_choice_computedeltas_time_seconds = Metrics.ForkChoiceComputeDeltasHistogram.init("lean_fork_choice_computedeltas_time_seconds", .{ .help = "Validator weight delta computation." }, .{}), + .lean_fork_choice_applydeltas_time_seconds = Metrics.ForkChoiceApplyDeltasHistogram.init("lean_fork_choice_applydeltas_time_seconds", .{ .help = "Weight delta propagation and best descendant updates." }, .{}), + .lean_chain_signature_verification_time_seconds = Metrics.ChainSignatureVerificationHistogram.init("lean_chain_signature_verification_time_seconds", .{ .help = "XMSS signature verification for block attestations." }, .{}), + .lean_chain_proposer_attestation_time_seconds = Metrics.ChainProposerAttestationHistogram.init("lean_chain_proposer_attestation_time_seconds", .{ .help = "Proposer attestation processing." }, .{}), }; // Set context for histogram wrappers (observe functions already assigned at compile time) @@ -268,6 +388,15 @@ pub fn init(allocator: std.mem.Allocator) !void { lean_attestation_validation_time_seconds.context = @ptrCast(&metrics.lean_attestation_validation_time_seconds); lean_pq_signature_attestation_signing_time_seconds.context = @ptrCast(&metrics.lean_pq_signature_attestation_signing_time_seconds); lean_pq_signature_attestation_verification_time_seconds.context = @ptrCast(&metrics.lean_pq_signature_attestation_verification_time_seconds); + lean_fork_choice_updatehead_time_seconds.context = @ptrCast(&metrics.lean_fork_choice_updatehead_time_seconds); + lean_chain_database_write_time_seconds.context = @ptrCast(&metrics.lean_chain_database_write_time_seconds); + lean_chain_attestation_loop_time_seconds.context = @ptrCast(&metrics.lean_chain_attestation_loop_time_seconds); + lean_chain_state_clone_time_seconds.context = @ptrCast(&metrics.lean_chain_state_clone_time_seconds); + lean_chain_onblockfollowup_time_seconds.context = @ptrCast(&metrics.lean_chain_onblockfollowup_time_seconds); + lean_fork_choice_computedeltas_time_seconds.context = @ptrCast(&metrics.lean_fork_choice_computedeltas_time_seconds); + lean_fork_choice_applydeltas_time_seconds.context = @ptrCast(&metrics.lean_fork_choice_applydeltas_time_seconds); + lean_chain_signature_verification_time_seconds.context = @ptrCast(&metrics.lean_chain_signature_verification_time_seconds); + lean_chain_proposer_attestation_time_seconds.context = @ptrCast(&metrics.lean_chain_proposer_attestation_time_seconds); g_initialized = true; } diff --git a/pkgs/node/src/chain.zig b/pkgs/node/src/chain.zig index 993aac543..10e6c3aac 100644 --- a/pkgs/node/src/chain.zig +++ b/pkgs/node/src/chain.zig @@ -519,10 +519,14 @@ pub const BeamChain = struct { // 1. get parent state const pre_state = self.states.get(block.parent_root) orelse return BlockProcessingError.MissingPreState; const cpost_state = try self.allocator.create(types.BeamState); + const clone_timer = zeam_metrics.lean_chain_state_clone_time_seconds.start(); try types.sszClone(self.allocator, types.BeamState, pre_state.*, cpost_state); + _ = clone_timer.observe(); // 2. verify XMSS signatures (independent step; placed before STF for now, parallelizable later) + const sig_verify_timer = zeam_metrics.lean_chain_signature_verification_time_seconds.start(); try stf.verifySignatures(self.allocator, pre_state, &signedBlock); + _ = sig_verify_timer.observe(); // 3. apply state transition assuming signatures are valid (STF does not re-verify) try stf.apply_transition(self.allocator, cpost_state, block, .{ @@ -552,6 +556,7 @@ pub const BeamChain = struct { block.slot, }); + const attestation_loop_timer = zeam_metrics.lean_chain_attestation_loop_time_seconds.start(); for (block.body.attestations.constSlice(), 0..) |attestation, index| { // Validate attestation before processing (from block = true) self.validateAttestation(attestation, true) catch |e| { @@ -577,15 +582,19 @@ pub const BeamChain = struct { }; zeam_metrics.incrementLeanAttestationsValid(true); } + _ = attestation_loop_timer.observe(); // 5. fc update head + const updatehead_timer = zeam_metrics.lean_fork_choice_updatehead_time_seconds.start(); _ = try self.forkChoice.updateHead(); + _ = updatehead_timer.observe(); break :fcprocessing freshFcBlock; }; try self.states.put(fcBlock.blockRoot, post_state); // 6. import proposer attestation as if it was transmitted on network after block + const proposer_attest_timer = zeam_metrics.lean_chain_proposer_attestation_time_seconds.start(); const proposer_signature = signatures[block.body.attestations.len()]; const signed_proposer_attestation = types.SignedAttestation{ .message = signedBlock.message.proposer_attestation, @@ -594,16 +603,19 @@ pub const BeamChain = struct { self.forkChoice.onAttestation(signed_proposer_attestation, false) catch |e| { self.module_logger.err("error processing proposer attestation={any} e={any}", .{ signed_proposer_attestation, e }); }; + _ = proposer_attest_timer.observe(); const processing_time = onblock_timer.observe(); // 7. Save block and state to database and confirm the block in forkchoice + const db_write_timer = zeam_metrics.lean_chain_database_write_time_seconds.start(); self.updateBlockDb(signedBlock, fcBlock.blockRoot, post_state.*, block.slot) catch |err| { self.module_logger.err("failed to update block database for block root=0x{s}: {any}", .{ std.fmt.fmtSliceHexLower(&fcBlock.blockRoot), err, }); }; + _ = db_write_timer.observe(); try self.forkChoice.confirmBlock(block_root); self.module_logger.info("processed block with root=0x{s} slot={d} processing time={d} (computed root={} computed state={})", .{ @@ -617,6 +629,9 @@ pub const BeamChain = struct { } pub fn onBlockFollowup(self: *Self, pruneForkchoice: bool) void { + const followup_timer = zeam_metrics.lean_chain_onblockfollowup_time_seconds.start(); + defer _ = followup_timer.observe(); + // 8. Asap emit new events via SSE (use forkchoice ProtoBlock directly) const new_head = self.forkChoice.head; if (api.events.NewHeadEvent.fromProtoBlock(self.allocator, new_head)) |head_event| { diff --git a/pkgs/node/src/forkchoice.zig b/pkgs/node/src/forkchoice.zig index 1c2218ceb..24b5ed670 100644 --- a/pkgs/node/src/forkchoice.zig +++ b/pkgs/node/src/forkchoice.zig @@ -86,6 +86,9 @@ pub const ProtoArray = struct { } pub fn applyDeltas(self: *Self, deltas: []isize, cutoff_weight: u64) !void { + const applydeltas_timer = zeam_metrics.lean_fork_choice_applydeltas_time_seconds.start(); + defer _ = applydeltas_timer.observe(); + if (deltas.len != self.nodes.items.len) { return ForkChoiceError.InvalidDeltas; } @@ -639,6 +642,9 @@ pub const ForkChoice = struct { } pub fn computeDeltas(self: *Self, from_known: bool) ![]isize { + const computedeltas_timer = zeam_metrics.lean_fork_choice_computedeltas_time_seconds.start(); + defer _ = computedeltas_timer.observe(); + // prep the deltas data structure while (self.deltas.items.len < self.protoArray.nodes.items.len) { try self.deltas.append(0); From 443740b3db63d27dee22c980ed93aedaa934b03b Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Mon, 29 Dec 2025 01:50:05 +0530 Subject: [PATCH 02/14] add: state transition internal metrics for bottleneck analysis --- pkgs/metrics/src/lib.zig | 72 ++++++++++++++++++++++++ pkgs/state-transition/src/transition.zig | 2 + pkgs/types/src/state.zig | 8 +++ 3 files changed, 82 insertions(+) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 14abe6b26..027844a8f 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -56,6 +56,12 @@ const Metrics = struct { lean_fork_choice_applydeltas_time_seconds: ForkChoiceApplyDeltasHistogram, lean_chain_signature_verification_time_seconds: ChainSignatureVerificationHistogram, lean_chain_proposer_attestation_time_seconds: ChainProposerAttestationHistogram, + // State transition internal metrics + lean_state_transition_state_root_validation_time_seconds: StateRootValidationHistogram, + lean_state_transition_state_root_in_slot_time_seconds: StateRootInSlotHistogram, + lean_state_transition_block_header_hash_time_seconds: BlockHeaderHashHistogram, + lean_state_transition_get_justification_time_seconds: GetJustificationHistogram, + lean_state_transition_with_justifications_time_seconds: WithJustificationsHistogram, const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); @@ -75,6 +81,12 @@ const Metrics = struct { const ForkChoiceApplyDeltasHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const ChainSignatureVerificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5 }); const ChainProposerAttestationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + // State transition internal histogram types + const StateRootValidationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5 }); + const StateRootInSlotHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const BlockHeaderHashHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const WithJustificationsHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const LeanHeadSlotGauge = metrics_lib.Gauge(u64); const LeanLatestJustifiedSlotGauge = metrics_lib.Gauge(u64); const LeanLatestFinalizedSlotGauge = metrics_lib.Gauge(u64); @@ -255,6 +267,36 @@ fn observeChainProposerAttestation(ctx: ?*anyopaque, value: f32) void { histogram.observe(value); } +fn observeStateRootValidation(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.StateRootValidationHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeStateRootInSlot(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.StateRootInSlotHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeBlockHeaderHash(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.BlockHeaderHashHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeGetJustification(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.GetJustificationHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeWithJustifications(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.WithJustificationsHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + /// The public variables the application interacts with. /// Calling `.start()` on these will start a new timer. pub var chain_onblock_duration_seconds: Histogram = .{ @@ -335,6 +377,26 @@ pub var lean_chain_proposer_attestation_time_seconds: Histogram = .{ .context = null, .observe = &observeChainProposerAttestation, }; +pub var lean_state_transition_state_root_validation_time_seconds: Histogram = .{ + .context = null, + .observe = &observeStateRootValidation, +}; +pub var lean_state_transition_state_root_in_slot_time_seconds: Histogram = .{ + .context = null, + .observe = &observeStateRootInSlot, +}; +pub var lean_state_transition_block_header_hash_time_seconds: Histogram = .{ + .context = null, + .observe = &observeBlockHeaderHash, +}; +pub var lean_state_transition_get_justification_time_seconds: Histogram = .{ + .context = null, + .observe = &observeGetJustification, +}; +pub var lean_state_transition_with_justifications_time_seconds: Histogram = .{ + .context = null, + .observe = &observeWithJustifications, +}; /// Initializes the metrics system. Must be called once at startup. pub fn init(allocator: std.mem.Allocator) !void { @@ -375,6 +437,11 @@ pub fn init(allocator: std.mem.Allocator) !void { .lean_fork_choice_applydeltas_time_seconds = Metrics.ForkChoiceApplyDeltasHistogram.init("lean_fork_choice_applydeltas_time_seconds", .{ .help = "Weight delta propagation and best descendant updates." }, .{}), .lean_chain_signature_verification_time_seconds = Metrics.ChainSignatureVerificationHistogram.init("lean_chain_signature_verification_time_seconds", .{ .help = "XMSS signature verification for block attestations." }, .{}), .lean_chain_proposer_attestation_time_seconds = Metrics.ChainProposerAttestationHistogram.init("lean_chain_proposer_attestation_time_seconds", .{ .help = "Proposer attestation processing." }, .{}), + .lean_state_transition_state_root_validation_time_seconds = Metrics.StateRootValidationHistogram.init("lean_state_transition_state_root_validation_time_seconds", .{ .help = "State root validation in apply_transition." }, .{}), + .lean_state_transition_state_root_in_slot_time_seconds = Metrics.StateRootInSlotHistogram.init("lean_state_transition_state_root_in_slot_time_seconds", .{ .help = "State root computation in process_slot." }, .{}), + .lean_state_transition_block_header_hash_time_seconds = Metrics.BlockHeaderHashHistogram.init("lean_state_transition_block_header_hash_time_seconds", .{ .help = "Block header hash in process_block_header." }, .{}), + .lean_state_transition_get_justification_time_seconds = Metrics.GetJustificationHistogram.init("lean_state_transition_get_justification_time_seconds", .{ .help = "Justifications HashMap creation from state." }, .{}), + .lean_state_transition_with_justifications_time_seconds = Metrics.WithJustificationsHistogram.init("lean_state_transition_with_justifications_time_seconds", .{ .help = "State update with justifications HashMap." }, .{}), }; // Set context for histogram wrappers (observe functions already assigned at compile time) @@ -397,6 +464,11 @@ pub fn init(allocator: std.mem.Allocator) !void { lean_fork_choice_applydeltas_time_seconds.context = @ptrCast(&metrics.lean_fork_choice_applydeltas_time_seconds); lean_chain_signature_verification_time_seconds.context = @ptrCast(&metrics.lean_chain_signature_verification_time_seconds); lean_chain_proposer_attestation_time_seconds.context = @ptrCast(&metrics.lean_chain_proposer_attestation_time_seconds); + lean_state_transition_state_root_validation_time_seconds.context = @ptrCast(&metrics.lean_state_transition_state_root_validation_time_seconds); + lean_state_transition_state_root_in_slot_time_seconds.context = @ptrCast(&metrics.lean_state_transition_state_root_in_slot_time_seconds); + lean_state_transition_block_header_hash_time_seconds.context = @ptrCast(&metrics.lean_state_transition_block_header_hash_time_seconds); + lean_state_transition_get_justification_time_seconds.context = @ptrCast(&metrics.lean_state_transition_get_justification_time_seconds); + lean_state_transition_with_justifications_time_seconds.context = @ptrCast(&metrics.lean_state_transition_with_justifications_time_seconds); g_initialized = true; } diff --git a/pkgs/state-transition/src/transition.zig b/pkgs/state-transition/src/transition.zig index f9c071175..d4e827c6c 100644 --- a/pkgs/state-transition/src/transition.zig +++ b/pkgs/state-transition/src/transition.zig @@ -135,8 +135,10 @@ pub fn apply_transition(allocator: Allocator, state: *types.BeamState, block: ty const validateResult = opts.validateResult; if (validateResult) { // verify the post state root + const validation_timer = zeam_metrics.lean_state_transition_state_root_validation_time_seconds.start(); var state_root: [32]u8 = undefined; try ssz.hashTreeRoot(*types.BeamState, state, &state_root, allocator); + _ = validation_timer.observe(); if (!std.mem.eql(u8, &state_root, &block.state_root)) { opts.logger.debug("state root={x:02} block root={x:02}\n", .{ state_root, block.state_root }); return StateTransitionError.InvalidPostState; diff --git a/pkgs/types/src/state.zig b/pkgs/types/src/state.zig index 9f399d4fb..82e5704f5 100644 --- a/pkgs/types/src/state.zig +++ b/pkgs/types/src/state.zig @@ -186,8 +186,10 @@ pub const BeamState = struct { // this completes latest block header for parentRoot checks of new block if (std.mem.eql(u8, &self.latest_block_header.state_root, &utils.ZERO_HASH)) { + const slot_timer = zeam_metrics.lean_state_transition_state_root_in_slot_time_seconds.start(); var prev_state_root: [32]u8 = undefined; try ssz.hashTreeRoot(*BeamState, self, &prev_state_root, allocator); + _ = slot_timer.observe(); self.latest_block_header.state_root = prev_state_root; } } @@ -237,8 +239,10 @@ pub const BeamState = struct { } // 4. verify latest block header is the parent + const header_timer = zeam_metrics.lean_state_transition_block_header_hash_time_seconds.start(); var head_root: [32]u8 = undefined; try ssz.hashTreeRoot(block.BeamBlockHeader, self.latest_block_header, &head_root, allocator); + _ = header_timer.observe(); if (!std.mem.eql(u8, &head_root, &staged_block.parent_root)) { logger.err("state root={x:02} block root={x:02}\n", .{ head_root, staged_block.parent_root }); return StateTransitionError.InvalidParentRoot; @@ -313,7 +317,9 @@ pub const BeamState = struct { justifications.deinit(allocator); } errdefer justifications.deinit(allocator); + const get_just_timer = zeam_metrics.lean_state_transition_get_justification_time_seconds.start(); try self.getJustification(allocator, &justifications); + _ = get_just_timer.observe(); // need to cast to usize for slicing ops but does this makes the STF target arch dependent? const num_validators: usize = @intCast(self.validatorCount()); @@ -427,7 +433,9 @@ pub const BeamState = struct { } } + const with_just_timer = zeam_metrics.lean_state_transition_with_justifications_time_seconds.start(); try self.withJustifications(allocator, &justifications); + _ = with_just_timer.observe(); logger.debug("poststate:historical hashes={d} justified slots ={d}\n justifications_roots:{d}\n justifications_validators={d}\n", .{ self.historical_block_hashes.len(), self.justified_slots.len(), self.justifications_roots.len(), self.justifications_validators.len() }); const justified_str_final = try self.latest_justified.toJsonString(allocator); From 5e889568a1afed7309ec7adfc24318a97b0dc47f Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Tue, 30 Dec 2025 17:15:07 +0530 Subject: [PATCH 03/14] add: justifications cache storage in BeamChain with config flag --- pkgs/node/src/chain.zig | 14 ++++++++++++++ pkgs/types/src/utils.zig | 2 ++ 2 files changed, 16 insertions(+) diff --git a/pkgs/node/src/chain.zig b/pkgs/node/src/chain.zig index 10e6c3aac..f283de90c 100644 --- a/pkgs/node/src/chain.zig +++ b/pkgs/node/src/chain.zig @@ -80,6 +80,7 @@ pub const BeamChain = struct { last_emitted_finalized: types.Checkpoint, connected_peers: *const std.StringHashMap(PeerInfo), node_registry: *const NodeNameRegistry, + justifications_cache: std.AutoHashMap(types.Root, std.AutoHashMapUnmanaged(types.Root, []u8)), const Self = @This(); @@ -116,6 +117,7 @@ pub const BeamChain = struct { .last_emitted_finalized = fork_choice.fcStore.latest_finalized, .connected_peers = connected_peers, .node_registry = opts.node_registry, + .justifications_cache = std.AutoHashMap(types.Root, std.AutoHashMapUnmanaged(types.Root, []u8)).init(allocator), }; } @@ -126,6 +128,18 @@ pub const BeamChain = struct { self.allocator.destroy(entry.value_ptr.*); } self.states.deinit(); + + // Clean up justifications cache + var cache_it = self.justifications_cache.iterator(); + while (cache_it.next()) |entry| { + var just_it = entry.value_ptr.iterator(); + while (just_it.next()) |just_entry| { + self.allocator.free(just_entry.value_ptr.*); + } + entry.value_ptr.deinit(self.allocator); + } + self.justifications_cache.deinit(); + // assume the allocator of config is same as self.allocator self.config.deinit(self.allocator); self.anchor_state.deinit(); diff --git a/pkgs/types/src/utils.zig b/pkgs/types/src/utils.zig index eba66b6e8..5ffc8a2b5 100644 --- a/pkgs/types/src/utils.zig +++ b/pkgs/types/src/utils.zig @@ -73,6 +73,7 @@ pub const GenesisSpec = struct { pub const ChainSpec = struct { preset: params.Preset, name: []u8, + cache_justifications: ?bool = null, pub fn deinit(self: *ChainSpec, allocator: Allocator) void { allocator.free(self.name); @@ -82,6 +83,7 @@ pub const ChainSpec = struct { var obj = json.ObjectMap.init(allocator); try obj.put("preset", json.Value{ .string = @tagName(self.preset) }); try obj.put("name", json.Value{ .string = self.name }); + try obj.put("cache_justifications", json.Value{ .bool = self.cache_justifications orelse false }); return json.Value{ .object = obj }; } From d37d252703de048248db9144d7b83a4bc90aff7b Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Wed, 31 Dec 2025 23:55:31 +0530 Subject: [PATCH 04/14] add: justifications caching with block processing path metrics --- pkgs/cli/src/node.zig | 2 +- pkgs/metrics/src/lib.zig | 7 +++ pkgs/node/src/chain.zig | 16 +++++- pkgs/state-transition/src/transition.zig | 6 ++- pkgs/types/src/state.zig | 68 ++++++++++++++++++------ 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/pkgs/cli/src/node.zig b/pkgs/cli/src/node.zig index 259b70bef..e8b99bf76 100644 --- a/pkgs/cli/src/node.zig +++ b/pkgs/cli/src/node.zig @@ -140,7 +140,7 @@ pub const Node = struct { // some base mainnet spec would be loaded to build this up const chain_spec = - \\{"preset": "mainnet", "name": "devnet0"} + \\{"preset": "mainnet", "name": "devnet0", "cache_justifications": true} ; const json_options = json.ParseOptions{ .ignore_unknown_fields = true, diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 027844a8f..2e0e89a2d 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -62,6 +62,9 @@ const Metrics = struct { lean_state_transition_block_header_hash_time_seconds: BlockHeaderHashHistogram, lean_state_transition_get_justification_time_seconds: GetJustificationHistogram, lean_state_transition_with_justifications_time_seconds: WithJustificationsHistogram, + // Block processing path counters + lean_chain_blocks_with_cached_state_total: BlocksWithCachedStateCounter, + lean_chain_blocks_with_computed_state_total: BlocksWithComputedStateCounter, const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); @@ -97,6 +100,8 @@ const Metrics = struct { const ForkChoiceAttestationsValidLabeledCounter = metrics_lib.CounterVec(u64, struct { source: []const u8 }); const ForkChoiceAttestationsInvalidLabeledCounter = metrics_lib.CounterVec(u64, struct { source: []const u8 }); const ForkChoiceAttestationValidationTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const BlocksWithCachedStateCounter = metrics_lib.Counter(u64); + const BlocksWithComputedStateCounter = metrics_lib.Counter(u64); }; /// Timer struct returned to the application. @@ -442,6 +447,8 @@ pub fn init(allocator: std.mem.Allocator) !void { .lean_state_transition_block_header_hash_time_seconds = Metrics.BlockHeaderHashHistogram.init("lean_state_transition_block_header_hash_time_seconds", .{ .help = "Block header hash in process_block_header." }, .{}), .lean_state_transition_get_justification_time_seconds = Metrics.GetJustificationHistogram.init("lean_state_transition_get_justification_time_seconds", .{ .help = "Justifications HashMap creation from state." }, .{}), .lean_state_transition_with_justifications_time_seconds = Metrics.WithJustificationsHistogram.init("lean_state_transition_with_justifications_time_seconds", .{ .help = "State update with justifications HashMap." }, .{}), + .lean_chain_blocks_with_cached_state_total = Metrics.BlocksWithCachedStateCounter.init("lean_chain_blocks_with_cached_state_total", .{ .help = "Blocks processed with precomputed state (skip apply_transition)." }, .{}), + .lean_chain_blocks_with_computed_state_total = Metrics.BlocksWithComputedStateCounter.init("lean_chain_blocks_with_computed_state_total", .{ .help = "Blocks processed with computed state (call apply_transition with cache)." }, .{}), }; // Set context for histogram wrappers (observe functions already assigned at compile time) diff --git a/pkgs/node/src/chain.zig b/pkgs/node/src/chain.zig index f283de90c..28c1d6bc2 100644 --- a/pkgs/node/src/chain.zig +++ b/pkgs/node/src/chain.zig @@ -529,7 +529,20 @@ pub const BeamChain = struct { break :computedroot cblock_root; }; - const post_state = if (blockInfo.postState) |post_state_ptr| post_state_ptr else computedstate: { + const post_state = if (blockInfo.postState) |post_state_ptr| cachedstate: { + // PATH 1: Block with precomputed state (proposer or DB replay) + // These blocks skip apply_transition() so cache metrics aren't recorded + // Track this path for visibility + if (comptime !zeam_metrics.isZKVM()) { + zeam_metrics.metrics.lean_chain_blocks_with_cached_state_total.incr(); + } + break :cachedstate post_state_ptr; + } else computedstate: { + // PATH 2: Block needs fresh validation - cache is used here + if (comptime !zeam_metrics.isZKVM()) { + zeam_metrics.metrics.lean_chain_blocks_with_computed_state_total.incr(); + } + // 1. get parent state const pre_state = self.states.get(block.parent_root) orelse return BlockProcessingError.MissingPreState; const cpost_state = try self.allocator.create(types.BeamState); @@ -547,6 +560,7 @@ pub const BeamChain = struct { // .logger = self.stf_logger, .validSignatures = true, + .justifications_cache = if (self.config.spec.cache_justifications orelse false) &self.justifications_cache else null, }); break :computedstate cpost_state; }; diff --git a/pkgs/state-transition/src/transition.zig b/pkgs/state-transition/src/transition.zig index d4e827c6c..6eaaf53c7 100644 --- a/pkgs/state-transition/src/transition.zig +++ b/pkgs/state-transition/src/transition.zig @@ -21,6 +21,8 @@ pub const StateTransitionOpts = struct { validSignatures: bool = true, validateResult: bool = true, logger: zeam_utils.ModuleLogger, + // Optional cache for justifications (block_root -> justifications_map) + justifications_cache: ?*std.AutoHashMap(types.Root, std.AutoHashMapUnmanaged(types.Root, []u8)) = null, }; // pub fn process_epoch(state: types.BeamState) void { @@ -46,7 +48,7 @@ pub fn apply_raw_block(allocator: Allocator, state: *types.BeamState, block: *ty try state.process_slots(allocator, block.slot, logger); // process block and modify the pre state to post state - try state.process_block(allocator, block.*, logger); + try state.process_block(allocator, block.*, .{ .logger = logger, .justifications_cache = null }); logger.debug("extracting state root\n", .{}); // extract the post state root @@ -130,7 +132,7 @@ pub fn apply_transition(allocator: Allocator, state: *types.BeamState, block: ty try state.process_slots(allocator, block.slot, opts.logger); // process the block - try state.process_block(allocator, block, opts.logger); + try state.process_block(allocator, block, opts); const validateResult = opts.validateResult; if (validateResult) { diff --git a/pkgs/types/src/state.zig b/pkgs/types/src/state.zig index 82e5704f5..9871d6aa8 100644 --- a/pkgs/types/src/state.zig +++ b/pkgs/types/src/state.zig @@ -272,23 +272,27 @@ pub const BeamState = struct { try staged_block.blockToLatestBlockHeader(allocator, &self.latest_block_header); } - pub fn process_block(self: *Self, allocator: Allocator, staged_block: BeamBlock, logger: zeam_utils.ModuleLogger) !void { + pub fn process_block(self: *Self, allocator: Allocator, staged_block: BeamBlock, opts: anytype) !void { const block_timer = zeam_metrics.lean_state_transition_block_processing_time_seconds.start(); defer _ = block_timer.observe(); // start block processing - try self.process_block_header(allocator, staged_block, logger); + try self.process_block_header(allocator, staged_block, opts.logger); // PQ devner-0 has no execution // try process_execution_payload_header(state, block); - try self.process_operations(allocator, staged_block, logger); + try self.process_operations(allocator, staged_block, opts); } - fn process_operations(self: *Self, allocator: Allocator, staged_block: BeamBlock, logger: zeam_utils.ModuleLogger) !void { + fn process_operations(self: *Self, allocator: Allocator, staged_block: BeamBlock, opts: anytype) !void { + // Compute current block root for cache population + var current_block_root: Root = undefined; + try ssz.hashTreeRoot(BeamBlock, staged_block, ¤t_block_root, allocator); + // 1. process attestations - try self.process_attestations(allocator, staged_block.body.attestations, logger); + try self.process_attestations(allocator, staged_block.body.attestations, staged_block.parent_root, current_block_root, opts); } - fn process_attestations(self: *Self, allocator: Allocator, attestations: Attestations, logger: zeam_utils.ModuleLogger) !void { + fn process_attestations(self: *Self, allocator: Allocator, attestations: Attestations, parent_root: Root, current_block_root: Root, opts: anytype) !void { const attestations_timer = zeam_metrics.lean_state_transition_attestations_processing_time_seconds.start(); defer _ = attestations_timer.observe(); @@ -297,13 +301,13 @@ pub const BeamState = struct { zeam_metrics.metrics.lean_state_transition_attestations_processed_total.incrBy(attestation_count); } - logger.debug("process attestations slot={d} \n prestate:historical hashes={d} justified slots ={d} attestations={d}, ", .{ self.slot, self.historical_block_hashes.len(), self.justified_slots.len(), attestations.constSlice().len }); + opts.logger.debug("process attestations slot={d} \n prestate:historical hashes={d} justified slots ={d} attestations={d}, ", .{ self.slot, self.historical_block_hashes.len(), self.justified_slots.len(), attestations.constSlice().len }); const justified_str = try self.latest_justified.toJsonString(allocator); defer allocator.free(justified_str); const finalized_str = try self.latest_finalized.toJsonString(allocator); defer allocator.free(finalized_str); - logger.debug("prestate justified={s} finalized={s}", .{ justified_str, finalized_str }); + opts.logger.debug("prestate justified={s} finalized={s}", .{ justified_str, finalized_str }); // work directly with SSZ types // historical_block_hashes and justified_slots are already SSZ types in state @@ -317,8 +321,25 @@ pub const BeamState = struct { justifications.deinit(allocator); } errdefer justifications.deinit(allocator); + + // Try to use cached justifications if available const get_just_timer = zeam_metrics.lean_state_transition_get_justification_time_seconds.start(); - try self.getJustification(allocator, &justifications); + if (opts.justifications_cache) |cache| { + if (cache.get(parent_root)) |cached_map| { + // Cache hit - clone the cached justifications map + var it = cached_map.iterator(); + while (it.next()) |entry| { + const cloned_value = try allocator.dupe(u8, entry.value_ptr.*); + try justifications.put(allocator, entry.key_ptr.*, cloned_value); + } + } else { + // Cache miss - build from state + try self.getJustification(allocator, &justifications); + } + } else { + // No cache available - build from state + try self.getJustification(allocator, &justifications); + } _ = get_just_timer.observe(); // need to cast to usize for slicing ops but does this makes the STF target arch dependent? @@ -332,7 +353,7 @@ pub const BeamState = struct { const attestation_str = try attestation_data.toJsonString(allocator); defer allocator.free(attestation_str); - logger.debug("processing attestation={s} validator_id={d}\n....\n", .{ attestation_str, validator_id }); + opts.logger.debug("processing attestation={s} validator_id={d}\n....\n", .{ attestation_str, validator_id }); if (source_slot >= self.justified_slots.len()) { return StateTransitionError.InvalidSlotIndex; @@ -363,7 +384,7 @@ pub const BeamState = struct { target_not_ahead or !is_target_justifiable) { - logger.debug("skipping the attestation as not viable: !(source_justified={}) or target_already_justified={} !(correct_source_root={}) or !(correct_target_root={}) or target_not_ahead={} or !(target_justifiable={})", .{ + opts.logger.debug("skipping the attestation as not viable: !(source_justified={}) or target_already_justified={} !(correct_source_root={}) or !(correct_target_root={}) or target_not_ahead={} or !(target_justifiable={})", .{ is_source_justified, is_target_already_justified, has_correct_source_root, @@ -395,7 +416,7 @@ pub const BeamState = struct { target_justifications_count += 1; } } - logger.debug("target jcount={d}: {any} justifications={any}\n", .{ target_justifications_count, attestation_data.target.root, target_justifications }); + opts.logger.debug("target jcount={d}: {any} justifications={any}\n", .{ target_justifications_count, attestation_data.target.root, target_justifications }); // as soon as we hit the threshold do justifications // note that this simplification works if weight of each validator is 1 @@ -412,7 +433,7 @@ pub const BeamState = struct { const justified_str_new = try self.latest_justified.toJsonString(allocator); defer allocator.free(justified_str_new); - logger.debug("\n\n\n-----------------HURRAY JUSTIFICATION ------------\n{s}\n--------------\n---------------\n-------------------------\n\n\n", .{justified_str_new}); + opts.logger.debug("\n\n\n-----------------HURRAY JUSTIFICATION ------------\n{s}\n--------------\n---------------\n-------------------------\n\n\n", .{justified_str_new}); // source is finalized if target is the next valid justifiable hash var can_target_finalize = true; @@ -422,13 +443,13 @@ pub const BeamState = struct { break; } } - logger.debug("----------------can_target_finalize ({d})={any}----------\n\n", .{ source_slot, can_target_finalize }); + opts.logger.debug("----------------can_target_finalize ({d})={any}----------\n\n", .{ source_slot, can_target_finalize }); if (can_target_finalize == true) { self.latest_finalized = attestation_data.source; const finalized_str_new = try self.latest_finalized.toJsonString(allocator); defer allocator.free(finalized_str_new); - logger.debug("\n\n\n-----------------DOUBLE HURRAY FINALIZATION ------------\n{s}\n--------------\n---------------\n-------------------------\n\n\n", .{finalized_str_new}); + opts.logger.debug("\n\n\n-----------------DOUBLE HURRAY FINALIZATION ------------\n{s}\n--------------\n---------------\n-------------------------\n\n\n", .{finalized_str_new}); } } } @@ -437,13 +458,26 @@ pub const BeamState = struct { try self.withJustifications(allocator, &justifications); _ = with_just_timer.observe(); - logger.debug("poststate:historical hashes={d} justified slots ={d}\n justifications_roots:{d}\n justifications_validators={d}\n", .{ self.historical_block_hashes.len(), self.justified_slots.len(), self.justifications_roots.len(), self.justifications_validators.len() }); + opts.logger.debug("poststate:historical hashes={d} justified slots ={d}\n justifications_roots:{d}\n justifications_validators={d}\n", .{ self.historical_block_hashes.len(), self.justified_slots.len(), self.justifications_roots.len(), self.justifications_validators.len() }); const justified_str_final = try self.latest_justified.toJsonString(allocator); defer allocator.free(justified_str_final); const finalized_str_final = try self.latest_finalized.toJsonString(allocator); defer allocator.free(finalized_str_final); - logger.debug("poststate: justified={s} finalized={s}", .{ justified_str_final, finalized_str_final }); + opts.logger.debug("poststate: justified={s} finalized={s}", .{ justified_str_final, finalized_str_final }); + + // Populate cache with processed justifications for next block to reuse + if (opts.justifications_cache) |cache| { + // Clone the justifications map before it gets freed + var cloned_map: std.AutoHashMapUnmanaged(Root, []u8) = .empty; + var it = justifications.iterator(); + while (it.next()) |entry| { + const cloned_value = try allocator.dupe(u8, entry.value_ptr.*); + try cloned_map.put(allocator, entry.key_ptr.*, cloned_value); + } + // Store in cache for future blocks + try cache.put(current_block_root, cloned_map); + } } pub fn genGenesisBlock(self: *const Self, allocator: Allocator, genesis_block: *block.BeamBlock) !void { From a947febca61ed47585aefb811ec156749c3bd75f Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Tue, 27 Jan 2026 02:18:40 +0530 Subject: [PATCH 05/14] fix: move to forked metrics library with correct histogram --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 1d7ab6677..a39d98bd4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,8 @@ .hash = "datetime-0.8.0-cJNXzP_YAQBxQ5hkNNP6ScnG5XsqciJmeP5RVV4xwCBA", }, .metrics = .{ - .url = "https://github.com/karlseguin/metrics.zig/archive/2b584b6209871b7215706028988169599451dc0d.tar.gz", - .hash = "metrics-0.0.0-W7G4eCG0AQCQXidzvV5kx4l0smr_WCw-8JLIwS_OHYoW", + .url = "https://github.com/chetanyb/metrics.zig/archive/ea0a0eab0502c08f9438f81db9ffe79a10b68a9b.tar.gz", + .hash = "metrics-0.0.0-W7G4eJW1AQCvJ4jSW0a0XlgBoaT86lvYRU0rjyQk04LE", }, .zig_enr = .{ .url = "git+https://github.com/blockblaz/enr#02f187591f8a9616623e0bc91d139f0d18c99bcf", From 16cbe1160fbdc6b14efb22783dfdb0c9c1d737e2 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Tue, 27 Jan 2026 03:05:47 +0530 Subject: [PATCH 06/14] fix: use finer histogram buckets for state transition timings --- pkgs/metrics/src/lib.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 0c73a1d44..2b5aabecd 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -82,7 +82,7 @@ const Metrics = struct { const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); - const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4 }); + const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.6, 0.8, 1, 1.5, 2 }); const SlotsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const BlockProcessingTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const AttestationsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); @@ -102,7 +102,7 @@ const Metrics = struct { const StateRootValidationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5 }); const StateRootInSlotHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const BlockHeaderHashHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); - const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01 }); const WithJustificationsHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const LeanHeadSlotGauge = metrics_lib.Gauge(u64); const LeanLatestJustifiedSlotGauge = metrics_lib.Gauge(u64); From bfcc952725842f5f4c93b898f06da8343c850f47 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Tue, 27 Jan 2026 03:37:24 +0530 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20add=20sub=E2=80=91ms=20buckets=20f?= =?UTF-8?q?or=20state=20transition=20histograms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkgs/metrics/src/lib.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 2b5aabecd..4a9e7671d 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -83,9 +83,9 @@ const Metrics = struct { const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.6, 0.8, 1, 1.5, 2 }); - const SlotsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); - const BlockProcessingTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); - const AttestationsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const SlotsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const BlockProcessingTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const AttestationsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const PQSignatureSigningHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const PQSignatureVerificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); // Granular histogram types @@ -102,8 +102,8 @@ const Metrics = struct { const StateRootValidationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5 }); const StateRootInSlotHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const BlockHeaderHashHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); - const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01 }); - const WithJustificationsHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); + const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); + const WithJustificationsHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); const LeanHeadSlotGauge = metrics_lib.Gauge(u64); const LeanLatestJustifiedSlotGauge = metrics_lib.Gauge(u64); const LeanLatestFinalizedSlotGauge = metrics_lib.Gauge(u64); From 2ee0a87111dedea5866cd48779874f066ae45064 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Tue, 27 Jan 2026 04:16:18 +0530 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20add=20sub=E2=80=9150ms=20buckets?= =?UTF-8?q?=20for=20state=20transition=20histogram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkgs/metrics/src/lib.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 4a9e7671d..8f0df0657 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -82,7 +82,7 @@ const Metrics = struct { const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); - const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.6, 0.8, 1, 1.5, 2 }); + const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.001, 0.002, 0.005, 0.01, 0.02, 0.03, 0.04, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.6, 0.8, 1, 1.5, 2 }); const SlotsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const BlockProcessingTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const AttestationsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); From d1a2ed2615c93b9bbee4bcc311faa24b73c5fd5b Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Wed, 28 Jan 2026 00:16:29 +0530 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20add=20sub=E2=80=91ms=20buckets=20f?= =?UTF-8?q?or=20state=20transition=20histogram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkgs/metrics/src/lib.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 8f0df0657..6e1263a4c 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -82,7 +82,7 @@ const Metrics = struct { const ChainHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); const BlockProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }); - const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.001, 0.002, 0.005, 0.01, 0.02, 0.03, 0.04, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.6, 0.8, 1, 1.5, 2 }); + const StateTransitionHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.03, 0.04, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.6, 0.8, 1, 1.5, 2 }); const SlotsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const BlockProcessingTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const AttestationsProcessingHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); From e77241a156aed5a92134f4b2ea7dfabe5b9b90be Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Wed, 28 Jan 2026 00:16:47 +0530 Subject: [PATCH 10/14] feat: add cache hit/miss counters and timers --- pkgs/metrics/src/lib.zig | 37 +++++++++++++++++++++++++++++++++++++ pkgs/types/src/state.zig | 17 ++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 6e1263a4c..0409918c6 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -62,6 +62,11 @@ const Metrics = struct { lean_state_transition_block_header_hash_time_seconds: BlockHeaderHashHistogram, lean_state_transition_get_justification_time_seconds: GetJustificationHistogram, lean_state_transition_with_justifications_time_seconds: WithJustificationsHistogram, + // Justifications cache metrics + lean_state_transition_justifications_cache_hits_total: JustificationsCacheHitsCounter, + lean_state_transition_justifications_cache_misses_total: JustificationsCacheMissesCounter, + lean_state_transition_justifications_cache_hit_time_seconds: JustificationsCacheHitTimeHistogram, + lean_state_transition_justifications_cache_miss_time_seconds: JustificationsCacheMissTimeHistogram, // Block processing path counters lean_chain_blocks_with_cached_state_total: BlocksWithCachedStateCounter, lean_chain_blocks_with_computed_state_total: BlocksWithComputedStateCounter, @@ -104,6 +109,11 @@ const Metrics = struct { const BlockHeaderHashHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); const WithJustificationsHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); + // Justifications cache metric types - hit time should be fast (clone), miss time slower (build from state) + const JustificationsCacheHitsCounter = metrics_lib.Counter(u64); + const JustificationsCacheMissesCounter = metrics_lib.Counter(u64); + const JustificationsCacheHitTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); + const JustificationsCacheMissTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1 }); const LeanHeadSlotGauge = metrics_lib.Gauge(u64); const LeanLatestJustifiedSlotGauge = metrics_lib.Gauge(u64); const LeanLatestFinalizedSlotGauge = metrics_lib.Gauge(u64); @@ -310,6 +320,18 @@ fn observeWithJustifications(ctx: ?*anyopaque, value: f32) void { histogram.observe(value); } +fn observeJustificationsCacheHitTime(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.JustificationsCacheHitTimeHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + +fn observeJustificationsCacheMissTime(ctx: ?*anyopaque, value: f32) void { + const histogram_ptr = ctx orelse return; + const histogram: *Metrics.JustificationsCacheMissTimeHistogram = @ptrCast(@alignCast(histogram_ptr)); + histogram.observe(value); +} + /// The public variables the application interacts with. /// Calling `.start()` on these will start a new timer. pub var chain_onblock_duration_seconds: Histogram = .{ @@ -410,6 +432,14 @@ pub var lean_state_transition_with_justifications_time_seconds: Histogram = .{ .context = null, .observe = &observeWithJustifications, }; +pub var lean_state_transition_justifications_cache_hit_time_seconds: Histogram = .{ + .context = null, + .observe = &observeJustificationsCacheHitTime, +}; +pub var lean_state_transition_justifications_cache_miss_time_seconds: Histogram = .{ + .context = null, + .observe = &observeJustificationsCacheMissTime, +}; /// Initializes the metrics system. Must be called once at startup. pub fn init(allocator: std.mem.Allocator) !void { @@ -455,6 +485,11 @@ pub fn init(allocator: std.mem.Allocator) !void { .lean_state_transition_block_header_hash_time_seconds = Metrics.BlockHeaderHashHistogram.init("lean_state_transition_block_header_hash_time_seconds", .{ .help = "Block header hash in process_block_header." }, .{}), .lean_state_transition_get_justification_time_seconds = Metrics.GetJustificationHistogram.init("lean_state_transition_get_justification_time_seconds", .{ .help = "Justifications HashMap creation from state." }, .{}), .lean_state_transition_with_justifications_time_seconds = Metrics.WithJustificationsHistogram.init("lean_state_transition_with_justifications_time_seconds", .{ .help = "State update with justifications HashMap." }, .{}), + // Justifications cache metrics + .lean_state_transition_justifications_cache_hits_total = Metrics.JustificationsCacheHitsCounter.init("lean_state_transition_justifications_cache_hits_total", .{ .help = "Total justifications cache hits (clone from cache)." }, .{}), + .lean_state_transition_justifications_cache_misses_total = Metrics.JustificationsCacheMissesCounter.init("lean_state_transition_justifications_cache_misses_total", .{ .help = "Total justifications cache misses (build from state)." }, .{}), + .lean_state_transition_justifications_cache_hit_time_seconds = Metrics.JustificationsCacheHitTimeHistogram.init("lean_state_transition_justifications_cache_hit_time_seconds", .{ .help = "Time to clone justifications from cache." }, .{}), + .lean_state_transition_justifications_cache_miss_time_seconds = Metrics.JustificationsCacheMissTimeHistogram.init("lean_state_transition_justifications_cache_miss_time_seconds", .{ .help = "Time to build justifications from state (cache miss)." }, .{}), .lean_chain_blocks_with_cached_state_total = Metrics.BlocksWithCachedStateCounter.init("lean_chain_blocks_with_cached_state_total", .{ .help = "Blocks processed with precomputed state (skip apply_transition)." }, .{}), .lean_chain_blocks_with_computed_state_total = Metrics.BlocksWithComputedStateCounter.init("lean_chain_blocks_with_computed_state_total", .{ .help = "Blocks processed with computed state (call apply_transition with cache)." }, .{}), // Network peer metrics @@ -501,6 +536,8 @@ pub fn init(allocator: std.mem.Allocator) !void { lean_state_transition_block_header_hash_time_seconds.context = @ptrCast(&metrics.lean_state_transition_block_header_hash_time_seconds); lean_state_transition_get_justification_time_seconds.context = @ptrCast(&metrics.lean_state_transition_get_justification_time_seconds); lean_state_transition_with_justifications_time_seconds.context = @ptrCast(&metrics.lean_state_transition_with_justifications_time_seconds); + lean_state_transition_justifications_cache_hit_time_seconds.context = @ptrCast(&metrics.lean_state_transition_justifications_cache_hit_time_seconds); + lean_state_transition_justifications_cache_miss_time_seconds.context = @ptrCast(&metrics.lean_state_transition_justifications_cache_miss_time_seconds); g_initialized = true; } diff --git a/pkgs/types/src/state.zig b/pkgs/types/src/state.zig index 3e5a6c8f9..141fd2b0e 100644 --- a/pkgs/types/src/state.zig +++ b/pkgs/types/src/state.zig @@ -390,18 +390,33 @@ pub const BeamState = struct { if (justifications_cache) |cache| { if (cache.get(parent_root)) |cached_map| { // Cache hit - clone the cached justifications map + const cache_hit_timer = zeam_metrics.lean_state_transition_justifications_cache_hit_time_seconds.start(); var it = cached_map.iterator(); while (it.next()) |entry| { const cloned_value = try allocator.dupe(u8, entry.value_ptr.*); try justifications.put(allocator, entry.key_ptr.*, cloned_value); } + _ = cache_hit_timer.observe(); + if (comptime !zeam_metrics.isZKVM()) { + zeam_metrics.metrics.lean_state_transition_justifications_cache_hits_total.incr(); + } } else { // Cache miss - build from state + const cache_miss_timer = zeam_metrics.lean_state_transition_justifications_cache_miss_time_seconds.start(); try self.getJustification(allocator, &justifications); + _ = cache_miss_timer.observe(); + if (comptime !zeam_metrics.isZKVM()) { + zeam_metrics.metrics.lean_state_transition_justifications_cache_misses_total.incr(); + } } } else { - // No cache available - build from state + // No cache available - build from state (counts as miss) + const cache_miss_timer = zeam_metrics.lean_state_transition_justifications_cache_miss_time_seconds.start(); try self.getJustification(allocator, &justifications); + _ = cache_miss_timer.observe(); + if (comptime !zeam_metrics.isZKVM()) { + zeam_metrics.metrics.lean_state_transition_justifications_cache_misses_total.incr(); + } } _ = get_just_timer.observe(); From 3907d3c6434dbe0a0b073298f314fe1cd3fb9646 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Thu, 29 Jan 2026 15:43:39 +0530 Subject: [PATCH 11/14] chore: comment cleanup --- pkgs/metrics/src/lib.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/metrics/src/lib.zig b/pkgs/metrics/src/lib.zig index 0409918c6..0be58d1e8 100644 --- a/pkgs/metrics/src/lib.zig +++ b/pkgs/metrics/src/lib.zig @@ -109,7 +109,7 @@ const Metrics = struct { const BlockHeaderHashHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.005, 0.01, 0.025, 0.05, 0.1, 1 }); const GetJustificationHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); const WithJustificationsHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); - // Justifications cache metric types - hit time should be fast (clone), miss time slower (build from state) + // Justifications cache metric types const JustificationsCacheHitsCounter = metrics_lib.Counter(u64); const JustificationsCacheMissesCounter = metrics_lib.Counter(u64); const JustificationsCacheHitTimeHistogram = metrics_lib.Histogram(f32, &[_]f32{ 0.0000005, 0.000001, 0.000002, 0.000005, 0.00001, 0.00002, 0.00005, 0.0001, 0.0002, 0.0005, 0.001 }); From 5bb88329cad9f63f931d9abe823091df924e6db9 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Fri, 30 Jan 2026 00:34:34 +0530 Subject: [PATCH 12/14] fix: point to blockblaz owned metrics library --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index a39d98bd4..9e9327b45 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,7 +20,7 @@ .hash = "datetime-0.8.0-cJNXzP_YAQBxQ5hkNNP6ScnG5XsqciJmeP5RVV4xwCBA", }, .metrics = .{ - .url = "https://github.com/chetanyb/metrics.zig/archive/ea0a0eab0502c08f9438f81db9ffe79a10b68a9b.tar.gz", + .url = "https://github.com/blockblaz/metrics.zig/archive/ea0a0eab0502c08f9438f81db9ffe79a10b68a9b.tar.gz", .hash = "metrics-0.0.0-W7G4eJW1AQCvJ4jSW0a0XlgBoaT86lvYRU0rjyQk04LE", }, .zig_enr = .{ From bd6c7deb2aa64c279d28264d316252ff4c840787 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Fri, 30 Jan 2026 17:25:29 +0530 Subject: [PATCH 13/14] feat: add justification cache eviction --- pkgs/types/src/state.zig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkgs/types/src/state.zig b/pkgs/types/src/state.zig index 141fd2b0e..774558be2 100644 --- a/pkgs/types/src/state.zig +++ b/pkgs/types/src/state.zig @@ -599,6 +599,21 @@ pub const BeamState = struct { } // Store in cache for future blocks try cache.put(current_block_root, cloned_map); + + // Evict all entries except current and parent (only parent is ever looked up) + var evict_it = cache.iterator(); + while (evict_it.next()) |entry| { + const key = entry.key_ptr.*; + if (std.mem.eql(u8, &key, ¤t_block_root) or std.mem.eql(u8, &key, &parent_root)) { + continue; + } + var val_it = entry.value_ptr.iterator(); + while (val_it.next()) |val_entry| { + allocator.free(val_entry.value_ptr.*); + } + entry.value_ptr.deinit(allocator); + cache.removeByPtr(entry.key_ptr); + } } } From 7dc6a4b534d09666e552f527f02d59bb257a1bb0 Mon Sep 17 00:00:00 2001 From: Chetany Bhardwaj Date: Thu, 19 Feb 2026 22:29:44 +0530 Subject: [PATCH 14/14] refactor: move justifications cache to chain layer - Cache logic moved from STF to chain.zig with zero-clone ownership transfer - Simplified eviction: clear-all instead of selective (parent check was dead code) - STF receives pre-populated justifications or builds from state internally - Remove cache_justifications config flag, add errdefer for memory safety --- build.zig | 1 + pkgs/cli/src/node.zig | 2 +- pkgs/node/src/chain.zig | 45 ++++++++++- pkgs/state-transition/src/transition.zig | 6 +- pkgs/types/src/state.zig | 97 +++++------------------- pkgs/types/src/utils.zig | 2 - 6 files changed, 66 insertions(+), 87 deletions(-) diff --git a/build.zig b/build.zig index 23ac33ae3..987fa4abc 100644 --- a/build.zig +++ b/build.zig @@ -808,6 +808,7 @@ fn build_zkvm_targets( .root_source_file = b.path("pkgs/state-transition-runtime/src/main.zig"), .target = target, .optimize = optimize, + .strip = true, // Strip debug info to avoid RISC-V relocation overflow }), }); // addimport to root module is even required afer declaring it in mod diff --git a/pkgs/cli/src/node.zig b/pkgs/cli/src/node.zig index 743227ac4..97b682007 100644 --- a/pkgs/cli/src/node.zig +++ b/pkgs/cli/src/node.zig @@ -138,7 +138,7 @@ pub const Node = struct { // some base mainnet spec would be loaded to build this up const chain_spec = - \\{"preset": "mainnet", "name": "devnet0", "cache_justifications": true} + \\{"preset": "mainnet", "name": "devnet0"} ; const json_options = json.ParseOptions{ .ignore_unknown_fields = true, diff --git a/pkgs/node/src/chain.zig b/pkgs/node/src/chain.zig index 2d957fffe..d2594752b 100644 --- a/pkgs/node/src/chain.zig +++ b/pkgs/node/src/chain.zig @@ -697,9 +697,11 @@ pub const BeamChain = struct { // 1. get parent state const pre_state = self.states.get(block.parent_root) orelse return BlockProcessingError.MissingPreState; const cpost_state = try self.allocator.create(types.BeamState); + errdefer self.allocator.destroy(cpost_state); const clone_timer = zeam_metrics.lean_chain_state_clone_time_seconds.start(); try types.sszClone(self.allocator, types.BeamState, pre_state.*, cpost_state); _ = clone_timer.observe(); + errdefer cpost_state.deinit(); // 2. verify XMSS signatures (independent step; placed before STF for now, parallelizable later) // Use public key cache to avoid repeated SSZ deserialization of validator public keys @@ -707,13 +709,50 @@ pub const BeamChain = struct { try stf.verifySignatures(self.allocator, pre_state, &signedBlock, &self.public_key_cache); _ = sig_verify_timer.observe(); - // 3. apply state transition assuming signatures are valid (STF does not re-verify) + // 3. prepare justifications (from cache or state) + var justifications: std.AutoHashMapUnmanaged(types.Root, []u8) = .empty; + var owns_justifications = true; + + const get_just_timer = zeam_metrics.lean_state_transition_get_justification_time_seconds.start(); + if (self.justifications_cache.fetchRemove(block.parent_root)) |kv| { + justifications = kv.value; + zeam_metrics.metrics.lean_state_transition_justifications_cache_hits_total.incr(); + } else { + try pre_state.getJustification(self.allocator, &justifications); + zeam_metrics.metrics.lean_state_transition_justifications_cache_misses_total.incr(); + } + _ = get_just_timer.observe(); + + defer if (owns_justifications) { + var it = justifications.iterator(); + while (it.next()) |entry| { + self.allocator.free(entry.value_ptr.*); + } + justifications.deinit(self.allocator); + }; + + // 4. apply state transition try stf.apply_transition(self.allocator, cpost_state, block, .{ - // .logger = self.stf_logger, .validSignatures = true, - .justifications_cache = if (self.config.spec.cache_justifications orelse false) &self.justifications_cache else null, + .justifications = &justifications, }); + + // 5. store justifications to cache (clear old entries first) + var clear_it = self.justifications_cache.iterator(); + while (clear_it.next()) |entry| { + var map = entry.value_ptr.*; + var map_it = map.iterator(); + while (map_it.next()) |e| { + self.allocator.free(e.value_ptr.*); + } + map.deinit(self.allocator); + } + self.justifications_cache.clearRetainingCapacity(); + + try self.justifications_cache.put(block_root, justifications); + owns_justifications = false; + break :computedstate cpost_state; }; diff --git a/pkgs/state-transition/src/transition.zig b/pkgs/state-transition/src/transition.zig index 4ce86f533..6f2bed88e 100644 --- a/pkgs/state-transition/src/transition.zig +++ b/pkgs/state-transition/src/transition.zig @@ -20,8 +20,8 @@ pub const StateTransitionOpts = struct { validSignatures: bool = true, validateResult: bool = true, logger: zeam_utils.ModuleLogger, - // Optional cache for justifications (block_root -> justifications_map) - justifications_cache: ?*std.AutoHashMap(types.Root, std.AutoHashMapUnmanaged(types.Root, []u8)) = null, + // Optional pre-populated justifications map; if null, built from state internally + justifications: ?*std.AutoHashMapUnmanaged(types.Root, []u8) = null, }; // pub fn process_epoch(state: types.BeamState) void { @@ -47,7 +47,7 @@ pub fn apply_raw_block(allocator: Allocator, state: *types.BeamState, block: *ty try state.process_slots(allocator, block.slot, logger); // process block and modify the pre state to post state - try state.process_block(allocator, block.*, .{ .logger = logger, .justifications_cache = null }); + try state.process_block(allocator, block.*, .{ .logger = logger, .justifications = null }); logger.debug("extracting state root\n", .{}); // extract the post state root diff --git a/pkgs/types/src/state.zig b/pkgs/types/src/state.zig index 87928c78d..2d8e27d23 100644 --- a/pkgs/types/src/state.zig +++ b/pkgs/types/src/state.zig @@ -344,20 +344,16 @@ pub const BeamState = struct { } fn process_operations(self: *Self, allocator: Allocator, staged_block: BeamBlock, opts: anytype) !void { - // Compute current block root for cache population - var current_block_root: Root = undefined; - try zeam_utils.hashTreeRoot(BeamBlock, staged_block, ¤t_block_root, allocator); - // 1. process attestations - try self.process_attestations(allocator, staged_block.body.attestations, staged_block.parent_root, current_block_root, opts); + try self.process_attestations(allocator, staged_block.body.attestations, opts); } - fn process_attestations(self: *Self, allocator: Allocator, attestations: AggregatedAttestations, parent_root: Root, current_block_root: Root, opts: anytype) !void { + fn process_attestations(self: *Self, allocator: Allocator, attestations: AggregatedAttestations, opts: anytype) !void { const attestations_timer = zeam_metrics.lean_state_transition_attestations_processing_time_seconds.start(); defer _ = attestations_timer.observe(); const logger = if (@hasField(@TypeOf(opts), "logger")) opts.logger else opts; - const justifications_cache = if (@hasField(@TypeOf(opts), "justifications_cache")) opts.justifications_cache else null; + const justifications_ptr = if (@hasField(@TypeOf(opts), "justifications")) opts.justifications else null; if (comptime !zeam_metrics.isZKVM()) { const attestation_count: u64 = @intCast(attestations.constSlice().len); @@ -375,50 +371,23 @@ pub const BeamState = struct { // work directly with SSZ types // historical_block_hashes and justified_slots are already SSZ types in state - var justifications: std.AutoHashMapUnmanaged(Root, []u8) = .empty; - defer { - var iterator = justifications.iterator(); + // Use provided justifications map or build from state + var local_justifications: std.AutoHashMapUnmanaged(Root, []u8) = .empty; + const owns_local = justifications_ptr == null; + defer if (owns_local) { + var iterator = local_justifications.iterator(); while (iterator.next()) |entry| { allocator.free(entry.value_ptr.*); } - justifications.deinit(allocator); - } - errdefer justifications.deinit(allocator); - - // Try to use cached justifications if available - const get_just_timer = zeam_metrics.lean_state_transition_get_justification_time_seconds.start(); - if (justifications_cache) |cache| { - if (cache.get(parent_root)) |cached_map| { - // Cache hit - clone the cached justifications map - const cache_hit_timer = zeam_metrics.lean_state_transition_justifications_cache_hit_time_seconds.start(); - var it = cached_map.iterator(); - while (it.next()) |entry| { - const cloned_value = try allocator.dupe(u8, entry.value_ptr.*); - try justifications.put(allocator, entry.key_ptr.*, cloned_value); - } - _ = cache_hit_timer.observe(); - if (comptime !zeam_metrics.isZKVM()) { - zeam_metrics.metrics.lean_state_transition_justifications_cache_hits_total.incr(); - } - } else { - // Cache miss - build from state - const cache_miss_timer = zeam_metrics.lean_state_transition_justifications_cache_miss_time_seconds.start(); - try self.getJustification(allocator, &justifications); - _ = cache_miss_timer.observe(); - if (comptime !zeam_metrics.isZKVM()) { - zeam_metrics.metrics.lean_state_transition_justifications_cache_misses_total.incr(); - } - } - } else { - // No cache available - build from state (counts as miss) - const cache_miss_timer = zeam_metrics.lean_state_transition_justifications_cache_miss_time_seconds.start(); - try self.getJustification(allocator, &justifications); - _ = cache_miss_timer.observe(); - if (comptime !zeam_metrics.isZKVM()) { - zeam_metrics.metrics.lean_state_transition_justifications_cache_misses_total.incr(); - } - } - _ = get_just_timer.observe(); + local_justifications.deinit(allocator); + }; + + const justifications: *std.AutoHashMapUnmanaged(Root, []u8) = if (justifications_ptr) |ptr| ptr else blk: { + const get_just_timer = zeam_metrics.lean_state_transition_get_justification_time_seconds.start(); + try self.getJustification(allocator, &local_justifications); + _ = get_just_timer.observe(); + break :blk &local_justifications; + }; var finalized_slot: Slot = self.latest_finalized.slot; @@ -577,7 +546,7 @@ pub const BeamState = struct { } const with_just_timer = zeam_metrics.lean_state_transition_with_justifications_time_seconds.start(); - try self.withJustifications(allocator, &justifications); + try self.withJustifications(allocator, justifications); _ = with_just_timer.observe(); logger.debug("poststate:historical hashes={d} justified slots={d}\n justifications_roots:{d}\n justifications_validators={d}\n", .{ self.historical_block_hashes.len(), self.justified_slots.len(), self.justifications_roots.len(), self.justifications_validators.len() }); @@ -587,34 +556,6 @@ pub const BeamState = struct { defer allocator.free(finalized_str_final); logger.debug("poststate: justified={s} finalized={s}", .{ justified_str_final, finalized_str_final }); - - // Populate cache with processed justifications for next block to reuse - if (justifications_cache) |cache| { - // Clone the justifications map before it gets freed - var cloned_map: std.AutoHashMapUnmanaged(Root, []u8) = .empty; - var it = justifications.iterator(); - while (it.next()) |entry| { - const cloned_value = try allocator.dupe(u8, entry.value_ptr.*); - try cloned_map.put(allocator, entry.key_ptr.*, cloned_value); - } - // Store in cache for future blocks - try cache.put(current_block_root, cloned_map); - - // Evict all entries except current and parent (only parent is ever looked up) - var evict_it = cache.iterator(); - while (evict_it.next()) |entry| { - const key = entry.key_ptr.*; - if (std.mem.eql(u8, &key, ¤t_block_root) or std.mem.eql(u8, &key, &parent_root)) { - continue; - } - var val_it = entry.value_ptr.iterator(); - while (val_it.next()) |val_entry| { - allocator.free(val_entry.value_ptr.*); - } - entry.value_ptr.deinit(allocator); - cache.removeByPtr(entry.key_ptr); - } - } } pub fn genGenesisBlock(self: *const Self, allocator: Allocator, genesis_block: *block.BeamBlock) !void { @@ -1054,7 +995,7 @@ test "pruning keeps pending justifications" { try attestations_list.append(att_1_to_2); att_1_to_2_transferred = true; - try state.process_attestations(std.testing.allocator, attestations_list, utils.ZERO_HASH, utils.ZERO_HASH, logger); + try state.process_attestations(std.testing.allocator, attestations_list, .{ .logger = logger }); try std.testing.expectEqual(@as(Slot, 1), state.latest_finalized.slot); try std.testing.expectEqual(@as(Slot, 2), state.latest_justified.slot); diff --git a/pkgs/types/src/utils.zig b/pkgs/types/src/utils.zig index 6932a4939..4622fe07a 100644 --- a/pkgs/types/src/utils.zig +++ b/pkgs/types/src/utils.zig @@ -125,7 +125,6 @@ pub const GenesisSpec = struct { pub const ChainSpec = struct { preset: params.Preset, name: []u8, - cache_justifications: ?bool = null, pub fn deinit(self: *ChainSpec, allocator: Allocator) void { allocator.free(self.name); @@ -135,7 +134,6 @@ pub const ChainSpec = struct { var obj = json.ObjectMap.init(allocator); try obj.put("preset", json.Value{ .string = @tagName(self.preset) }); try obj.put("name", json.Value{ .string = self.name }); - try obj.put("cache_justifications", json.Value{ .bool = self.cache_justifications orelse false }); return json.Value{ .object = obj }; }