diff --git a/specs/_features/maxeb_increase/capella.py b/specs/_features/maxeb_increase/capella.py index 3e5c01b372..3992d8f7ba 100644 --- a/specs/_features/maxeb_increase/capella.py +++ b/specs/_features/maxeb_increase/capella.py @@ -233,7 +233,9 @@ 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 = 1 +PENDING_CONSOLIDATIONS_LIMIT = uint64(262144) # MAX_CONSOLIDATIONS * SLOTS_PER_EPOCH * 8192 class Configuration(NamedTuple): PRESET_BASE: str @@ -247,7 +249,8 @@ class Configuration(NamedTuple): SHARD_COMMITTEE_PERIOD: uint64 ETH1_FOLLOW_DISTANCE: uint64 EJECTION_BALANCE: Gwei - MIN_PER_EPOCH_CHURN_LIMIT: uint64 + MIN_PER_EPOCH_CHURN_LIMIT: Gwei + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: Gwei CHURN_LIMIT_QUOTIENT: uint64 PROPOSER_SCORE_BOOST: uint64 INACTIVITY_SCORE_BIAS: uint64 @@ -262,6 +265,7 @@ class Configuration(NamedTuple): CAPELLA_FORK_VERSION: Version CAPELLA_FORK_EPOCH: Epoch + config = Configuration( PRESET_BASE="mainnet", @@ -275,7 +279,8 @@ class Configuration(NamedTuple): SHARD_COMMITTEE_PERIOD=uint64(256), ETH1_FOLLOW_DISTANCE=uint64(2048), EJECTION_BALANCE=Gwei(16000000000), - MIN_PER_EPOCH_CHURN_LIMIT=uint64(4), + MIN_PER_EPOCH_CHURN_LIMIT=uint64(128), + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT=uint64(256), CHURN_LIMIT_QUOTIENT=uint64(65536), PROPOSER_SCORE_BOOST=uint64(40), INACTIVITY_SCORE_BIAS=uint64(4), @@ -422,6 +427,19 @@ class SignedVoluntaryExit(Container): message: VoluntaryExit signature: BLSSignature +class Consolidation(Container): + source_index: ValidatorIndex + target_index: ValidatorIndex + epoch: Epoch + + +class SignedConsolidation(Container): + message: Consolidation + signature: BLSSignature + +class PendingConsolidation(Container): + source_index: ValidatorIndex + target_index: ValidatorIndex class SignedBeaconBlockHeader(Container): message: BeaconBlockHeader @@ -633,6 +651,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): @@ -676,8 +696,10 @@ class BeaconState(Container): validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] deposit_balance_to_consume: Gwei - exit_balance_to_consume: Gwei # Should be initialized with get_validator_churn_limit(state) + exit_balance_to_consume: Gwei # Should be initialized with get_churn_limit(state) earliest_exit_epoch: Epoch # Should be initialized with the max([v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH]) + 1 + consolidation_balance_to_consume: Gwei # Should be initialized with get_consolidation_churn_limit(state) + earliest_consolidation_epoch: Epoch # Randomness randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] # Slashings @@ -704,6 +726,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) @@ -1038,6 +1061,8 @@ def get_randao_mix(state: BeaconState, epoch: Epoch) -> Bytes32: return state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] + + def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]: """ Return the sequence of active validator indices at ``epoch``. @@ -1045,13 +1070,23 @@ def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[V return [ValidatorIndex(i) for i, v in enumerate(state.validators) if is_active_validator(v, epoch)] -def get_validator_churn_limit(state: BeaconState) -> Gwei: +def get_churn_limit(state: BeaconState) -> Gwei: """ - Return the validator churn limit for the current epoch. + Return the churn limit for the current epoch. """ - churn = max(config.MIN_PER_EPOCH_CHURN_LIMIT * MIN_ACTIVATION_BALANCE, get_total_active_balance(state) // config.CHURN_LIMIT_QUOTIENT) + churn = max(config.MIN_PER_EPOCH_CHURN_LIMIT, + get_total_active_balance(state) // config.CHURN_LIMIT_QUOTIENT) return churn - churn % EFFECTIVE_BALANCE_INCREMENT +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_churn_limit(state)) + +def get_consolidation_churn_limit(state: BeaconState) -> Gwei: + return get_churn_limit(state) - get_activation_exit_churn_limit(state) + def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Bytes32: """ @@ -1160,7 +1195,7 @@ def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: earliest_exit_epoch = compute_activation_exit_epoch(get_current_epoch(state)) - per_epoch_churn = get_validator_churn_limit(state) + per_epoch_churn = get_activation_exit_churn_limit(state) # New epoch for exits. if state.earliest_exit_epoch < earliest_exit_epoch: state.earliest_exit_epoch = earliest_exit_epoch @@ -1176,6 +1211,22 @@ def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) state.exit_balance_to_consume = per_epoch_churn - remainder return state.earliest_exit_epoch +def compute_consolidation_epoch_and_update_churn(state: BeaconState, consolidation_balance: Gwei) -> Epoch: + earliest_consolidation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + per_epoch_consolidation_churn = get_consolidation_churn_limit(state) + # New epoch for consolidations. + if state.earliest_consolidation_epoch < earliest_consolidation_epoch: + state.earliest_consolidation_epoch = earliest_consolidation_epoch + state.consolidation_balance_to_consume = per_epoch_consolidation_churn + # Consolidation fits in the current earliest consolidation epoch. + if consolidation_balance <= state.consolidation_balance_to_consume: + state.consolidation_balance_to_consume -= consolidation_balance + else: # Consolidation doesn't fit in the current earliest epoch. + balance_to_process = consolidation_balance - state.consolidation_balance_to_consume + additional_epochs, remainder = divmod(balance_to_process, per_epoch_consolidation_churn) + state.earliest_consolidation_epoch += additional_epochs + 1 + state.consolidation_balance_to_consume = per_epoch_consolidation_churn - remainder + return state.earliest_consolidation_epoch def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: """ @@ -1324,6 +1375,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) @@ -1611,7 +1663,7 @@ def process_eth1_data_reset(state: BeaconState) -> None: def process_pending_balance_deposits(state: BeaconState) -> None: - state.deposit_balance_to_consume += get_validator_churn_limit(state) + state.deposit_balance_to_consume += get_activation_exit_churn_limit(state) next_pending_deposit_index = 0 for pending_balance_deposit in state.pending_balance_deposits: if state.deposit_balance_to_consume < pending_balance_deposit.amount: @@ -1624,6 +1676,36 @@ def process_pending_balance_deposits(state: BeaconState) -> None: state.pending_balance_deposits = state.pending_balance_deposits[next_pending_deposit_index:] +def get_active_balance(state: BeaconState, validator: Validator) -> Gwei: + active_balance_ceil = MIN_ACTIVATION_BALANCE if has_eth1_withdrawal_credential(validator) else MAX_EFFECTIVE_BALANCE + return min(state.balances[validator.index], active_balance_ceil) + +def apply_pending_consolidation(state: BeaconState, pending_consolidation: PendingConsolidation) -> None: + source_validator = state.validators[pending_consolidation.source_index] + target_validator = state.validators[pending_consolidation.target_index] + # Move active balance to target. Excess balance will be withdrawn. + active_balance = get_active_balance(state, source_validator) + state.balances[source_validator.index] -= active_balance + state.balances[target_validator.index] += active_balance + + +def process_pending_consolidations(state: BeaconState) -> None: + next_pending_consolidation = 0 + for pending_consolidation in state.pending_consolidations: + source_validator = state.validators[pending_consolidation.source_index] + if source_validator.withdrawable_epoch > get_current_epoch(state): + break + + if not source_validator.slashed: + 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): @@ -1631,7 +1713,7 @@ def process_effective_balance_updates(state: BeaconState) -> None: HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER - EFFECTIVE_BALANCE_LIMIT = MAX_EFFECTIVE_BALANCE if has_compounding_withdrawal_credential(validator) else MIN_ACTIVATION_BALANCE + EFFECTIVE_BALANCE_LIMIT = MAX_EFFECTIVE_BALANCE if has_compounding_withdrawal_credential(validator) else MIN_ACTIVATION_BALANCE if ( balance + DOWNWARD_THRESHOLD < validator.effective_balance or validator.effective_balance + UPWARD_THRESHOLD < balance @@ -1732,6 +1814,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( @@ -1928,6 +2011,40 @@ def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVolu initiate_validator_exit(state, voluntary_exit.validator_index) +def process_consolidation(state: BeaconState, signed_consolidation: SignedConsolidation) -> None: + assert(len(state.pending_consolidations) < PENDING_CONSOLIDATIONS_LIMIT) + 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 + assert is_active_validator(source_validator) + assert is_active_validator(target_validator) + # Verify exits for source and target have not been initiated + assert source_validator.exit_epoch == FAR_FUTURE_EPOCH + assert target_validator.exit_epoch == FAR_FUTURE_EPOCH + # Consolidations must specify an epoch when they become valid; they are not valid before then + assert get_current_epoch(state) >= consolidation.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) + + # Initiate source validator exit and append pending consolidation + active_balance = get_active_balance(state, source_validator) + source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn(state, active_balance) + source_validator.withdrawable_epoch = Epoch(source_validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + state.pending_consolidations.append(PendingConsolidation(source_index = source_validator.index, + target_index = target_validator.index)) + + def is_previous_epoch_justified(store: Store) -> bool: current_slot = get_current_slot(store) current_epoch = compute_epoch_at_slot(current_slot)