Skip to content

feat: comptime fork#190

Merged
wemeetagain merged 27 commits intomainfrom
comptime-fork
Jan 29, 2026
Merged

feat: comptime fork#190
wemeetagain merged 27 commits intomainfrom
comptime-fork

Conversation

@wemeetagain
Copy link
Copy Markdown
Member

@wemeetagain wemeetagain commented Jan 24, 2026

  • Refactor fork-specific consensus types into a fork_types module
    • runtime dispatched fork types are exported as Any* types. Only AnySignedBeaconBlock and AnyBeaconState are currently used
    • comptime fork types are exported as ForkTypes(f) (for all types) and Fork* for block and state, eg: ForkBeaconState(comptime f: ForkSeq)
  • state transition code is refactored to use comptime f: ForkSeq internally. The top-level stfn takes AnyBeaconState/CachedBeaconState and AnySignedBeaconBlock, and then does a single dispatch into fork-specific code.
  • state transition code is refactored to "decompose" CachedBeaconState from being passed deeply into utility functions and only pass the pieces required per function, Eg: converting functions like getFinalityDelay(cached_state: *CachedBeaconState) !u64 into getFinalityDelay(current_epoch: u64, finalized_epoch: u64) u64

wemeetagain and others added 17 commits January 19, 2026 19:00
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Signed-off-by: Chen Kai <281165273grape@gmail.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @wemeetagain, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the codebase to implement a more robust and type-safe approach to handling different Ethereum Beacon Chain forks. By leveraging Zig's compile-time generics, the changes introduce specialized types for each fork, enhancing type safety and enabling compiler optimizations. This refactoring centralizes fork-related logic within a new fork_types module and updates numerous state transition and utility functions to seamlessly integrate with the new fork-aware type system, streamlining future development and maintenance across various network upgrades.

Highlights

  • Compile-time Fork Generics: Introduced ForkBeaconState(fork) and ForkBeaconBlock(fork, block_type) to enable compile-time specialization for different Beacon Chain forks, improving type safety and potentially performance.
  • Dynamic Fork Handling: New AnyBeaconState and AnySignedBeaconBlock unions provide a unified interface for handling state and blocks across various forks when the specific fork is not known at compile time.
  • Refactored State Transition Functions: All core state_transition functions (e.g., processBlock, processEpoch, processWithdrawals) have been updated to accept comptime fork: ForkSeq and utilize the new fork-aware types.
  • Centralized Fork Type Definitions: A new src/fork_types module has been added to encapsulate all fork-specific type definitions and their dynamic/compile-time wrappers.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an impressive and large-scale refactoring that introduces compile-time generics for handling fork-specific logic. The new fork_types module with Any* and Fork* types is a great architectural improvement that enhances type safety and likely performance. The changes are consistent and significantly improve the maintainability of the codebase, especially with the removal of the old types directory and the simplification of tests. I've found one critical issue that will cause a compilation error and a couple of style guide violations regarding function length that should be addressed.

