Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement EIP-7251 #5507

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4489,7 +4489,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&self,
forkchoice_update_params: &ForkchoiceUpdateParameters,
proposal_slot: Slot,
) -> Result<Withdrawals<T::EthSpec>, Error> {
) -> Result<(Withdrawals<T::EthSpec>, usize), Error> {
let cached_head = self.canonical_head.cached_head();
let head_state = &cached_head.snapshot.beacon_state;

Expand Down Expand Up @@ -5647,7 +5647,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
"prepare_beacon_proposer_withdrawals",
)
.await?
.map(Some)?
.map(|(withdrawals, _)| Some(withdrawals))?
}
};

Expand Down
8 changes: 5 additions & 3 deletions beacon_node/beacon_chain/src/execution_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,11 @@ pub fn get_execution_payload<T: BeaconChainTypes>(
let latest_execution_payload_header_block_hash =
state.latest_execution_payload_header()?.block_hash();
let withdrawals = match state {
&BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => {
Some(get_expected_withdrawals(state, spec)?.into())
}
&BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => Some(
get_expected_withdrawals(state, spec)
.map(|(withdrawals, _)| withdrawals)?
.into(),
),
&BeaconState::Merge(_) => None,
// These shouldn't happen but they're here to make the pattern irrefutable
&BeaconState::Base(_) | &BeaconState::Altair(_) => None,
Expand Down
2 changes: 1 addition & 1 deletion beacon_node/http_api/src/builder_states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn get_next_withdrawals<T: BeaconChainTypes>(
}

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
Expand Down
43 changes: 41 additions & 2 deletions beacon_node/store/src/partial_beacon_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,27 @@ where
#[ssz(skip_serializing, skip_deserializing)]
#[superstruct(only(Capella, Deneb, Electra))]
pub historical_summaries: Option<VariableList<HistoricalSummary, T::HistoricalRootsLimit>>,

// Electra
// EIP-7251
#[superstruct(only(Electra))]
pub deposit_balance_to_consume: u64,
#[superstruct(only(Electra))]
pub exit_balance_to_consume: u64,
#[superstruct(only(Electra))]
pub earliest_exit_epoch: Epoch,
#[superstruct(only(Electra))]
pub consolidation_balance_to_consume: u64,
#[superstruct(only(Electra))]
pub earliest_consolidation_epoch: Epoch,
#[superstruct(only(Electra))]
pub pending_balance_deposits:
VariableList<PendingBalanceDeposit, T::PendingBalanceDepositsLimit>,
#[superstruct(only(Electra))]
pub pending_partial_withdrawals:
VariableList<PartialWithdrawal, T::PendingPartialWithdrawalsLimit>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to PendingPartialWithdrawal

#[superstruct(only(Electra))]
pub pending_consolidations: VariableList<PendingConsolidation, T::PendingConsolidationsLimit>,
}

/// Implement the conversion function from BeaconState -> PartialBeaconState.
Expand Down Expand Up @@ -261,7 +282,16 @@ impl<T: EthSpec> PartialBeaconState<T> {
inactivity_scores,
latest_execution_payload_header,
next_withdrawal_index,
next_withdrawal_validator_index
next_withdrawal_validator_index,
// TODO: check this
deposit_balance_to_consume,
exit_balance_to_consume,
earliest_exit_epoch,
consolidation_balance_to_consume,
earliest_consolidation_epoch,
pending_balance_deposits,
pending_partial_withdrawals,
pending_consolidations
],
[historical_summaries]
),
Expand Down Expand Up @@ -522,7 +552,16 @@ impl<E: EthSpec> TryInto<BeaconState<E>> for PartialBeaconState<E> {
inactivity_scores,
latest_execution_payload_header,
next_withdrawal_index,
next_withdrawal_validator_index
next_withdrawal_validator_index,
// TODO: check this
deposit_balance_to_consume,
exit_balance_to_consume,
earliest_exit_epoch,
consolidation_balance_to_consume,
earliest_consolidation_epoch,
pending_balance_deposits,
pending_partial_withdrawals,
pending_consolidations
],
[historical_summaries]
),
Expand Down
91 changes: 72 additions & 19 deletions consensus/state_processing/src/common/initiate_validator_exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,85 @@ pub fn initiate_validator_exit<T: EthSpec>(
index: usize,
spec: &ChainSpec,
) -> Result<(), Error> {
// TODO: try to minimize lookups of the validator while satisfying the borrow checker
// Return if the validator already initiated exit
if state.get_validator(index)?.exit_epoch != spec.far_future_epoch {
let validator = state.get_validator(index)?;
if validator.exit_epoch != spec.far_future_epoch {
return Ok(());
}

// Ensure the exit cache is built.
state.build_exit_cache(spec)?;
match &state {
&BeaconState::Base(_)
| &BeaconState::Altair(_)
| &BeaconState::Merge(_)
| &BeaconState::Capella(_)
| &BeaconState::Deneb(_) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do fork >= deneb { new logic } else { current logic }? No change required for the next fork.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I try to make fork-related patterns enumerate the possibilities so as to draw our attention to things that have changed from one fork to the next. When we implement new forks, the compiler will draw your attention to places where things change. Match statements of this kind also turn the code into a kind of living documentation of where things change in which fork.

Consider the activation churn limit for example.

Copy link
Collaborator

@dapplion dapplion Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before joining LH, I hated this. There's an insane amount of boilerplate that you force everyone to deal with when they may not have to. A new fork developer will know where and when to change code, and it is almost always covered by spec tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should put in effort to reduce this boiler plate, this is an example external contrib EIP implementation where it's really hard to even find the changes related to the EIP: https://github.com/kevinbogner/lighthouse/pull/3/files . Trying to merge this in post-electra boiler plate would probably be more painful than re-implementing it.

From our POV it's a once-per fork pain but it can be a per-POC pain for any LH contributors. Maybe adding a generic and permanent "NextFork" would help here too. Kind of ugly though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaning on fork order does make sense, we're attempting to embedding fork order into superstructs: sigp/superstruct#33

the places we've been burned on not doing complete matches are usually in networking code, not consensus code. We can evaluate on a case-by-case basis but I think so far it's been easier to default to complete matches only.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state transition / networking is a good place to draw the line, since state transition is covered by spec tests. In Lodestar we use fork comparisons and have never had related bugs to forgetting this for forks.

// Ensure the exit cache is built.
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)?;
// 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_churn_limit(spec)? {
exit_queue_epoch.safe_add_assign(1)?;
}
if exit_queue_churn >= state.get_churn_limit(spec)? {
exit_queue_epoch.safe_add_assign(1)?;
}
state
.exit_cache_mut()
.record_validator_exit(exit_queue_epoch)?;

state
.exit_cache_mut()
.record_validator_exit(exit_queue_epoch)?;
state.get_validator_mut(index)?.exit_epoch = exit_queue_epoch;
state.get_validator_mut(index)?.withdrawable_epoch =
exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?;
let validator = state.get_validator_mut(index)?;
validator.exit_epoch = exit_queue_epoch;
validator.withdrawable_epoch =
exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?;
}
&BeaconState::Electra(_) => {
// Compute exit queue epoch [Modified in Electra:EIP7251]
let exit_queue_epoch =
compute_exit_epoch_and_update_churn(state, validator.effective_balance, spec)?;
let validator = state.get_validator_mut(index)?;
// Set validator exit epoch and withdrawable epoch
validator.exit_epoch = exit_queue_epoch;
validator.withdrawable_epoch = validator
.exit_epoch
.safe_add(spec.min_validator_withdrawability_delay)?;
// TODO: consider impact on exit cache
}
}

