// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // This file is part of Pezcumulus. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 // Pezcumulus is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Pezcumulus is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Pezcumulus. If not, see . use codec::Decode; use pezkuwi_primitives::Hash as RelayHash; use pezcumulus_primitives_core::{ relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption}, ParaId, }; use pezcumulus_relay_chain_interface::{RelayChainError, RelayChainInterface}; use pezsc_client_api::{Backend, HeaderBackend}; use pezsp_blockchain::{Backend as BlockchainBackend, TreeRoute}; use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT}; const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents"; /// Parameters when searching for suitable parents to build on top of. #[derive(Debug)] pub struct ParentSearchParams { /// The relay-parent that is intended to be used. pub relay_parent: RelayHash, /// The ID of the teyrchain. pub para_id: ParaId, /// A limitation on the age of relay parents for teyrchain blocks that are being /// considered. This is relative to the `relay_parent` number. pub ancestry_lookback: usize, /// How "deep" parents can be relative to the included teyrchain block at the relay-parent. /// The included block has depth 0. pub max_depth: usize, /// Whether to only ignore "alternative" branches, i.e. branches of the chain /// which do not contain the block pending availability. pub ignore_alternative_branches: bool, } /// A potential parent block returned from [`find_potential_parents`] #[derive(PartialEq)] pub struct PotentialParent { /// The hash of the block. pub hash: B::Hash, /// The header of the block. pub header: B::Header, /// The depth of the block with respect to the included block. pub depth: usize, /// Whether the block is the included block, is itself pending on-chain, or descends /// from the block pending availability. pub aligned_with_pending: bool, } impl std::fmt::Debug for PotentialParent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PotentialParent") .field("hash", &self.hash) .field("depth", &self.depth) .field("aligned_with_pending", &self.aligned_with_pending) .field("number", &self.header.number()) .finish() } } /// Perform a recursive search through blocks to find potential /// parent blocks for a new block. /// /// This accepts a relay-chain block to be used as an anchor and a maximum search depth, /// along with some arguments for filtering teyrchain blocks and performs a recursive search /// for teyrchain blocks. The search begins at the last included teyrchain block and returns /// a set of [`PotentialParent`]s which could be potential parents of a new block with this /// relay-parent according to the search parameters. /// /// A teyrchain block is a potential parent if it is either the last included teyrchain block, the /// pending teyrchain block (when `max_depth` >= 1), or all of the following hold: /// * its parent is a potential parent /// * its relay-parent is within `ancestry_lookback` of the targeted relay-parent. /// * its relay-parent is within the same session as the targeted relay-parent. /// * the block number is within `max_depth` blocks of the included block pub async fn find_potential_parents( params: ParentSearchParams, backend: &impl Backend, relay_client: &impl RelayChainInterface, ) -> Result>, RelayChainError> { tracing::trace!("Parent search parameters: {params:?}"); // Get the included block. let Some((included_header, included_hash)) = fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent) .await? else { return Ok(Default::default()); }; let only_included = vec![PotentialParent { hash: included_hash, header: included_header.clone(), depth: 0, aligned_with_pending: true, }]; if params.max_depth == 0 { return Ok(only_included); }; // Pending header and hash. let maybe_pending = { // Fetch the most recent pending header from the relay chain. We use // `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted // before being returned to us. let pending_header = relay_client .persisted_validation_data( params.relay_parent, params.para_id, OccupiedCoreAssumption::Included, ) .await? .and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok()) .filter(|x| x.hash() != included_hash); // If the pending block is not locally known, we can't do anything. if let Some(header) = pending_header { let pending_hash = header.hash(); match backend.blockchain().header(pending_hash) { // We are supposed to ignore branches that don't contain the pending block, but we // do not know the pending block locally. Ok(None) | Err(_) if params.ignore_alternative_branches => { tracing::warn!( target: PARENT_SEARCH_LOG_TARGET, %pending_hash, "Failed to get header for pending block.", ); return Ok(Default::default()); }, Ok(Some(_)) => Some((header, pending_hash)), _ => None, } } else { None } }; let maybe_route_to_last_pending = maybe_pending .as_ref() .map(|(_, pending)| { pezsp_blockchain::tree_route(backend.blockchain(), included_hash, *pending) }) .transpose()?; // If we want to ignore alternative branches there is no reason to start // the parent search at the included block. We can add the included block and // the path to the pending block to the potential parents directly (limited by max_depth). let (frontier, potential_parents) = match ( &maybe_pending, params.ignore_alternative_branches, &maybe_route_to_last_pending, ) { (Some((pending_header, pending_hash)), true, Some(ref route_to_pending)) => { let mut potential_parents = only_included; // This is a defensive check, should never happen. if !route_to_pending.retracted().is_empty() { tracing::warn!(target: PARENT_SEARCH_LOG_TARGET, "Included block not an ancestor of pending block. This should not happen."); return Ok(Default::default()); } // Add all items on the path included -> pending - 1 to the potential parents, but // not more than `max_depth`. let num_parents_on_path = route_to_pending.enacted().len().saturating_sub(1).min(params.max_depth); for (num, block) in route_to_pending.enacted().iter().take(num_parents_on_path).enumerate() { let Ok(Some(header)) = backend.blockchain().header(block.hash) else { continue }; potential_parents.push(PotentialParent { hash: block.hash, header, depth: 1 + num, aligned_with_pending: true, }); } // The search for additional potential parents should now start at the children of // the pending block. ( vec![PotentialParent { hash: *pending_hash, header: pending_header.clone(), depth: route_to_pending.enacted().len(), aligned_with_pending: true, }], potential_parents, ) }, _ => (only_included, Default::default()), }; if potential_parents.len() > params.max_depth { return Ok(potential_parents); } // Build up the ancestry record of the relay chain to compare against. let rp_ancestry = build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client) .await?; Ok(search_child_branches_for_parents( frontier, maybe_route_to_last_pending, included_header, maybe_pending.map(|(_, hash)| hash), backend, params.max_depth, params.ignore_alternative_branches, rp_ancestry, potential_parents, )) } /// Fetch the included block from the relay chain. async fn fetch_included_from_relay_chain( relay_client: &impl RelayChainInterface, backend: &impl Backend, para_id: ParaId, relay_parent: RelayHash, ) -> Result, RelayChainError> { // Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut` // so that even if there is a pending candidate, we assume it is timed out and we get the // included head. let included_header = relay_client .persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut) .await?; let included_header = match included_header { Some(pvd) => pvd.parent_head, None => return Ok(None), // this implies the para doesn't exist. }; let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() { None => return Ok(None), Some(x) => x, }; let included_hash = included_header.hash(); // If the included block is not locally known, we can't do anything. match backend.blockchain().header(included_hash) { Ok(None) => { tracing::warn!( target: PARENT_SEARCH_LOG_TARGET, %included_hash, "Failed to get header for included block.", ); return Ok(None); }, Err(e) => { tracing::warn!( target: PARENT_SEARCH_LOG_TARGET, %included_hash, %e, "Failed to get header for included block.", ); return Ok(None); }, _ => {}, }; Ok(Some((included_header, included_hash))) } /// Build an ancestry of relay parents that are acceptable. /// /// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the /// relay parent we want to build on. Teyrchain blocks anchored on relay parents older than that can /// not be considered potential parents for block building. They have no chance of still getting /// included, so our newly build teyrchain block would also not get included. /// /// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain /// ancestry blocks. async fn build_relay_parent_ancestry( ancestry_lookback: usize, relay_parent: RelayHash, relay_client: &impl RelayChainInterface, ) -> Result, RelayChainError> { let mut ancestry = Vec::with_capacity(ancestry_lookback + 1); let mut current_rp = relay_parent; let mut required_session = None; while ancestry.len() <= ancestry_lookback { let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break }; let session = relay_client.session_index_for_child(current_rp).await?; if required_session.get_or_insert(session) != &session { // Respect the relay-chain rule not to cross session boundaries. break; } ancestry.push((current_rp, *header.state_root())); current_rp = *header.parent_hash(); // don't iterate back into the genesis block. if header.number == 1 { break; } } Ok(ancestry) } /// Start search for child blocks that can be used as parents. pub fn search_child_branches_for_parents( mut frontier: Vec>, maybe_route_to_last_pending: Option>, included_header: Block::Header, pending_hash: Option, backend: &impl Backend, max_depth: usize, ignore_alternative_branches: bool, rp_ancestry: Vec<(RelayHash, RelayHash)>, mut potential_parents: Vec>, ) -> Vec> { let included_hash = included_header.hash(); let is_hash_in_ancestry = |hash| rp_ancestry.iter().any(|x| x.0 == hash); let is_root_in_ancestry = |root| rp_ancestry.iter().any(|x| x.1 == root); // The distance between pending and included block. Is later used to check if a child // is aligned with pending when it is between pending and included block. let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len()); // If a block is on the path included -> pending, we consider it `aligned_with_pending`. let is_child_pending = |hash| { maybe_route_to_last_pending .as_ref() .map_or(true, |route| route.enacted().iter().any(|x| x.hash == hash)) }; tracing::trace!( target: PARENT_SEARCH_LOG_TARGET, ?included_hash, included_num = ?included_header.number(), ?pending_hash , ?rp_ancestry, "Searching relay chain ancestry." ); while let Some(entry) = frontier.pop() { let is_pending = pending_hash.as_ref().map_or(false, |h| &entry.hash == h); let is_included = included_hash == entry.hash; // note: even if the pending block or included block have a relay parent // outside of the expected part of the relay chain, they are always allowed // because they have already been posted on chain. let is_potential = is_pending || is_included || { let digest = entry.header.digest(); let is_hash_in_ancestry_check = pezcumulus_primitives_core::extract_relay_parent(digest) .map_or(false, is_hash_in_ancestry); let is_root_in_ancestry_check = pezcumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest) .map(|(r, _n)| r) .map_or(false, is_root_in_ancestry); is_hash_in_ancestry_check || is_root_in_ancestry_check }; let parent_aligned_with_pending = entry.aligned_with_pending; let child_depth = entry.depth + 1; let hash = entry.hash; tracing::trace!( target: PARENT_SEARCH_LOG_TARGET, ?hash, is_potential, is_pending, is_included, "Checking potential parent." ); if is_potential { potential_parents.push(entry); } if !is_potential || child_depth > max_depth { continue; } // push children onto search frontier. for child in backend.blockchain().children(hash).ok().into_iter().flatten() { tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, child_depth, ?pending_distance, "Looking at child."); let aligned_with_pending = parent_aligned_with_pending && (pending_distance.map_or(true, |dist| child_depth > dist) || is_child_pending(child)); if ignore_alternative_branches && !aligned_with_pending { tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, "Child is not aligned with pending block."); continue; } let Ok(Some(header)) = backend.blockchain().header(child) else { continue }; frontier.push(PotentialParent { hash: child, header, depth: child_depth, aligned_with_pending, }); } } potential_parents }