Comment on lines 618 to 620
switch (self.*) {
.bellatrix => |*state| try state.setValue("latest_execution_payload_header", &header.bellatrix),
.capella => |*state| try state.setValue("latest_execution_payload_header", &header.capella),
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.

critical

The switch statement is not exhaustive. It's missing cases for .deneb, .electra, .fulu, and an else case. This will result in a compilation error.

Similar to how latestExecutionPayloadHeader handles these forks by mapping them to .deneb (since they share the same payload header structure), you should do the same here.

        switch (self.*) {
            .bellatrix => |*state| try state.setValue("latest_execution_payload_header", &header.bellatrix),
            .capella => |*state| try state.setValue("latest_execution_payload_header", &header.capella),
            .deneb, .electra, .fulu => |*state| try state.setValue("latest_execution_payload_header", &header.deneb),
            else => return error.InvalidAtFork,
        }

Comment on lines +38 to +132
pub fn deserialize(allocator: std.mem.Allocator, block_type: BlockType, fork_seq: ForkSeq, bytes: []const u8) !AnySignedBeaconBlock {
switch (fork_seq) {
.phase0 => {
if (block_type != .full) return error.InvalidBlockTypeForFork;
const signed_block = try allocator.create(ct.phase0.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.phase0.SignedBeaconBlock.default_value;
try ct.phase0.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .phase0 = signed_block };
},
.altair => {
if (block_type != .full) return error.InvalidBlockTypeForFork;
const signed_block = try allocator.create(ct.altair.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.altair.SignedBeaconBlock.default_value;
try ct.altair.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .altair = signed_block };
},
.bellatrix => {
if (block_type == .full) {
const signed_block = try allocator.create(ct.bellatrix.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.bellatrix.SignedBeaconBlock.default_value;
try ct.bellatrix.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .full_bellatrix = signed_block };
} else {
const signed_block = try allocator.create(ct.bellatrix.SignedBlindedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.bellatrix.SignedBlindedBeaconBlock.default_value;
try ct.bellatrix.SignedBlindedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .blinded_bellatrix = signed_block };
}
},
.capella => {
if (block_type == .full) {
const signed_block = try allocator.create(ct.capella.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.capella.SignedBeaconBlock.default_value;
try ct.capella.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .full_capella = signed_block };
} else {
const signed_block = try allocator.create(ct.capella.SignedBlindedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.capella.SignedBlindedBeaconBlock.default_value;
try ct.capella.SignedBlindedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .blinded_capella = signed_block };
}
},
.deneb => {
if (block_type == .full) {
const signed_block = try allocator.create(ct.deneb.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.deneb.SignedBeaconBlock.default_value;
try ct.deneb.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .full_deneb = signed_block };
} else {
const signed_block = try allocator.create(ct.deneb.SignedBlindedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.deneb.SignedBlindedBeaconBlock.default_value;
try ct.deneb.SignedBlindedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .blinded_deneb = signed_block };
}
},
.electra => {
if (block_type == .full) {
const signed_block = try allocator.create(ct.electra.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.electra.SignedBeaconBlock.default_value;
try ct.electra.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .full_electra = signed_block };
} else {
const signed_block = try allocator.create(ct.electra.SignedBlindedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.electra.SignedBlindedBeaconBlock.default_value;
try ct.electra.SignedBlindedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .blinded_electra = signed_block };
}
},
.fulu => {
if (block_type == .full) {
const signed_block = try allocator.create(ct.fulu.SignedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.fulu.SignedBeaconBlock.default_value;
try ct.fulu.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .full_fulu = signed_block };
} else {
const signed_block = try allocator.create(ct.fulu.SignedBlindedBeaconBlock.Type);
errdefer allocator.destroy(signed_block);
signed_block.* = ct.fulu.SignedBlindedBeaconBlock.default_value;
try ct.fulu.SignedBlindedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block);
return .{ .blinded_fulu = signed_block };
}
},
}
}
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.

medium

This function exceeds the 70-line limit specified in the style guide (line 104). Please refactor it to be shorter. You could extract the logic for each fork into a separate helper function.

References
  1. Functions should not exceed a hard limit of 70 lines to reduce complexity and improve readability. (link)

