feat: impl rewards/merkle_proof/fork spec test - #81
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR implements spec test coverage for rewards, merkle_proof, and fork transitions in the state transition module. It introduces new test runners and writers following the existing test infrastructure pattern, adds necessary dependency imports, and fixes several latent bugs discovered during implementation.
- Adds rewards spec test runner with delta accumulation logic for various reward/penalty types
- Implements merkle proof verification test runner using persistent merkle trees
- Adds fork transition test runner supporting upgrades from phase0 through electra
- Refactors epoch computation logic to improve code reuse and correctness
Reviewed Changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| zbuild.zon | Adds persistent_merkle_tree dependency for merkle proof tests |
| test/spec/writer/rewards.zig | New test writer for rewards spec tests with pyspec test suite support |
| test/spec/writer/merkle_proof.zig | New test writer for merkle proof spec tests with nested suite/case structure |
| test/spec/writer/fork.zig | New test writer for fork transition spec tests |
| test/spec/write_spec_tests.zig | Updates test generator to support new runners and nested test case directories |
| test/spec/runner_kind.zig | Adds new runner kinds and hasSuiteCase helper for merkle_proof |
| test/spec/runner/rewards.zig | Implements rewards test runner with delta loading and accumulation |
| test/spec/runner/merkle_proof.zig | Implements merkle proof test runner with YAML parsing and proof verification |
| test/spec/runner/fork.zig | Implements fork transition test runner with upgrade function dispatch |
| test/spec/root.zig | Registers new test modules in the test root |
| src/state_transition/utils/finality.zig | Refactors finality delay calculation to use computePreviousEpoch helper |
| src/state_transition/utils/epoch.zig | Extracts computePreviousEpoch helper function for code reuse |
| src/state_transition/slot/upgrade_state_to_altair.zig | Uses new computePreviousEpoch helper for consistency |
| src/state_transition/root.zig | Exports upgrade functions and getRewardsAndPenalties for test access |
| src/state_transition/cache/epoch_cache.zig | Uses computePreviousEpoch helper in shuffling lookup |
| src/ssz/type/byte_list.zig | Fixes memory leak by freeing allocated nodes array |
| build.zig | Adds persistent_merkle_tree and hex module imports for spec tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try writer.print(test_template, .{ | ||
| @tagName(fork), | ||
| test_suite_name, | ||
| test_case_name, | ||
|
|
||
| @tagName(fork), | ||
| @tagName(handler), | ||
| test_suite_name, | ||
| test_case_name, | ||
|
|
||
| @tagName(fork), | ||
| @tagName(fork), | ||
| }); |
There was a problem hiding this comment.
Format argument mismatch: The test template has 4 {s} placeholders on line 23, 4 on line 27, and 1 on line 32 (total 9 placeholders), but only 8 format arguments are provided. Lines 50-52 provide 3 args, lines 54-57 provide 4 args, and lines 59-60 provide 2 args. The test name on line 23 expects 4 arguments but only gets 3. The handler name appears to be missing. Line 51 should likely include @tagName(handler), after test_suite_name,.
| self.actual_rewards = epoch_cache.rewards; | ||
| self.actual_penalties = epoch_cache.penalties; |
There was a problem hiding this comment.
Memory safety issue: self.actual_rewards and self.actual_penalties are assigned to slices owned by epoch_cache (lines 176-177), but epoch_cache is destroyed via deinit() at scope exit (lines 169-172). This results in dangling pointers. When the test tries to read these slices at line 156, it will access freed memory. The rewards and penalties should be copied to owned memory, or the epoch_cache should not be freed until after the test completes.
There was a problem hiding this comment.
These two fields were in the reusable cache which not be freed lines 169-172
Signed-off-by: grapebaba <grapebaba@grapebabadeMacBook-Pro.local>
wemeetagain
left a comment
There was a problem hiding this comment.
Looks good, only source change is preventing an underflow?
you mean calc previous epoch? |
Signed-off-by: grapebaba <grapebaba@grapebabadeMacBook-Pro.local>
Not sure I get it, |
it's more like a refactor, we have to handle underflow everywhere so it's more convenient to have a separate function for it |
twoeths
left a comment
There was a problem hiding this comment.
I don't see we run merkle_proof at all on my generated code:
test "deneb merkle_proof single_merkle_proof BeaconBlockBody blob_kzg_commitment_merkle_proof__random_block_1" {
const test_dir_name = try std.fs.path.join(allocator, &[_][]const u8{
spec_test_options.spec_test_out_dir,
spec_test_options.spec_test_version,
@tagName(active_preset) ++ "/tests/" ++ @tagName(active_preset) ++ "/single_merkle_proof/merkle_proof/BeaconBlockBody/blob_kzg_commitment_merkle_proof__random_block_1/deneb",
});
defer allocator.free(test_dir_name);
const test_dir = std.fs.cwd().openDir(test_dir_name, .{}) catch return error.SkipZigTest;
try MerkleProof.TestCase(.deneb).execute(allocator, test_dir);
}the test path seems to be incorrect
we need to improve spec test to error in that case instead of silently returning error.SkipZigTest
Signed-off-by: grapebaba <grapebaba@grapebabadeMacBook-Pro.local>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub const Handler = enum { | ||
| single_merkle_proof, | ||
|
|
||
| pub fn suiteName(self: Handler) []const u8 { |
There was a problem hiding this comment.
The suiteName function should accept comptime self: Handler parameter for consistency with other handlers (rewards.zig) and because it's called in a comptime context in write_spec_tests.zig. Consider also adding the inline keyword like rewards.zig for better optimization.
| pub fn suiteName(self: Handler) []const u8 { | |
| pub inline fn suiteName(comptime self: Handler) []const u8 { |
| pub const Handler = enum { | ||
| fork, | ||
|
|
||
| pub fn suiteName(self: Handler) []const u8 { |
There was a problem hiding this comment.
The suiteName function should accept comptime self: Handler parameter for consistency with other handlers (rewards.zig) and because it's called in a comptime context in write_spec_tests.zig. Consider also adding the inline keyword like rewards.zig for better optimization.
| pub fn suiteName(self: Handler) []const u8 { | |
| pub inline fn suiteName(comptime self: Handler) []const u8 { |
| return .{ | ||
| .body = body, | ||
| .expect_proof = proof_data, | ||
| .actual_proof = undefined, |
There was a problem hiding this comment.
The actual_proof field is initialized to undefined but is unconditionally deinitialized in the deinit method (line 74). If an error occurs before actual_proof is properly assigned in the process method, deinit will attempt to free an undefined pointer, causing undefined behavior. Consider initializing actual_proof to a safe empty state (e.g., .{ .leaf = undefined, .leaf_gindex = Gindex.fromUint(1), .branch = &.{} }) or tracking whether it has been initialized with a separate boolean flag.
| return .{ | |
| .body = body, | |
| .expect_proof = proof_data, | |
| .actual_proof = undefined, | |
| // Initialize actual_proof to a safe empty state | |
| var empty_branch = try allocator.alloc([66]u8, 0); | |
| return .{ | |
| .body = body, | |
| .expect_proof = proof_data, | |
| .actual_proof = MerkleProof{ | |
| .leaf = undefined, | |
| .leaf_gindex = Gindex.fromUint(1), | |
| .branch = empty_branch, | |
| }, |
| pub fn writeTest( | ||
| writer: std.io.AnyWriter, | ||
| fork: ForkSeq, | ||
| handler: MerkleProof.Handler, |
There was a problem hiding this comment.
The handler parameter should be marked as comptime for consistency with other test writers (rewards.zig, fork.zig) and because it's passed from a comptime loop in write_spec_tests.zig. Change to comptime handler: MerkleProof.Handler,
| handler: MerkleProof.Handler, | |
| comptime handler: MerkleProof.Handler, |
Fixed the path issue, would like to check the |
Motivation
Implement rewards/merkel_proof/fork spec test in state transition zig.
Description
merkle_proofrunner and writer follow the current design .merkle_proofrunner: registers the handler, ensures output directories exist, iterates nested suite/case folders, and emits generated cases via the new writer module.forkspec test based on transition upgrade functions