diff --git a/specs/_features/maxeb_increase/capella.py b/specs/_features/maxeb_increase/capella.py index 39bf47cf38..d0ede5b910 100644 --- a/specs/_features/maxeb_increase/capella.py +++ b/specs/_features/maxeb_increase/capella.py @@ -183,6 +183,8 @@ def floorlog2(x: int) -> uint64: DOMAIN_BLS_TO_EXECUTION_CHANGE = DomainType('0x0A000000') SLASHED_ATTESTER_FLAG_INDEX = 0 SLASHED_PROPOSER_FLAG_INDEX = 1 +DOMAIN_CONSOLIDATION = DomainType('0x0B000000') +UNSET_CONSOLIDATED_TO = Epoch(2**64 - 1) # Preset vars MAX_COMMITTEES_PER_SLOT = uint64(64) @@ -321,6 +323,8 @@ class Validator(Container): activation_epoch: Epoch exit_epoch: Epoch withdrawable_epoch: Epoch # When validator can withdraw funds + # TODO: may compress into some other validator field + consolidated_to: ValidatorIndex class AttestationData(Container): @@ -621,6 +625,13 @@ class SignedBLSToExecutionChange(Container): signature: BLSSignature +class Consolidation(Container): + source_index: ValidatorIndex + target_index: ValidatorIndex + target_signature: BLSSignature + source_address: ExecutionAddress + + class BeaconBlockBody(Container): randao_reveal: BLSSignature eth1_data: Eth1Data # Eth1 data vote @@ -1203,6 +1214,7 @@ def slash_validator(state: BeaconState, """ Slash the validator with index ``slashed_index``. """ + slashed_index = resolve_slashed_index(state, slashed_index) epoch = get_current_epoch(state) initiate_validator_exit(state, slashed_index) validator = state.validators[slashed_index] @@ -1776,6 +1788,49 @@ def process_execution_layer_withdraw_request( )) +def is_consolidated(validator: Validator) -> bool: + validator.consolidated_to != UNSET_CONSOLIDATED_TO + + +def resolve_slashed_index(state: BeaconState, index: ValidatorIndex) -> ValidatorIndex: + if is_consolidated(state.validators[index]): + # Recursively resolve consolidated index + return resolve_slashed_index(state, state.validators[index].consolidated_to) + else: + return index + + +def process_consolidation(state: BeaconState, consolidation: Consolidation) -> None: + target_validator = state[consolidation.target_index] + source_validator = state[consolidation.source_index] + + assert target_validator.exit_epoch == FAR_FUTURE_EPOCH + assert source_validator.exit_epoch == FAR_FUTURE_EPOCH + + # verify source withdrawal credentials, which have authority over validating keys + assert source_validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + assert source_validator.withdrawal_credentials[12:] == consolidation.source_address + + # target validator accepts consolidation operation onto itself + # Fork-agnostic domain since address changes are valid across forks + domain = compute_domain(DOMAIN_CONSOLIDATION, genesis_validators_root=state.genesis_validators_root) + signing_root = compute_signing_root(source_validator.pubkey, domain) + assert bls.Verify(target_validator.pubkey, signing_root, consolidation.target_signature) + + active_balance = min(state.balances[consolidation.source_index], get_max_effective_balance(source_validator)) + excess_balance = state.balances[consolidation.source_index] - active_balance + state.balances[consolidation.target_index] += active_balance + state.balances[consolidation.source_index] = 0 + + # Excess balance above current active balance ceil, send to inbound churn + state.pending_balance_deposits.append(PendingBalanceDeposit(consolidation.target_index, excess_balance)) + + source_validator.consolidated_to = consolidation.target_index + # Balance is not exiting the active set, do not apply churn + source_validator.exit_epoch = get_current_epoch(state) + source_validator.withdrawable_epoch = Epoch(source_validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: header_1 = proposer_slashing.signed_header_1.message header_2 = proposer_slashing.signed_header_2.message @@ -3668,6 +3723,14 @@ def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: and balance > 0 ) + +def get_max_effective_balance(validator: Validator) -> Gwei: + if (has_compounding_withdrawal_credential(validator): + return MAX_EFFECTIVE_BALANCE + else: + return MIN_ACTIVATION_BALANCE + + def get_validator_excess_balance(validator: Validator, balance: Gwei) -> Gwei: """ Get excess balance for partial withdrawals for ``validator``.