diff --git a/.github/mergify.yml b/.github/mergify.yml index 4c4046cf67..73267904b8 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,3 +1,37 @@ +pull_request_rules: + - name: Ask to resolve conflict + conditions: + - conflict + - -author=dependabot[bot] + - or: + - -draft # Don't report conflicts on regular draft. + - and: # Do report conflicts on draft that are scheduled for the next major release. + - draft + - milestone~=v[0-9]\.[0-9]{2} + actions: + comment: + message: This pull request has merge conflicts. Could you please resolve them + @{{author}}? 🙏 + + - name: Approve trivial maintainer PRs + conditions: + - base!=stable + - label=trivial + - author=@sigp/lighthouse + - -conflict + actions: + review: + type: APPROVE + + - name: Add ready-to-merge labeled PRs to merge queue + conditions: + # All branch protection rules are implicit: https://docs.mergify.com/conditions/#about-branch-protection + - base!=stable + - label=ready-for-merge + - label!=do-not-merge + actions: + queue: + queue_rules: - name: default batch_size: 8 @@ -6,14 +40,16 @@ queue_rules: merge_method: squash commit_message_template: | {{ title }} (#{{ number }}) - - {% for commit in commits %} - * {{ commit.commit_message }} - {% endfor %} + + {{ body | get_section("## Issue Addressed", "") }} + + + {{ body | get_section("## Proposed Changes", "") }} queue_conditions: - "#approved-reviews-by >= 1" - "check-success=license/cla" - "check-success=target-branch-check" + - "label!=do-not-merge" merge_conditions: - "check-success=test-suite-success" - "check-success=local-testnet-success" diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index 8808a3f121..ecaa4f45e7 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -135,7 +135,7 @@ impl BeaconChain { state .get_validator(proposer_slashing.proposer_index() as usize)? .effective_balance - .safe_div(self.spec.whistleblower_reward_quotient)?, + .safe_div(self.spec.whistleblower_reward_quotient_for_state(state))?, )?; } @@ -157,7 +157,7 @@ impl BeaconChain { state .get_validator(attester_index as usize)? .effective_balance - .safe_div(self.spec.whistleblower_reward_quotient)?, + .safe_div(self.spec.whistleblower_reward_quotient_for_state(state))?, )?; } } diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index 6226ed39cb..fa2d028f22 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -254,6 +254,35 @@ async fn test_rewards_base_inactivity_leak_justification_epoch() { ); } +#[tokio::test] +async fn test_rewards_electra_slashings() { + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let harness = get_electra_harness(spec); + let state = harness.get_current_state(); + + harness.extend_slots(E::slots_per_epoch() as usize).await; + + let mut initial_balances = harness.get_current_state().balances().to_vec(); + + // add an attester slashing and calculate slashing penalties + harness.add_attester_slashing(vec![0]).unwrap(); + let slashed_balance_1 = initial_balances.get_mut(0).unwrap(); + let validator_1_effective_balance = state.get_effective_balance(0).unwrap(); + let delta_1 = validator_1_effective_balance + / harness.spec.min_slashing_penalty_quotient_for_state(&state); + *slashed_balance_1 -= delta_1; + + // add a proposer slashing and calculating slashing penalties + harness.add_proposer_slashing(1).unwrap(); + let slashed_balance_2 = initial_balances.get_mut(1).unwrap(); + let validator_2_effective_balance = state.get_effective_balance(1).unwrap(); + let delta_2 = validator_2_effective_balance + / harness.spec.min_slashing_penalty_quotient_for_state(&state); + *slashed_balance_2 -= delta_2; + + check_all_electra_rewards(&harness, initial_balances).await; +} + #[tokio::test] async fn test_rewards_base_slashings() { let spec = ForkName::Base.make_genesis_spec(E::default_spec()); @@ -693,6 +722,75 @@ async fn test_rewards_base_subset_only() { check_all_base_rewards_for_subset(&harness, initial_balances, validators_subset).await; } +async fn check_all_electra_rewards( + harness: &BeaconChainHarness>, + mut balances: Vec, +) { + let mut proposal_rewards_map = HashMap::new(); + let mut sync_committee_rewards_map = HashMap::new(); + for _ in 0..E::slots_per_epoch() { + let state = harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + harness.make_block_return_pre_state(state, slot).await; + let beacon_block_reward = harness + .chain + .compute_beacon_block_reward(signed_block.message(), &mut state) + .unwrap(); + + let total_proposer_reward = proposal_rewards_map + .entry(beacon_block_reward.proposer_index) + .or_insert(0); + *total_proposer_reward += beacon_block_reward.total as i64; + + // calculate sync committee rewards / penalties + let reward_payload = harness + .chain + .compute_sync_committee_rewards(signed_block.message(), &mut state) + .unwrap(); + + for reward in reward_payload { + let total_sync_reward = sync_committee_rewards_map + .entry(reward.validator_index) + .or_insert(0); + *total_sync_reward += reward.reward; + } + + harness.extend_slots(1).await; + } + + // compute reward deltas for all validators in epoch 0 + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(0), vec![]) + .unwrap(); + + // assert ideal rewards are greater than 0 + assert_eq!( + ideal_rewards.len() as u64, + harness.spec.max_effective_balance_electra / harness.spec.effective_balance_increment + ); + + assert!(ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + + // apply attestation, proposal, and sync committee rewards and penalties to initial balances + apply_attestation_rewards(&mut balances, total_rewards); + apply_other_rewards(&mut balances, &proposal_rewards_map); + apply_other_rewards(&mut balances, &sync_committee_rewards_map); + + // verify expected balances against actual balances + let actual_balances: Vec = harness.get_current_state().balances().to_vec(); + + assert_eq!(balances, actual_balances); +} + async fn check_all_base_rewards( harness: &BeaconChainHarness>, balances: Vec, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 31fecfeb99..5c9504d4a5 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6363,6 +6363,34 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(true)); } + + async fn test_get_beacon_rewards_blocks_at_head(&self) -> StandardBlockReward { + self.client + .get_beacon_rewards_blocks(CoreBlockId::Head) + .await + .unwrap() + .data + } + + async fn test_beacon_block_rewards_electra(self) -> Self { + for _ in 0..E::slots_per_epoch() { + let state = self.harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + self.harness.make_block_return_pre_state(state, slot).await; + + let beacon_block_reward = self + .harness + .chain + .compute_beacon_block_reward(signed_block.message(), &mut state) + .unwrap(); + self.harness.extend_slots(1).await; + let api_beacon_block_reward = self.test_get_beacon_rewards_blocks_at_head().await; + assert_eq!(beacon_block_reward, api_beacon_block_reward); + } + self + } } async fn poll_events, eth2::Error>> + Unpin, E: EthSpec>( @@ -7502,3 +7530,17 @@ async fn expected_withdrawals_valid_capella() { .test_get_expected_withdrawals_capella() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_beacon_rewards_blocks_electra() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_beacon_block_rewards_electra() + .await; +} diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index ef4799c245..6339f9003d 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -517,7 +517,7 @@ pub fn get_expected_withdrawals( 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 mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); let fork_name = state.fork_name_unchecked(); // [New in Electra:EIP7251] @@ -532,19 +532,27 @@ pub fn get_expected_withdrawals( break; } - let withdrawal_balance = state.get_balance(withdrawal.validator_index as usize)?; let validator = state.get_validator(withdrawal.validator_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; + let total_withdrawn = withdrawals + .iter() + .filter_map(|w| { + (w.validator_index == withdrawal.validator_index).then_some(w.amount) + }) + .safe_sum()?; + let balance = state + .get_balance(withdrawal.validator_index as usize)? + .safe_sub(total_withdrawn)?; + let has_excess_balance = 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)?, + balance.safe_sub(spec.min_activation_balance)?, withdrawal.amount, ); withdrawals.push(Withdrawal {