Comment on lines +187 to +262
pub fn serialize(self: AnySignedBeaconBlock, allocator: std.mem.Allocator) ![]u8 {
switch (self) {
.phase0 => |signed_block| {
const out = try allocator.alloc(u8, ct.phase0.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.phase0.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.altair => |signed_block| {
const out = try allocator.alloc(u8, ct.altair.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.altair.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.full_bellatrix => |signed_block| {
const out = try allocator.alloc(u8, ct.bellatrix.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.bellatrix.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.blinded_bellatrix => |signed_block| {
const out = try allocator.alloc(u8, ct.bellatrix.SignedBlindedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.bellatrix.SignedBlindedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.full_capella => |signed_block| {
const out = try allocator.alloc(u8, ct.capella.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.capella.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.blinded_capella => |signed_block| {
const out = try allocator.alloc(u8, ct.capella.SignedBlindedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.capella.SignedBlindedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.full_deneb => |signed_block| {
const out = try allocator.alloc(u8, ct.deneb.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.deneb.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.blinded_deneb => |signed_block| {
const out = try allocator.alloc(u8, ct.deneb.SignedBlindedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.deneb.SignedBlindedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.full_electra => |signed_block| {
const out = try allocator.alloc(u8, ct.electra.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.electra.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.blinded_electra => |signed_block| {
const out = try allocator.alloc(u8, ct.electra.SignedBlindedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.electra.SignedBlindedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.full_fulu => |signed_block| {
const out = try allocator.alloc(u8, ct.fulu.SignedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.fulu.SignedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
.blinded_fulu => |signed_block| {
const out = try allocator.alloc(u8, ct.fulu.SignedBlindedBeaconBlock.serializedSize(signed_block));
errdefer allocator.free(out);
_ = ct.fulu.SignedBlindedBeaconBlock.serializeIntoBytes(signed_block, out);
return out;
},
}
}
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.

medium

This function exceeds the 70-line limit specified in the style guide (line 104). Please refactor it to be shorter, for example by extracting the logic for each fork into a helper function.

References
  1. Functions should not exceed a hard limit of 70 lines to reduce complexity and improve readability. (link)

@wemeetagain wemeetagain changed the title chore: comptime fork feat: comptime fork Jan 27, 2026
@wemeetagain wemeetagain marked this pull request as ready for review January 27, 2026 19:33
@wemeetagain wemeetagain requested a review from a team as a code owner January 27, 2026 19:33
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cae60996ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/state_transition/block/process_block_header.zig Outdated
Signed-off-by: Chen Kai <281165273grape@gmail.com>
Copy link
Copy Markdown
Collaborator

@spiral-ladder spiral-ladder left a comment

Choose a reason for hiding this comment

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

partial review

@@ -0,0 +1,24 @@
const ct = @import("consensus_types");

pub const AnyAttestations = union(enum) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a benefit to the Any prefix for this (and all other types)? Most data structures differ by fork types anyway, so I'm not sure if the Any prefix is helpful

Feels like we can either go with:

  1. ForkBeaconState versus BeaconState, or
  2. BeaconState versus AnyBeaconState

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Preference for 2. or keeping as is

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

went with option 2.

Comment thread src/fork_types/any_execution_payload.zig Outdated
Comment on lines +27 to +28
full_bellatrix: *ct.bellatrix.SignedBeaconBlock.Type,
blinded_bellatrix: *ct.bellatrix.SignedBlindedBeaconBlock.Type,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I personally prefer <name>_<variant> for naming, think it makes more sense naming wise for fork to come before the type of block since we have alphabetical ordering.

Suggested change
full_bellatrix: *ct.bellatrix.SignedBeaconBlock.Type,
blinded_bellatrix: *ct.bellatrix.SignedBlindedBeaconBlock.Type,
bellatrix_full: *ct.bellatrix.SignedBeaconBlock.Type,
bellatrix_blinded: *ct.bellatrix.SignedBlindedBeaconBlock.Type,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

One thing that needs to be more consistent in this PR is the ordering between block type and fork.

Its not clear that fork needs to be first, nor vice versa.

I think that block type being primary reads slightly better, and would like to see (bt, f) everywhere. Eg BeaconBlockBody(.full, .fulu)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

made that update, now (bt, f) is consistently everywhere.

Comment thread src/fork_types/any_beacon_block.zig
Copy link
Copy Markdown
Collaborator

@spiral-ladder spiral-ladder left a comment

Choose a reason for hiding this comment

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

lgtm w some comments

};
}

pub fn BeaconBlock(comptime bt: BlockType, comptime f: ForkSeq) type {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit and maybe in a followup PR: should we capitalize comptime types? eg. BT and F

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think we should only capitalize types

}

pub fn nextWithdrawalIndex(self: *Self) !u64 {
if (comptime (f == .phase0 or f == .altair or f == .bellatrix)) return error.InvalidAtFork;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we use lt() for these and the other checks? Seems pretty long and unreadable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah, current impl isn't great. (currently unused codepath)

var state_eth1_data_view = try state.eth1Data();
var state_eth1_data: Eth1Data = undefined;
try state_eth1_data_view.toValue(allocator, &state_eth1_data);
try state_eth1_data_view.toValue(undefined, &state_eth1_data);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i know this is because this doesn't require an allocator but this seems like a smell maybe for a future refactor

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes this is annoying, this is why the ssz types are split into fixed and variable types, so that we avoid this ^. (eg ct.phase0.Checkpoint.deserializeFromBytes doesn't take an allocator bc of the FixedContainerType function signature).
Our tree view code is not splt between fixed and variable, so we have situations like this...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

would there be any value in having toValue and toValueAlloc?

@wemeetagain wemeetagain merged commit ce37c88 into main Jan 29, 2026
9 checks passed
@wemeetagain wemeetagain deleted the comptime-fork branch January 29, 2026 15:55
twoeths added a commit that referenced this pull request Mar 3, 2026
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.

3 participants