Transition Block Proposer Preparation (#3088)

## Issue Addressed

- #3058 

Co-authored-by: Paul Hauner <paul@paulhauner.com>
This commit is contained in:
ethDreamer 2022-04-07 14:03:34 +00:00
parent 5ff4013263
commit 22002a4e68
3 changed files with 200 additions and 75 deletions

View File

@ -3481,9 +3481,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.beacon_state .beacon_state
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current); .attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current);
// Used later for the execution engine.
let is_merge_transition_complete = is_merge_transition_complete(&new_head.beacon_state);
drop(lag_timer); drop(lag_timer);
// Clear the early attester cache in case it conflicts with `self.canonical_head`. // Clear the early attester cache in case it conflicts with `self.canonical_head`.
@ -3690,32 +3687,28 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
} }
} }
// If this is a post-merge block, update the execution layer. // Update the execution layer.
if is_merge_transition_complete { if let Err(e) = self.update_execution_engine_forkchoice_blocking(self.slot()?) {
let current_slot = self.slot()?; crit!(
self.log,
"Failed to update execution head";
"error" => ?e
);
}
if let Err(e) = self.update_execution_engine_forkchoice_blocking(current_slot) { // Performing this call immediately after
crit!( // `update_execution_engine_forkchoice_blocking` might result in two calls to fork
self.log, // choice updated, one *without* payload attributes and then a second *with*
"Failed to update execution head"; // payload attributes.
"error" => ?e //
); // This seems OK. It's not a significant waste of EL<>CL bandwidth or resources, as
} // far as I know.
if let Err(e) = self.prepare_beacon_proposer_blocking() {
// Performing this call immediately after crit!(
// `update_execution_engine_forkchoice_blocking` might result in two calls to fork self.log,
// choice updated, one *without* payload attributes and then a second *with* "Failed to prepare proposers after fork choice";
// payload attributes. "error" => ?e
// );
// This seems OK. It's not a significant waste of EL<>CL bandwidth or resources, as
// far as I know.
if let Err(e) = self.prepare_beacon_proposer_blocking() {
crit!(
self.log,
"Failed to prepare proposers after fork choice";
"error" => ?e
);
}
} }
Ok(()) Ok(())
@ -3745,6 +3738,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// 2. The head block is one slot (or less) behind the prepare slot (e.g., we're preparing for /// 2. The head block is one slot (or less) behind the prepare slot (e.g., we're preparing for
/// the next slot and the block at the current slot is already known). /// the next slot and the block at the current slot is already known).
pub async fn prepare_beacon_proposer_async(&self) -> Result<(), Error> { pub async fn prepare_beacon_proposer_async(&self) -> Result<(), Error> {
let current_slot = self.slot()?;
let prepare_slot = current_slot + 1;
let prepare_epoch = prepare_slot.epoch(T::EthSpec::slots_per_epoch());
// There's no need to run the proposer preparation routine before the bellatrix fork.
if self
.spec
.bellatrix_fork_epoch
.map_or(true, |bellatrix| prepare_epoch < bellatrix)
{
return Ok(());
}
let execution_layer = self let execution_layer = self
.execution_layer .execution_layer
.clone() .clone()
@ -3757,7 +3763,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
} }
let head = self.head_info()?; let head = self.head_info()?;
let current_slot = self.slot()?; let head_epoch = head.slot.epoch(T::EthSpec::slots_per_epoch());
// Don't bother with proposer prep if the head is more than // Don't bother with proposer prep if the head is more than
// `PREPARE_PROPOSER_HISTORIC_EPOCHS` prior to the current slot. // `PREPARE_PROPOSER_HISTORIC_EPOCHS` prior to the current slot.
@ -3775,19 +3781,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
return Ok(()); return Ok(());
} }
// We only start to push preparation data for some chain *after* the transition block
// has been imported.
//
// There is no payload preparation for the transition block (i.e., the first block with
// execution enabled in some chain).
if head.execution_payload_block_hash.is_none() {
return Ok(());
};
let head_epoch = head.slot.epoch(T::EthSpec::slots_per_epoch());
let prepare_slot = current_slot + 1;
let prepare_epoch = prepare_slot.epoch(T::EthSpec::slots_per_epoch());
// Ensure that the shuffling decision root is correct relative to the epoch we wish to // Ensure that the shuffling decision root is correct relative to the epoch we wish to
// query. // query.
let shuffling_decision_root = if head_epoch == prepare_epoch { let shuffling_decision_root = if head_epoch == prepare_epoch {
@ -3968,6 +3961,23 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&self, &self,
current_slot: Slot, current_slot: Slot,
) -> Result<(), Error> { ) -> Result<(), Error> {
let next_slot = current_slot + 1;
// There is no need to issue a `forkchoiceUpdated` (fcU) message unless the Bellatrix fork
// has:
//
// 1. Already happened.
// 2. Will happen in the next slot.
//
// The reason for a fcU message in the slot prior to the Bellatrix fork is in case the
// terminal difficulty has already been reached and a payload preparation message needs to
// be issued.
if self.spec.bellatrix_fork_epoch.map_or(true, |bellatrix| {
next_slot.epoch(T::EthSpec::slots_per_epoch()) < bellatrix
}) {
return Ok(());
}
let execution_layer = self let execution_layer = self
.execution_layer .execution_layer
.as_ref() .as_ref()
@ -3994,29 +4004,69 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// We are taking the `self.fork_choice` lock whilst holding the `forkchoice_lock`. This // We are taking the `self.fork_choice` lock whilst holding the `forkchoice_lock`. This
// is intentional, since it allows us to ensure a consistent ordering of messages to the // is intentional, since it allows us to ensure a consistent ordering of messages to the
// execution layer. // execution layer.
let (head_block_root, head_hash, finalized_hash) = let forkchoice_update_parameters =
if let Some(params) = self.fork_choice.read().get_forkchoice_update_parameters() { self.fork_choice.read().get_forkchoice_update_parameters();
if let Some(head_hash) = params.head_hash { let (head_block_root, head_hash, finalized_hash) = if let Some(params) =
( forkchoice_update_parameters
params.head_root, {
head_hash, if let Some(head_hash) = params.head_hash {
params (
.finalized_hash params.head_root,
.unwrap_or_else(ExecutionBlockHash::zero), head_hash,
) params
} else { .finalized_hash
// The head block does not have an execution block hash, there is no need to .unwrap_or_else(ExecutionBlockHash::zero),
// send an update to the EL. )
return Ok(());
}
} else { } else {
warn!( // The head block does not have an execution block hash. We must check to see if we
self.log, // happen to be the proposer of the transition block, in which case we still need to
"Missing forkchoice params"; // send forkchoice_updated.
"msg" => "please report this non-critical bug" match self.spec.fork_name_at_slot::<T::EthSpec>(next_slot) {
); // We are pre-bellatrix; no need to update the EL.
return Ok(()); ForkName::Base | ForkName::Altair => return Ok(()),
}; _ => {
// We are post-bellatrix
if execution_layer
.payload_attributes(next_slot, params.head_root)
.await
.is_some()
{
// We are a proposer, check for terminal_pow_block_hash
if let Some(terminal_pow_block_hash) = execution_layer
.get_terminal_pow_block_hash(&self.spec)
.await
.map_err(Error::ForkchoiceUpdate)?
{
info!(
self.log,
"Prepared POS transition block proposer"; "slot" => next_slot
);
(
params.head_root,
terminal_pow_block_hash,
params
.finalized_hash
.unwrap_or_else(ExecutionBlockHash::zero),
)
} else {
// TTD hasn't been reached yet, no need to update the EL.
return Ok(());
}
} else {
// We are not a proposer, no need to update the EL.
return Ok(());
}
}
}
}
} else {
warn!(
self.log,
"Missing forkchoice params";
"msg" => "please report this non-critical bug"
);
return Ok(());
};
let forkchoice_updated_response = self let forkchoice_updated_response = self
.execution_layer .execution_layer

View File

@ -6,7 +6,8 @@ use beacon_chain::{
WhenSlotSkipped, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, WhenSlotSkipped, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON,
}; };
use execution_layer::{ use execution_layer::{
json_structures::JsonPayloadAttributesV1, ExecutionLayer, PayloadAttributes, json_structures::{JsonForkChoiceStateV1, JsonPayloadAttributesV1},
ExecutionLayer, ForkChoiceState, PayloadAttributes,
}; };
use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus}; use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus};
use proto_array::{Error as ProtoArrayError, ExecutionStatus}; use proto_array::{Error as ProtoArrayError, ExecutionStatus};
@ -95,17 +96,28 @@ impl InvalidPayloadRig {
self.harness.chain.head_info().unwrap() self.harness.chain.head_info().unwrap()
} }
fn previous_payload_attributes(&self) -> PayloadAttributes { fn previous_forkchoice_update_params(&self) -> (ForkChoiceState, PayloadAttributes) {
let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap();
let json = mock_execution_layer let json = mock_execution_layer
.server .server
.take_previous_request() .take_previous_request()
.expect("no previous request"); .expect("no previous request");
let params = json.get("params").expect("no params"); let params = json.get("params").expect("no params");
let fork_choice_state_json = params.get(0).expect("no payload param");
let fork_choice_state: JsonForkChoiceStateV1 =
serde_json::from_value(fork_choice_state_json.clone()).unwrap();
let payload_param_json = params.get(1).expect("no payload param"); let payload_param_json = params.get(1).expect("no payload param");
let attributes: JsonPayloadAttributesV1 = let attributes: JsonPayloadAttributesV1 =
serde_json::from_value(payload_param_json.clone()).unwrap(); serde_json::from_value(payload_param_json.clone()).unwrap();
attributes.into()
(fork_choice_state.into(), attributes.into())
}
fn previous_payload_attributes(&self) -> PayloadAttributes {
let (_, payload_attributes) = self.previous_forkchoice_update_params();
payload_attributes
} }
fn move_to_terminal_block(&self) { fn move_to_terminal_block(&self) {
@ -117,6 +129,16 @@ impl InvalidPayloadRig {
.unwrap(); .unwrap();
} }
fn latest_execution_block_hash(&self) -> ExecutionBlockHash {
let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap();
mock_execution_layer
.server
.execution_block_generator()
.latest_execution_block()
.unwrap()
.block_hash
}
fn build_blocks(&mut self, num_blocks: u64, is_valid: Payload) -> Vec<Hash256> { fn build_blocks(&mut self, num_blocks: u64, is_valid: Payload) -> Vec<Hash256> {
(0..num_blocks) (0..num_blocks)
.map(|_| self.import_block(is_valid.clone())) .map(|_| self.import_block(is_valid.clone()))
@ -810,3 +832,56 @@ fn invalid_parent() {
)) ))
)); ));
} }
/// Tests to ensure that we will still send a proposer preparation
#[test]
fn payload_preparation_before_transition_block() {
let rig = InvalidPayloadRig::new();
let el = rig.execution_layer();
let head = rig.harness.chain.head().unwrap();
let head_info = rig.head_info();
assert!(
!head_info.is_merge_transition_complete,
"the head block is pre-transition"
);
assert_eq!(
head_info.execution_payload_block_hash,
Some(ExecutionBlockHash::zero()),
"the head block is post-bellatrix"
);
let current_slot = rig.harness.chain.slot().unwrap();
let next_slot = current_slot + 1;
let proposer = head
.beacon_state
.get_beacon_proposer_index(next_slot, &rig.harness.chain.spec)
.unwrap();
let fee_recipient = Address::repeat_byte(99);
// Provide preparation data to the EL for `proposer`.
el.update_proposer_preparation_blocking(
Epoch::new(0),
&[ProposerPreparationData {
validator_index: proposer as u64,
fee_recipient,
}],
)
.unwrap();
rig.move_to_terminal_block();
rig.harness
.chain
.prepare_beacon_proposer_blocking()
.unwrap();
rig.harness
.chain
.update_execution_engine_forkchoice_blocking(current_slot)
.unwrap();
let (fork_choice_state, payload_attributes) = rig.previous_forkchoice_update_params();
let latest_block_hash = rig.latest_execution_block_hash();
assert_eq!(payload_attributes.suggested_fee_recipient, fee_recipient);
assert_eq!(fork_choice_state.head_block_hash, latest_block_hash);
}

View File

@ -4,11 +4,17 @@
//! This crate only provides useful functionality for "The Merge", it does not provide any of the //! This crate only provides useful functionality for "The Merge", it does not provide any of the
//! deposit-contract functionality that the `beacon_node/eth1` crate already provides. //! deposit-contract functionality that the `beacon_node/eth1` crate already provides.
use crate::engine_api::Builder;
use crate::engines::Builders;
use auth::{Auth, JwtKey}; use auth::{Auth, JwtKey};
use engine_api::{Error as ApiError, *}; use engine_api::Error as ApiError;
use engines::{Engine, EngineError, Engines, ForkChoiceState, Logging}; pub use engine_api::*;
pub use engine_api::{http, http::HttpJsonRpc};
pub use engines::ForkChoiceState;
use engines::{Engine, EngineError, Engines, Logging};
use lru::LruCache; use lru::LruCache;
use payload_status::process_multiple_payload_statuses; use payload_status::process_multiple_payload_statuses;
pub use payload_status::PayloadStatus;
use sensitive_url::SensitiveUrl; use sensitive_url::SensitiveUrl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use slog::{crit, debug, error, info, trace, Logger}; use slog::{crit, debug, error, info, trace, Logger};
@ -30,12 +36,6 @@ use types::{
ProposerPreparationData, SignedBeaconBlock, Slot, ProposerPreparationData, SignedBeaconBlock, Slot,
}; };
use crate::engine_api::Builder;
use crate::engines::Builders;
pub use engine_api::*;
pub use engine_api::{http, http::HttpJsonRpc};
pub use payload_status::PayloadStatus;
mod engine_api; mod engine_api;
mod engines; mod engines;
mod metrics; mod metrics;