mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-02 04:03:35 +00:00
Gloas filter conflicting voluntary exits (#9183)
Parent envelope execution requests can invalidate voluntary exits. We should filter out any conflicting voluntary exits during block production to avoid triggering failures. Spec change: https://github.com/ethereum/consensus-specs/pull/5176 Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bls::Signature;
|
use bls::{PublicKeyBytes, Signature};
|
||||||
use execution_layer::{
|
use execution_layer::{
|
||||||
BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters,
|
BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters,
|
||||||
};
|
};
|
||||||
@@ -28,7 +28,7 @@ use types::consts::gloas::BUILDER_INDEX_SELF_BUILD;
|
|||||||
use types::{
|
use types::{
|
||||||
Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra,
|
Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra,
|
||||||
BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError,
|
BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError,
|
||||||
BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid,
|
BuilderIndex, ChainSpec, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid,
|
||||||
ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti,
|
ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti,
|
||||||
Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock,
|
Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock,
|
||||||
SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope,
|
SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope,
|
||||||
@@ -137,6 +137,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
|||||||
graffiti_settings: GraffitiSettings,
|
graffiti_settings: GraffitiSettings,
|
||||||
verification: ProduceBlockVerification,
|
verification: ProduceBlockVerification,
|
||||||
) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> {
|
) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> {
|
||||||
|
// Extract the parent's execution requests from the envelope (if parent was full).
|
||||||
|
let parent_execution_requests = if parent_payload_status == PayloadStatus::Full {
|
||||||
|
parent_envelope
|
||||||
|
.as_ref()
|
||||||
|
.map(|env| env.message.execution_requests.clone())
|
||||||
|
.ok_or(BlockProductionError::MissingParentExecutionPayload)?
|
||||||
|
} else {
|
||||||
|
ExecutionRequests::default()
|
||||||
|
};
|
||||||
|
|
||||||
// Part 1/3 (blocking)
|
// Part 1/3 (blocking)
|
||||||
//
|
//
|
||||||
// Perform the state advance and block-packing functions.
|
// Perform the state advance and block-packing functions.
|
||||||
@@ -145,6 +155,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
|||||||
.graffiti_calculator
|
.graffiti_calculator
|
||||||
.get_graffiti(graffiti_settings)
|
.get_graffiti(graffiti_settings)
|
||||||
.await;
|
.await;
|
||||||
|
let parent_execution_requests_ref = parent_execution_requests.clone();
|
||||||
let (partial_beacon_block, state) = self
|
let (partial_beacon_block, state) = self
|
||||||
.task_executor
|
.task_executor
|
||||||
.spawn_blocking_handle(
|
.spawn_blocking_handle(
|
||||||
@@ -155,6 +166,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
|||||||
produce_at_slot,
|
produce_at_slot,
|
||||||
randao_reveal,
|
randao_reveal,
|
||||||
graffiti,
|
graffiti,
|
||||||
|
&parent_execution_requests_ref,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"produce_partial_beacon_block_gloas",
|
"produce_partial_beacon_block_gloas",
|
||||||
@@ -163,16 +175,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
|||||||
.await
|
.await
|
||||||
.map_err(BlockProductionError::TokioJoin)??;
|
.map_err(BlockProductionError::TokioJoin)??;
|
||||||
|
|
||||||
// Extract the parent's execution requests from the envelope (if parent was full).
|
|
||||||
let parent_execution_requests = if parent_payload_status == PayloadStatus::Full {
|
|
||||||
parent_envelope
|
|
||||||
.as_ref()
|
|
||||||
.map(|env| env.message.execution_requests.clone())
|
|
||||||
.ok_or(BlockProductionError::MissingParentExecutionPayload)?
|
|
||||||
} else {
|
|
||||||
ExecutionRequests::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Part 2/3 (async)
|
// Part 2/3 (async)
|
||||||
//
|
//
|
||||||
// Produce the execution payload bid.
|
// Produce the execution payload bid.
|
||||||
@@ -223,6 +225,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
|||||||
produce_at_slot: Slot,
|
produce_at_slot: Slot,
|
||||||
randao_reveal: Signature,
|
randao_reveal: Signature,
|
||||||
graffiti: Graffiti,
|
graffiti: Graffiti,
|
||||||
|
parent_execution_requests: &ExecutionRequests<T::EthSpec>,
|
||||||
) -> Result<(PartialBeaconBlock<T::EthSpec>, BeaconState<T::EthSpec>), BlockProductionError>
|
) -> Result<(PartialBeaconBlock<T::EthSpec>, BeaconState<T::EthSpec>), BlockProductionError>
|
||||||
{
|
{
|
||||||
// It is invalid to try to produce a block using a state from a future slot.
|
// It is invalid to try to produce a block using a state from a future slot.
|
||||||
@@ -257,6 +260,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
|||||||
let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) =
|
let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) =
|
||||||
self.op_pool.get_slashings_and_exits(&state, &self.spec);
|
self.op_pool.get_slashings_and_exits(&state, &self.spec);
|
||||||
|
|
||||||
|
filter_voluntary_exits_for_parent_execution_requests(
|
||||||
|
&mut voluntary_exits,
|
||||||
|
parent_execution_requests,
|
||||||
|
|idx| state.validators().get(idx as usize).map(|v| v.pubkey),
|
||||||
|
&self.spec,
|
||||||
|
);
|
||||||
|
|
||||||
drop(slashings_and_exits_span);
|
drop(slashings_and_exits_span);
|
||||||
|
|
||||||
let eth1_data = state.eth1_data().clone();
|
let eth1_data = state.eth1_data().clone();
|
||||||
@@ -958,3 +968,178 @@ where
|
|||||||
|
|
||||||
Ok(block_contents)
|
Ok(block_contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop voluntary exits whose target validators will be exited by the parent envelope's
|
||||||
|
/// execution requests.
|
||||||
|
///
|
||||||
|
/// In Gloas the parent execution payload is processed before voluntary exits during block
|
||||||
|
/// processing. EL-triggered withdrawal-full-exit requests (EIP-7002) and cross-pubkey
|
||||||
|
/// consolidation requests (EIP-7251) call `initiate_validator_exit`, setting the target's
|
||||||
|
/// `exit_epoch`. A voluntary exit for the same validator would then fail with `AlreadyExited`.
|
||||||
|
fn filter_voluntary_exits_for_parent_execution_requests<E: EthSpec>(
|
||||||
|
voluntary_exits: &mut Vec<SignedVoluntaryExit>,
|
||||||
|
parent_execution_requests: &ExecutionRequests<E>,
|
||||||
|
pubkey_at_index: impl Fn(u64) -> Option<PublicKeyBytes>,
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) {
|
||||||
|
let mut exited_pubkeys = HashSet::with_capacity(
|
||||||
|
parent_execution_requests.withdrawals.len()
|
||||||
|
+ parent_execution_requests.consolidations.len(),
|
||||||
|
);
|
||||||
|
for req in &parent_execution_requests.withdrawals {
|
||||||
|
if req.amount == spec.full_exit_request_amount {
|
||||||
|
exited_pubkeys.insert(req.validator_pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for req in &parent_execution_requests.consolidations {
|
||||||
|
if req.source_pubkey != req.target_pubkey {
|
||||||
|
exited_pubkeys.insert(req.source_pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exited_pubkeys.is_empty() {
|
||||||
|
voluntary_exits.retain(|exit| {
|
||||||
|
pubkey_at_index(exit.message.validator_index)
|
||||||
|
.map(|pk| !exited_pubkeys.contains(&pk))
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use ssz_types::VariableList;
|
||||||
|
use types::{ConsolidationRequest, Epoch, MainnetEthSpec, VoluntaryExit, WithdrawalRequest};
|
||||||
|
|
||||||
|
type TestSpec = MainnetEthSpec;
|
||||||
|
|
||||||
|
fn pubkey(byte: u8) -> PublicKeyBytes {
|
||||||
|
PublicKeyBytes::deserialize(&[byte; 48]).expect("valid pubkey byte length")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(validator_index: u64) -> SignedVoluntaryExit {
|
||||||
|
SignedVoluntaryExit {
|
||||||
|
message: VoluntaryExit {
|
||||||
|
epoch: Epoch::new(0),
|
||||||
|
validator_index,
|
||||||
|
},
|
||||||
|
signature: Signature::empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requests(
|
||||||
|
withdrawals: Vec<WithdrawalRequest>,
|
||||||
|
consolidations: Vec<ConsolidationRequest>,
|
||||||
|
) -> ExecutionRequests<TestSpec> {
|
||||||
|
ExecutionRequests {
|
||||||
|
deposits: VariableList::empty(),
|
||||||
|
withdrawals: VariableList::new(withdrawals).unwrap(),
|
||||||
|
consolidations: VariableList::new(consolidations).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_filter(
|
||||||
|
exits: &mut Vec<SignedVoluntaryExit>,
|
||||||
|
requests: &ExecutionRequests<TestSpec>,
|
||||||
|
validator_pubkeys: &[PublicKeyBytes],
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) {
|
||||||
|
filter_voluntary_exits_for_parent_execution_requests(
|
||||||
|
exits,
|
||||||
|
requests,
|
||||||
|
|idx| validator_pubkeys.get(idx as usize).copied(),
|
||||||
|
spec,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_exit_withdrawal_request_filters_matching_voluntary_exit() {
|
||||||
|
let spec = ChainSpec::mainnet();
|
||||||
|
let validators = vec![pubkey(1), pubkey(2)];
|
||||||
|
let mut exits = vec![exit(0), exit(1)];
|
||||||
|
let reqs = requests(
|
||||||
|
vec![WithdrawalRequest {
|
||||||
|
source_address: Address::repeat_byte(0xaa),
|
||||||
|
validator_pubkey: validators[0],
|
||||||
|
amount: spec.full_exit_request_amount,
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
run_filter(&mut exits, &reqs, &validators, &spec);
|
||||||
|
|
||||||
|
assert_eq!(exits.len(), 1);
|
||||||
|
assert_eq!(exits[0].message.validator_index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_withdrawal_request_does_not_filter_voluntary_exit() {
|
||||||
|
let spec = ChainSpec::mainnet();
|
||||||
|
let validators = vec![pubkey(1)];
|
||||||
|
let mut exits = vec![exit(0)];
|
||||||
|
let reqs = requests(
|
||||||
|
vec![WithdrawalRequest {
|
||||||
|
source_address: Address::repeat_byte(0xaa),
|
||||||
|
validator_pubkey: validators[0],
|
||||||
|
amount: spec.full_exit_request_amount + 1,
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
run_filter(&mut exits, &reqs, &validators, &spec);
|
||||||
|
|
||||||
|
assert_eq!(exits.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_pubkey_consolidation_filters_voluntary_exit_for_source_only() {
|
||||||
|
let spec = ChainSpec::mainnet();
|
||||||
|
let validators = vec![pubkey(1), pubkey(2), pubkey(3)];
|
||||||
|
let mut exits = vec![exit(0), exit(1), exit(2)];
|
||||||
|
let reqs = requests(
|
||||||
|
vec![],
|
||||||
|
vec![ConsolidationRequest {
|
||||||
|
source_address: Address::repeat_byte(0xaa),
|
||||||
|
source_pubkey: validators[1],
|
||||||
|
target_pubkey: validators[2],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
|
||||||
|
run_filter(&mut exits, &reqs, &validators, &spec);
|
||||||
|
|
||||||
|
// The source (validator 1) is exited; the target (validator 2) is not.
|
||||||
|
let remaining: Vec<u64> = exits.iter().map(|e| e.message.validator_index).collect();
|
||||||
|
assert_eq!(remaining, vec![0, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_consolidation_does_not_filter_voluntary_exit() {
|
||||||
|
let spec = ChainSpec::mainnet();
|
||||||
|
let validators = vec![pubkey(1)];
|
||||||
|
let mut exits = vec![exit(0)];
|
||||||
|
let reqs = requests(
|
||||||
|
vec![],
|
||||||
|
vec![ConsolidationRequest {
|
||||||
|
source_address: Address::repeat_byte(0xaa),
|
||||||
|
source_pubkey: validators[0],
|
||||||
|
target_pubkey: validators[0],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
|
||||||
|
run_filter(&mut exits, &reqs, &validators, &spec);
|
||||||
|
|
||||||
|
assert_eq!(exits.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_parent_requests_preserve_voluntary_exits() {
|
||||||
|
let spec = ChainSpec::mainnet();
|
||||||
|
let validators = vec![pubkey(1), pubkey(2)];
|
||||||
|
let mut exits = vec![exit(0), exit(1)];
|
||||||
|
let reqs = requests(vec![], vec![]);
|
||||||
|
|
||||||
|
run_filter(&mut exits, &reqs, &validators, &spec);
|
||||||
|
|
||||||
|
assert_eq!(exits.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user