diff --git a/electra-gap.md b/electra-gap.md index 58696950d..fd7133e7a 100644 --- a/electra-gap.md +++ b/electra-gap.md @@ -39,16 +39,16 @@ This document outlines the gaps in the current implementation of the Electra. It - [x] Modified `get_next_sync_committee_indices` ([Spec](docs/specs/electra/beacon-chain.md#modified-get_next_sync_committee_indices), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1417)) - [x] New `get_balance_churn_limit` ([Spec](docs/specs/electra/beacon-chain.md#new-get_balance_churn_limit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1420)) - [x] New `get_activation_exit_churn_limit` ([Spec](docs/specs/electra/beacon-chain.md#new-get_activation_exit_churn_limit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1420)) -- [ ] New `get_consolidation_churn_limit` ([Spec](docs/specs/electra/beacon-chain.md#new-get_consolidation_churn_limit)) -- [ ] New `get_pending_balance_to_withdraw` ([Spec](docs/specs/electra/beacon-chain.md#new-get_pending_balance_to_withdraw)) +- [x] New `get_consolidation_churn_limit` ([Spec](docs/specs/electra/beacon-chain.md#new-get_consolidation_churn_limit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) +- [x] New `get_pending_balance_to_withdraw` ([Spec](docs/specs/electra/beacon-chain.md#new-get_pending_balance_to_withdraw), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) ## Beacon State Mutators - [x] Modified `initiate_validator_exit` ([Spec](docs/specs/electra/beacon-chain.md#modified-initiate_validator_exit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1420)) -- [ ] New `switch_to_compounding_validator` ([Spec](docs/specs/electra/beacon-chain.md#new-switch_to_compounding_validator)) -- [ ] New `queue_excess_active_balance` ([Spec](docs/specs/electra/beacon-chain.md#new-queue_excess_active_balance)) +- [x] New `switch_to_compounding_validator` ([Spec](docs/specs/electra/beacon-chain.md#new-switch_to_compounding_validator), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) +- [x] New `queue_excess_active_balance` ([Spec](docs/specs/electra/beacon-chain.md#new-queue_excess_active_balance), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) - [x] New `compute_exit_epoch_and_update_churn` ([Spec](docs/specs/electra/beacon-chain.md#new-compute_exit_epoch_and_update_churn), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1420)) -- [ ] New `compute_consolidation_epoch_and_update_churn` ([Spec](docs/specs/electra/beacon-chain.md#new-compute_consolidation_epoch_and_update_churn)) +- [x] New `compute_consolidation_epoch_and_update_churn` ([Spec](docs/specs/electra/beacon-chain.md#new-compute_consolidation_epoch_and_update_churn), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) - [x] Modified `slash_validator` ([Spec](docs/specs/electra/beacon-chain.md#modified-slash_validator), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1420)) ## Miscellaneous @@ -73,12 +73,12 @@ This document outlines the gaps in the current implementation of the Electra. It - [ ] Modified `process_withdrawals` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_withdrawals)) - [ ] Modified `process_execution_payload` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_execution_payload)) - [x] Modified `process_operations` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_operations), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1424)) -- [ ] Modified `process_attestation` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_attestation)) +- [x] Modified `process_attestation` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_attestation), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) - [x] Modified `process_deposit` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_deposit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1424)) -- [ ] Modified `process_voluntary_exit` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_voluntary_exit)) -- [ ] New `process_withdrawal_request` ([Spec](docs/specs/electra/beacon-chain.md#new-process_withdrawal_request)) +- [x] Modified `process_voluntary_exit` ([Spec](docs/specs/electra/beacon-chain.md#modified-process_voluntary_exit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) +- [x] New `process_withdrawal_request` ([Spec](docs/specs/electra/beacon-chain.md#new-process_withdrawal_request), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) - [x] New `process_deposit_request` ([Spec](docs/specs/electra/beacon-chain.md#new-process_deposit_request), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1424)) -- [ ] New `process_consolidation_request` ([Spec](docs/specs/electra/beacon-chain.md#new-process_consolidation_request)) +- [x] New `process_consolidation_request` ([Spec](docs/specs/electra/beacon-chain.md#new-process_consolidation_request), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1426)) - [x] New `is_valid_deposit_signature` ([Spec](docs/specs/electra/beacon-chain.md#new-is_valid_deposit_signature), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1424)) - [x] Modified `add_validator_to_registry` ([Spec](docs/specs/electra/beacon-chain.md#modified-add_validator_to_registry), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1424)) - [x] Modified `apply_deposit` ([Spec](docs/specs/electra/beacon-chain.md#modified-apply_deposit), [PR](https://github.com/lambdaclass/lambda_ethereum_consensus/pull/1424)) diff --git a/lib/constants.ex b/lib/constants.ex index a072d60de..03a6039d5 100644 --- a/lib/constants.ex +++ b/lib/constants.ex @@ -151,4 +151,7 @@ defmodule Constants do @spec consolidation_request_type() :: Types.bytes1() def consolidation_request_type(), do: <<2>> + + @spec g2_point_at_infinity() :: Types.bls_signature() + def g2_point_at_infinity(), do: <<0xC0, 0::8*95>> end diff --git a/lib/lambda_ethereum_consensus/state_transition/accessors.ex b/lib/lambda_ethereum_consensus/state_transition/accessors.ex index d35709fec..a6d0a6dcd 100644 --- a/lib/lambda_ethereum_consensus/state_transition/accessors.ex +++ b/lib/lambda_ethereum_consensus/state_transition/accessors.ex @@ -653,7 +653,11 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do @spec get_committee_indices(Types.bitvector()) :: Enumerable.t(Types.commitee_index()) def get_committee_indices(committee_bits) do - bitlist = committee_bits |> :binary.bin_to_list() |> Enum.reverse() + bitlist = + for <> do + bit + end + |> Enum.reverse() for {bit, index} <- Enum.with_index(bitlist), bit == 1, do: index end @@ -682,4 +686,19 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do get_balance_churn_limit(state) ) end + + @spec get_pending_balance_to_withdraw(BeaconState.t(), Types.validator_index()) :: Types.gwei() + def get_pending_balance_to_withdraw(state, validator_index) do + for( + withdrawal <- state.pending_partial_withdrawals, + withdrawal.validator_index == validator_index, + do: withdrawal.amount + ) + |> Enum.sum() + end + + @spec get_consolidation_churn_limit(BeaconState.t()) :: Types.gwei() + def get_consolidation_churn_limit(state) do + get_balance_churn_limit(state) - get_activation_exit_churn_limit(state) + end end diff --git a/lib/lambda_ethereum_consensus/state_transition/mutators.ex b/lib/lambda_ethereum_consensus/state_transition/mutators.ex index 3112b58cd..60d26f8e0 100644 --- a/lib/lambda_ethereum_consensus/state_transition/mutators.ex +++ b/lib/lambda_ethereum_consensus/state_transition/mutators.ex @@ -218,4 +218,95 @@ defmodule LambdaEthereumConsensus.StateTransition.Mutators do earliest_exit_epoch: earliest_exit_epoch } end + + @spec compute_consolidation_epoch_and_update_churn(Types.BeaconState.t(), Types.gwei()) :: + Types.BeaconState.t() + def compute_consolidation_epoch_and_update_churn(state, consolidation_balance) do + current_epoch = Accessors.get_current_epoch(state) + + earliest_consolidation_epoch = + max(state.earliest_consolidation_epoch, Misc.compute_activation_exit_epoch(current_epoch)) + + per_epoch_consolidation_churn = Accessors.get_consolidation_churn_limit(state) + + consolidation_balance_to_consume = + if state.earliest_consolidation_epoch < earliest_consolidation_epoch do + per_epoch_consolidation_churn + else + state.consolidation_balance_to_consume + end + + {earliest_consolidation_epoch, consolidation_balance_to_consume} = + if consolidation_balance > consolidation_balance_to_consume do + balance_to_process = consolidation_balance - consolidation_balance_to_consume + additional_epochs = div(balance_to_process - 1, per_epoch_consolidation_churn) + 1 + + { + earliest_consolidation_epoch + additional_epochs, + consolidation_balance_to_consume + additional_epochs * per_epoch_consolidation_churn + } + else + {earliest_consolidation_epoch, consolidation_balance_to_consume} + end + + %BeaconState{ + state + | consolidation_balance_to_consume: + consolidation_balance_to_consume - consolidation_balance, + earliest_consolidation_epoch: earliest_consolidation_epoch + } + end + + @spec switch_to_compounding_validator(BeaconState.t(), Types.validator_index()) :: + BeaconState.t() + def switch_to_compounding_validator(state, index) do + validator = Aja.Enum.at(state.validators, index) + <<_first_byte::binary-size(1), rest::binary>> = validator.withdrawal_credentials + + withdrawal_credentials = + Constants.compounding_withdrawal_prefix() <> rest + + updated_validator = %Validator{ + validator + | withdrawal_credentials: withdrawal_credentials + } + + state = %BeaconState{ + state + | validators: Aja.Vector.replace_at(state.validators, index, updated_validator) + } + + queue_excess_active_balance(state, index) + end + + @spec queue_excess_active_balance(BeaconState.t(), Types.validator_index()) :: + BeaconState.t() + def queue_excess_active_balance(state, index) do + min_activation_balance = ChainSpec.get("MIN_ACTIVATION_BALANCE") + balance = Aja.Vector.at(state.balances, index) + + if balance > min_activation_balance do + excess_balance = balance - min_activation_balance + validator = Aja.Vector.at(state.validators, index) + + updated_balances = Aja.Vector.replace_at(state.balances, index, min_activation_balance) + # Use bls.G2_POINT_AT_INFINITY as a signature field placeholder + # and GENESIS_SLOT to distinguish from a pending deposit request + pending_deposit = %PendingDeposit{ + pubkey: validator.pubkey, + withdrawal_credentials: validator.withdrawal_credentials, + amount: excess_balance, + signature: Constants.g2_point_at_infinity(), + slot: Constants.genesis_slot() + } + + %BeaconState{ + state + | balances: updated_balances, + pending_deposits: state.pending_deposits ++ [pending_deposit] + } + else + state + end + end end diff --git a/lib/lambda_ethereum_consensus/state_transition/operations.ex b/lib/lambda_ethereum_consensus/state_transition/operations.ex index c8f513631..924dec218 100644 --- a/lib/lambda_ethereum_consensus/state_transition/operations.ex +++ b/lib/lambda_ethereum_consensus/state_transition/operations.ex @@ -13,7 +13,9 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do alias LambdaEthereumConsensus.Utils.BitList alias LambdaEthereumConsensus.Utils.BitVector alias LambdaEthereumConsensus.Utils.Randao + alias Types.PendingConsolidation alias Types.PendingDeposit + alias Types.PendingPartialWithdrawal alias Types.Attestation alias Types.BeaconBlock @@ -565,6 +567,9 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do current_epoch < validator.activation_epoch + ChainSpec.get("SHARD_COMMITTEE_PERIOD") -> {:error, "validator cannot exit yet"} + Accessors.get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) != 0 -> + {:error, "validator has pending withdrawals in the queue"} + not (Misc.compute_domain( Constants.domain_voluntary_exit(), fork_version: ChainSpec.get("CAPELLA_FORK_VERSION"), @@ -592,16 +597,18 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do end @spec validate_attestation(BeaconState.t(), Attestation.t()) :: :ok | {:error, String.t()} - def validate_attestation(state, %Attestation{data: data} = attestation) do + def validate_attestation( + state, + %Attestation{data: data, aggregation_bits: aggregation_bits} = attestation + ) do with :ok <- check_valid_target_epoch(data, state), :ok <- check_epoch_matches(data), :ok <- check_valid_slot_range(data, state), - :ok <- check_committee_count(data, state), - {:ok, beacon_committee} <- Accessors.get_beacon_committee(state, data.slot, data.index), - :ok <- check_matching_aggregation_bits_length(attestation, beacon_committee) do - beacon_committee - |> Accessors.get_committee_indexed_attestation(attestation) - |> then(&check_valid_indexed_attestation(state, &1)) + :ok <- check_data_index_zero(data), + {:ok, committee_offset} <- check_committee_indices(attestation, state), + :ok <- check_matching_aggregation_bits_length(aggregation_bits, committee_offset), + {:ok, indexed_attestation} <- Accessors.get_indexed_attestation(state, attestation) do + check_valid_indexed_attestation(state, indexed_attestation) end end @@ -699,7 +706,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do def fast_process_attestation( state, - %Attestation{data: data, aggregation_bits: aggregation_bits} = att, + %Attestation{data: data} = att, previous_epoch_updates, current_epoch_updates, attestation_index @@ -708,10 +715,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do slot = state.slot - data.slot, {:ok, flag_indices} <- Accessors.get_attestation_participation_flag_indices(state, data, slot), - {:ok, committee} <- Accessors.get_beacon_committee(state, data.slot, data.index) do - attesting_indices = - Accessors.get_committee_attesting_indices(committee, aggregation_bits) - + {:ok, attesting_indices} <- Accessors.get_attesting_indices(state, att) do is_current_epoch = data.target.epoch == Accessors.get_current_epoch(state) epoch_updates = if is_current_epoch, do: current_epoch_updates, else: previous_epoch_updates @@ -726,6 +730,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do new_epoch_updates = attesting_indices + |> Enum.to_list() |> Enum.reduce(epoch_updates, fn i, epoch_updates -> Map.update(epoch_updates, i, [v], &merge_masks(&1, v)) end) @@ -842,23 +847,22 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do end end - defp check_committee_count(data, state) do - if data.index >= Accessors.get_committee_count_per_slot(state, data.target.epoch) do - {:error, "Index exceeds committee count"} + defp check_committee_count(comittee_index, data, state) do + if comittee_index >= Accessors.get_committee_count_per_slot(state, data.target.epoch) do + {:error, "Comitee index exceeds committee count"} else :ok end end - defp check_matching_aggregation_bits_length(attestation, beacon_committee) do - aggregation_bits_length = BitList.length(attestation.aggregation_bits) - beacon_committee_length = length(beacon_committee) + defp check_matching_aggregation_bits_length(aggregation_bits, committe_offset) do + aggregation_bits_length = BitList.length(aggregation_bits) - if aggregation_bits_length == beacon_committee_length do + if aggregation_bits_length == committe_offset do :ok else {:error, - "Mismatched length. aggregation_bits: #{aggregation_bits_length}. beacon_committee: #{beacon_committee_length}"} + "Mismatched length. aggregation_bits: #{aggregation_bits_length}. committee_offset: #{committe_offset}"} end end @@ -870,6 +874,36 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do end end + defp check_data_index_zero(%{index: 0}), do: :ok + defp check_data_index_zero(_data), do: {:error, "Data index should be zero"} + + defp check_committee_attesters_exists(committee, aggregation_bits, committee_offset) do + committee + |> Enum.with_index() + |> Enum.any?(&BitList.set?(aggregation_bits, elem(&1, 1) + committee_offset)) + |> case do + true -> :ok + false -> {:error, "No committee attesters exist"} + end + end + + defp check_committee_indices(attestation, state) do + %Attestation{data: data, aggregation_bits: aggregation_bits, committee_bits: committee_bits} = + attestation + + committee_bits + |> Accessors.get_committee_indices() + |> Enum.reduce_while({:ok, 0}, fn committee_index, {:ok, committee_offset} -> + with :ok <- check_committee_count(committee_index, data, state), + {:ok, committee} <- Accessors.get_beacon_committee(state, data.slot, committee_index), + :ok <- check_committee_attesters_exists(committee, aggregation_bits, committee_offset) do + {:cont, {:ok, committee_offset + length(committee)}} + else + error -> {:halt, error} + end + end) + end + def process_bls_to_execution_change(state, signed_address_change) do address_change = signed_address_change.message @@ -946,17 +980,299 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do end @spec process_withdrawal_request(BeaconState.t(), WithdrawalRequest.t()) :: - {:ok, BeaconState.t()} - def process_withdrawal_request(state, _withdrawal_request) do - # TODO: Not implemented yet - {:ok, state} + {:ok, BeaconState.t()} | {:error, String.t()} + def process_withdrawal_request(state, withdrawal_request) do + amount = withdrawal_request.amount + is_full_exit_request = amount == Constants.full_exit_request_amount() + request_pubkey = withdrawal_request.validator_pubkey + current_epoch = Accessors.get_current_epoch(state) + far_future_epoch = Constants.far_future_epoch() + + with false <- partial_withdrawal_on_full_queue?(state, is_full_exit_request), + {validator, validator_index} <- find_validator(state, request_pubkey), + true <- + not invalid_withdrawal_credentials?(validator, withdrawal_request.source_address), + true <- Predicates.active_validator?(validator, current_epoch), + true <- validator.exit_epoch == far_future_epoch, + true <- + current_epoch >= validator.activation_epoch + ChainSpec.get("SHARD_COMMITTEE_PERIOD") do + pending_balance_to_withdraw = + Accessors.get_pending_balance_to_withdraw(state, validator_index) + + withdrawal_request_type = + cond do + is_full_exit_request and pending_balance_to_withdraw == 0 -> :full_exit + is_full_exit_request -> :full_exit_with_pending_balance + true -> :partial_exit + end + + handle_valid_withdrawal_request( + state, + validator, + validator_index, + amount, + pending_balance_to_withdraw, + withdrawal_request_type + ) + else + _ -> + {:ok, state} + end + end + + defp partial_withdrawal_on_full_queue?(state, is_full_exit_request) do + length(state.pending_partial_withdrawals) == + ChainSpec.get("PENDING_PARTIAL_WITHDRAWALS_LIMIT") && !is_full_exit_request + end + + defp invalid_withdrawal_credentials?(validator, address) do + has_correct_credential = Validator.has_execution_withdrawal_credential(validator) + + is_correct_source_address = + case validator.withdrawal_credentials do + <<_::binary-size(12), rest>> -> rest == address + _ -> false + end + + !(has_correct_credential && is_correct_source_address) + end + + @spec find_validator(Types.BeaconState.t(), Types.bls_pubkey()) :: + {Types.Validator.t(), non_neg_integer()} | nil + defp find_validator(state, request_pubkey) do + state.validators + |> Aja.Enum.find_index(fn validator -> validator.pubkey == request_pubkey end) + |> then(fn + nil -> nil + index -> {Aja.Vector.at(state.validators, index), index} + end) + end + + defp handle_valid_withdrawal_request(state, _, validator_index, _, _, :full_exit) do + with {:ok, {state, validator}} <- Mutators.initiate_validator_exit(state, validator_index) do + {:ok, + %Types.BeaconState{ + state + | validators: Aja.Vector.replace_at(state.validators, validator_index, validator) + }} + end + end + + defp handle_valid_withdrawal_request(state, _, _, _, _, :full_exit_with_pending_balance), + do: {:ok, state} + + defp handle_valid_withdrawal_request( + state, + validator, + validator_index, + amount, + pending_balance_to_withdraw, + :partial_exit + ) do + min_activation_balance = ChainSpec.get("MIN_ACTIVATION_BALANCE") + + has_sufficient_effective_balance = + validator.effective_balance >= min_activation_balance + + has_excess_balance = + Aja.Vector.at(state.balances, validator_index) > + min_activation_balance + pending_balance_to_withdraw + + if Validator.has_compounding_withdrawal_credential(validator) && + has_sufficient_effective_balance && has_excess_balance do + to_withdraw = + min( + Aja.Vector.at(state.balances, validator_index) - min_activation_balance - + pending_balance_to_withdraw, + amount + ) + + state = Mutators.compute_exit_epoch_and_update_churn(state, to_withdraw) + exit_queue_epoch = state.earliest_exit_epoch + + withdrawable_epoch = + exit_queue_epoch + ChainSpec.get("MIN_VALIDATOR_WITHDRAWABILITY_DELAY") + + pending_partial_withdrawal = %PendingPartialWithdrawal{ + validator_index: validator_index, + amount: to_withdraw, + withdrawable_epoch: withdrawable_epoch + } + + {:ok, + %BeaconState{ + state + | pending_partial_withdrawals: + state.pending_partial_withdrawals ++ [pending_partial_withdrawal] + }} + else + {:ok, state} + end end @spec process_consolidation_request(BeaconState.t(), ConsolidationRequest.t()) :: {:ok, BeaconState.t()} - def process_consolidation_request(state, _consolidation_request) do - # TODO: Not implemented yet - {:ok, state} + def process_consolidation_request(state, consolidation_request) do + request_type = + if valid_switch_to_compounding_request?(state, consolidation_request), + do: :compounding, + else: :consolidation + + do_process_consolidation_request(state, consolidation_request, request_type) + end + + defp do_process_consolidation_request(state, consolidation_request, :compounding) do + case find_validator(state, consolidation_request.source_pubkey) do + {_validator, validator_index} -> + {:ok, Mutators.switch_to_compounding_validator(state, validator_index)} + + nil -> + {:ok, state} + end + end + + defp do_process_consolidation_request(state, consolidation_request, :consolidation) do + with :ok <- verify_consolidation_request(state, consolidation_request), + {source_validator, source_index} <- + find_validator(state, consolidation_request.source_pubkey), + {_target_validator, target_index} <- + find_validator(state, consolidation_request.target_pubkey), + :ok <- + verify_consolidation_validators( + state, + source_index, + target_index, + consolidation_request + ) do + state = + Mutators.compute_consolidation_epoch_and_update_churn( + state, + source_validator.effective_balance + ) + + consolidation_epoch = state.earliest_consolidation_epoch + + withdrawable_epoch = + consolidation_epoch + ChainSpec.get("MIN_VALIDATOR_WITHDRAWABILITY_DELAY") + + updated_source_validator = %Validator{ + source_validator + | exit_epoch: consolidation_epoch, + withdrawable_epoch: withdrawable_epoch + } + + pending_consolidation = %PendingConsolidation{ + source_index: source_index, + target_index: target_index + } + + updated_state = %BeaconState{ + state + | validators: + Aja.Vector.replace_at(state.validators, source_index, updated_source_validator), + pending_consolidations: state.pending_consolidations ++ [pending_consolidation] + } + + {:ok, updated_state} + else + _error -> {:ok, state} + end + end + + defp verify_consolidation_request(state, consolidation_request) do + cond do + consolidation_request.source_pubkey == consolidation_request.target_pubkey -> + {:error, :source_target_same} + + # If the pending consolidations queue is full, consolidation requests are ignored + length(state.pending_consolidations) >= ChainSpec.get("PENDING_CONSOLIDATIONS_LIMIT") -> + {:error, :queue_full} + + # If there is too little available consolidation churn limit, consolidation requests are ignored + Accessors.get_consolidation_churn_limit(state) <= ChainSpec.get("MIN_ACTIVATION_BALANCE") -> + {:error, :churn_limit_not_met} + + true -> + :ok + end + end + + defp verify_consolidation_validators(state, source_index, target_index, consolidation_request) do + source_validator = Aja.Vector.at(state.validators, source_index) + target_validator = Aja.Vector.at(state.validators, target_index) + current_epoch = Accessors.get_current_epoch(state) + far_future_epoch = Constants.far_future_epoch() + + cond do + invalid_consolidation_request_credentials?( + source_validator, + target_validator, + consolidation_request + ) -> + {:error, :invalid_credentials} + + # Verify the source and the target are active + !Predicates.active_validator?(source_validator, current_epoch) || + !Predicates.active_validator?(target_validator, current_epoch) -> + {:error, :validator_not_active} + + # Verify exits for source and target have not been initiated + source_validator.exit_epoch != far_future_epoch || + target_validator.exit_epoch != far_future_epoch -> + {:error, :validator_exiting} + + # Verify the source has been active long enough + current_epoch < + source_validator.activation_epoch + ChainSpec.get("SHARD_COMMITTEE_PERIOD") -> + {:error, :validator_not_active_long_enough} + + # Verify the source has no pending withdrawals in the queue + Accessors.get_pending_balance_to_withdraw(state, source_index) > 0 -> + {:error, :validator_has_pending_balance} + + # Initiate source validator exit and append pending consolidation + true -> + :ok + end + end + + defp invalid_consolidation_request_credentials?( + source_validator, + target_validator, + consolidation_request + ) do + invalid_withdrawal_credentials?(source_validator, consolidation_request.source_address) || + not Validator.has_compounding_withdrawal_credential(target_validator) + end + + @spec valid_switch_to_compounding_request?(BeaconState.t(), ConsolidationRequest.t()) :: + boolean() + def valid_switch_to_compounding_request?(state, consolidation_request) do + current_epoch = Accessors.get_current_epoch(state) + far_future_epoch = Constants.far_future_epoch() + + # Verify pubkey exists + with {source_validator, _source_index} <- + find_validator(state, consolidation_request.source_pubkey), + # Switch to compounding requires source and target be equal + true <- consolidation_request.source_pubkey == consolidation_request.target_pubkey, + # Verify request has been authorized + true <- + not invalid_withdrawal_credentials?( + source_validator, + consolidation_request.source_address + ), + # Verify source withdrawal credentials + true <- Validator.has_eth1_withdrawal_credential(source_validator), + # Verify the source is active + true <- Predicates.active_validator?(source_validator, current_epoch), + # Verify exit for source has not been initiated + true <- source_validator.exit_epoch == far_future_epoch do + true + else + _ -> + false + end end @spec process_operations(BeaconState.t(), BeaconBlockBody.t()) ::