diff --git a/eth2/beacon/configs.py b/eth2/beacon/configs.py index da5af79667..fe58f06c5d 100644 --- a/eth2/beacon/configs.py +++ b/eth2/beacon/configs.py @@ -54,6 +54,7 @@ ('WHISTLEBLOWER_REWARD_QUOTIENT', int), ('ATTESTATION_INCLUSION_REWARD_QUOTIENT', int), ('INACTIVITY_PENALTY_QUOTIENT', int), + ('MIN_PENALTY_QUOTIENT', int), # Max operations per block ('MAX_PROPOSER_SLASHINGS', int), ('MAX_ATTESTER_SLASHINGS', int), diff --git a/eth2/beacon/state_machines/forks/serenity/configs.py b/eth2/beacon/state_machines/forks/serenity/configs.py index 6a5a510ea8..ae891e9142 100644 --- a/eth2/beacon/state_machines/forks/serenity/configs.py +++ b/eth2/beacon/state_machines/forks/serenity/configs.py @@ -54,6 +54,7 @@ WHISTLEBLOWER_REWARD_QUOTIENT=2**9, # (= 512) ATTESTATION_INCLUSION_REWARD_QUOTIENT=2**3, # (= 8) INACTIVITY_PENALTY_QUOTIENT=2**24, # (= 16,777,216) + MIN_PENALTY_QUOTIENT=2**5, # Max operations per block MAX_PROPOSER_SLASHINGS=2**4, # (= 16) MAX_ATTESTER_SLASHINGS=2**0, # (= 1) diff --git a/eth2/beacon/state_machines/forks/serenity/epoch_processing.py b/eth2/beacon/state_machines/forks/serenity/epoch_processing.py index bb65cb113c..cc58025590 100644 --- a/eth2/beacon/state_machines/forks/serenity/epoch_processing.py +++ b/eth2/beacon/state_machines/forks/serenity/epoch_processing.py @@ -1085,7 +1085,7 @@ def process_validator_registry(state: BeaconState, state = validator_registry_transition(state, config) - # TODO: state = process_slashings(state, config) + state = process_slashings(state, config) # TODO: state = process_exit_queue(state, config) @@ -1130,6 +1130,78 @@ def _update_latest_active_index_roots(state: BeaconState, ) +def _compute_total_penalties(state: BeaconState, + config: BeaconConfig, + current_epoch: Epoch) -> Gwei: + epoch_index = current_epoch % config.LATEST_SLASHED_EXIT_LENGTH + start_index_in_latest_slashed_balances = ( + (epoch_index + 1) % config.LATEST_SLASHED_EXIT_LENGTH + ) + total_at_start = state.latest_slashed_balances[start_index_in_latest_slashed_balances] + total_at_end = state.latest_slashed_balances[epoch_index] + return Gwei(total_at_end - total_at_start) + + +def _compute_individual_penalty(state: BeaconState, + config: BeaconConfig, + validator_index: ValidatorIndex, + total_penalties: Gwei, + total_balance: Gwei) -> Gwei: + effective_balance = get_effective_balance( + state.validator_balances, + validator_index, + config.MAX_DEPOSIT_AMOUNT, + ) + return Gwei( + max( + effective_balance * min(total_penalties * 3, total_balance) // total_balance, + effective_balance // config.MIN_PENALTY_QUOTIENT, + ) + ) + + +def process_slashings(state: BeaconState, + config: BeaconConfig) -> BeaconState: + """ + Process the slashings. + """ + latest_slashed_exit_length = config.LATEST_SLASHED_EXIT_LENGTH + max_deposit_amount = config.MAX_DEPOSIT_AMOUNT + + current_epoch = state.current_epoch(config.SLOTS_PER_EPOCH) + active_validator_indices = get_active_validator_indices(state.validator_registry, current_epoch) + total_balance = Gwei( + sum( + get_effective_balance(state.validator_balances, i, max_deposit_amount) + for i in active_validator_indices + ) + ) + total_penalties = _compute_total_penalties( + state, + config, + current_epoch, + ) + + for validator_index, validator in enumerate(state.validator_registry): + validator_index = ValidatorIndex(validator_index) + is_halfway_to_withdrawable_epoch = ( + current_epoch == validator.withdrawable_epoch - latest_slashed_exit_length // 2 + ) + if validator.slashed and is_halfway_to_withdrawable_epoch: + penalty = _compute_individual_penalty( + state=state, + config=config, + validator_index=validator_index, + total_penalties=total_penalties, + total_balance=total_balance, + ) + state = state.update_validator_balance( + validator_index=validator_index, + balance=state.validator_balances[validator_index] - penalty, + ) + return state + + def process_final_updates(state: BeaconState, config: BeaconConfig) -> BeaconState: current_epoch = state.current_epoch(config.SLOTS_PER_EPOCH) diff --git a/tests/eth2/beacon/conftest.py b/tests/eth2/beacon/conftest.py index bdb19120f8..d642ec9982 100644 --- a/tests/eth2/beacon/conftest.py +++ b/tests/eth2/beacon/conftest.py @@ -561,6 +561,11 @@ def inactivity_penalty_quotient(): return SERENITY_CONFIG.INACTIVITY_PENALTY_QUOTIENT +@pytest.fixture +def min_penalty_quotient(): + return SERENITY_CONFIG.MIN_PENALTY_QUOTIENT + + @pytest.fixture def max_proposer_slashings(): return SERENITY_CONFIG.MAX_PROPOSER_SLASHINGS @@ -706,6 +711,7 @@ def config( whistleblower_reward_quotient, attestation_inclusion_reward_quotient, inactivity_penalty_quotient, + min_penalty_quotient, max_proposer_slashings, max_attester_slashings, max_attestations, @@ -744,6 +750,7 @@ def config( WHISTLEBLOWER_REWARD_QUOTIENT=whistleblower_reward_quotient, ATTESTATION_INCLUSION_REWARD_QUOTIENT=attestation_inclusion_reward_quotient, INACTIVITY_PENALTY_QUOTIENT=inactivity_penalty_quotient, + MIN_PENALTY_QUOTIENT=min_penalty_quotient, MAX_PROPOSER_SLASHINGS=max_proposer_slashings, MAX_ATTESTER_SLASHINGS=max_attester_slashings, MAX_ATTESTATIONS=max_attestations, diff --git a/tests/eth2/beacon/state_machines/forks/test_serenity_epoch_processing.py b/tests/eth2/beacon/state_machines/forks/test_serenity_epoch_processing.py index b15b93095d..7cd734fe9c 100644 --- a/tests/eth2/beacon/state_machines/forks/test_serenity_epoch_processing.py +++ b/tests/eth2/beacon/state_machines/forks/test_serenity_epoch_processing.py @@ -54,6 +54,8 @@ from eth2.beacon.types.pending_attestation_records import PendingAttestationRecord from eth2.beacon.state_machines.forks.serenity.epoch_processing import ( _check_if_update_validator_registry, + _compute_individual_penalty, + _compute_total_penalties, _current_previous_epochs_justifiable, _get_finalized_epoch, _process_rewards_and_penalties_for_attestation_inclusion, @@ -64,6 +66,7 @@ process_ejections, process_final_updates, process_justification, + process_slashings, process_validator_registry, update_validator_registry, ) @@ -1145,6 +1148,145 @@ def mock_generate_seed(state, assert result_state.current_shuffling_seed != new_seed +@pytest.mark.parametrize( + ( + 'slots_per_epoch', + 'genesis_slot', + 'current_epoch', + 'latest_slashed_exit_length', + 'latest_slashed_balances', + 'expected_total_penalties', + ), + [ + (4, 8, 8, 8, (30, 10) + (0,) * 6, 30 - 10) + ] +) +def test_compute_total_penalties(genesis_state, + config, + slots_per_epoch, + current_epoch, + latest_slashed_balances, + expected_total_penalties): + state = genesis_state.copy( + slot=get_epoch_start_slot(current_epoch, slots_per_epoch), + latest_slashed_balances=latest_slashed_balances, + ) + total_penalties = _compute_total_penalties( + state, + config, + current_epoch, + ) + assert total_penalties == expected_total_penalties + + +@pytest.mark.parametrize( + ( + 'num_validators', + 'slots_per_epoch', + 'genesis_slot', + 'current_epoch', + 'latest_slashed_exit_length', + ), + [ + ( + 10, 4, 8, 8, 8, + ) + ] +) +@pytest.mark.parametrize( + ( + 'total_penalties', + 'total_balance', + 'min_penalty_quotient', + 'expected_penalty', + ), + [ + ( + 10**9, # 1 ETH + (32 * 10**9 * 10), + 2**5, + # effective_balance // MIN_PENALTY_QUOTIENT, + 32 * 10**9 // 2**5, + ), + ( + 10**9, # 1 ETH + (32 * 10**9 * 10), + 2**10, # Make MIN_PENALTY_QUOTIENT greater + # effective_balance * min(total_penalties * 3, total_balance) // total_balance, + 32 * 10**9 * min(10**9 * 3, (32 * 10**9 * 10)) // (32 * 10**9 * 10), + ), + ] +) +def test_compute_individual_penalty(genesis_state, + config, + slots_per_epoch, + current_epoch, + latest_slashed_exit_length, + total_penalties, + total_balance, + expected_penalty): + state = genesis_state.copy( + slot=get_epoch_start_slot(current_epoch, slots_per_epoch), + ) + validator_index = 0 + penalty = _compute_individual_penalty( + state=state, + config=config, + validator_index=validator_index, + total_penalties=total_penalties, + total_balance=total_balance, + ) + assert penalty == expected_penalty + + +@pytest.mark.parametrize( + ( + 'num_validators', + 'slots_per_epoch', + 'genesis_slot', + 'current_epoch', + 'latest_slashed_exit_length', + 'latest_slashed_balances', + 'expected_penalty', + ), + [ + ( + 10, + 4, + 8, + 8, + 8, + (2 * 10**9, 10**9) + (0,) * 6, + 32 * 10**9 // 2**5, + ), + ] +) +def test_process_slashings(genesis_state, + config, + current_epoch, + latest_slashed_balances, + slots_per_epoch, + latest_slashed_exit_length, + expected_penalty): + state = genesis_state.copy( + slot=get_epoch_start_slot(current_epoch, slots_per_epoch), + latest_slashed_balances=latest_slashed_balances, + ) + slashing_validator_index = 0 + validator = state.validator_registry[slashing_validator_index].copy( + slashed=True, + withdrawable_epoch=current_epoch + latest_slashed_exit_length // 2 + ) + state = state.update_validator_registry(slashing_validator_index, validator) + + result_state = process_slashings(state, config) + penalty = ( + state.validator_balances[slashing_validator_index] - + result_state.validator_balances[slashing_validator_index] + ) + assert penalty == expected_penalty + + # # Final updates #