diff --git a/specs/_features/maxeb_increase/capella.py b/specs/_features/maxeb_increase/capella.py index 57f08c1067..c9d3b788cc 100644 --- a/specs/_features/maxeb_increase/capella.py +++ b/specs/_features/maxeb_increase/capella.py @@ -236,7 +236,10 @@ def floorlog2(x: int) -> uint64: MAX_BLS_TO_EXECUTION_CHANGES = 16 MAX_WITHDRAWALS_PER_PAYLOAD = uint64(16) MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP = 16384 - +DOMAIN_CONSOLIDATION = DomainType('0x0B000000') +MAX_CONSOLIDATIONS = 4 +PENDING_CONSOLIDATIONS_LIMIT = uint64(1048576) # MAX_CONSOLIDATIONS * SLOTS_PER_EPOCH * 8192 +UNSET_CONSOLIDATED_TO = ValidatorIndex(2**64 - 1) class Configuration(NamedTuple): PRESET_BASE: str @@ -321,6 +324,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): @@ -426,6 +431,22 @@ class SignedVoluntaryExit(Container): signature: BLSSignature +class Consolidation(Container): + source_index: ValidatorIndex + target_index: ValidatorIndex + + +class SignedConsolidation(Container): + message: Consolidation + signature: BLSSignature + + +class PendingConsolidation(Container): + source_index: ValidatorIndex + target_index: ValidatorIndex + epoch: Epoch + + class SignedBeaconBlockHeader(Container): message: BeaconBlockHeader signature: BLSSignature @@ -636,6 +657,8 @@ class BeaconBlockBody(Container): execution_payload: ExecutionPayload # Capella operations bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # [New in Capella] + # MAXEB operations + consolidations: List[SignedConsolidation, MAX_CONSOLIDATIONS] # [New in MAXEB] class BeaconBlock(Container): @@ -707,6 +730,7 @@ class BeaconState(Container): historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # [New in Capella] pending_balance_deposits: List[PendingBalanceDeposit] pending_partial_withdrawals: List[PartialWithdrawal] + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] @dataclass(eq=True, frozen=True) @@ -1327,6 +1351,7 @@ def process_epoch(state: BeaconState) -> None: process_slashings(state) process_eth1_data_reset(state) process_pending_balance_deposits(state) + process_pending_consolidations(state) process_effective_balance_updates(state) process_slashings_reset(state) process_randao_mixes_reset(state) @@ -1627,6 +1652,86 @@ def process_pending_balance_deposits(state: BeaconState) -> None: state.pending_balance_deposits = state.pending_balance_deposits[next_pending_deposit_index:] +def apply_pending_consolidation(state: BeaconState, pending_consolidation: PendingConsolidation) -> None: + current_epoch = get_current_epoch(state) + + # Resolve the target of consolidation + target_validator = state.validators[pending_consolidation.target_index] + source_validator = state.validators[pending_consolidation.source_index] + + # Abort consolidation if the target has been consolidated + if target_validator.consolidated_to != UNSET_CONSOLIDATED_TO: + return + + # Apply consolidation + source_validator.consolidated_to = pending_consolidation.target_index + + # Don't move the balance and slash the target if the source was slashed + if is_slashed(source_validator): + if is_slashed_proposer(source_validator): + penalty = PROPOSER_EQUIVOCATION_PENALTY_FACTOR * EFFECTIVE_BALANCE_INCREMENT + decrease_balance(state, target_validator, penalty) + initiate_validator_exit(state, pending_consolidation.target_index) + target_validator.slashed = add_flag(target_validator.slashed, SLASHED_PROPOSER_FLAG_INDEX) + + if is_slashed_attester(source_validator): + if is_attester_slashable_validator(target_validator, current_epoch): + slash_validator(state, pending_consolidation.target_index) + + return + + # Don't move the balance and slash the source if the target was slashed + if is_slashed(target_validator): + if is_slashed_proposer(target_validator): + penalty = PROPOSER_EQUIVOCATION_PENALTY_FACTOR * EFFECTIVE_BALANCE_INCREMENT + decrease_balance(state, source_validator, penalty) + initiate_validator_exit(state, pending_consolidation.source_index) + source_validator.slashed = add_flag(source_validator.slashed, SLASHED_PROPOSER_FLAG_INDEX) + + if is_slashed_attester(target_validator): + if is_attester_slashable_validator(source_validator, current_epoch): + slash_validator(state, pending_consolidation.source_index) + + return + + # Don't move the balance if the source withdrew + if source_validator.withdrawable_epoch < current_epoch: + return + + # Move active balance + active_balance_ceil = MIN_ACTIVATION_BALANCE if has_eth1_withdrawal_credential(source_validator) else MAX_EFFECTIVE_BALANCE + active_balance = min(state.balances[pending_consolidation.source_index], active_balance_ceil) + state.balances[pending_consolidation.target_index] += active_balance + # Excess balance above current active balance ceil will be withdrawn + state.balances[pending_consolidation.source_index] = state.balances[pending_consolidation.source_index] - active_balance + + # Update target exit and withdrawable epochs if the target exited + if target_validator.exit_epoch != FAR_FUTURE_EPOCH: + target_validator.exit_epoch = compute_exit_epoch_and_update_churn(state, active_balance) + target_validator.withdrawable_epoch = Epoch(target_validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + # Balance is not exiting the active set, do not apply churn + if source_validator.exit_epoch > current_epoch: + source_validator.exit_epoch = current_epoch + + # Reset withdrawable epoch when it is closer than expected + withdrawable_epoch = Epoch(source_validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + if source_validator.withdrawable_epoch < withdrawable_epoch: + source_validator.withdrawable_epoch = withdrawable_epoch + + +def process_pending_consolidations(state: BeaconState) -> None: + next_pending_consolidation = 0 + for pending_consolidation in state.pending_consolidations: + if pending_consolidation.epoch >= state.finalized_checkpoint.epoch: + break + + apply_pending_consolidation(state, pending_consolidation) + next_pending_consolidation += 1 + + state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:] + + def process_effective_balance_updates(state: BeaconState) -> None: # Update effective balances with hysteresis for index, validator in enumerate(state.validators): @@ -1735,6 +1840,7 @@ def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) - for_ops(body.voluntary_exits, process_voluntary_exit) for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) # [New in Capella] for_ops(body.execution_payload.withdraw_request, process_execution_layer_withdraw_request) + for_ops(body.consolidations, process_consolidation) def process_execution_layer_withdraw_request( @@ -1784,11 +1890,13 @@ def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSla # Verify header slots match assert header_1.slot == header_2.slot # Verify header proposer indices match - assert header_1.proposer_index == header_2.proposer_index + header_1_proposer_index = resolve_consolidated_to(state, header_1.proposer_index) + header_2_proposer_index = resolve_consolidated_to(state, header_2.proposer_index) + assert header_1_proposer_index == header_2_proposer_index # Verify the headers are different assert header_1 != header_2 # Verify the proposer is slashable - proposer = state.validators[header_1.proposer_index] + proposer = state.validators[header_1_proposer_index] assert proposer.activation_epoch <= get_current_epoch(state) and not is_slashed_proposer(proposer) # Verify signatures for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2): @@ -1798,8 +1906,8 @@ def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSla # Apply penalty penalty = PROPOSER_EQUIVOCATION_PENALTY_FACTOR * EFFECTIVE_BALANCE_INCREMENT - decrease_balance(state, header_1.proposer_index, penalty) - initiate_validator_exit(state, header_1.proposer_index) + decrease_balance(state, header_1_proposer_index, penalty) + initiate_validator_exit(state, header_1_proposer_index) proposer.slashed = add_flag(proposer.slashed, SLASHED_PROPOSER_FLAG_INDEX) # Apply proposer and whistleblower rewards @@ -1815,7 +1923,8 @@ def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSla assert is_valid_indexed_attestation(state, attestation_2) slashed_any = False - indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) + indices = set([resolve_consolidated_to(i) for i in attestation_1.attesting_indices]).intersection( + [resolve_consolidated_to(i) for i in attestation_2.attesting_indices]) for index in sorted(indices): if is_attester_slashable_validator(state.validators[index], get_current_epoch(state)): slash_validator(state, index) @@ -1939,6 +2048,44 @@ def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVolu initiate_validator_exit(state, voluntary_exit.validator_index) +def resolve_consolidated_to(state: BeaconState, index: ValidatorIndex) -> ValidatorIndex: + if state.validators[index].consolidated_to != UNSET_CONSOLIDATED_TO: + # Recursively resolve consolidated index + return resolve_consolidated_to(state, state.validators[index].consolidated_to) + else: + return index + + +def process_consolidation(state: BeaconState, signed_consolidation: SignedConsolidation) -> None: + consolidation = signed_consolidation.message + target_validator = state.validators[consolidation.target_index] + source_validator = state.validators[consolidation.source_index] + + # Verify the source and the target are active and not yet exited + assert is_active_validator(source_validator) + assert is_active_validator(target_validator) + assert source_validator.exit_epoch == FAR_FUTURE_EPOCH + assert target_validator.exit_epoch == FAR_FUTURE_EPOCH + + # Verify the source and the target have Execution layer withdrawal credentials + assert source_validator.withdrawal_credentials[:1] in (ETH1_ADDRESS_WITHDRAWAL_PREFIX, COMPOUNDING_WITHDRAWAL_PREFIX) + assert target_validator.withdrawal_credentials[:1] in (ETH1_ADDRESS_WITHDRAWAL_PREFIX, COMPOUNDING_WITHDRAWAL_PREFIX) + # Verify the same withdrawal address + assert source_validator.withdrawal_credentials[1:] == target_validator.withdrawal_credentials[1:] + + # Verify consolidation is signed by the source and the target + domain = compute_domain(DOMAIN_CONSOLIDATION, genesis_validators_root=state.genesis_validators_root) + signing_root = compute_signing_root(consolidation, domain) + pubkeys = [source_validator.pubkey, target_validator.pubkey] + assert bls.FastAggregateVerify(pubkeys, signing_root, signed_consolidation.signature) + + # Queue consolidation for further processing + consolidation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + state.pending_consolidations.append(PendingConsolidation(source_index = consolidation.source_index, + target_index = consolidation.target_index, + epoch = consolidation_epoch)) + + def is_previous_epoch_justified(store: Store) -> bool: current_slot = get_current_slot(store) current_epoch = compute_epoch_at_slot(current_slot) @@ -2976,6 +3123,10 @@ def is_slashed_attester(validator: Validator) -> bool: return has_flag(ParticipationFlags(validator.slashed), SLASHED_ATTESTER_FLAG_INDEX) +def is_slashed(validator: Validator) -> bool: + return is_slashed_attester(validator) or is_slashed_proposer(validator) + + def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: """ Return the sync committee indices, with possible duplicates, for the next sync committee.