-
Notifications
You must be signed in to change notification settings - Fork 12
refactor: optimize process_slashings #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3ee0cc3
4b2fd98
1bd6cab
8c2b1dd
b80bba7
1e37e44
011ea81
064c413
a13ffb1
ab58507
1876bfe
600beaa
a689e5f
fbbb1a3
71b16d8
a951492
8616e50
9f754ff
6ba877c
e0d4509
b6d7f94
2f8c62f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,6 +17,7 @@ pub fn processRewardsAndPenalties( | |||||||||||||||||||||||||||||||||||||||
| epoch_cache: *const EpochCache, | ||||||||||||||||||||||||||||||||||||||||
| state: *BeaconState(fork), | ||||||||||||||||||||||||||||||||||||||||
| cache: *const EpochTransitionCache, | ||||||||||||||||||||||||||||||||||||||||
| slashing_penalties: ?[]const u64, | ||||||||||||||||||||||||||||||||||||||||
| ) !void { | ||||||||||||||||||||||||||||||||||||||||
| // No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch | ||||||||||||||||||||||||||||||||||||||||
| if (cache.current_epoch == GENESIS_EPOCH) { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,9 +31,15 @@ pub fn processRewardsAndPenalties( | |||||||||||||||||||||||||||||||||||||||
| const balances = try state.balancesSlice(allocator); | ||||||||||||||||||||||||||||||||||||||||
| defer allocator.free(balances); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| for (rewards, penalties, balances) |reward, penalty, *balance| { | ||||||||||||||||||||||||||||||||||||||||
| const result = balance.* + reward -| penalty; | ||||||||||||||||||||||||||||||||||||||||
| balance.* = result; | ||||||||||||||||||||||||||||||||||||||||
| if (slashing_penalties) |slashings| { | ||||||||||||||||||||||||||||||||||||||||
| for (rewards, penalties, balances, 0..) |reward, penalty, *balance, i| { | ||||||||||||||||||||||||||||||||||||||||
| const slashing: u64 = if (i < slashings.len) slashings[i] else 0; | ||||||||||||||||||||||||||||||||||||||||
| balance.* = (try std.math.add(u64, balance.*, reward)) -| penalty -| slashing; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+39
|
||||||||||||||||||||||||||||||||||||||||
| if (slashing_penalties) |slashings| { | |
| for (rewards, penalties, balances, 0..) |reward, penalty, *balance, i| { | |
| const slashing: u64 = if (i < slashings.len) slashings[i] else 0; | |
| balance.* = (try std.math.add(u64, balance.*, reward)) -| penalty -| slashing; | |
| } | |
| } else { | |
| if (slashing_penalties) |slashings| { | |
| // NOTE: The original implementation used wrapping addition (e.g. `balance.* + reward`), | |
| // which would silently overflow. We now use checked addition via `std.math.add` and | |
| // propagate any overflow as an error via `try`. Overflow of a validator balance due | |
| // to rewards is considered a violation of protocol invariants, so we intentionally | |
| // fail the epoch transition instead of wrapping on overflow. | |
| for (rewards, penalties, balances, 0..) |reward, penalty, *balance, i| { | |
| const slashing: u64 = if (i < slashings.len) slashings[i] else 0; | |
| balance.* = (try std.math.add(u64, balance.*, reward)) -| penalty -| slashing; | |
| } | |
| } else { | |
| // Same reasoning as above: treat any overflow in `balance + reward` as a hard error | |
| // rather than allowing it to wrap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the correct behavior in the spec
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop at line 35 uses a numeric index but the loop at line 40 doesn't. This creates an inconsistency where the slashing_penalties slice might be shorter than the rewards/penalties/balances slices. If there are more validators than slashing_penalties entries, the check 'if (i < slashings.len)' will treat missing entries as 0, which could be correct behavior. However, this asymmetry creates confusion - consider using the same iteration pattern for both branches or adding a comment explaining why the lengths might differ.
| for (rewards, penalties, balances) |reward, penalty, *balance| { | |
| // Use the same indexed iteration pattern as the slashing branch to keep behavior and intent symmetric. | |
| for (rewards, penalties, balances, 0..) |reward, penalty, *balance, _| { |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same behavioral change as line 37: the original code used wrapping addition, while the new code uses checked addition with std.math.add. This change should be applied consistently in both branches (with and without slashing_penalties), but the API contract change should be documented.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -11,17 +11,23 @@ const PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR = preset.PROPORTIONAL_SLASHING_MUL | |||||
| const PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX = preset.PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX; | ||||||
| const Node = @import("persistent_merkle_tree").Node; | ||||||
|
|
||||||
| /// TODO: consider returning number[] when we switch to TreeView | ||||||
| pub fn processSlashings( | ||||||
| comptime fork: ForkSeq, | ||||||
| allocator: std.mem.Allocator, | ||||||
| epoch_cache: *const EpochCache, | ||||||
| state: *BeaconState(fork), | ||||||
| cache: *const EpochTransitionCache, | ||||||
| ) !void { | ||||||
| update_balance: bool, | ||||||
| ) ![]const u64 { | ||||||
| const slashing_penalties = cache.slashing_penalties; | ||||||
| const empty_penalties = &[_]u64{}; | ||||||
| if (!update_balance) { | ||||||
| @memset(slashing_penalties, 0); | ||||||
| } | ||||||
|
|
||||||
| // Return early if there no index to slash | ||||||
|
||||||
| // Return early if there no index to slash | |
| // Return early if there are no indices to slash |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When update_balance is false and there are no indices to slash, the function returns the slashing_penalties slice which has been zeroed at line 25. However, when update_balance is true with no indices to slash, it returns empty_penalties (an empty slice). This asymmetry could be confusing. Consider either: (1) always returning slashing_penalties when update_balance=false, even if empty, for consistency, or (2) documenting why the return values differ in this edge case.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -122,8 +122,8 @@ pub fn TestCase(comptime fork: ForkSeq, comptime epoch_process_fn: EpochProcessi | |||||||||
| .participation_record_updates => try state_transition.processParticipationRecordUpdates(fork, fork_state), | ||||||||||
| .randao_mixes_reset => try state_transition.processRandaoMixesReset(fork, fork_state, &epoch_transition_cache), | ||||||||||
| .registry_updates => try state_transition.processRegistryUpdates(fork, config, epoch_cache, fork_state, &epoch_transition_cache), | ||||||||||
| .rewards_and_penalties => try state_transition.processRewardsAndPenalties(fork, allocator, config, epoch_cache, fork_state, &epoch_transition_cache), | ||||||||||
| .slashings => try state_transition.processSlashings(fork, allocator, epoch_cache, fork_state, &epoch_transition_cache), | ||||||||||
| .rewards_and_penalties => try state_transition.processRewardsAndPenalties(fork, allocator, config, epoch_cache, fork_state, &epoch_transition_cache, null), | ||||||||||
|
||||||||||
| .rewards_and_penalties => try state_transition.processRewardsAndPenalties(fork, allocator, config, epoch_cache, fork_state, &epoch_transition_cache, null), | |
| .rewards_and_penalties => try state_transition.processRewardsAndPenalties(fork, allocator, config, epoch_cache, fork_state, &epoch_transition_cache, null), | |
| // When update_balance = true, processSlashings applies penalties directly and | |
| // returns an empty slice; the result is intentionally discarded here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The errdefer chain for cleanup is incomplete. If allocation of slashing_penalties succeeds (line 99) but then its errdefer (line 100) is triggered, all previously allocated fields from lines 75-97 will be properly cleaned up. However, there's no cleanup of self.allocator if an error occurs. While this is technically correct since allocator is just assigned (not allocated), the pattern is inconsistent. For clarity and to avoid potential future bugs, consider whether self.allocator should be set to undefined in the errdefer handlers, or add a comment explaining why it doesn't need cleanup.