diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index f1d9ce791e0..66e2964669e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4538,6 +4538,7 @@ impl BeaconChain { let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); if head_state.current_epoch() == proposal_epoch { return get_expected_withdrawals(&unadvanced_state, &self.spec) + .map(|(withdrawals, _)| withdrawals) .map_err(Error::PrepareProposerFailed); } @@ -4555,7 +4556,9 @@ impl BeaconChain { proposal_epoch.start_slot(T::EthSpec::slots_per_epoch()), &self.spec, )?; - get_expected_withdrawals(&advanced_state, &self.spec).map_err(Error::PrepareProposerFailed) + get_expected_withdrawals(&advanced_state, &self.spec) + .map(|(withdrawals, _)| withdrawals) + .map_err(Error::PrepareProposerFailed) } /// Determine whether a fork choice update to the execution layer should be overridden. diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index cbffe363422..a6e0d247dc2 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -413,7 +413,7 @@ pub fn get_execution_payload( state.latest_execution_payload_header()?.block_hash(); let withdrawals = match state { &BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { - Some(get_expected_withdrawals(state, spec)?.into()) + Some(get_expected_withdrawals(state, spec)?.0.into()) } &BeaconState::Bellatrix(_) => None, // These shouldn't happen but they're here to make the pattern irrefutable diff --git a/beacon_node/genesis/src/eth1_genesis_service.rs b/beacon_node/genesis/src/eth1_genesis_service.rs index 0ede74ba754..70157050278 100644 --- a/beacon_node/genesis/src/eth1_genesis_service.rs +++ b/beacon_node/genesis/src/eth1_genesis_service.rs @@ -432,8 +432,14 @@ impl Eth1GenesisService { // Such an optimization would only be useful in a scenario where `MIN_GENESIS_TIME` // is reached _prior_ to `MIN_ACTIVE_VALIDATOR_COUNT`. I suspect this won't be the // case for mainnet, so we defer this optimization. + let Deposit { proof, data } = deposit; + let proof = if PROOF_VERIFICATION { + Some(proof) + } else { + None + }; - apply_deposit(&mut state, &deposit, spec, PROOF_VERIFICATION) + apply_deposit(&mut state, data, proof, true, spec) .map_err(|e| format!("Error whilst processing deposit: {:?}", e)) })?; diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index a540113ab43..54f2c0efa8d 100644 --- a/beacon_node/http_api/src/builder_states.rs +++ b/beacon_node/http_api/src/builder_states.rs @@ -33,7 +33,7 @@ pub fn get_next_withdrawals( } match get_expected_withdrawals(&state, &chain.spec) { - Ok(withdrawals) => Ok(withdrawals), + Ok((withdrawals, _)) => Ok(withdrawals), Err(e) => Err(warp_utils::reject::custom_server_error(format!( "failed to get expected withdrawal: {:?}", e diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 529dc852e98..711820ccac6 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -610,6 +610,7 @@ pub async fn proposer_boost_re_org_test( assert_eq!(state_b.slot(), slot_b); let pre_advance_withdrawals = get_expected_withdrawals(&state_b, &harness.chain.spec) .unwrap() + .0 .to_vec(); complete_state_advance(&mut state_b, None, slot_c, &harness.chain.spec).unwrap(); @@ -696,6 +697,7 @@ pub async fn proposer_boost_re_org_test( get_expected_withdrawals(&state_b, &harness.chain.spec) } .unwrap() + .0 .to_vec(); let payload_attribs_withdrawals = payload_attribs.withdrawals().unwrap(); assert_eq!(expected_withdrawals, *payload_attribs_withdrawals); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 86f2096224b..4213fd4ab8d 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -5471,7 +5471,9 @@ impl ApiTester { &self.chain.spec, ); } - let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec).unwrap(); + let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec) + .unwrap() + .0; // fetch expected withdrawals from the client let result = self.client.get_expected_withdrawals(&state_id).await; diff --git a/consensus/state_processing/src/common/initiate_validator_exit.rs b/consensus/state_processing/src/common/initiate_validator_exit.rs index a40a9dfd398..8874e9ed4b8 100644 --- a/consensus/state_processing/src/common/initiate_validator_exit.rs +++ b/consensus/state_processing/src/common/initiate_validator_exit.rs @@ -19,16 +19,22 @@ pub fn initiate_validator_exit( state.build_exit_cache(spec)?; // Compute exit queue epoch - let delayed_epoch = state.compute_activation_exit_epoch(state.current_epoch(), spec)?; - let mut exit_queue_epoch = state - .exit_cache() - .max_epoch()? - .map_or(delayed_epoch, |epoch| max(epoch, delayed_epoch)); - let exit_queue_churn = state.exit_cache().get_churn_at(exit_queue_epoch)?; - - if exit_queue_churn >= state.get_validator_churn_limit(spec)? { - exit_queue_epoch.safe_add_assign(1)?; - } + let exit_queue_epoch = if state.fork_name_unchecked() >= ForkName::Electra { + let effective_balance = state.get_validator(index)?.effective_balance; + state.compute_exit_epoch_and_update_churn(effective_balance, spec)? + } else { + let delayed_epoch = state.compute_activation_exit_epoch(state.current_epoch(), spec)?; + let mut exit_queue_epoch = state + .exit_cache() + .max_epoch()? + .map_or(delayed_epoch, |epoch| max(epoch, delayed_epoch)); + let exit_queue_churn = state.exit_cache().get_churn_at(exit_queue_epoch)?; + + if exit_queue_churn >= state.get_validator_churn_limit(spec)? { + exit_queue_epoch.safe_add_assign(1)?; + } + exit_queue_epoch + }; let validator = state.get_validator_cow(index)?; diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index 520b58a8af3..80d857cc009 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -53,8 +53,8 @@ pub fn slash_validator( // Apply proposer and whistleblower rewards let proposer_index = ctxt.get_proposer_index(state, spec)? as usize; let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index); - let whistleblower_reward = - validator_effective_balance.safe_div(spec.whistleblower_reward_quotient)?; + let whistleblower_reward = validator_effective_balance + .safe_div(spec.whistleblower_reward_quotient_for_state(state))?; let proposer_reward = match state { BeaconState::Base(_) => whistleblower_reward.safe_div(spec.proposer_reward_quotient)?, BeaconState::Altair(_) diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index a84f359389c..c73417077ae 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -32,12 +32,13 @@ pub fn initialize_beacon_state_from_eth1( let mut deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); - for deposit in deposits.iter() { + for deposit in deposits.into_iter() { deposit_tree .push_leaf(deposit.data.tree_hash_root()) .map_err(BlockProcessingError::MerkleTreeError)?; state.eth1_data_mut().deposit_root = deposit_tree.root(); - apply_deposit(&mut state, deposit, spec, true)?; + let Deposit { proof, data } = deposit; + apply_deposit(&mut state, data, Some(proof), true, spec)?; } process_activations(&mut state, spec)?; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 98671f82b9f..e7655b453a8 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -20,7 +20,7 @@ pub use verify_attestation::{ }; pub use verify_bls_to_execution_change::verify_bls_to_execution_change; pub use verify_deposit::{ - get_existing_validator_index, verify_deposit_merkle_proof, verify_deposit_signature, + get_existing_validator_index, is_valid_deposit_signature, verify_deposit_merkle_proof, }; pub use verify_exit::verify_exit; @@ -503,13 +503,55 @@ pub fn compute_timestamp_at_slot( pub fn get_expected_withdrawals( state: &BeaconState, spec: &ChainSpec, -) -> Result, BlockProcessingError> { +) -> Result<(Withdrawals, Option), BlockProcessingError> { let epoch = state.current_epoch(); let mut withdrawal_index = state.next_withdrawal_index()?; let mut validator_index = state.next_withdrawal_validator_index()?; let mut withdrawals = vec![]; let fork_name = state.fork_name_unchecked(); + // [New in Electra:EIP7251] + // Consume pending partial withdrawals + let partial_withdrawals_count = + if let Ok(partial_withdrawals) = state.pending_partial_withdrawals() { + for withdrawal in partial_withdrawals { + if withdrawal.withdrawable_epoch > epoch + || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize + { + break; + } + + let withdrawal_balance = state.get_balance(withdrawal.index as usize)?; + let validator = state.get_validator(withdrawal.index as usize)?; + + let has_sufficient_effective_balance = + validator.effective_balance >= spec.min_activation_balance; + let has_excess_balance = withdrawal_balance > spec.min_activation_balance; + + if validator.exit_epoch == spec.far_future_epoch + && has_sufficient_effective_balance + && has_excess_balance + { + let withdrawable_balance = std::cmp::min( + withdrawal_balance.safe_sub(spec.min_activation_balance)?, + withdrawal.amount, + ); + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index: withdrawal.index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BeaconStateError::NonExecutionAddresWithdrawalCredential)?, + amount: withdrawable_balance, + }); + withdrawal_index.safe_add_assign(1)?; + } + } + Some(withdrawals.len()) + } else { + None + }; + let bound = std::cmp::min( state.validators().len() as u64, spec.max_validators_per_withdrawals_sweep, @@ -524,7 +566,7 @@ pub fn get_expected_withdrawals( index: withdrawal_index, validator_index, address: validator - .get_eth1_withdrawal_address(spec) + .get_execution_withdrawal_address(spec) .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, amount: balance, }); @@ -534,9 +576,12 @@ pub fn get_expected_withdrawals( index: withdrawal_index, validator_index, address: validator - .get_eth1_withdrawal_address(spec) + .get_execution_withdrawal_address(spec) .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, - amount: balance.safe_sub(spec.max_effective_balance)?, + amount: balance.safe_sub( + validator + .get_validator_max_effective_balance(spec, state.fork_name_unchecked()), + )?, }); withdrawal_index.safe_add_assign(1)?; } @@ -548,7 +593,7 @@ pub fn get_expected_withdrawals( .safe_rem(state.validators().len() as u64)?; } - Ok(withdrawals.into()) + Ok((withdrawals.into(), partial_withdrawals_count)) } /// Apply withdrawals to the state. @@ -558,9 +603,9 @@ pub fn process_withdrawals>( spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { match state { - BeaconState::Bellatrix(_) => Ok(()), BeaconState::Capella(_) | BeaconState::Deneb(_) | BeaconState::Electra(_) => { - let expected_withdrawals = get_expected_withdrawals(state, spec)?; + let (expected_withdrawals, partial_withdrawals_count) = + get_expected_withdrawals(state, spec)?; let expected_root = expected_withdrawals.tree_hash_root(); let withdrawals_root = payload.withdrawals_root()?; @@ -579,6 +624,17 @@ pub fn process_withdrawals>( )?; } + // Update pending partial withdrawals [New in Electra:EIP7251] + if let Some(partial_withdrawals_count) = partial_withdrawals_count { + // TODO(electra): Use efficient pop_front after milhouse release https://github.com/sigp/milhouse/pull/38 + let new_partial_withdrawals = state + .pending_partial_withdrawals()? + .iter_from(partial_withdrawals_count)? + .cloned() + .collect::>(); + *state.pending_partial_withdrawals_mut()? = List::new(new_partial_withdrawals)?; + } + // Update the next withdrawal index if this block contained withdrawals if let Some(latest_withdrawal) = expected_withdrawals.last() { *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; @@ -606,6 +662,6 @@ pub fn process_withdrawals>( Ok(()) } // these shouldn't even be encountered but they're here for completeness - BeaconState::Base(_) | BeaconState::Altair(_) => Ok(()), + BeaconState::Base(_) | BeaconState::Altair(_) | BeaconState::Bellatrix(_) => Ok(()), } } diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index 74477f5e481..a0c044219d5 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -171,6 +171,7 @@ where self.include_exits(block)?; self.include_sync_aggregate(block)?; self.include_bls_to_execution_changes(block)?; + self.include_consolidations(block)?; Ok(()) } @@ -359,6 +360,27 @@ where Ok(()) } + /// Includes all signatures in `self.block.body.consolidations` for verification. + pub fn include_consolidations>( + &mut self, + block: &'a SignedBeaconBlock, + ) -> Result<()> { + if let Ok(consolidations) = block.message().body().consolidations() { + self.sets.sets.reserve(consolidations.len()); + for consolidation in consolidations { + let set = consolidation_signature_set( + self.state, + self.get_pubkey.clone(), + consolidation, + self.spec, + )?; + + self.sets.push(set); + } + } + Ok(()) + } + /// Verify all the signatures that have been included in `self`, returning `true` if and only if /// all the signatures are valid. /// diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 71a284dea49..cebb10b6071 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -89,6 +89,46 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + TooManyPendingConsolidations { + consolidations: usize, + limit: usize, + }, + ConsolidationChurnLimitTooLow { + churn_limit: u64, + minimum: u64, + }, + MatchingSourceTargetConsolidation { + index: u64, + }, + InactiveConsolidationSource { + index: u64, + current_epoch: Epoch, + }, + InactiveConsolidationTarget { + index: u64, + current_epoch: Epoch, + }, + SourceValidatorExiting { + index: u64, + }, + TargetValidatorExiting { + index: u64, + }, + FutureConsolidationEpoch { + current_epoch: Epoch, + consolidation_epoch: Epoch, + }, + NoSourceExecutionWithdrawalCredential { + index: u64, + }, + NoTargetExecutionWithdrawalCredential { + index: u64, + }, + MismatchedWithdrawalCredentials { + source_address: Address, + target_address: Address, + }, + InavlidConsolidationSignature, PendingAttestationInElectra, } @@ -412,7 +452,10 @@ pub enum ExitInvalid { /// The specified validator has already initiated exit. AlreadyInitiatedExit(u64), /// The exit is for a future epoch. - FutureEpoch { state: Epoch, exit: Epoch }, + FutureEpoch { + state: Epoch, + exit: Epoch, + }, /// The validator has not been active for long enough. TooYoungToExit { current_epoch: Epoch, @@ -423,6 +466,7 @@ pub enum ExitInvalid { /// There was an error whilst attempting to get a set of signatures. The signatures may have /// been invalid or an internal error occurred. SignatureSetError(SignatureSetError), + PendingWithdrawalInQueue(u64), } #[derive(Debug, PartialEq, Clone)] diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index bd354901a8b..ff126beabef 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -4,8 +4,11 @@ use crate::common::{ slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::signature_sets::consolidation_signature_set; use crate::VerifySignatures; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; +use types::typenum::U33; +use types::validator::is_compounding_withdrawal_credential; pub fn process_operations>( state: &mut BeaconState, @@ -36,6 +39,18 @@ pub fn process_operations>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } + if state.fork_name_unchecked().electra_enabled() { + let requests = block_body.execution_payload()?.withdrawal_requests()?; + if let Some(requests) = requests { + process_execution_layer_withdrawal_requests(state, &requests, spec)?; + } + let receipts = block_body.execution_payload()?.deposit_receipts()?; + if let Some(receipts) = receipts { + process_deposit_receipts(state, &receipts, spec)?; + } + process_consolidations(state, block_body.consolidations()?, verify_signatures, spec)?; + } + Ok(()) } @@ -354,17 +369,34 @@ pub fn process_deposits( deposits: &[Deposit], spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - let expected_deposit_len = std::cmp::min( - E::MaxDeposits::to_u64(), - state.get_outstanding_deposit_len()?, - ); - block_verify!( - deposits.len() as u64 == expected_deposit_len, - BlockProcessingError::DepositCountInvalid { - expected: expected_deposit_len as usize, - found: deposits.len(), - } - ); + // [Modified in Electra:EIP6110] + // Disable former deposit mechanism once all prior deposits are processed + // + // If `deposit_receipts_start_index` does not exist as a field on `state`, electra is disabled + // which means we always want to use the old check, so this field defaults to `u64::MAX`. + let eth1_deposit_index_limit = state.deposit_receipts_start_index().unwrap_or(u64::MAX); + + if state.eth1_deposit_index() < eth1_deposit_index_limit { + let expected_deposit_len = std::cmp::min( + E::MaxDeposits::to_u64(), + state.get_outstanding_deposit_len()?, + ); + block_verify!( + deposits.len() as u64 == expected_deposit_len, + BlockProcessingError::DepositCountInvalid { + expected: expected_deposit_len as usize, + found: deposits.len(), + } + ); + } else { + block_verify!( + deposits.len() as u64 == 0, + BlockProcessingError::DepositCountInvalid { + expected: 0, + found: deposits.len(), + } + ); + } // Verify merkle proofs in parallel. deposits @@ -382,60 +414,96 @@ pub fn process_deposits( // Update the state in series. for deposit in deposits { - apply_deposit(state, deposit, spec, false)?; + apply_deposit(state, deposit.data.clone(), None, true, spec)?; } Ok(()) } -/// Process a single deposit, optionally verifying its merkle proof. +/// Process a single deposit, verifying its merkle proof if provided. pub fn apply_deposit( state: &mut BeaconState, - deposit: &Deposit, + deposit_data: DepositData, + proof: Option>, + increment_eth1_deposit_index: bool, spec: &ChainSpec, - verify_merkle_proof: bool, ) -> Result<(), BlockProcessingError> { let deposit_index = state.eth1_deposit_index() as usize; - if verify_merkle_proof { - verify_deposit_merkle_proof(state, deposit, state.eth1_deposit_index(), spec) + if let Some(proof) = proof { + let deposit = Deposit { + proof, + data: deposit_data.clone(), + }; + verify_deposit_merkle_proof(state, &deposit, state.eth1_deposit_index(), spec) .map_err(|e| e.into_with_index(deposit_index))?; } - state.eth1_deposit_index_mut().safe_add_assign(1)?; + if increment_eth1_deposit_index { + state.eth1_deposit_index_mut().safe_add_assign(1)?; + } // Get an `Option` where `u64` is the validator index if this deposit public key // already exists in the beacon_state. - let validator_index = get_existing_validator_index(state, &deposit.data.pubkey) + let validator_index = get_existing_validator_index(state, &deposit_data.pubkey) .map_err(|e| e.into_with_index(deposit_index))?; - let amount = deposit.data.amount; + let amount = deposit_data.amount; if let Some(index) = validator_index { - // Update the existing validator balance. - increase_balance(state, index as usize, amount)?; + // [Modified in Electra:EIP7251] + if let Ok(pending_balance_deposits) = state.pending_balance_deposits_mut() { + pending_balance_deposits.push(PendingBalanceDeposit { index, amount })?; + + let validator = state + .validators() + .get(index as usize) + .ok_or(BeaconStateError::UnknownValidator(index as usize))?; + + if is_compounding_withdrawal_credential(deposit_data.withdrawal_credentials, spec) + && validator.has_eth1_withdrawal_credential(spec) + && is_valid_deposit_signature(&deposit_data, spec).is_ok() + { + state.switch_to_compounding_validator(index as usize, spec)?; + } + } else { + // Update the existing validator balance. + increase_balance(state, index as usize, amount)?; + } } else { // The signature should be checked for new validators. Return early for a bad // signature. - if verify_deposit_signature(&deposit.data, spec).is_err() { + if is_valid_deposit_signature(&deposit_data, spec).is_err() { return Ok(()); } + let new_validator_index = state.validators().len(); + + // [Modified in Electra:EIP7251] + let (effective_balance, state_balance) = if state.fork_name_unchecked() >= ForkName::Electra + { + (0, 0) + } else { + ( + std::cmp::min( + amount.safe_sub(amount.safe_rem(spec.effective_balance_increment)?)?, + spec.max_effective_balance, + ), + amount, + ) + }; // Create a new validator. let validator = Validator { - pubkey: deposit.data.pubkey, - withdrawal_credentials: deposit.data.withdrawal_credentials, + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, activation_eligibility_epoch: spec.far_future_epoch, activation_epoch: spec.far_future_epoch, exit_epoch: spec.far_future_epoch, withdrawable_epoch: spec.far_future_epoch, - effective_balance: std::cmp::min( - amount.safe_sub(amount.safe_rem(spec.effective_balance_increment)?)?, - spec.max_effective_balance, - ), + effective_balance, slashed: false, }; state.validators_mut().push(validator)?; - state.balances_mut().push(deposit.data.amount)?; + state.balances_mut().push(state_balance)?; // Altair or later initializations. if let Ok(previous_epoch_participation) = state.previous_epoch_participation_mut() { @@ -447,6 +515,280 @@ pub fn apply_deposit( if let Ok(inactivity_scores) = state.inactivity_scores_mut() { inactivity_scores.push(0)?; } + + // [New in Electra:EIP7251] + if let Ok(pending_balance_deposits) = state.pending_balance_deposits_mut() { + pending_balance_deposits.push(PendingBalanceDeposit { + index: new_validator_index as u64, + amount, + })?; + } + } + + Ok(()) +} + +pub fn process_execution_layer_withdrawal_requests( + state: &mut BeaconState, + requests: &[ExecutionLayerWithdrawalRequest], + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + for request in requests { + let amount = request.amount; + let is_full_exit_request = amount == spec.full_exit_request_amount; + + // If partial withdrawal queue is full, only full exits are processed + if state.pending_partial_withdrawals()?.len() == E::pending_partial_withdrawals_limit() + && !is_full_exit_request + { + continue; + } + + // Verify pubkey exists + let index_opt = state.get_validator_index(&request.validator_pubkey)?; + let Some(index) = index_opt else { + continue; + }; + + let validator = state.get_validator(index)?; + + // Verify withdrawal credentials + let has_correct_credential = validator.has_execution_withdrawal_credential(spec); + let is_correct_source_address = validator + .get_execution_withdrawal_address(spec) + .map(|addr| addr == request.source_address) + .unwrap_or(false); + + if !(has_correct_credential && is_correct_source_address) { + continue; + } + + // Verify the validator is active + if !validator.is_active_at(state.current_epoch()) { + continue; + } + + // Verify exit has not been initiated + if validator.exit_epoch != spec.far_future_epoch { + continue; + } + + // Verify the validator has been active long enough + if state.current_epoch() + < validator + .activation_epoch + .safe_add(spec.shard_committee_period)? + { + continue; + } + + let pending_balance_to_withdraw = state.get_pending_balance_to_withdraw(index)?; + if is_full_exit_request { + // Only exit validator if it has no pending withdrawals in the queue + if pending_balance_to_withdraw == 0 { + initiate_validator_exit(state, index, spec)? + } + continue; + } + + let balance = state.get_balance(index)?; + let has_sufficient_effective_balance = + validator.effective_balance >= spec.min_activation_balance; + let has_excess_balance = balance + > spec + .min_activation_balance + .safe_add(pending_balance_to_withdraw)?; + + // Only allow partial withdrawals with compounding withdrawal credentials + if validator.has_compounding_withdrawal_credential(spec) + && has_sufficient_effective_balance + && has_excess_balance + { + let to_withdraw = std::cmp::min( + balance + .safe_sub(spec.min_activation_balance)? + .safe_sub(pending_balance_to_withdraw)?, + amount, + ); + let exit_queue_epoch = state.compute_exit_epoch_and_update_churn(to_withdraw, spec)?; + let withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + state + .pending_partial_withdrawals_mut()? + .push(PendingPartialWithdrawal { + index: index as u64, + amount: to_withdraw, + withdrawable_epoch, + })?; + } + } + Ok(()) +} + +pub fn process_deposit_receipts( + state: &mut BeaconState, + receipts: &[DepositReceipt], + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + for receipt in receipts { + // Set deposit receipt start index + if state.deposit_receipts_start_index()? == spec.unset_deposit_receipts_start_index { + *state.deposit_receipts_start_index_mut()? = receipt.index + } + let deposit_data = DepositData { + pubkey: receipt.pubkey, + withdrawal_credentials: receipt.withdrawal_credentials, + amount: receipt.amount, + signature: receipt.signature.clone().into(), + }; + apply_deposit(state, deposit_data, None, false, spec)? + } + + Ok(()) +} + +pub fn process_consolidations( + state: &mut BeaconState, + consolidations: &[SignedConsolidation], + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + if consolidations.is_empty() { + return Ok(()); + } + + // If the pending consolidations queue is full, no consolidations are allowed in the block + let pending_consolidations = state.pending_consolidations()?.len(); + let pending_consolidations_limit = E::pending_consolidations_limit(); + block_verify! { + pending_consolidations < pending_consolidations_limit, + BlockProcessingError::TooManyPendingConsolidations { + consolidations: pending_consolidations, + limit: pending_consolidations_limit + } + } + + // If there is too little available consolidation churn limit, no consolidations are allowed in the block + let churn_limit = state.get_consolidation_churn_limit(spec)?; + block_verify! { + churn_limit > spec.min_activation_balance, + BlockProcessingError::ConsolidationChurnLimitTooLow { + churn_limit, + minimum: spec.min_activation_balance + } + } + + for signed_consolidation in consolidations { + let consolidation = signed_consolidation.message.clone(); + + // Verify that source != target, so a consolidation cannot be used as an exit. + block_verify! { + consolidation.source_index != consolidation.target_index, + BlockProcessingError::MatchingSourceTargetConsolidation { + index: consolidation.source_index + } + } + + let source_validator = state.get_validator(consolidation.source_index as usize)?; + let target_validator = state.get_validator(consolidation.target_index as usize)?; + + // Verify the source and the target are active + let current_epoch = state.current_epoch(); + block_verify! { + source_validator.is_active_at(current_epoch), + BlockProcessingError::InactiveConsolidationSource{ + index: consolidation.source_index, + current_epoch + } + } + block_verify! { + target_validator.is_active_at(current_epoch), + BlockProcessingError::InactiveConsolidationTarget{ + index: consolidation.target_index, + current_epoch + } + } + + // Verify exits for source and target have not been initiated + block_verify! { + source_validator.exit_epoch == spec.far_future_epoch, + BlockProcessingError::SourceValidatorExiting{ + index: consolidation.source_index, + } + } + block_verify! { + target_validator.exit_epoch == spec.far_future_epoch, + BlockProcessingError::TargetValidatorExiting{ + index: consolidation.target_index, + } + } + + // Consolidations must specify an epoch when they become valid; they are not valid before then + block_verify! { + current_epoch >= consolidation.epoch, + BlockProcessingError::FutureConsolidationEpoch { + current_epoch, + consolidation_epoch: consolidation.epoch + } + } + + // Verify the source and the target have Execution layer withdrawal credentials + block_verify! { + source_validator.has_execution_withdrawal_credential(spec), + BlockProcessingError::NoSourceExecutionWithdrawalCredential { + index: consolidation.source_index, + } + } + block_verify! { + target_validator.has_execution_withdrawal_credential(spec), + BlockProcessingError::NoTargetExecutionWithdrawalCredential { + index: consolidation.target_index, + } + } + + // Verify the same withdrawal address + let source_address = source_validator + .get_execution_withdrawal_address(spec) + .ok_or(BeaconStateError::NonExecutionAddresWithdrawalCredential)?; + let target_address = target_validator + .get_execution_withdrawal_address(spec) + .ok_or(BeaconStateError::NonExecutionAddresWithdrawalCredential)?; + block_verify! { + source_address == target_address, + BlockProcessingError::MismatchedWithdrawalCredentials { + source_address, + target_address + } + } + + if verify_signatures.is_true() { + let signature_set = consolidation_signature_set( + state, + |i| get_pubkey_from_state(state, i), + signed_consolidation, + spec, + )?; + block_verify! { + signature_set.verify(), + BlockProcessingError::InavlidConsolidationSignature + } + } + let exit_epoch = state.compute_consolidation_epoch_and_update_churn( + source_validator.effective_balance, + spec, + )?; + let source_validator = state.get_validator_mut(consolidation.source_index as usize)?; + // Initiate source validator exit and append pending consolidation + source_validator.exit_epoch = exit_epoch; + source_validator.withdrawable_epoch = source_validator + .exit_epoch + .safe_add(spec.min_validator_withdrawability_delay)?; + state + .pending_consolidations_mut()? + .push(PendingConsolidation { + source_index: consolidation.source_index, + target_index: consolidation.target_index, + })?; } Ok(()) diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 2e00ee03418..3c683766adb 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -11,8 +11,8 @@ use types::{ BeaconStateError, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, IndexedAttestation, IndexedAttestationRef, ProposerSlashing, PublicKey, PublicKeyBytes, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, - SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, Unsigned, + SignedBlsToExecutionChange, SignedConsolidation, SignedContributionAndProof, SignedRoot, + SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, Unsigned, }; pub type Result = std::result::Result; @@ -664,3 +664,37 @@ where message, ))) } + +/// Returns two signature sets, one for the source and one for the target validator +/// in the `SignedConsolidation`. +pub fn consolidation_signature_set<'a, E, F>( + state: &'a BeaconState, + get_pubkey: F, + consolidation: &'a SignedConsolidation, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + let source_index = consolidation.message.source_index as usize; + let target_index = consolidation.message.target_index as usize; + + let domain = spec.compute_domain( + Domain::Consolidation, + spec.genesis_fork_version, + state.genesis_validators_root(), + ); + + let message = consolidation.message.signing_root(domain); + let source_pubkey = + get_pubkey(source_index).ok_or(Error::ValidatorUnknown(source_index as u64))?; + let target_pubkey = + get_pubkey(target_index).ok_or(Error::ValidatorUnknown(target_index as u64))?; + + Ok(SignatureSet::multiple_pubkeys( + &consolidation.signature, + vec![source_pubkey, target_pubkey], + message, + )) +} diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 6bfb5d7cfe7..6bfb51d475b 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -68,10 +68,20 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( ) -> Result> { let data = attestation.data(); - verify!( - data.index < state.get_committee_count_at_slot(data.slot)?, - Invalid::BadCommitteeIndex - ); + // TODO(electra) choosing a validation based on the attestation's fork + // rather than the state's fork makes this simple, but technically the spec + // defines this verification based on the state's fork. + match attestation { + AttestationRef::Base(_) => { + verify!( + data.index < state.get_committee_count_at_slot(data.slot)?, + Invalid::BadCommitteeIndex + ); + } + AttestationRef::Electra(_) => { + verify!(data.index == 0, Invalid::BadCommitteeIndex); + } + } // Verify the Casper FFG vote. verify_casper_ffg_vote(attestation, state)?; diff --git a/consensus/state_processing/src/per_block_processing/verify_deposit.rs b/consensus/state_processing/src/per_block_processing/verify_deposit.rs index a964f3b5740..c996e580a78 100644 --- a/consensus/state_processing/src/per_block_processing/verify_deposit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_deposit.rs @@ -14,7 +14,7 @@ fn error(reason: DepositInvalid) -> BlockOperationError { /// Verify `Deposit.pubkey` signed `Deposit.signature`. /// /// Spec v0.12.1 -pub fn verify_deposit_signature(deposit_data: &DepositData, spec: &ChainSpec) -> Result<()> { +pub fn is_valid_deposit_signature(deposit_data: &DepositData, spec: &ChainSpec) -> Result<()> { let (public_key, signature, msg) = deposit_pubkey_signature_message(deposit_data, spec) .ok_or_else(|| error(DepositInvalid::BadBlsBytes))?; diff --git a/consensus/state_processing/src/per_block_processing/verify_exit.rs b/consensus/state_processing/src/per_block_processing/verify_exit.rs index fc258d38298..dea17dbc0c4 100644 --- a/consensus/state_processing/src/per_block_processing/verify_exit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_exit.rs @@ -79,5 +79,16 @@ pub fn verify_exit( ); } + // [New in Electra:EIP7251] + // Only exit validator if it has no pending withdrawals in the queue + if let Ok(pending_balance_to_withdraw) = + state.get_pending_balance_to_withdraw(exit.validator_index as usize) + { + verify!( + pending_balance_to_withdraw == 0, + ExitInvalid::PendingWithdrawalInQueue(exit.validator_index) + ); + } + Ok(()) } diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index bc20f3aa7bb..0426d43cacb 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -159,6 +159,8 @@ pub enum Error { IndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), + PartialWithdrawalCountInvalid(usize), + NonExecutionAddresWithdrawalCredential, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), InvalidSelectionProof { @@ -1475,6 +1477,14 @@ impl BeaconState { } } + /// Get the balance of a single validator. + pub fn get_balance(&self, validator_index: usize) -> Result { + self.balances() + .get(validator_index) + .ok_or(Error::BalancesOutOfBounds(validator_index)) + .copied() + } + /// Get a mutable reference to the balance of a single validator. pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { self.balances_mut() @@ -2105,11 +2115,12 @@ impl BeaconState { &self, validator_index: usize, spec: &ChainSpec, + current_fork: ForkName, ) -> Result { let max_effective_balance = self .validators() .get(validator_index) - .map(|validator| validator.get_validator_max_effective_balance(spec)) + .map(|validator| validator.get_validator_max_effective_balance(spec, current_fork)) .ok_or(Error::UnknownValidator(validator_index))?; Ok(std::cmp::min( *self diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 7609e360355..d2f59093966 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -387,6 +387,19 @@ impl ChainSpec { } } + /// For a given `BeaconState`, return the whistleblower reward quotient associated with its variant. + pub fn whistleblower_reward_quotient_for_state( + &self, + state: &BeaconState, + ) -> u64 { + let fork_name = state.fork_name_unchecked(); + if fork_name >= ForkName::Electra { + self.whistleblower_reward_quotient_electra + } else { + self.whistleblower_reward_quotient + } + } + /// Returns a full `Fork` struct for a given epoch. pub fn fork_at_epoch(&self, epoch: Epoch) -> Fork { let current_fork_name = self.fork_name_at_epoch(epoch); diff --git a/consensus/types/src/consolidation.rs b/consensus/types/src/consolidation.rs index 09a2d4bb0c3..6cc4aa90f27 100644 --- a/consensus/types/src/consolidation.rs +++ b/consensus/types/src/consolidation.rs @@ -1,5 +1,5 @@ -use crate::test_utils::TestRandom; use crate::Epoch; +use crate::{test_utils::TestRandom, SignedRoot}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -27,6 +27,8 @@ pub struct Consolidation { pub epoch: Epoch, } +impl SignedRoot for Consolidation {} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index 80a70c171f5..644d401ec7e 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -39,6 +39,15 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + /// fork-specific fields fn withdrawals_root(&self) -> Result; fn blob_gas_used(&self) -> Result; + fn withdrawal_requests( + &self, + ) -> Result< + Option>, + Error, + >; + fn deposit_receipts( + &self, + ) -> Result>, Error>; /// Is this a default payload with 0x0 roots for transactions and withdrawals? fn is_default_with_zero_roots(&self) -> bool; @@ -278,6 +287,35 @@ impl ExecPayload for FullPayload { } } + fn withdrawal_requests( + &self, + ) -> Result< + Option>, + Error, + > { + match self { + FullPayload::Bellatrix(_) | FullPayload::Capella(_) | FullPayload::Deneb(_) => { + Err(Error::IncorrectStateVariant) + } + FullPayload::Electra(inner) => { + Ok(Some(inner.execution_payload.withdrawal_requests.clone())) + } + } + } + + fn deposit_receipts( + &self, + ) -> Result>, Error> { + match self { + FullPayload::Bellatrix(_) | FullPayload::Capella(_) | FullPayload::Deneb(_) => { + Err(Error::IncorrectStateVariant) + } + FullPayload::Electra(inner) => { + Ok(Some(inner.execution_payload.deposit_receipts.clone())) + } + } + } + fn is_default_with_zero_roots<'a>(&'a self) -> bool { map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { cons(payload); @@ -410,6 +448,35 @@ impl<'b, E: EthSpec> ExecPayload for FullPayloadRef<'b, E> { } } + fn withdrawal_requests( + &self, + ) -> Result< + Option>, + Error, + > { + match self { + FullPayloadRef::Bellatrix(_) + | FullPayloadRef::Capella(_) + | FullPayloadRef::Deneb(_) => Err(Error::IncorrectStateVariant), + FullPayloadRef::Electra(inner) => { + Ok(Some(inner.execution_payload.withdrawal_requests.clone())) + } + } + } + + fn deposit_receipts( + &self, + ) -> Result>, Error> { + match self { + FullPayloadRef::Bellatrix(_) + | FullPayloadRef::Capella(_) + | FullPayloadRef::Deneb(_) => Err(Error::IncorrectStateVariant), + FullPayloadRef::Electra(inner) => { + Ok(Some(inner.execution_payload.deposit_receipts.clone())) + } + } + } + fn is_default_with_zero_roots<'a>(&'a self) -> bool { map_full_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); @@ -590,6 +657,21 @@ impl ExecPayload for BlindedPayload { } } + fn withdrawal_requests( + &self, + ) -> Result< + Option>, + Error, + > { + Ok(None) + } + + fn deposit_receipts( + &self, + ) -> Result>, Error> { + Ok(None) + } + fn is_default_with_zero_roots(&self) -> bool { self.to_ref().is_default_with_zero_roots() } @@ -691,6 +773,21 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { } } + fn withdrawal_requests( + &self, + ) -> Result< + Option>, + Error, + > { + Ok(None) + } + + fn deposit_receipts( + &self, + ) -> Result>, Error> { + Ok(None) + } + fn is_default_with_zero_roots<'a>(&'a self) -> bool { map_blinded_payload_ref!(&'b _, self, move |payload, cons| { cons(payload); @@ -717,7 +814,9 @@ macro_rules! impl_exec_payload_common { $is_default_with_empty_roots:block, $f:block, $g:block, - $h:block) => { + $h:block, + $i:block, + $j:block) => { impl ExecPayload for $wrapper_type { fn block_type() -> BlockType { BlockType::$block_type_variant @@ -780,6 +879,23 @@ macro_rules! impl_exec_payload_common { let h = $h; h(self) } + + fn withdrawal_requests( + &self, + ) -> Result< + Option>, + Error, + > { + let i = $i; + i(self) + } + + fn deposit_receipts( + &self, + ) -> Result>, Error> { + let j = $j; + j(self) + } } impl From<$wrapped_type> for $wrapper_type { @@ -825,7 +941,9 @@ macro_rules! impl_exec_payload_for_fork { wrapper_ref_type.blob_gas_used() }; c - } + }, + { |_| { Ok(None) } }, + { |_| { Ok(None) } } ); impl TryInto<$wrapper_type_header> for BlindedPayload { @@ -912,6 +1030,35 @@ macro_rules! impl_exec_payload_for_fork { wrapper_ref_type.blob_gas_used() }; c + }, + { + let c: for<'a> fn( + &'a $wrapper_type_full, + ) -> Result< + Option< + VariableList< + ExecutionLayerWithdrawalRequest, + E::MaxWithdrawalRequestsPerPayload, + >, + >, + Error, + > = |payload: &$wrapper_type_full| { + let wrapper_ref_type = FullPayloadRef::$fork_variant(&payload); + wrapper_ref_type.withdrawal_requests() + }; + c + }, + { + let c: for<'a> fn( + &'a $wrapper_type_full, + ) -> Result< + Option>, + Error, + > = |payload: &$wrapper_type_full| { + let wrapper_ref_type = FullPayloadRef::$fork_variant(&payload); + wrapper_ref_type.deposit_receipts() + }; + c } ); diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index f2b36ee1533..0054e95f9d1 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -130,9 +130,9 @@ impl Validator { is_compounding_withdrawal_credential(self.withdrawal_credentials, spec) } - /// Get the eth1 withdrawal address if this validator has one initialized. - pub fn get_eth1_withdrawal_address(&self, spec: &ChainSpec) -> Option
{ - self.has_eth1_withdrawal_credential(spec) + /// Get the execution withdrawal address if this validator has one initialized. + pub fn get_execution_withdrawal_address(&self, spec: &ChainSpec) -> Option
{ + self.has_execution_withdrawal_credential(spec) .then(|| { self.withdrawal_credentials .as_bytes() @@ -203,7 +203,7 @@ impl Validator { current_fork: ForkName, ) -> bool { if current_fork.electra_enabled() { - self.is_partially_withdrawable_validator_electra(balance, spec) + self.is_partially_withdrawable_validator_electra(balance, spec, current_fork) } else { self.is_partially_withdrawable_validator_capella(balance, spec) } @@ -223,8 +223,9 @@ impl Validator { &self, balance: u64, spec: &ChainSpec, + current_fork: ForkName, ) -> bool { - let max_effective_balance = self.get_validator_max_effective_balance(spec); + let max_effective_balance = self.get_validator_max_effective_balance(spec, current_fork); let has_max_effective_balance = self.effective_balance == max_effective_balance; let has_excess_balance = balance > max_effective_balance; self.has_execution_withdrawal_credential(spec) @@ -239,11 +240,19 @@ impl Validator { } /// Returns the max effective balance for a validator in gwei. - pub fn get_validator_max_effective_balance(&self, spec: &ChainSpec) -> u64 { - if self.has_compounding_withdrawal_credential(spec) { - spec.max_effective_balance_electra + pub fn get_validator_max_effective_balance( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> u64 { + if current_fork >= ForkName::Electra { + if self.has_compounding_withdrawal_credential(spec) { + spec.max_effective_balance_electra + } else { + spec.min_activation_balance + } } else { - spec.min_activation_balance + spec.max_effective_balance } } }