diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index afbf3278fe0..b09026b949a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5874,7 +5874,7 @@ impl BeaconChain { chain .canonical_head .fork_choice_read_lock() - .get_justified_block() + .get_justified_or_anchor_block() }, "invalid_payload_fork_choice_get_justified", ) diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 440388661c2..d8ca62084b3 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -187,13 +187,8 @@ where if anchor_block_header.state_root.is_zero() { anchor_block_header.state_root = unadvanced_state_root; } - let anchor_block_root = anchor_block_header.canonical_root(); - let anchor_epoch = anchor_state.current_epoch(); - let justified_checkpoint = Checkpoint { - epoch: anchor_epoch, - root: anchor_block_root, - }; - let finalized_checkpoint = justified_checkpoint; + let justified_checkpoint = anchor_state.current_justified_checkpoint(); + let finalized_checkpoint = anchor_state.finalized_checkpoint(); let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; let justified_state_root = anchor_state.canonical_root()?; diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index cfc7a9637b2..f2f3861e28c 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -115,6 +115,7 @@ pub struct CachedHead { justified_hash: Option, /// The `execution_payload.block_hash` of the finalized block. Set to `None` before Bellatrix. finalized_hash: Option, + pub anchor_block: (Hash256, Slot), } impl CachedHead { @@ -221,6 +222,10 @@ impl CachedHead { self.justified_checkpoint } + pub fn finalized_checkpoint_from_state(&self) -> Checkpoint { + self.snapshot.beacon_state.finalized_checkpoint() + } + /// Returns the cached values of `ForkChoice::forkchoice_update_parameters`. /// /// Useful for supplying to the execution layer. @@ -272,6 +277,7 @@ impl CanonicalHead { head_hash: forkchoice_update_params.head_hash, justified_hash: forkchoice_update_params.justified_hash, finalized_hash: forkchoice_update_params.finalized_hash, + anchor_block: fork_choice.get_anchor_block(), }; Self { @@ -323,6 +329,7 @@ impl CanonicalHead { head_hash: forkchoice_update_params.head_hash, justified_hash: forkchoice_update_params.justified_hash, finalized_hash: forkchoice_update_params.finalized_hash, + anchor_block: fork_choice.get_anchor_block(), }; *fork_choice_write_lock = fork_choice; @@ -608,8 +615,9 @@ impl BeaconChain { // Check to ensure that the finalized block hasn't been marked as invalid. If it has, // shut down Lighthouse. - let finalized_proto_block = fork_choice_read_lock.get_finalized_block()?; + let finalized_proto_block = fork_choice_read_lock.get_finalized_or_anchor_block()?; check_finalized_payload_validity(self, &finalized_proto_block)?; + let anchor_block = fork_choice_read_lock.get_anchor_block(); // Sanity check the finalized checkpoint. // @@ -700,6 +708,7 @@ impl BeaconChain { head_hash: new_forkchoice_update_parameters.head_hash, justified_hash: new_forkchoice_update_parameters.justified_hash, finalized_hash: new_forkchoice_update_parameters.finalized_hash, + anchor_block, }; let new_head = { @@ -727,6 +736,7 @@ impl BeaconChain { head_hash: new_forkchoice_update_parameters.head_hash, justified_hash: new_forkchoice_update_parameters.justified_hash, finalized_hash: new_forkchoice_update_parameters.finalized_hash, + anchor_block, }; let mut cached_head_write_lock = self.canonical_head.cached_head_write_lock(); diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index 4db79790d38..be2a03b5c04 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -98,59 +98,49 @@ pub fn reset_fork_choice_to_finalization, Cold: It current_slot: Option, spec: &ChainSpec, ) -> Result, E>, String> { - // Fetch finalized block. - let finalized_checkpoint = head_state.finalized_checkpoint(); - let finalized_block_root = finalized_checkpoint.root; - let finalized_block = store - .get_full_block(&finalized_block_root) - .map_err(|e| format!("Error loading finalized block: {:?}", e))? + // Fetch the store split as the most recent state internally considered finalized. If the node + // started with checkpoint sync and before its first finalization the split state does not equal + // to the last finalized state; and the last finalized state is unavailable. + let split = store.get_split_info(); + let new_anchor_block_root = split.block_root; + let new_anchor_slot = split.slot; + let new_anchor_block = store + .get_full_block(&new_anchor_block_root) + .map_err(|e| format!("Error loading split block: {:?}", e))? .ok_or_else(|| { format!( - "Finalized block missing for revert: {:?}", - finalized_block_root + "Split block missing for revert: {:?}", + new_anchor_block_root ) })?; // Advance finalized state to finalized epoch (to handle skipped slots). - let finalized_state_root = finalized_block.state_root(); + let new_anchor_state_root = split.state_root; // The enshrined finalized state should be in the state cache. - let mut finalized_state = store - .get_state(&finalized_state_root, Some(finalized_block.slot()), true) - .map_err(|e| format!("Error loading finalized state: {:?}", e))? + let new_anchor_state = store + .get_state(&new_anchor_state_root, Some(new_anchor_slot), true) + .map_err(|e| format!("Error loading split state: {:?}", e))? .ok_or_else(|| { format!( - "Finalized block state missing from database: {:?}", - finalized_state_root + "Split block state missing from database: {:?}", + new_anchor_state_root ) })?; - let finalized_slot = finalized_checkpoint.epoch.start_slot(E::slots_per_epoch()); - complete_state_advance( - &mut finalized_state, - Some(finalized_state_root), - finalized_slot, - spec, - ) - .map_err(|e| { - format!( - "Error advancing finalized state to finalized epoch: {:?}", - e - ) - })?; - let finalized_snapshot = BeaconSnapshot { - beacon_block_root: finalized_block_root, - beacon_block: Arc::new(finalized_block), - beacon_state: finalized_state, + let new_anchor_snapshot = BeaconSnapshot { + beacon_block_root: new_anchor_block_root, + beacon_block: Arc::new(new_anchor_block), + beacon_state: new_anchor_state, }; let fc_store = - BeaconForkChoiceStore::get_forkchoice_store(store.clone(), finalized_snapshot.clone()) + BeaconForkChoiceStore::get_forkchoice_store(store.clone(), new_anchor_snapshot.clone()) .map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?; let mut fork_choice = ForkChoice::from_anchor( fc_store, - finalized_block_root, - &finalized_snapshot.beacon_block, - &finalized_snapshot.beacon_state, + new_anchor_block_root, + &new_anchor_snapshot.beacon_block, + &new_anchor_snapshot.beacon_state, current_slot, spec, ) @@ -160,10 +150,10 @@ pub fn reset_fork_choice_to_finalization, Cold: It // We do not replay attestations presently, relying on the absence of other blocks // to guarantee `head_block_root` as the head. let blocks = store - .load_blocks_to_replay(finalized_slot + 1, head_state.slot(), head_block_root) + .load_blocks_to_replay(new_anchor_slot + 1, head_state.slot(), head_block_root) .map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?; - let mut state = finalized_snapshot.beacon_state; + let mut state = new_anchor_snapshot.beacon_state; for block in blocks { complete_state_advance(&mut state, None, block.slot(), spec) .map_err(|e| format!("State advance failed: {:?}", e))?; diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index c83cdad7e01..3c08df17d7b 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -120,7 +120,24 @@ pub fn spawn_notifier( let cached_head = beacon_chain.canonical_head.cached_head(); let head_slot = cached_head.head_slot(); let head_root = cached_head.head_block_root(); - let finalized_checkpoint = cached_head.finalized_checkpoint(); + let finalized_checkpoint = cached_head.finalized_checkpoint_from_state(); + let finalized_root_str = { + let finalized_slot = finalized_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + if cached_head.anchor_block.1 > finalized_slot { + // Anchor block ahead of finalized checkpoint, the node has a subjective concept + // of finality and will reject blocks that other nodes in the network will + // accept + format!( + "{}/anchor/{}", + finalized_checkpoint.root, cached_head.anchor_block.0 + ) + } else { + // Regular mode + format!("{}", finalized_checkpoint.root) + } + }; metrics::set_gauge(&metrics::NOTIFIER_HEAD_SLOT, head_slot.as_u64() as i64); @@ -179,7 +196,7 @@ pub fn spawn_notifier( debug!( peers = peer_count_pretty(connected_peer_count), - finalized_root = %finalized_checkpoint.root, + finalized_root = finalized_root_str, finalized_epoch = %finalized_checkpoint.epoch, head_block = %head_root, %head_slot, @@ -253,6 +270,7 @@ pub fn spawn_notifier( speed = sync_speed_pretty(speed), est_time = estimated_time_pretty(speedo.estimated_time_till_slot(current_slot)), + finalized_root = finalized_root_str, "Syncing" ); } else { @@ -261,6 +279,7 @@ pub fn spawn_notifier( distance, est_time = estimated_time_pretty(speedo.estimated_time_till_slot(current_slot)), + finalized_root = finalized_root_str, "Syncing" ); } @@ -298,7 +317,7 @@ pub fn spawn_notifier( info!( peers = peer_count_pretty(connected_peer_count), exec_hash = block_hash, - finalized_root = %finalized_checkpoint.root, + finalized_root = finalized_root_str, finalized_epoch = %finalized_checkpoint.epoch, epoch = %current_epoch, block = block_info, @@ -309,7 +328,7 @@ pub fn spawn_notifier( metrics::set_gauge(&metrics::IS_SYNCED, 0); info!( peers = peer_count_pretty(connected_peer_count), - finalized_root = %finalized_checkpoint.root, + finalized_root = finalized_root_str, finalized_epoch = %finalized_checkpoint.epoch, %head_slot, %current_slot, diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 9ddba86b81d..3c0d3c8bf09 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -108,11 +108,7 @@ impl NetworkBeaconProcessor { } else { // Remote finalized epoch is less than ours. let remote_finalized_slot = start_slot(*remote.finalized_epoch()); - if remote_finalized_slot < self.chain.store.get_oldest_block_slot() { - // Peer's finalized checkpoint is older than anything in our DB. We are unlikely - // to be able to help them sync. - Some("Old finality out of range".to_string()) - } else if remote_finalized_slot < self.chain.store.get_split_slot() { + if remote_finalized_slot < self.chain.store.get_split_slot() { // Peer's finalized slot is in range for a quick block root check in our freezer DB. // If that block root check fails, reject them as they're on a different finalized // chain. diff --git a/beacon_node/network/src/status.rs b/beacon_node/network/src/status.rs index ebf5c1829e5..d8f1b1f3457 100644 --- a/beacon_node/network/src/status.rs +++ b/beacon_node/network/src/status.rs @@ -20,7 +20,7 @@ impl ToStatusMessage for BeaconChain { pub(crate) fn status_message(beacon_chain: &BeaconChain) -> StatusMessage { let fork_digest = beacon_chain.enr_fork_id().fork_digest; let cached_head = beacon_chain.canonical_head.cached_head(); - let mut finalized_checkpoint = cached_head.finalized_checkpoint(); + let mut finalized_checkpoint = cached_head.finalized_checkpoint_from_state(); // Alias the genesis checkpoint root to `0x00`. let spec = &beacon_chain.spec; diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 465edd3697f..f89e7d048c9 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -147,11 +147,19 @@ where let target_head_slot = remote_finalized_slot + (2 * T::EthSpec::slots_per_epoch()) + 1; + // If the node started with checkpoint sync, local_info.finalized_epoch might be + // older than the current store split. + let start_epoch = self + .beacon_chain + .store + .get_split_slot() + .epoch(T::EthSpec::slots_per_epoch()); + // Note: We keep current head chains. These can continue syncing whilst we complete // this new finalized chain. self.chains.add_peer_or_create_chain( - local_info.finalized_epoch, + start_epoch, remote_info.finalized_root, target_head_slot, peer_id, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index fe1f5fba9e4..91139e15c72 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -341,6 +341,12 @@ where E: EthSpec, { /// Instantiates `Self` from an anchor (genesis or another finalized checkpoint). + /// + /// # Parameters + /// - `anchor_block_root`: Hash of the `anchor_block`. + /// - `anchor_block`: The most recent block for `anchor_state` (the block that produced it). + /// - `anchor_state`: Beacon state **after** applying `anchor_block` and advanced to the closest + /// epoch boundary. pub fn from_anchor( fc_store: T, anchor_block_root: Hash256, @@ -357,16 +363,16 @@ where }); } - let finalized_block_slot = anchor_block.slot(); - let finalized_block_state_root = anchor_block.state_root(); - let current_epoch_shuffling_id = + let anchor_block_slot = anchor_block.slot(); + let anchor_block_state_root = anchor_block.state_root(); + let anchor_block_current_epoch_shuffling_id = AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Current) .map_err(Error::BeaconStateError)?; - let next_epoch_shuffling_id = + let anchor_block_next_epoch_shuffling_id = AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Next) .map_err(Error::BeaconStateError)?; - let execution_status = anchor_block.message().execution_payload().map_or_else( + let anchor_block_execution_status = anchor_block.message().execution_payload().map_or_else( // If the block doesn't have an execution payload then it can't have // execution enabled. |_| ExecutionStatus::irrelevant(), @@ -387,13 +393,14 @@ where let proto_array = ProtoArrayForkChoice::new::( current_slot, - finalized_block_slot, - finalized_block_state_root, + anchor_block_slot, + anchor_block_root, + anchor_block_state_root, *fc_store.justified_checkpoint(), *fc_store.finalized_checkpoint(), - current_epoch_shuffling_id, - next_epoch_shuffling_id, - execution_status, + anchor_block_current_epoch_shuffling_id, + anchor_block_next_epoch_shuffling_id, + anchor_block_execution_status, )?; let mut fork_choice = Self { @@ -501,13 +508,17 @@ where let head_hash = self .get_block(&head_root) .and_then(|b| b.execution_status.block_hash()); - let justified_root = self.justified_checkpoint().root; - let finalized_root = self.finalized_checkpoint().root; + // After starting with checkpoint sync and before finalizing the finalized and justified + // ProtoNodes are not available. + // TODO(non-fin): Is it safe to tell the EL that the anchor block is finalized? Should we + // pass zero in that case? let justified_hash = self - .get_block(&justified_root) + .get_justified_or_anchor_block() + .ok() .and_then(|b| b.execution_status.block_hash()); let finalized_hash = self - .get_block(&finalized_root) + .get_finalized_or_anchor_block() + .ok() .and_then(|b| b.execution_status.block_hash()); self.forkchoice_update_parameters = ForkchoiceUpdateParameters { head_root, @@ -716,12 +727,17 @@ where // (trivial), but more importantly, it means we don't need to have added `block` to // `self.proto_array` to do this search. See: // + // After starting with checkpoint sync and before finalizing there is no ProtoNode for the + // finalized root. Use the anchor block (initial block) are root to assert this new block is + // descendant of it. + // // https://github.com/ethereum/eth2.0-specs/pull/1884 - let block_ancestor = self.get_ancestor(block.parent_root(), finalized_slot)?; - let finalized_root = self.fc_store.finalized_checkpoint().root; - if block_ancestor != Some(finalized_root) { + let finalized_or_anchor_block = self.get_finalized_or_anchor_block()?; + let block_ancestor = + self.get_ancestor(block.parent_root(), finalized_or_anchor_block.slot)?; + if block_ancestor != Some(finalized_or_anchor_block.root) { return Err(Error::InvalidBlock(InvalidBlock::NotFinalizedDescendant { - finalized_root, + finalized_root: finalized_or_anchor_block.root, block_ancestor, })); } @@ -1262,24 +1278,34 @@ where self.proto_array.get_weight(block_root) } + pub fn get_anchor_block(&self) -> (Hash256, Slot) { + self.proto_array.get_anchor_block() + } + /// Returns the `ProtoBlock` for the justified checkpoint. + /// If the node started with checkpoint sync and has not justified yet, it returns the + /// `ProtoBlock` of the anchor block. /// /// ## Notes /// /// This does *not* return the "best justified checkpoint". It returns the justified checkpoint /// that is used for computing balances. - pub fn get_justified_block(&self) -> Result> { + pub fn get_justified_or_anchor_block(&self) -> Result> { let justified_checkpoint = self.justified_checkpoint(); self.get_block(&justified_checkpoint.root) + .or_else(|| self.get_block(&self.proto_array.get_anchor_block_root())) .ok_or(Error::MissingJustifiedBlock { justified_checkpoint, }) } /// Returns the `ProtoBlock` for the finalized checkpoint. - pub fn get_finalized_block(&self) -> Result> { + /// If the node started with checkpoint sync and has not finalized yet, it returns the + /// `ProtoBlock` of the anchor block. + pub fn get_finalized_or_anchor_block(&self) -> Result> { let finalized_checkpoint = self.finalized_checkpoint(); self.get_block(&finalized_checkpoint.root) + .or_else(|| self.get_block(&self.proto_array.get_anchor_block_root())) .ok_or(Error::MissingFinalizedBlock { finalized_checkpoint, }) @@ -1313,7 +1339,7 @@ where Ok(status.is_optimistic_or_invalid()) } else { Ok(self - .get_finalized_block()? + .get_finalized_or_anchor_block()? .execution_status .is_optimistic_or_invalid()) } @@ -1408,7 +1434,7 @@ where reset_payload_statuses: ResetPayloadStatuses, spec: &ChainSpec, ) -> Result> { - let mut proto_array = ProtoArrayForkChoice::from_container( + let mut proto_array = ProtoArrayForkChoice::from_container::( persisted_proto_array.clone(), justified_balances.clone(), ) @@ -1440,7 +1466,7 @@ where info = "please report this error", "Failed to reset payload statuses" ); - ProtoArrayForkChoice::from_container(persisted_proto_array, justified_balances) + ProtoArrayForkChoice::from_container::(persisted_proto_array, justified_balances) .map_err(Error::InvalidProtoArrayBytes) } else { debug!("Successfully reset all payload statuses"); diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 25c3f03d3b9..becabdaae92 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -524,12 +524,12 @@ fn justified_and_finalized_blocks() { let justified_checkpoint = fork_choice.justified_checkpoint(); assert_eq!(justified_checkpoint.epoch, 0); assert!(justified_checkpoint.root != Hash256::zero()); - assert!(fork_choice.get_justified_block().is_ok()); + assert!(fork_choice.get_justified_or_anchor_block().is_ok()); let finalized_checkpoint = fork_choice.finalized_checkpoint(); assert_eq!(finalized_checkpoint.epoch, 0); assert!(finalized_checkpoint.root != Hash256::zero()); - assert!(fork_choice.get_finalized_block().is_ok()); + assert!(fork_choice.get_finalized_or_anchor_block().is_ok()); } /// - The new justified checkpoint descends from the current. Near genesis. diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index 35cb4007b78..97c220f5669 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -54,6 +54,7 @@ pub enum Error { }, InvalidEpochOffset(u64), Arith(ArithError), + EmptyNodes, } impl From for Error { diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 20987dff26d..7300d1af247 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -17,6 +17,8 @@ pub use ffg_updates::*; pub use no_votes::*; pub use votes::*; +type E = MainnetEthSpec; + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Operation { FindHead { @@ -82,6 +84,7 @@ impl ForkChoiceTestDefinition { let mut fork_choice = ProtoArrayForkChoice::new::( self.finalized_block_slot, self.finalized_block_slot, + self.finalized_checkpoint.root, Hash256::zero(), self.justified_checkpoint, self.finalized_checkpoint, @@ -122,7 +125,7 @@ impl ForkChoiceTestDefinition { "Operation at index {} failed head check. Operation: {:?}", op_index, op ); - check_bytes_round_trip(&fork_choice); + check_bytes_round_trip::(&fork_choice); } Operation::ProposerBoostFindHead { justified_checkpoint, @@ -153,7 +156,7 @@ impl ForkChoiceTestDefinition { "Operation at index {} failed head check. Operation: {:?}", op_index, op ); - check_bytes_round_trip(&fork_choice); + check_bytes_round_trip::(&fork_choice); } Operation::InvalidFindHead { justified_checkpoint, @@ -179,7 +182,7 @@ impl ForkChoiceTestDefinition { op_index, op ); - check_bytes_round_trip(&fork_choice); + check_bytes_round_trip::(&fork_choice); } Operation::ProcessBlock { slot, @@ -219,7 +222,7 @@ impl ForkChoiceTestDefinition { op_index, e ) }); - check_bytes_round_trip(&fork_choice); + check_bytes_round_trip::(&fork_choice); } Operation::ProcessAttestation { validator_index, @@ -234,7 +237,7 @@ impl ForkChoiceTestDefinition { op_index ) }); - check_bytes_round_trip(&fork_choice); + check_bytes_round_trip::(&fork_choice); } Operation::Prune { finalized_root, @@ -304,9 +307,9 @@ fn get_checkpoint(i: u64) -> Checkpoint { } } -fn check_bytes_round_trip(original: &ProtoArrayForkChoice) { +fn check_bytes_round_trip(original: &ProtoArrayForkChoice) { let bytes = original.as_bytes(); - let decoded = ProtoArrayForkChoice::from_bytes(&bytes, original.balances.clone()) + let decoded = ProtoArrayForkChoice::from_bytes::(&bytes, original.balances.clone()) .expect("fork choice should decode from bytes"); assert!( *original == decoded, diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 18af2dfc24c..5e37b4e6630 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -135,6 +135,12 @@ pub struct ProtoArray { pub nodes: Vec, pub indices: HashMap, pub previous_proposer_boost: ProposerBoost, + /// The block hash of the block used to initialize the ProtoArray. Invariants: + /// - At initialization there is always a ProtoNode for `anchor_block_root` + /// - After pruning there may not be a ProtoNode for `anchor_block_root` + /// - At any point there is either or both ProtoNodes for `anchor_block_root` and + /// `finalized_checkpoint.root` + pub anchor_block: (Hash256, Slot), } impl ProtoArray { @@ -963,10 +969,16 @@ impl ProtoArray { /// *checkpoint* not the finalized *block*. pub fn is_finalized_checkpoint_or_descendant(&self, root: Hash256) -> bool { let finalized_root = self.finalized_checkpoint.root; - let finalized_slot = self - .finalized_checkpoint - .epoch - .start_slot(E::slots_per_epoch()); + let (finalized_root, finalized_slot) = if self.indices.contains_key(&finalized_root) { + ( + finalized_root, + self.finalized_checkpoint + .epoch + .start_slot(E::slots_per_epoch()), + ) + } else { + self.anchor_block + }; let Some(mut node) = self .indices diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index dea853d245d..a9f364c30e6 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -414,13 +414,14 @@ impl ProtoArrayForkChoice { #[allow(clippy::too_many_arguments)] pub fn new( current_slot: Slot, - finalized_block_slot: Slot, - finalized_block_state_root: Hash256, + anchor_block_slot: Slot, + anchor_block_root: Hash256, + anchor_block_state_root: Hash256, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, - current_epoch_shuffling_id: AttestationShufflingId, - next_epoch_shuffling_id: AttestationShufflingId, - execution_status: ExecutionStatus, + anchor_block_current_epoch_shuffling_id: AttestationShufflingId, + anchor_block_next_epoch_shuffling_id: AttestationShufflingId, + anchor_block_execution_status: ExecutionStatus, ) -> Result { let mut proto_array = ProtoArray { prune_threshold: DEFAULT_PRUNE_THRESHOLD, @@ -429,21 +430,22 @@ impl ProtoArrayForkChoice { nodes: Vec::with_capacity(1), indices: HashMap::with_capacity(1), previous_proposer_boost: ProposerBoost::default(), + anchor_block: (anchor_block_root, anchor_block_slot), }; let block = Block { - slot: finalized_block_slot, - root: finalized_checkpoint.root, + slot: anchor_block_slot, + root: anchor_block_root, parent_root: None, - state_root: finalized_block_state_root, + state_root: anchor_block_state_root, // We are using the finalized_root as the target_root, since it always lies on an // epoch boundary. target_root: finalized_checkpoint.root, - current_epoch_shuffling_id, - next_epoch_shuffling_id, + current_epoch_shuffling_id: anchor_block_current_epoch_shuffling_id, + next_epoch_shuffling_id: anchor_block_next_epoch_shuffling_id, justified_checkpoint, finalized_checkpoint, - execution_status, + execution_status: anchor_block_execution_status, unrealized_justified_checkpoint: Some(justified_checkpoint), unrealized_finalized_checkpoint: Some(finalized_checkpoint), }; @@ -924,18 +926,20 @@ impl ProtoArrayForkChoice { SszContainer::from(self).as_ssz_bytes() } - pub fn from_bytes(bytes: &[u8], balances: JustifiedBalances) -> Result { + pub fn from_bytes( + bytes: &[u8], + balances: JustifiedBalances, + ) -> Result { let container = SszContainer::from_ssz_bytes(bytes) .map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e))?; - Self::from_container(container, balances) + Self::from_container::(container, balances) } - pub fn from_container( + pub fn from_container( container: SszContainer, balances: JustifiedBalances, ) -> Result { - (container, balances) - .try_into() + Self::from_ssz::(container, balances) .map_err(|e| format!("Failed to initialize ProtoArrayForkChoice: {e:?}")) } @@ -957,6 +961,15 @@ impl ProtoArrayForkChoice { pub fn heads_descended_from_finalization(&self) -> Vec<&ProtoNode> { self.proto_array.heads_descended_from_finalization::() } + + /// Returns the anchor_block_root + pub fn get_anchor_block_root(&self) -> Hash256 { + self.proto_array.anchor_block.0 + } + + pub fn get_anchor_block(&self) -> (Hash256, Slot) { + self.proto_array.anchor_block + } } /// Returns a list of `deltas`, where there is one delta for each of the indices in @@ -1098,6 +1111,7 @@ mod test_compute_deltas { let mut fc = ProtoArrayForkChoice::new::( genesis_slot, genesis_slot, + genesis_checkpoint.root, state_root, genesis_checkpoint, genesis_checkpoint, @@ -1224,6 +1238,7 @@ mod test_compute_deltas { let mut fc = ProtoArrayForkChoice::new::( genesis_slot, genesis_slot, + genesis_checkpoint.root, junk_state_root, genesis_checkpoint, genesis_checkpoint, diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index 0bb3f2b35d8..6f8678b7f53 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -8,7 +8,7 @@ use ssz::{Encode, four_byte_option_impl}; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; use superstruct::superstruct; -use types::{Checkpoint, Hash256}; +use types::{Checkpoint, EthSpec, Hash256}; // Define a "legacy" implementation of `Option` which uses four bytes for encoding the union // selector. @@ -49,10 +49,30 @@ impl From<&ProtoArrayForkChoice> for SszContainer { } } -impl TryFrom<(SszContainer, JustifiedBalances)> for ProtoArrayForkChoice { - type Error = Error; +impl ProtoArrayForkChoice { + pub fn from_ssz( + from: SszContainer, + balances: JustifiedBalances, + ) -> Result { + let anchor_block = if from + .nodes + .iter() + .any(|node| node.root == from.finalized_checkpoint.root) + { + // There's a node for the finalized checkpoint, use that as anchor block + ( + from.finalized_checkpoint.root, + from.finalized_checkpoint + .epoch + .start_slot(E::slots_per_epoch()), + ) + } else { + // Otherwise we initialized fork-choice from a block more recent than finalization and + // have not finalized yet. Use the first node as anchor block. + let anchor_node = from.nodes.first().ok_or(Error::EmptyNodes)?; + (anchor_node.root, anchor_node.slot) + }; - fn try_from((from, balances): (SszContainer, JustifiedBalances)) -> Result { let proto_array = ProtoArray { prune_threshold: from.prune_threshold, justified_checkpoint: from.justified_checkpoint, @@ -60,6 +80,7 @@ impl TryFrom<(SszContainer, JustifiedBalances)> for ProtoArrayForkChoice { nodes: from.nodes, indices: from.indices.into_iter().collect::>(), previous_proposer_boost: from.previous_proposer_boost, + anchor_block, }; Ok(Self {