Skip to content

refactor the vote state with union type#1232

Open
hamza-syndica wants to merge 12 commits intomainfrom
refactor-vote-state-with-union
Open

refactor the vote state with union type#1232
hamza-syndica wants to merge 12 commits intomainfrom
refactor-vote-state-with-union

Conversation

@hamza-syndica
Copy link
Contributor

@hamza-syndica hamza-syndica commented Feb 17, 2026

Refactored VoteState to use a union type, extracted into a dedicated state.zig module. Updated all consumers across consensus, replay, rewards, and runtime to use the new type.
Changes

  • Introduced src/runtime/program/vote/state.zig (around ~1000 lines) with union-based vote state representation
  • Refactored vote/execute.zig to use the new state types
  • Updated stake, bank, leader schedule, and reward calculation modules accordingly
  • Added tests for the new vote state logic

@github-project-automation github-project-automation bot moved this to 🏗 In progress in Sig Feb 17, 2026
@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

❌ Patch coverage is 93.16406% with 35 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/runtime/program/vote/execute.zig 71.21% 19 Missing ⚠️
src/runtime/program/vote/state.zig 97.17% 11 Missing ⚠️
src/core/stakes.zig 70.00% 3 Missing ⚠️
src/core/bank.zig 0.00% 1 Missing ⚠️
src/replay/consensus/core.zig 85.71% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/consensus/replay_tower.zig 96.16% <ø> (ø)
src/consensus/tower.zig 90.24% <100.00%> (ø)
src/core/leader_schedule.zig 96.62% <100.00%> (ø)
src/replay/epoch_transitions.zig 66.15% <100.00%> (ø)
src/replay/rewards/calculation.zig 88.61% <100.00%> (-0.41%) ⬇️
src/replay/rewards/inflation_rewards.zig 99.62% <100.00%> (-0.14%) ⬇️
src/replay/update_sysvar.zig 97.35% <100.00%> (ø)
src/rpc/methods.zig 78.12% <ø> (-16.40%) ⬇️
src/runtime/program/stake/lib.zig 88.03% <100.00%> (-2.69%) ⬇️
src/runtime/program/vote/state_v4.zig 90.36% <100.00%> (-3.48%) ⬇️
... and 5 more

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hamza-syndica hamza-syndica marked this pull request as ready for review February 17, 2026 14:26
dnut
dnut previously approved these changes Feb 24, 2026
@dnut dnut force-pushed the refactor-vote-state-with-union branch from efc3c35 to e362462 Compare February 24, 2026 00:49
dnut
dnut previously approved these changes Feb 24, 2026
dnut
dnut previously approved these changes Feb 25, 2026
yewman
yewman previously approved these changes Feb 25, 2026
@github-project-automation github-project-automation bot moved this from 🏗 In progress to 👀 In review in Sig Feb 25, 2026
@hamza-syndica hamza-syndica dismissed stale reviews from yewman and dnut via 8297063 February 26, 2026 17:42
@hamza-syndica hamza-syndica force-pushed the refactor-vote-state-with-union branch from 8838642 to 8297063 Compare February 26, 2026 17:42
Comment on lines 1003 to 1008
pub fn votes(self: *const VoteState) *const std.ArrayListUnmanaged(LandedVote) {
return switch (self.*) {
.v3 => |*s| &s.votes,
.v4 => |*s| &s.votes,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

There's not much sense in returning a constant referenco the arraylist.

Suggested change
pub fn votes(self: *const VoteState) *const std.ArrayListUnmanaged(LandedVote) {
return switch (self.*) {
.v3 => |*s| &s.votes,
.v4 => |*s| &s.votes,
};
}
pub fn votes(self: *const VoteState) []const LandedVote {
return switch (self.*) {
inline .v3, .v4 => |s| s.votes.items,
};
}

Comment on lines 1045 to 1050
pub fn epochCreditsList(self: *const VoteState) *const std.ArrayListUnmanaged(EpochCredit) {
return switch (self.*) {
.v3 => |*s| &s.epoch_credits,
.v4 => |*s| &s.epoch_credits,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

Suggested change
pub fn epochCreditsList(self: *const VoteState) *const std.ArrayListUnmanaged(EpochCredit) {
return switch (self.*) {
.v3 => |*s| &s.epoch_credits,
.v4 => |*s| &s.epoch_credits,
};
}
pub fn epochCreditsList(self: *const VoteState) []const EpochCredit {
return switch (self.*) {
inline .v3, .v4 => |s| s.epoch_credits.items,
};
}

Comment on lines 1112 to 1116
pub fn epochCredits(self: *const VoteState) u64 {
return switch (self.*) {
inline else => |*s| s.epochCredits(),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I would use inline .v3, .v4 instead of inline else. Expresses intent more clearly.

Copy link
Contributor

Choose a reason for hiding this comment

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

when would you use inline else?

i think the intent is clear either way. there's an expectation for all the types to have this method. if not, it will fail to compile. what's the problem?

Comment on lines 1348 to 1354
pub fn isV4(self: VoteState) bool {
return self == .v4;
}

pub fn isV3(self: VoteState) bool {
return self == .v3;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These seem somewhat pointless compared to the caller just writing vote_state == .<tag>.

Comment on lines 1357 to 1384
pub fn asV3(self: *VoteState) ?*VoteStateV3 {
return switch (self.*) {
.v3 => |*s| s,
.v4 => null,
};
}

/// Access the underlying VoteStateV4, if this is a V4 state.
pub fn asV4(self: *VoteState) ?*VoteStateV4 {
return switch (self.*) {
.v3 => null,
.v4 => |*s| s,
};
}

pub fn asV3Const(self: *const VoteState) ?*const VoteStateV3 {
return switch (self.*) {
.v3 => |*s| s,
.v4 => null,
};
}

pub fn asV4Const(self: *const VoteState) ?*const VoteStateV4 {
return switch (self.*) {
.v3 => null,
.v4 => |*s| s,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the benefit of these methods over just directly switching over the vote state.

Comment on lines 892 to 928
pub fn initV3(
allocator: Allocator,
node_pubkey: Pubkey,
authorized_voter: Pubkey,
withdrawer: Pubkey,
commission_val: u8,
voter_epoch: Epoch,
) Allocator.Error!VoteState {
return .{ .v3 = try VoteStateV3.init(
allocator,
node_pubkey,
authorized_voter,
withdrawer,
commission_val,
voter_epoch,
) };
}

pub fn initV4(
allocator: Allocator,
node_pubkey: Pubkey,
authorized_voter: Pubkey,
withdrawer: Pubkey,
commission_pct: u8,
voter_epoch: Epoch,
vote_pubkey: Pubkey,
) Allocator.Error!VoteState {
return .{ .v4 = try VoteStateV4.init(
allocator,
node_pubkey,
authorized_voter,
withdrawer,
commission_pct,
voter_epoch,
vote_pubkey,
) };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These initialization methods don't seem very purposeful. The caller can just as easily write .{ .v4 = try .init(all, the, same, parameters) }. You even bypass these just a hundred or so lines prior, so that you can initialize the payload directly with managed memory.

Comment on lines 685 to 686
var vote_state_to_deinit: ?*const anyerror!VoteStateVersions = &vote_state;
if (vote_state_to_deinit) |fallible| if (fallible.*) |*vs| vs.deinit(allocator) else |_| {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this missing a defer? Or otherwise, what's going on here.

Comment on lines 1047 to 1053
if (std.meta.isError(account.setDataLength(
allocator,
resize_delta,
VoteStateV4.MAX_VOTE_STATE_SIZE,
))) {
return InstructionError.AccountNotRentExempt;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I highly advise advise against using std.meta in general unless there is no better alternative:

Suggested change
if (std.meta.isError(account.setDataLength(
allocator,
resize_delta,
VoteStateV4.MAX_VOTE_STATE_SIZE,
))) {
return InstructionError.AccountNotRentExempt;
}
try account.setDataLength(
allocator,
resize_delta,
VoteStateV4.MAX_VOTE_STATE_SIZE,
);

I would have suggested catch return error.AccountNotRentExempt;, but I noticed that this function returns Allocator.Error || InstructionError, which is identical to the enclosing function's error set. Is there any reason not to propagate it directly?

Comment on lines 1061 to 1068
if (account.constAccountData().len < VoteStateV3.MAX_VOTE_STATE_SIZE and
(!rent.isExempt(account.account.lamports, VoteStateV3.MAX_VOTE_STATE_SIZE) or
std.meta.isError(account.setDataLength(
allocator,
resize_delta,
VoteStateV3.MAX_VOTE_STATE_SIZE,
))))
{
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bad idea, the isError will treat error.OutOfMemory equally to an InstructionError.

@hamza-syndica
Copy link
Contributor Author

@InKryption thanks for all of your comments. I have resolved your concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

5 participants