Skip to content

feat: impl rewards/merkle_proof/fork spec test - #81

Merged
wemeetagain merged 75 commits into
mainfrom
rewards_spec_test
Dec 1, 2025
Merged

feat: impl rewards/merkle_proof/fork spec test#81
wemeetagain merged 75 commits into
mainfrom
rewards_spec_test

Conversation

@GrapeBaBa

@GrapeBaBa GrapeBaBa commented Nov 12, 2025

Copy link
Copy Markdown
Contributor

Motivation

Implement rewards/merkel_proof/fork spec test in state transition zig.

Description

  • Add coverage for consensus rewards spec vectors, resolve latent bugs in rewards handling while enabling the suite.
  • Introduces merkle_proof runner and writer follow the current design .
  • Expands the spec test generator to support the new merkle_proof runner: registers the handler, ensures output directories exist, iterates nested suite/case folders, and emits generated cases via the new writer module.
  • Implement fork spec test based on transition upgrade functions

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +49 to +61
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),
});

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment thread test/spec/runner/merkle_proof.zig
Comment on lines +176 to +177
self.actual_rewards = epoch_cache.rewards;
self.actual_penalties = epoch_cache.penalties;

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two fields were in the reusable cache which not be freed lines 169-172

Comment thread test/spec/runner/merkle_proof.zig
Signed-off-by: grapebaba <grapebaba@grapebabadeMacBook-Pro.local>
wemeetagain
wemeetagain previously approved these changes Nov 20, 2025

@wemeetagain wemeetagain left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, only source change is preventing an underflow?

@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

Looks good, only source change is preventing an underflow?

you mean calc previous epoch?

Signed-off-by: grapebaba <grapebaba@grapebabadeMacBook-Pro.local>
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

Looks good, only source change is preventing an underflow?

you mean calc previous epoch?

Not sure I get it, computePrevEpoch should be a helper function in the spec.

Comment thread src/ssz/type/byte_list.zig
@twoeths

twoeths commented Nov 29, 2025

Copy link
Copy Markdown
Member

Looks good, only source change is preventing an underflow?

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 twoeths left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copilot AI Nov 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
pub fn suiteName(self: Handler) []const u8 {
pub inline fn suiteName(comptime self: Handler) []const u8 {

Copilot uses AI. Check for mistakes.
Comment thread test/spec/runner/fork.zig
pub const Handler = enum {
fork,

pub fn suiteName(self: Handler) []const u8 {

Copilot AI Nov 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
pub fn suiteName(self: Handler) []const u8 {
pub inline fn suiteName(comptime self: Handler) []const u8 {

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +67
return .{
.body = body,
.expect_proof = proof_data,
.actual_proof = undefined,

Copilot AI Nov 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,
},

Copilot uses AI. Check for mistakes.
pub fn writeTest(
writer: std.io.AnyWriter,
fork: ForkSeq,
handler: MerkleProof.Handler,

Copilot AI Nov 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Suggested change
handler: MerkleProof.Handler,
comptime handler: MerkleProof.Handler,

Copilot uses AI. Check for mistakes.
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

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

Fixed the path issue, would like to check the error.SkipZigTest improvement in further PR.

@twoeths twoeths left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me

@wemeetagain
wemeetagain merged commit b837ed3 into main Dec 1, 2025
11 checks passed
@wemeetagain
wemeetagain deleted the rewards_spec_test branch August 17, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants