diff --git a/src/consensus/helpers/epoch.zig b/src/consensus/helpers/epoch.zig index 2a8063c..64f8e9e 100644 --- a/src/consensus/helpers/epoch.zig +++ b/src/consensus/helpers/epoch.zig @@ -6,6 +6,8 @@ const constants = @import("../../primitives/constants.zig"); const preset = @import("../../presets/preset.zig"); const phase0 = @import("../../consensus/phase0/types.zig"); const altair = @import("../../consensus/altair/types.zig"); +const electra = @import("../../consensus/electra/types.zig"); +const validator_helper = @import("../../consensus/helpers/validator.zig"); /// getCurrentEpoch returns the current epoch for the given state. /// @return The current epoch. @@ -78,6 +80,206 @@ pub fn computeActivationExitEpoch(epoch: primitives.Epoch) primitives.Epoch { return @as(primitives.Epoch, epoch + 1 + preset.ActivePreset.get().MAX_SEED_LOOKAHEAD); } +/// computeExitEpochAndUpdateChurn computes the exit epoch and updates the churn for the given state. +/// @param state - The state. +/// @param exit_balance - The exit balance. +/// @param allocator - The allocator. +/// @return The exit epoch and the updated churn. +/// Spec pseudocode definition: +/// def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: +/// earliest_exit_epoch = max(state.earliest_exit_epoch, compute_activation_exit_epoch(get_current_epoch(state))) +/// per_epoch_churn = get_activation_exit_churn_limit(state) +/// # New epoch for exits. +/// if state.earliest_exit_epoch < earliest_exit_epoch: +/// exit_balance_to_consume = per_epoch_churn +/// else: +/// exit_balance_to_consume = state.exit_balance_to_consume +/// +/// # Exit doesn't fit in the current earliest epoch. +/// if exit_balance > exit_balance_to_consume: +/// balance_to_process = exit_balance - exit_balance_to_consume +/// additional_epochs = (balance_to_process - 1) // per_epoch_churn + 1 +/// earliest_exit_epoch += additional_epochs +/// exit_balance_to_consume += additional_epochs * per_epoch_churn +/// +/// # Consume the balance and update state variables. +/// state.exit_balance_to_consume = exit_balance_to_consume - exit_balance +/// state.earliest_exit_epoch = earliest_exit_epoch +/// +/// return state.earliest_exit_epoch +pub fn computeExitEpochAndUpdateChurn(state: *consensus.BeaconState, exit_balance: primitives.Gwei, allocator: std.mem.Allocator) !primitives.Epoch { + var earliest_exit_epoch = @max(state.electra.earliest_exit_epoch, computeActivationExitEpoch(getCurrentEpoch(state))); + const per_epoch_churn = try getActivationExitChurnLimit(state, allocator); + var exit_balance_to_consume: primitives.Gwei = undefined; + + // New epoch for exits. + if (state.electra.earliest_exit_epoch < earliest_exit_epoch) { + exit_balance_to_consume = per_epoch_churn; + } else { + exit_balance_to_consume = state.electra.exit_balance_to_consume; + } + + // Exit doesn't fit in the current earliest epoch. + if (exit_balance > exit_balance_to_consume) { + const balance_to_process = exit_balance - exit_balance_to_consume; + const additional_epochs = @divFloor((balance_to_process - 1), per_epoch_churn) + 1; + earliest_exit_epoch += additional_epochs; + exit_balance_to_consume += additional_epochs * per_epoch_churn; + } + + // Consume the balance and update state variables. + state.electra.exit_balance_to_consume = exit_balance_to_consume - exit_balance; + state.electra.earliest_exit_epoch = earliest_exit_epoch; + + return state.electra.earliest_exit_epoch; +} + +/// getActivationExitChurnLimit returns the churn limit for the current epoch dedicated to activations and exits. +/// @param state - The state. +/// @param allocator - The allocator. +/// @return The churn limit for the current epoch dedicated to activations and exits. +/// Spec pseudocode definition: +/// def get_activation_exit_churn_limit(state: BeaconState) -> Gwei: +/// """ +/// Return the churn limit for the current epoch dedicated to activations and exits. +/// """ +/// return min(config.MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, get_balance_churn_limit(state)) +pub fn getActivationExitChurnLimit(state: *const consensus.BeaconState, allocator: std.mem.Allocator) !primitives.Gwei { + const balance_churn_limit = try validator_helper.getBalanceChurnLimit(state, allocator); + // Return the churn limit for the current epoch dedicated to activations and exits. + return @min(configs.ActiveConfig.get().MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, balance_churn_limit); +} + +test "test compute_exit_epoch_and_update_churn" { + preset.ActivePreset.set(preset.Presets.minimal); + defer preset.ActivePreset.reset(); + configs.ActiveConfig.set(preset.Presets.minimal); + defer configs.ActiveConfig.reset(); + var validators = std.ArrayList(consensus.Validator).init(std.testing.allocator); + defer validators.deinit(); + const validator1 = consensus.Validator{ + .pubkey = undefined, + .withdrawal_credentials = undefined, + .effective_balance = 10000000000000, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 10, + .withdrawable_epoch = 10, + }; + const validator2 = consensus.Validator{ + .pubkey = undefined, + .withdrawal_credentials = undefined, + .effective_balance = 10000000000000, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 20, + .withdrawable_epoch = 20, + }; + try validators.append(validator1); + try validators.append(validator2); + var state = consensus.BeaconState{ + .electra = electra.BeaconState{ + .genesis_time = 0, + .genesis_validators_root = .{0} ** 32, + .slot = 0, + .fork = undefined, + .block_roots = undefined, + .state_roots = undefined, + .historical_roots = undefined, + .eth1_data = undefined, + .eth1_data_votes = undefined, + .eth1_deposit_index = 0, + .validators = validators.items, + .balances = undefined, + .randao_mixes = undefined, + .slashings = undefined, + .previous_epoch_attestations = undefined, + .current_epoch_attestations = undefined, + .justification_bits = undefined, + .previous_justified_checkpoint = undefined, + .current_justified_checkpoint = undefined, + .finalized_checkpoint = undefined, + .latest_block_header = undefined, + .inactivity_scores = undefined, + .current_sync_committee = undefined, + .next_sync_committee = undefined, + .earliest_exit_epoch = 5, + .exit_balance_to_consume = 10000000000000, + .latest_execution_payload_header = undefined, + .historical_summaries = undefined, + .pending_balance_deposits = undefined, + .pending_partial_withdrawals = undefined, + .pending_consolidations = undefined, + }, + }; + + const exit_epoch = try computeExitEpochAndUpdateChurn(&state, 10000000000000, std.testing.allocator); + try std.testing.expectEqual(5, exit_epoch); +} + +test "test get_activation_exit_churn_limit" { + preset.ActivePreset.set(preset.Presets.minimal); + defer preset.ActivePreset.reset(); + configs.ActiveConfig.set(preset.Presets.minimal); + defer configs.ActiveConfig.reset(); + var validators = std.ArrayList(consensus.Validator).init(std.testing.allocator); + defer validators.deinit(); + const validator1 = consensus.Validator{ + .pubkey = undefined, + .withdrawal_credentials = undefined, + .effective_balance = 10000000000000, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 10, + .withdrawable_epoch = 10, + }; + const validator2 = consensus.Validator{ + .pubkey = undefined, + .withdrawal_credentials = undefined, + .effective_balance = 10000000000000, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 20, + .withdrawable_epoch = 20, + }; + try validators.append(validator1); + try validators.append(validator2); + var state = consensus.BeaconState{ + .altair = altair.BeaconState{ + .genesis_time = 0, + .genesis_validators_root = .{0} ** 32, + .slot = 0, + .fork = undefined, + .block_roots = undefined, + .state_roots = undefined, + .historical_roots = undefined, + .eth1_data = undefined, + .eth1_data_votes = undefined, + .eth1_deposit_index = 0, + .validators = validators.items, + .balances = undefined, + .randao_mixes = undefined, + .slashings = undefined, + .previous_epoch_attestations = undefined, + .current_epoch_attestations = undefined, + .justification_bits = undefined, + .previous_justified_checkpoint = undefined, + .current_justified_checkpoint = undefined, + .finalized_checkpoint = undefined, + .latest_block_header = undefined, + .inactivity_scores = undefined, + .current_sync_committee = undefined, + .next_sync_committee = undefined, + }, + }; + const churn_limit = try getActivationExitChurnLimit(&state, std.testing.allocator); + try std.testing.expectEqual(128000000000, churn_limit); +} + test "test compute_epoch_at_slot" { preset.ActivePreset.set(preset.Presets.mainnet); defer preset.ActivePreset.reset(); diff --git a/src/consensus/helpers/validator.zig b/src/consensus/helpers/validator.zig index a9ef0f7..f4ece8a 100644 --- a/src/consensus/helpers/validator.zig +++ b/src/consensus/helpers/validator.zig @@ -8,6 +8,7 @@ const phase0 = @import("../../consensus/phase0/types.zig"); const altair = @import("../../consensus/altair/types.zig"); const epoch_helper = @import("../../consensus/helpers/epoch.zig"); const shuffle_helper = @import("../../consensus/helpers/shuffle.zig"); +const balance = @import("../../consensus/helpers/balance.zig"); /// Check if a validator is active at a given epoch. /// A validator is active if the current epoch is greater than or equal to the validator's activation epoch and less than the validator's exit epoch. @@ -174,6 +175,130 @@ pub fn computeProposerIndex(state: *const consensus.BeaconState, indices: []cons } } +/// getBalanceChurnLimit returns the balance churn limit for the current epoch. +/// The churn limit is the maximum number of validators who can leave the validator set in one epoch. +/// @param state The beacon state. +/// @param allocator The allocator. +/// @return The balance churn limit. +/// Spec pseudocode definition: +/// def get_balance_churn_limit(state: BeaconState) -> Gwei: +/// """ +/// Return the churn limit for the current epoch. +/// """ +/// churn = max( +/// config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, +/// get_total_active_balance(state) // config.CHURN_LIMIT_QUOTIENT +/// ) +/// return churn - churn % EFFECTIVE_BALANCE_INCREMENT +pub fn getBalanceChurnLimit(state: *const consensus.BeaconState, allocator: std.mem.Allocator) !primitives.Gwei { + // Return the churn limit for the current epoch. + const total_active_balance = try balance.getTotalActiveBalance(state, allocator); + const churn = @max(configs.ActiveConfig.get().MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, @divFloor(total_active_balance, configs.ActiveConfig.get().CHURN_LIMIT_QUOTIENT)); + return churn - @mod(churn, preset.ActivePreset.get().EFFECTIVE_BALANCE_INCREMENT); +} + +pub fn initiateValidatorExit(state: *const consensus.BeaconState, index: primitives.ValidatorIndex, allocator: std.mem.Allocator) !void { + // Return if validator already initiated exit + var validator = state.validators()[index]; + if (validator.exit_epoch != constants.FAR_FUTURE_EPOCH) { + return; + } + + // Compute exit queue epoch + var exit_epochs = std.ArrayList(primitives.Epoch).init(allocator); + defer exit_epochs.deinit(); + + for (state.validators()) |v| { + if (v.exit_epoch != constants.FAR_FUTURE_EPOCH) { + try exit_epochs.append(v.exit_epoch); + } + } + + var exit_queue_epoch = @max(std.mem.max(primitives.Epoch, exit_epochs.items), epoch_helper.computeActivationExitEpoch(epoch_helper.getCurrentEpoch(state))); + + var exit_queue_churn: usize = 0; + for (state.validators()) |v| { + if (v.exit_epoch == exit_queue_epoch) { + exit_queue_churn += 1; + } + } + + if (exit_queue_churn >= try getValidatorChurnLimit(state, allocator)) { + exit_queue_epoch += 1; + } + + // Set validator exit epoch and withdrawable epoch + validator.exit_epoch = exit_queue_epoch; + validator.withdrawable_epoch = exit_queue_epoch + configs.ActiveConfig.get().MIN_VALIDATOR_WITHDRAWABILITY_DELAY; +} + +test "test getBalanceChurnLimit" { + preset.ActivePreset.set(preset.Presets.minimal); + defer preset.ActivePreset.reset(); + configs.ActiveConfig.set(preset.Presets.minimal); + defer configs.ActiveConfig.reset(); + const finalized_checkpoint = consensus.Checkpoint{ + .epoch = 5, + .root = .{0} ** 32, + }; + var validators = std.ArrayList(consensus.Validator).init(std.testing.allocator); + defer validators.deinit(); + const validator1 = consensus.Validator{ + .pubkey = undefined, + .withdrawal_credentials = undefined, + .effective_balance = 12312312312, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 0, + .withdrawable_epoch = 0, + }; + try validators.append(validator1); + const validator2 = consensus.Validator{ + .pubkey = undefined, + .withdrawal_credentials = undefined, + .effective_balance = 232323232332, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 0, + .withdrawable_epoch = 0, + }; + try validators.append(validator2); + + const state = consensus.BeaconState{ + .altair = altair.BeaconState{ + .genesis_time = 0, + .genesis_validators_root = .{0} ** 32, + .slot = 100, + .fork = undefined, + .block_roots = undefined, + .state_roots = undefined, + .historical_roots = undefined, + .eth1_data = undefined, + .eth1_data_votes = undefined, + .eth1_deposit_index = 0, + .validators = validators.items, + .balances = undefined, + .randao_mixes = undefined, + .slashings = undefined, + .previous_epoch_attestations = undefined, + .current_epoch_attestations = undefined, + .justification_bits = undefined, + .previous_justified_checkpoint = undefined, + .current_justified_checkpoint = undefined, + .finalized_checkpoint = finalized_checkpoint, + .latest_block_header = undefined, + .inactivity_scores = undefined, + .current_sync_committee = undefined, + .next_sync_committee = undefined, + }, + }; + + const result = try getBalanceChurnLimit(&state, std.testing.allocator); + try std.testing.expectEqual(@as(primitives.Gwei, 64000000000), result); +} + test "test getValidatorChurnLimit" { preset.ActivePreset.set(preset.Presets.minimal); defer preset.ActivePreset.reset();