Ok(())
}

// TODO: should this function be moved to its own file?
pub fn compute_exit_epoch_and_update_churn<E: EthSpec>(
state: &mut BeaconState<E>,
exit_balance: u64,
spec: &ChainSpec,
) -> Result<Epoch, Error> {
let earliest_exit_epoch = state.compute_activation_exit_epoch(state.current_epoch(), spec)?;
let per_epoch_churn = state.get_activation_exit_churn_limit(spec)?;

if state.earliest_exit_epoch()? < earliest_exit_epoch {
*state.earliest_exit_epoch_mut()? = earliest_exit_epoch;
*state.exit_balance_to_consume_mut()? = per_epoch_churn;
}
if exit_balance <= state.exit_balance_to_consume()? {
// Exit fits in the current earliest epoch
state
.exit_balance_to_consume_mut()?
.safe_sub_assign(exit_balance)?;
} else {
// Exit does not fit in the current earliest epoch
let balance_to_process = exit_balance.safe_sub(state.exit_balance_to_consume()?)?;
let additional_epochs = balance_to_process.safe_div(per_epoch_churn)?;
let remainder = balance_to_process.safe_rem(per_epoch_churn)?;
*state.earliest_exit_epoch_mut()? = Epoch::new(additional_epochs.safe_add(1)?);
*state.exit_balance_to_consume_mut()? = per_epoch_churn.safe_sub(remainder)?;
}

state.earliest_exit_epoch()
}
14 changes: 12 additions & 2 deletions consensus/state_processing/src/common/slash_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,18 @@ pub fn slash_validator<T: EthSpec>(
// 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 = match state {
BeaconState::Base(_)
| BeaconState::Altair(_)
| BeaconState::Merge(_)
| BeaconState::Capella(_)
| BeaconState::Deneb(_) => {
validator_effective_balance.safe_div(spec.whistleblower_reward_quotient)?
}
BeaconState::Electra(_) => {
validator_effective_balance.safe_div(spec.whistleblower_reward_quotient_eip7251)?
}
};
let proposer_reward = match state {
BeaconState::Base(_) => whistleblower_reward.safe_div(spec.proposer_reward_quotient)?,
BeaconState::Altair(_)
Expand Down
56 changes: 52 additions & 4 deletions consensus/state_processing/src/per_block_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub use verify_exit::verify_exit;
pub mod altair;
pub mod block_signature_verifier;
pub mod deneb;
pub mod electra;
pub mod errors;
mod is_valid_indexed_attestation;
pub mod process_operations;
Expand Down Expand Up @@ -500,12 +501,50 @@ pub fn compute_timestamp_at_slot<T: EthSpec>(
pub fn get_expected_withdrawals<T: EthSpec>(
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Result<Withdrawals<T>, BlockProcessingError> {
) -> Result<(Withdrawals<T>, usize), 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 partial_withdrawals_count = match state {
BeaconState::Base(_) | BeaconState::Altair(_) | BeaconState::Merge(_) => {
return Err(BlockProcessingError::IncorrectStateType);
}
BeaconState::Capella(_) | BeaconState::Deneb(_) => 0,
BeaconState::Electra(_) => {
for withdrawal in state.pending_partial_withdrawals()? {
if withdrawal.withdrawable_epoch > epoch
|| withdrawals.len() == spec.max_partial_withdrawals_per_payload as usize
{
break;
}
let validator = state.get_validator(withdrawal.index as usize)?;
let balance = *state.balances().get(withdrawal.index as usize).ok_or(
BeaconStateError::BalancesOutOfBounds(withdrawal.index as usize),
)?;
if validator.exit_epoch == spec.far_future_epoch
&& balance > spec.min_activation_balance
{
let withdrawable_balance = std::cmp::min(
balance.safe_sub(spec.min_activation_balance)?,
withdrawal.amount,
);
withdrawals.push(Withdrawal {
index: withdrawal_index,
validator_index: withdrawal.index,
address: validator
.get_eth1_withdrawal_address(spec)
.ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?,
amount: withdrawable_balance,
});
withdrawal_index.safe_add_assign(1)?;
}
}
withdrawals.len()
}
};

let bound = std::cmp::min(
state.validators().len() as u64,
spec.max_validators_per_withdrawals_sweep,
Expand All @@ -532,7 +571,7 @@ pub fn get_expected_withdrawals<T: EthSpec>(
address: validator
.get_eth1_withdrawal_address(spec)
.ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?,
amount: balance.safe_sub(spec.max_effective_balance)?,
amount: balance.safe_sub(validator.get_max_effective_balance(spec))?,
});
withdrawal_index.safe_add_assign(1)?;
}
Expand All @@ -544,7 +583,7 @@ pub fn get_expected_withdrawals<T: EthSpec>(
.safe_rem(state.validators().len() as u64)?;
}

Ok(withdrawals.into())
Ok((withdrawals.into(), partial_withdrawals_count))
}

/// Apply withdrawals to the state.
Expand All @@ -556,7 +595,8 @@ pub fn process_withdrawals<T: EthSpec, Payload: AbstractExecPayload<T>>(
match state {
BeaconState::Merge(_) => 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()?;

Expand All @@ -575,6 +615,14 @@ pub fn process_withdrawals<T: EthSpec, Payload: AbstractExecPayload<T>>(
)?;
}

if let Ok(mut pending_partial_withdrawals) = state
.pending_partial_withdrawals_mut()
.map(|ppw| Vec::from(std::mem::replace(ppw, Vec::new().into())))
{
pending_partial_withdrawals.drain(0..partial_withdrawals_count);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be quite expensive with tree states done naively. It's possible to do a specialized operation on milhouse that splits the tree at some index while potentially preserving part of the hash cache @michaelsproul

@ethDreamer can you add a TODO(EIP7251) to not forget?

*state.pending_partial_withdrawals_mut()? = pending_partial_withdrawals.into();
}

// 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)?;
Expand Down
42 changes: 42 additions & 0 deletions consensus/state_processing/src/per_block_processing/electra.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use safe_arith::SafeArith;
use types::BeaconStateError as Error;
use types::{BeaconState, ChainSpec, Epoch, EthSpec};

// Thus function will return an error if not called on a post-electra state
//
// TODO: finish commenting
pub fn compute_consolidation_epoch_and_update_churn<E: EthSpec>(
state: &mut BeaconState<E>,
consolidation_balance: u64,
spec: &ChainSpec,
) -> Result<Epoch, Error> {
let earliest_consolidation_epoch =
state.compute_activation_exit_epoch(state.current_epoch(), spec)?;
let per_epoch_consolidation_churn = state.get_consolidation_churn_limit(spec)?;
// New epoch for consolidations
if state.earliest_consolidation_epoch()? < earliest_consolidation_epoch {
*state.earliest_consolidation_epoch_mut()? = earliest_consolidation_epoch;
*state.consolidation_balance_to_consume_mut()? = per_epoch_consolidation_churn;
}

if consolidation_balance <= state.consolidation_balance_to_consume()? {
// Consolidation fits in the current earliest consolidation epoch
state
.consolidation_balance_to_consume_mut()?
.safe_sub_assign(consolidation_balance)?;
} else {
// Consolidation doesn't fit in the current earliest consolidation epoch
let balance_to_process =
consolidation_balance.safe_sub(state.consolidation_balance_to_consume()?)?;
let additional_epochs = balance_to_process.safe_div(per_epoch_consolidation_churn)?;
let remainder = balance_to_process.safe_rem(per_epoch_consolidation_churn)?;

state
.earliest_consolidation_epoch_mut()?
.safe_add_assign(additional_epochs.safe_add(1)?)?;
*state.consolidation_balance_to_consume_mut()? =
per_epoch_consolidation_churn.safe_sub(remainder)?;
}

state.earliest_consolidation_epoch()
}
6 changes: 3 additions & 3 deletions consensus/state_processing/src/per_epoch_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod altair;
pub mod base;
pub mod capella;
pub mod effective_balance_updates;
pub mod electra;
pub mod epoch_processing_summary;
pub mod errors;
pub mod historical_roots_update;
Expand All @@ -40,9 +41,8 @@ pub fn process_epoch<T: EthSpec>(
match state {
BeaconState::Base(_) => base::process_epoch(state, spec),
BeaconState::Altair(_) | BeaconState::Merge(_) => altair::process_epoch(state, spec),
BeaconState::Capella(_) | BeaconState::Deneb(_) | BeaconState::Electra(_) => {
capella::process_epoch(state, spec)
}
BeaconState::Capella(_) | BeaconState::Deneb(_) => capella::process_epoch(state, spec),
BeaconState::Electra(_) => electra::process_epoch(state, spec),
}
}

Expand Down
Loading
Loading