diff --git a/specs/_features/maxeb_increase/capella.py b/specs/_features/maxeb_increase/capella.py index 6b11a8a07a..e75a1f48ca 100644 --- a/specs/_features/maxeb_increase/capella.py +++ b/specs/_features/maxeb_increase/capella.py @@ -144,6 +144,7 @@ def floorlog2(x: int) -> uint64: ENDIANNESS: Final = 'little' BLS_WITHDRAWAL_PREFIX = Bytes1('0x00') ETH1_ADDRESS_WITHDRAWAL_PREFIX = Bytes1('0x01') +COMPOUNDING_WITHDRAWAL_PREFIX = Bytes1('0x02') DOMAIN_BEACON_PROPOSER = DomainType('0x00000000') DOMAIN_BEACON_ATTESTER = DomainType('0x01000000') DOMAIN_RANDAO = DomainType('0x02000000') @@ -190,7 +191,8 @@ def floorlog2(x: int) -> uint64: HYSTERESIS_DOWNWARD_MULTIPLIER = uint64(1) HYSTERESIS_UPWARD_MULTIPLIER = uint64(5) MIN_DEPOSIT_AMOUNT = Gwei(1000000000) -MAX_EFFECTIVE_BALANCE = Gwei(32000000000) +MIN_ACTIVATION_BALANCE = Gwei(32000000000) +MAX_EFFECTIVE_BALANCE = Gwei(2048000000000) EFFECTIVE_BALANCE_INCREMENT = Gwei(1000000000) MIN_ATTESTATION_INCLUSION_DELAY = uint64(1) SLOTS_PER_EPOCH = uint64(32) @@ -231,7 +233,10 @@ def floorlog2(x: int) -> uint64: MAX_BLS_TO_EXECUTION_CHANGES = 16 MAX_WITHDRAWALS_PER_PAYLOAD = uint64(16) MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP = 16384 - +MIN_SLASHING_PENALTY_QUOTIENT_MAXEB = uint64(65536) +DOMAIN_CONSOLIDATION = DomainType('0x0B000000') +MAX_CONSOLIDATIONS = 1 +PENDING_CONSOLIDATIONS_LIMIT = uint64(262144) # MAX_CONSOLIDATIONS * SLOTS_PER_EPOCH * 8192 class Configuration(NamedTuple): PRESET_BASE: str @@ -245,7 +250,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 @@ -260,6 +266,7 @@ class Configuration(NamedTuple): CAPELLA_FORK_VERSION: Version CAPELLA_FORK_EPOCH: Epoch + config = Configuration( PRESET_BASE="mainnet", @@ -273,7 +280,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), @@ -394,6 +402,23 @@ class Deposit(Container): data: DepositData +class PendingBalanceDeposit(Container): + index: ValidatorIndex + amount: Gwei + + +class PartialWithdrawal(Container): + index: ValidatorIndex + amount: Gwei + withdrawable_epoch: Epoch + + +class ExecutionLayerWithdrawRequest(Container): + source_address: ExecutionAddress + validator_pubkey: BLSPubkey + balance: Gwei + + class VoluntaryExit(Container): epoch: Epoch # Earliest epoch when voluntary exit can be processed validator_index: ValidatorIndex @@ -403,6 +428,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 @@ -614,6 +652,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): @@ -656,6 +696,11 @@ class BeaconState(Container): # Registry 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_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 @@ -680,6 +725,9 @@ class BeaconState(Container): next_withdrawal_validator_index: ValidatorIndex # [New in Capella] # Deep history valid from Capella onwards 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) @@ -798,7 +846,7 @@ def is_eligible_for_activation_queue(validator: Validator) -> bool: """ return ( validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH - and validator.effective_balance == MAX_EFFECTIVE_BALANCE + and validator.effective_balance >= MIN_ACTIVATION_BALANCE ) @@ -1014,6 +1062,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``. @@ -1021,13 +1071,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) -> uint64: +def get_churn_limit(state: BeaconState) -> Gwei: + """ + Return the churn limit for the current epoch. + """ + 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 validator churn limit for the current epoch. + Return the churn limit for the current epoch dedicated to activations and exits. """ - active_validator_indices = get_active_validator_indices(state, get_current_epoch(state)) - return max(config.MIN_PER_EPOCH_CHURN_LIMIT, uint64(len(active_validator_indices)) // config.CHURN_LIMIT_QUOTIENT) + 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: """ @@ -1134,6 +1194,41 @@ def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta +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_activation_exit_churn_limit(state) + # New epoch for exits. + if state.earliest_exit_epoch < earliest_exit_epoch: + state.earliest_exit_epoch = earliest_exit_epoch + state.exit_balance_to_consume = per_epoch_churn + + # Exit fits in the current earliest epoch. + if exit_balance <= state.exit_balance_to_consume: + state.exit_balance_to_consume -= exit_balance + else: # Exit doesn't fit in the current earliest epoch. + balance_to_process = exit_balance - state.exit_balance_to_consume + additional_epochs, remainder = divmod(balance_to_process, per_epoch_churn) + state.earliest_exit_epoch += additional_epochs + 1 + 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: """ Initiate the exit of the validator with index ``index``. @@ -1144,11 +1239,7 @@ def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: return # Compute exit queue epoch - exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] - exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) - exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) - if exit_queue_churn >= get_validator_churn_limit(state): - exit_queue_epoch += Epoch(1) + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) # Set validator exit epoch and withdrawable epoch validator.exit_epoch = exit_queue_epoch @@ -1167,7 +1258,7 @@ def slash_validator(state: BeaconState, validator.slashed = True validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance - slashing_penalty = validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX # [Modified in Bellatrix] + slashing_penalty = validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_MAXEB # [Modified in MAXEB] decrease_balance(state, slashed_index, slashing_penalty) # Apply proposer and whistleblower rewards @@ -1209,7 +1300,7 @@ def initialize_beacon_state_from_eth1(eth1_block_hash: Hash32, for index, validator in enumerate(state.validators): balance = state.balances[index] validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) - if validator.effective_balance == MAX_EFFECTIVE_BALANCE: + if validator.effective_balance >= MIN_ACTIVATION_BALANCE: validator.activation_eligibility_epoch = GENESIS_EPOCH validator.activation_epoch = GENESIS_EPOCH @@ -1284,6 +1375,8 @@ def process_epoch(state: BeaconState) -> None: process_registry_updates(state) 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) @@ -1541,16 +1634,11 @@ def process_registry_updates(state: BeaconState) -> None: ): initiate_validator_exit(state, ValidatorIndex(index)) - # Queue validators eligible for activation and not yet dequeued for activation - activation_queue = sorted([ - index for index, validator in enumerate(state.validators) - if is_eligible_for_activation(state, validator) - # Order by the sequence of activation_eligibility_epoch setting and then index - ], key=lambda index: (state.validators[index].activation_eligibility_epoch, index)) - # Dequeued validators for activation up to churn limit - for index in activation_queue[:get_validator_churn_limit(state)]: - validator = state.validators[index] - validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + # Activate all eligible validators + activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + for validator in state.validators: + if is_eligible_for_activation(state, validator): + validator.activation_epoch = activation_epoch def process_slashings(state: BeaconState) -> None: @@ -1575,6 +1663,48 @@ def process_eth1_data_reset(state: BeaconState) -> None: state.eth1_data_votes = [] +def process_pending_balance_deposits(state: BeaconState) -> None: + 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: + break + + state.deposit_balance_to_consume -= pending_balance_deposit.amount + increase_balance(state, pending_balance_deposit.index, pending_balance_deposit.amount) + next_pending_deposit_index += 1 + + state.pending_balance_deposits = state.pending_balance_deposits[next_pending_deposit_index:] + + +def get_active_balance(state: BeaconState, validator_index: ValidatorIndex) -> Gwei: + active_balance_ceil = MIN_ACTIVATION_BALANCE if has_eth1_withdrawal_credential(state.validators[validator_index]) else MAX_EFFECTIVE_BALANCE + return min(state.balances[validator_index], active_balance_ceil) + +def apply_pending_consolidation(state: BeaconState, pending_consolidation: PendingConsolidation) -> None: + # Move active balance to target. Excess balance will be withdrawn. + active_balance = get_active_balance(state, pending_consolidation.source_index) + state.balances[pending_consolidation.source_index] -= active_balance + state.balances[pending_consolidation.target_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): @@ -1582,11 +1712,12 @@ 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 if ( balance + DOWNWARD_THRESHOLD < validator.effective_balance or validator.effective_balance + UPWARD_THRESHOLD < balance ): - validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) + validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, EFFECTIVE_BALANCE_LIMIT) def process_slashings_reset(state: BeaconState) -> None: @@ -1681,6 +1812,48 @@ def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) - for_ops(body.deposits, process_deposit) 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( + state: BeaconState, + execution_layer_withdraw_request: ExecutionLayerWithdrawRequest + ) -> None: + validator_pubkeys = [v.pubkey for v in state.validators] + validator_index = ValidatorIndex(validator_pubkeys.index(execution_layer_withdraw_request.validator_pubkey)) + validator = state.validators[validator_index] + + # Same conditions as in EIP7002 https://github.com/ethereum/consensus-specs/pull/3349/files#diff-7a6e2ba480d22d8bd035bd88ca91358456caf9d7c2d48a74e1e900fe63d5c4f8R223 + # Verify withdrawal credentials + is_execution_address = validator.withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX + is_correct_source_address = validator.withdrawal_credentials[12:] == execution_layer_withdraw_request.source_address + if not (is_execution_address and is_correct_source_address): + return + # Verify the validator is active + if not is_active_validator(validator, get_current_epoch(state)): + return + # Verify exit has not been initiated, and slashed + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + # Verify the validator has been active long enough + if get_current_epoch(state) < validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD: + return + + pending_balance_to_withdraw = sum(item.amount for item in state.pending_partial_withdrawals if item.index == validator_index) + # TODO: Should substract `MIN_ACTIVATION_BALANCE` or ejection balance? + available_balance = state.balances[validator_index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw + if available_balance < execution_layer_withdraw_request.balance: + return + + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, available_balance) + withdrawable_epoch = Epoch(exit_queue_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + state.pending_partial_withdrawals.append(PartialWithdrawal( + index=validator_index, + amount=available_balance, + withdrawable_epoch=withdrawable_epoch, + )) def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: @@ -1756,9 +1929,7 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: increase_balance(state, get_beacon_proposer_index(state), proposer_reward) -def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64) -> Validator: - effective_balance = min(amount - amount % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) - +def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32) -> Validator: return Validator( pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, @@ -1766,7 +1937,7 @@ def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes3 activation_epoch=FAR_FUTURE_EPOCH, exit_epoch=FAR_FUTURE_EPOCH, withdrawable_epoch=FAR_FUTURE_EPOCH, - effective_balance=effective_balance, + effective_balance=0, ) @@ -1787,16 +1958,17 @@ def apply_deposit(state: BeaconState, signing_root = compute_signing_root(deposit_message, domain) # Initialize validator if the deposit signature is valid if bls.Verify(pubkey, signing_root, signature): - state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials, amount)) - state.balances.append(amount) + state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials)) + state.balances.append(0) # [New in Altair] state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000)) state.current_epoch_participation.append(ParticipationFlags(0b0000_0000)) state.inactivity_scores.append(uint64(0)) + index = len(state.validators) - 1 + state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) else: - # Increase balance by deposit amount index = ValidatorIndex(validator_pubkeys.index(pubkey)) - increase_balance(state, index, amount) + state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) def process_deposit(state: BeaconState, deposit: Deposit) -> None: @@ -1840,6 +2012,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, get_current_epoch(state)) + assert is_active_validator(target_validator, get_current_epoch(state)) + # 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, consolidation.source_index) + 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 = consolidation.source_index, + target_index = consolidation.target_index)) + + def is_previous_epoch_justified(store: Store) -> bool: current_slot = get_current_slot(store) current_epoch = compute_epoch_at_slot(current_slot) @@ -2334,9 +2540,14 @@ def get_slot_signature(state: BeaconState, slot: Slot, privkey: int) -> BLSSigna return bls.Sign(privkey, signing_root) -def is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex, slot_signature: BLSSignature) -> bool: +def is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex, validator_index: ValidatorIndex, slot_signature: BLSSignature) -> bool: + validator = state.validators[validator_index] committee = get_beacon_committee(state, slot, index) - modulo = max(1, len(committee) // TARGET_AGGREGATORS_PER_COMMITTEE) + min_balance_increments = validator.effective_balance // MIN_ACTIVATION_BALANCE + committee_balance_increments = get_total_balance(state, set(committee)) // MIN_ACTIVATION_BALANCE + denominator = committee_balance_increments ** min_balance_increments + numerator = denominator - (committee_balance_increments - TARGET_AGGREGATORS_PER_COMMITTEE) ** min_balance_increments + modulo = denominator // numerator return bytes_to_uint64(hash(slot_signature)[0:8]) % modulo == 0 @@ -2378,7 +2589,7 @@ def compute_weak_subjectivity_period(state: BeaconState) -> uint64: N = len(get_active_validator_indices(state, get_current_epoch(state))) t = get_total_active_balance(state) // N // ETH_TO_GWEI T = MAX_EFFECTIVE_BALANCE // ETH_TO_GWEI - delta = get_validator_churn_limit(state) + delta = get_validator_churn_limit(state) // MIN_ACTIVATION_BALANCE Delta = MAX_DEPOSITS * SLOTS_PER_EPOCH D = SAFETY_DECAY @@ -3540,24 +3751,41 @@ def has_eth1_withdrawal_credential(validator: Validator) -> bool: return validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX +def has_compounding_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. + """ + return validator.withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX + + def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: """ Check if ``validator`` is fully withdrawable. """ return ( - has_eth1_withdrawal_credential(validator) + (has_eth1_withdrawal_credential(validator) or has_compounding_withdrawal_credential(validator)) and validator.withdrawable_epoch <= epoch and balance > 0 ) +def get_validator_excess_balance(validator: Validator, balance: Gwei) -> Gwei: + """ + Get excess balance for partial withdrawals for ``validator``. + """ + if has_eth1_withdrawal_credential(validator) and balance > MIN_ACTIVATION_BALANCE: + return balance - MIN_ACTIVATION_BALANCE + if has_compounding_withdrawal_credential(validator) and balance > MAX_EFFECTIVE_BALANCE: + return balance - MAX_EFFECTIVE_BALANCE + return Gwei(0) + def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: """ Check if ``validator`` is partially withdrawable. """ - has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE - has_excess_balance = balance > MAX_EFFECTIVE_BALANCE - return has_eth1_withdrawal_credential(validator) and has_max_effective_balance and has_excess_balance + if not (has_eth1_withdrawal_credential(validator) or has_compounding_withdrawal_credential(validator)): + return False + return get_validator_excess_balance(validator, balance) > 0 def process_historical_summaries_update(state: BeaconState) -> None: @@ -3576,6 +3804,26 @@ def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: withdrawal_index = state.next_withdrawal_index validator_index = state.next_withdrawal_validator_index withdrawals: List[Withdrawal] = [] + consumed = 0 + for withdrawal in state.pending_partial_withdrawals: + if withdrawal.withdrawable_epoch > epoch or len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD // 2: + break + + validator = state.validators[withdrawal.index] + if validator.exit_epoch == FAR_FUTURE_EPOCH and state.balances[withdrawal.index] > MIN_ACTIVATION_BALANCE: + withdrawable_balance = min(state.balances[withdrawal.index] - MIN_ACTIVATION_BALANCE, withdrawal.amount) + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=withdrawal.index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=withdrawable_balance, + )) + withdrawal_index += WithdrawalIndex(1) + consumed += 1 + + state.pending_partial_withdrawals = state.pending_partial_withdrawals[consumed:] + + # Sweep for remaining. bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) for _ in range(bound): validator = state.validators[validator_index] @@ -3593,7 +3841,7 @@ def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: index=withdrawal_index, validator_index=validator_index, address=ExecutionAddress(validator.withdrawal_credentials[12:]), - amount=balance - MAX_EFFECTIVE_BALANCE, + amount=get_validator_excess_balance(validator, balance), )) withdrawal_index += WithdrawalIndex(1) if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: