[Merge] Optimistic Sync: Stage 1 (#2686)

* Add payload verification status to fork choice

* Pass payload verification status to import_block

* Add valid back-propagation

* Add head safety status latch to API

* Remove ExecutionLayerStatus

* Add execution info to client notifier

* Update notifier logs

* Change use of "hash" to refer to beacon block

* Shutdown on invalid finalized block

* Tidy, add comments

* Fix failing FC tests

* Allow blocks with unsafe head

* Fix forkchoiceUpdate call on startup
This commit is contained in:
Paul Hauner
2021-10-07 22:24:57 +11:00
parent aa1d57aa55
commit 6dde12f311
17 changed files with 395 additions and 156 deletions

View File

@@ -30,4 +30,8 @@ pub enum Error {
head_justified_epoch: Epoch,
head_finalized_epoch: Epoch,
},
InvalidAncestorOfValidPayload {
ancestor_block_root: Hash256,
ancestor_payload_block_hash: Hash256,
},
}

View File

@@ -2,7 +2,7 @@ mod ffg_updates;
mod no_votes;
mod votes;
use crate::proto_array_fork_choice::{Block, ProtoArrayForkChoice};
use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
use serde_derive::{Deserialize, Serialize};
use types::{AttestationShufflingId, Epoch, Hash256, Slot};
@@ -57,7 +57,7 @@ impl ForkChoiceTestDefinition {
pub fn run(self) {
let junk_shuffling_id =
AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero());
let execution_block_hash = Hash256::zero();
let execution_status = ExecutionStatus::irrelevant();
let mut fork_choice = ProtoArrayForkChoice::new(
self.finalized_block_slot,
Hash256::zero(),
@@ -66,7 +66,7 @@ impl ForkChoiceTestDefinition {
self.finalized_root,
junk_shuffling_id.clone(),
junk_shuffling_id,
execution_block_hash,
execution_status,
)
.expect("should create fork choice struct");
@@ -141,7 +141,7 @@ impl ForkChoiceTestDefinition {
),
justified_epoch,
finalized_epoch,
execution_block_hash,
execution_status,
};
fork_choice.process_block(block).unwrap_or_else(|e| {
panic!(

View File

@@ -4,7 +4,7 @@ mod proto_array;
mod proto_array_fork_choice;
mod ssz_container;
pub use crate::proto_array_fork_choice::{Block, ProtoArrayForkChoice};
pub use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
pub use error::Error;
pub mod core {

View File

@@ -1,4 +1,4 @@
use crate::{error::Error, Block};
use crate::{error::Error, Block, ExecutionStatus};
use serde_derive::{Deserialize, Serialize};
use ssz::four_byte_option_impl;
use ssz_derive::{Decode, Encode};
@@ -35,11 +35,9 @@ pub struct ProtoNode {
best_child: Option<usize>,
#[ssz(with = "four_byte_option_usize")]
best_descendant: Option<usize>,
/// It's necessary to track this so that we can refuse to propagate post-merge blocks without
/// execution payloads, without confusing these with pre-merge blocks.
///
/// Relevant spec issue: https://github.com/ethereum/consensus-specs/issues/2618
pub execution_block_hash: Hash256,
/// Indicates if an execution node has marked this block as valid. Also contains the execution
/// block hash.
pub execution_status: ExecutionStatus,
}
/// Only used for SSZ deserialization of the persisted fork choice during the database migration
@@ -78,7 +76,11 @@ impl Into<ProtoNode> for LegacyProtoNode {
weight: self.weight,
best_child: self.best_child,
best_descendant: self.best_descendant,
execution_block_hash: Hash256::zero(),
// We set the following execution value as if the block is a pre-merge-fork block. This
// is safe as long as we never import a merge block with the old version of proto-array.
// This will be safe since we can't actually process merge blocks until we've made this
// change to fork choice.
execution_status: ExecutionStatus::irrelevant(),
}
}
}
@@ -224,7 +226,7 @@ impl ProtoArray {
weight: 0,
best_child: None,
best_descendant: None,
execution_block_hash: block.execution_block_hash,
execution_status: block.execution_status,
};
self.indices.insert(node.root, node_index);
@@ -232,11 +234,58 @@ impl ProtoArray {
if let Some(parent_index) = node.parent {
self.maybe_update_best_child_and_descendant(parent_index, node_index)?;
if matches!(block.execution_status, ExecutionStatus::Valid(_)) {
self.propagate_execution_payload_verification(parent_index)?;
}
}
Ok(())
}
pub fn propagate_execution_payload_verification(
&mut self,
verified_node_index: usize,
) -> Result<(), Error> {
let mut index = verified_node_index;
loop {
let node = self
.nodes
.get_mut(index)
.ok_or(Error::InvalidNodeIndex(index))?;
let parent_index = match node.execution_status {
// We have reached a node that we already know is valid. No need to iterate further
// since we assume an ancestors have already been set to valid.
ExecutionStatus::Valid(_) => return Ok(()),
// We have reached an irrelevant node, this node is prior to a terminal execution
// block. There's no need to iterate further, it's impossible for this block to have
// any relevant ancestors.
ExecutionStatus::Irrelevant(_) => return Ok(()),
// The block has an unknown status, set it to valid since any ancestor of a valid
// payload can be considered valid.
ExecutionStatus::Unknown(payload_block_hash) => {
node.execution_status = ExecutionStatus::Valid(payload_block_hash);
if let Some(parent_index) = node.parent {
parent_index
} else {
// We have reached the root block, iteration complete.
return Ok(());
}
}
// An ancestor of the valid payload was invalid. This is a serious error which
// indicates a consensus failure in the execution node. This is unrecoverable.
ExecutionStatus::Invalid(ancestor_payload_block_hash) => {
return Err(Error::InvalidAncestorOfValidPayload {
ancestor_block_root: node.root,
ancestor_payload_block_hash,
})
}
};
index = parent_index;
}
}
/// Follows the best-descendant links to find the best-block (i.e., head-block).
///
/// ## Notes

View File

@@ -1,6 +1,7 @@
use crate::error::Error;
use crate::proto_array::ProtoArray;
use crate::ssz_container::{LegacySszContainer, SszContainer};
use serde_derive::{Deserialize, Serialize};
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
use std::collections::HashMap;
@@ -15,6 +16,32 @@ pub struct VoteTracker {
next_epoch: Epoch,
}
/// Represents the verification status of an execution payload.
#[derive(Clone, Copy, Debug, PartialEq, Encode, Decode, Serialize, Deserialize)]
#[ssz(enum_behaviour = "union")]
pub enum ExecutionStatus {
/// An EL has determined that the payload is valid.
Valid(Hash256),
/// An EL has determined that the payload is invalid.
Invalid(Hash256),
/// An EL has not yet verified the execution payload.
Unknown(Hash256),
/// The block is either prior to the merge fork, or after the merge fork but before the terminal
/// PoW block has been found.
///
/// # Note:
///
/// This `bool` only exists to satisfy our SSZ implementation which requires all variants
/// to have a value. It can be set to anything.
Irrelevant(bool), // TODO(merge): fix bool.
}
impl ExecutionStatus {
pub fn irrelevant() -> Self {
ExecutionStatus::Irrelevant(false)
}
}
/// A block that is to be applied to the fork choice.
///
/// A simplified version of `types::BeaconBlock`.
@@ -29,7 +56,9 @@ pub struct Block {
pub next_epoch_shuffling_id: AttestationShufflingId,
pub justified_epoch: Epoch,
pub finalized_epoch: Epoch,
pub execution_block_hash: Hash256,
/// Indicates if an execution node has marked this block as valid. Also contains the execution
/// block hash.
pub execution_status: ExecutionStatus,
}
/// A Vec-wrapper which will grow to match any request.
@@ -76,7 +105,7 @@ impl ProtoArrayForkChoice {
finalized_root: Hash256,
current_epoch_shuffling_id: AttestationShufflingId,
next_epoch_shuffling_id: AttestationShufflingId,
execution_block_hash: Hash256,
execution_status: ExecutionStatus,
) -> Result<Self, String> {
let mut proto_array = ProtoArray {
prune_threshold: DEFAULT_PRUNE_THRESHOLD,
@@ -98,7 +127,7 @@ impl ProtoArrayForkChoice {
next_epoch_shuffling_id,
justified_epoch,
finalized_epoch,
execution_block_hash,
execution_status,
};
proto_array
@@ -208,7 +237,7 @@ impl ProtoArrayForkChoice {
next_epoch_shuffling_id: block.next_epoch_shuffling_id.clone(),
justified_epoch: block.justified_epoch,
finalized_epoch: block.finalized_epoch,
execution_block_hash: block.execution_block_hash,
execution_status: block.execution_status,
})
}
@@ -372,7 +401,7 @@ mod test_compute_deltas {
let unknown = Hash256::from_low_u64_be(4);
let junk_shuffling_id =
AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero());
let execution_block_hash = Hash256::zero();
let execution_status = ExecutionStatus::irrelevant();
let mut fc = ProtoArrayForkChoice::new(
genesis_slot,
@@ -382,7 +411,7 @@ mod test_compute_deltas {
finalized_root,
junk_shuffling_id.clone(),
junk_shuffling_id.clone(),
execution_block_hash,
execution_status,
)
.unwrap();
@@ -398,7 +427,7 @@ mod test_compute_deltas {
next_epoch_shuffling_id: junk_shuffling_id.clone(),
justified_epoch: genesis_epoch,
finalized_epoch: genesis_epoch,
execution_block_hash,
execution_status,
})
.unwrap();
@@ -414,7 +443,7 @@ mod test_compute_deltas {
next_epoch_shuffling_id: junk_shuffling_id,
justified_epoch: genesis_epoch,
finalized_epoch: genesis_epoch,
execution_block_hash,
execution_status,
})
.unwrap();