From 6df4d6e779cef81cac2b6b938eebf383616107fc Mon Sep 17 00:00:00 2001 From: Alexander Lednev <57529355+iceseer@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:48:17 +0300 Subject: [PATCH] Feature/fragment chain (#2207) * candidate storage Signed-off-by: iceseer * candidate storage tests Signed-off-by: iceseer * fragment Signed-off-by: iceseer * constraints Signed-off-by: iceseer * backed chain Signed-off-by: iceseer * scope Signed-off-by: iceseer * fragment node Signed-off-by: iceseer * fragment chain Signed-off-by: iceseer * scope tests Signed-off-by: iceseer * fragment chain test Signed-off-by: iceseer * backing implicit view fixup Signed-off-by: iceseer * prospective parachains Signed-off-by: iceseer * backing implicit view reqork Signed-off-by: iceseer * parachain core Signed-off-by: iceseer * fragment chain errors Signed-off-by: iceseer * remove fragment tree Signed-off-by: iceseer * remove redundant logs Signed-off-by: iceseer --------- Signed-off-by: turuslan Signed-off-by: iceseer --- core/log/configurator.cpp | 1 + core/network/peer_state.hpp | 11 +- core/parachain/CMakeLists.txt | 4 +- core/parachain/backing/grid_tracker.cpp | 24 +- core/parachain/backing/grid_tracker.hpp | 7 +- core/parachain/types.hpp | 15 +- .../validator/backing_implicit_view.cpp | 161 +- .../validator/backing_implicit_view.hpp | 102 +- core/parachain/validator/collations.hpp | 137 +- core/parachain/validator/fragment_tree.hpp | 1079 ------ .../validator/impl/parachain_processor.cpp | 506 ++- .../validator/parachain_processor.hpp | 45 +- .../validator/prospective_parachains.hpp | 816 ----- .../prospective_parachains/CMakeLists.txt | 26 + .../prospective_parachains/backed_chain.cpp | 61 + .../prospective_parachains/backed_chain.hpp | 32 + .../candidate_storage.cpp | 243 ++ .../candidate_storage.hpp | 145 + .../prospective_parachains/common.hpp | 115 + .../constraints.cpp} | 137 +- .../prospective_parachains/fragment.cpp | 193 + .../prospective_parachains/fragment.hpp | 69 + .../prospective_parachains/fragment_chain.cpp | 482 +++ .../prospective_parachains/fragment_chain.hpp | 268 ++ .../fragment_chain_errors.cpp | 47 + .../fragment_chain_errors.hpp | 31 + .../prospective_parachains/fragment_node.hpp | 37 + .../prospective_parachains.cpp | 824 +++++ .../prospective_parachains.hpp | 161 + .../prospective_parachains/scope.cpp | 94 + .../prospective_parachains/scope.hpp | 87 + test/core/parachain/CMakeLists.txt | 50 +- test/core/parachain/candidate_storage.cpp | 153 + test/core/parachain/fragment_chain.cpp | 1259 +++++++ .../core/parachain/parachain_test_harness.hpp | 153 + .../core/parachain/prospective_parachains.cpp | 3236 +---------------- test/core/parachain/scope.cpp | 145 + 37 files changed, 5366 insertions(+), 5590 deletions(-) delete mode 100644 core/parachain/validator/fragment_tree.hpp delete mode 100644 core/parachain/validator/prospective_parachains.hpp create mode 100644 core/parachain/validator/prospective_parachains/CMakeLists.txt create mode 100644 core/parachain/validator/prospective_parachains/backed_chain.cpp create mode 100644 core/parachain/validator/prospective_parachains/backed_chain.hpp create mode 100644 core/parachain/validator/prospective_parachains/candidate_storage.cpp create mode 100644 core/parachain/validator/prospective_parachains/candidate_storage.hpp create mode 100644 core/parachain/validator/prospective_parachains/common.hpp rename core/parachain/validator/{impl/fragment_tree.cpp => prospective_parachains/constraints.cpp} (50%) create mode 100644 core/parachain/validator/prospective_parachains/fragment.cpp create mode 100644 core/parachain/validator/prospective_parachains/fragment.hpp create mode 100644 core/parachain/validator/prospective_parachains/fragment_chain.cpp create mode 100644 core/parachain/validator/prospective_parachains/fragment_chain.hpp create mode 100644 core/parachain/validator/prospective_parachains/fragment_chain_errors.cpp create mode 100644 core/parachain/validator/prospective_parachains/fragment_chain_errors.hpp create mode 100644 core/parachain/validator/prospective_parachains/fragment_node.hpp create mode 100644 core/parachain/validator/prospective_parachains/prospective_parachains.cpp create mode 100644 core/parachain/validator/prospective_parachains/prospective_parachains.hpp create mode 100644 core/parachain/validator/prospective_parachains/scope.cpp create mode 100644 core/parachain/validator/prospective_parachains/scope.hpp create mode 100644 test/core/parachain/candidate_storage.cpp create mode 100644 test/core/parachain/fragment_chain.cpp create mode 100644 test/core/parachain/parachain_test_harness.hpp create mode 100644 test/core/parachain/scope.cpp diff --git a/core/log/configurator.cpp b/core/log/configurator.cpp index bd8049f09f..e3a91dd494 100644 --- a/core/log/configurator.cpp +++ b/core/log/configurator.cpp @@ -70,6 +70,7 @@ namespace kagome::log { - name: parachain children: - name: pvf_executor + - name: fragment_chain - name: dispute - name: runtime children: diff --git a/core/network/peer_state.hpp b/core/network/peer_state.hpp index a677232a0c..13f23bea91 100644 --- a/core/network/peer_state.hpp +++ b/core/network/peer_state.hpp @@ -17,6 +17,7 @@ #include "consensus/grandpa/common.hpp" #include "network/types/collator_messages_vstaging.hpp" +#include "network/types/roles.hpp" #include "outcome/outcome.hpp" #include "parachain/validator/backing_implicit_view.hpp" #include "primitives/common.hpp" @@ -70,8 +71,11 @@ namespace kagome::network { const View &new_view, const parachain::ImplicitView &local_implicit) { std::unordered_set next_implicit; for (const auto &x : new_view.heads_) { - auto t = local_implicit.knownAllowedRelayParentsUnder(x, std::nullopt); - next_implicit.insert(t.begin(), t.end()); + auto t = + local_implicit.known_allowed_relay_parents_under(x, std::nullopt); + if (t) { + next_implicit.insert(t->begin(), t->end()); + } } std::vector fresh_implicit; @@ -156,8 +160,7 @@ namespace kagome::network { template <> struct std::hash { - size_t operator()( - const kagome::network::FetchedCollation &value) const { + size_t operator()(const kagome::network::FetchedCollation &value) const { using CollatorId = kagome::parachain::CollatorId; using CandidateHash = kagome::parachain::CandidateHash; using RelayHash = kagome::parachain::RelayHash; diff --git a/core/parachain/CMakeLists.txt b/core/parachain/CMakeLists.txt index 667c101c65..0a2fa031b4 100644 --- a/core/parachain/CMakeLists.txt +++ b/core/parachain/CMakeLists.txt @@ -4,6 +4,8 @@ # SPDX-License-Identifier: Apache-2.0 # +add_subdirectory(validator/prospective_parachains) + add_library(grid_tracker backing/grid_tracker.cpp ) @@ -38,7 +40,6 @@ add_library(validator_parachain approval/approval.cpp backing/store_impl.cpp backing/cluster.cpp - validator/impl/fragment_tree.cpp validator/backing_implicit_view.cpp ) @@ -52,6 +53,7 @@ target_link_libraries(validator_parachain waitable_timer kagome_pvf_worker runtime_common + prospective_parachains ) add_library(kagome_pvf_worker diff --git a/core/parachain/backing/grid_tracker.cpp b/core/parachain/backing/grid_tracker.cpp index c5cf1164e7..e4e30a1300 100644 --- a/core/parachain/backing/grid_tracker.cpp +++ b/core/parachain/backing/grid_tracker.cpp @@ -9,10 +9,16 @@ OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::grid, GridTracker::Error, e) { using E = kagome::parachain::grid::GridTracker::Error; switch (e) { - case E::DISALLOWED: - return "Manifest disallowed"; - case E::MALFORMED: - return "Malformed"; + case E::DISALLOWED_GROUP_INDEX: + return "Manifest disallowed group index"; + case E::DISALLOWED_DIRECTION: + return "Manifest disallowed direction"; + case E::MALFORMED_BACKING_THRESHOLD: + return "Malformed backing threshold"; + case E::MALFORMED_REMOTE_KNOWLEDGE_LEN: + return "Malformed remote knowledge len"; + case E::MALFORMED_HAS_SECONDED: + return "Malformed has seconded"; case E::INSUFFICIENT: return "Insufficient"; case E::CONFLICTING: @@ -115,7 +121,7 @@ namespace kagome::parachain::grid { ValidatorIndex sender) { const auto claimed_group_index = manifest.claimed_group_index; if (claimed_group_index >= session_topology.size()) { - return Error::DISALLOWED; + return Error::DISALLOWED_GROUP_INDEX; } const auto &group_topology = session_topology[manifest.claimed_group_index]; @@ -131,22 +137,22 @@ namespace kagome::parachain::grid { && it->second.has_sent_manifest_to(sender); } if (!manifest_allowed) { - return Error::DISALLOWED; + return Error::DISALLOWED_DIRECTION; } auto group_size_backing_threshold = groups.get_size_and_backing_threshold(manifest.claimed_group_index); if (!group_size_backing_threshold) { - return Error::MALFORMED; + return Error::MALFORMED_BACKING_THRESHOLD; } const auto [group_size, backing_threshold] = *group_size_backing_threshold; auto remote_knowledge = manifest.statement_knowledge; if (!remote_knowledge.has_len(group_size)) { - return Error::MALFORMED; + return Error::MALFORMED_REMOTE_KNOWLEDGE_LEN; } if (!remote_knowledge.has_seconded()) { - return Error::MALFORMED; + return Error::MALFORMED_HAS_SECONDED; } const auto votes = remote_knowledge.backing_validators(); diff --git a/core/parachain/backing/grid_tracker.hpp b/core/parachain/backing/grid_tracker.hpp index a5f3ac8bad..c4de09cae1 100644 --- a/core/parachain/backing/grid_tracker.hpp +++ b/core/parachain/backing/grid_tracker.hpp @@ -176,8 +176,11 @@ namespace kagome::parachain::grid { /// relay-parent. struct GridTracker { enum class Error { - DISALLOWED = 1, - MALFORMED, + DISALLOWED_GROUP_INDEX = 1, + DISALLOWED_DIRECTION, + MALFORMED_BACKING_THRESHOLD, + MALFORMED_REMOTE_KNOWLEDGE_LEN, + MALFORMED_HAS_SECONDED, INSUFFICIENT, CONFLICTING, SECONDING_OVERFLOW diff --git a/core/parachain/types.hpp b/core/parachain/types.hpp index d6a22b1408..f3843f8b46 100644 --- a/core/parachain/types.hpp +++ b/core/parachain/types.hpp @@ -103,9 +103,11 @@ namespace kagome::network { struct OutboundHorizontal { SCALE_TIE(2); - parachain::ParachainId para_id; /// Parachain Id is recepient id - parachain::UpwardMessage - upward_msg; /// upward message for parallel parachain + /// The para that will get this message in its downward message queue. + parachain::ParachainId recipient; + + /// The message payload. + common::Buffer data; }; struct InboundDownwardMessage { @@ -324,10 +326,11 @@ namespace kagome::parachain::fragment { std::optional> future_validation_code; - outcome::result applyModifications( + outcome::result apply_modifications( const ConstraintModifications &modifications) const; - bool checkModifications(const ConstraintModifications &modifications) const; + outcome::result check_modifications( + const ConstraintModifications &modifications) const; }; struct BackingState { @@ -357,3 +360,5 @@ namespace kagome::parachain::fragment { }; } // namespace kagome::parachain::fragment + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, Constraints::Error); diff --git a/core/parachain/validator/backing_implicit_view.cpp b/core/parachain/validator/backing_implicit_view.cpp index 09c9c509a2..7021e6c901 100644 --- a/core/parachain/validator/backing_implicit_view.cpp +++ b/core/parachain/validator/backing_implicit_view.cpp @@ -5,27 +5,42 @@ */ #include "parachain/validator/backing_implicit_view.hpp" +#include "parachain/validator/prospective_parachains/prospective_parachains.hpp" #include #include "parachain/types.hpp" #include "primitives/math.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT BackingImplicitView +#define COMPONENT_NAME STRINGIFY(COMPONENT) OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain, ImplicitView::Error, e) { using E = decltype(e); switch (e) { case E::ALREADY_KNOWN: - return "Already known leaf"; + return COMPONENT_NAME ": Already known leaf"; + case E::NOT_INITIALIZED_WITH_PROSPECTIVE_PARACHAINS: + return COMPONENT_NAME ": Not initialized with prospective parachains"; } - return "ImplicitView failed."; + return COMPONENT_NAME ": unknown error"; } namespace kagome::parachain { ImplicitView::ImplicitView( - std::shared_ptr prospective_parachains) - : prospective_parachains_{std::move(prospective_parachains)} { - BOOST_ASSERT(prospective_parachains_); + std::weak_ptr prospective_parachains, + std::shared_ptr parachain_host_, + std::shared_ptr block_tree, + std::optional collating_for_) + : parachain_host(std::move(parachain_host_)), + collating_for{collating_for_}, + prospective_parachains_{std::move(prospective_parachains)}, + block_tree_{std::move(block_tree)} { + BOOST_ASSERT(!prospective_parachains_.expired()); + BOOST_ASSERT(parachain_host); + BOOST_ASSERT(block_tree_); } std::span @@ -49,7 +64,51 @@ namespace kagome::parachain { return {}; } - std::span ImplicitView::knownAllowedRelayParentsUnder( + void ImplicitView::activate_leaf_from_prospective_parachains( + fragment::BlockInfoProspectiveParachains leaf, + const std::vector &ancestors) { + if (leaves.contains(leaf.hash)) { + return; + } + + const auto retain_minimum = + std::min(ancestors.empty() ? 0 : ancestors.back().number, + math::sat_sub_unsigned(leaf.number, MINIMUM_RETAIN_LENGTH)); + + leaves.insert_or_assign(leaf.hash, + ActiveLeafPruningInfo{ + .retain_minimum = retain_minimum, + }); + AllowedRelayParents allowed_relay_parents{ + .minimum_relay_parents = {}, + .allowed_relay_parents_contiguous = {}, + }; + allowed_relay_parents.allowed_relay_parents_contiguous.reserve( + ancestors.size()); + + for (const auto &ancestor : ancestors) { + block_info_storage.insert_or_assign( + ancestor.hash, + BlockInfo{ + .block_number = ancestor.number, + .maybe_allowed_relay_parents = {}, + .parent_hash = ancestor.parent_hash, + }); + allowed_relay_parents.allowed_relay_parents_contiguous.emplace_back( + ancestor.hash); + } + + block_info_storage.insert_or_assign( + leaf.hash, + BlockInfo{ + .block_number = leaf.number, + .maybe_allowed_relay_parents = allowed_relay_parents, + .parent_hash = leaf.parent_hash, + }); + } + + std::optional> + ImplicitView::known_allowed_relay_parents_under( const Hash &block_hash, const std::optional ¶_id) const { if (auto it = block_info_storage.find(block_hash); it != block_info_storage.end()) { @@ -59,7 +118,7 @@ namespace kagome::parachain { para_id, block_info.block_number); } } - return {}; + return std::nullopt; } std::vector ImplicitView::deactivate_leaf(const Hash &leaf_hash) { @@ -88,8 +147,7 @@ namespace kagome::parachain { return removed; } - outcome::result> ImplicitView::activate_leaf( - const Hash &leaf_hash) { + outcome::result ImplicitView::activate_leaf(const Hash &leaf_hash) { if (leaves.contains(leaf_hash)) { return Error::ALREADY_KNOWN; } @@ -99,28 +157,80 @@ namespace kagome::parachain { fetched.minimum_ancestor_number, math::sat_sub_unsigned(fetched.leaf_number, MINIMUM_RETAIN_LENGTH)); - leaves.emplace(leaf_hash, - ActiveLeafPruningInfo{.retain_minimum = retain_minimum}); - return fetched.relevant_paras; + leaves.insert_or_assign( + leaf_hash, ActiveLeafPruningInfo{.retain_minimum = retain_minimum}); + return outcome::success(); + } + + outcome::result> + ImplicitView::fetch_min_relay_parents_for_collator(const Hash &leaf_hash, + BlockNumber leaf_number) { + auto prospective_parachains = prospective_parachains_.lock(); + if (!prospective_parachains) { + return outcome::failure( + Error::NOT_INITIALIZED_WITH_PROSPECTIVE_PARACHAINS); + } + + size_t allowed_ancestry_len = 0; + if (auto mode = + prospective_parachains->prospectiveParachainsMode(leaf_hash)) { + allowed_ancestry_len = mode->allowed_ancestry_len; + } else { + return std::nullopt; + } + + BlockNumber min = leaf_number; + OUTCOME_TRY(required_session, + parachain_host->session_index_for_child(leaf_hash)); + OUTCOME_TRY(hashes, + block_tree_->getDescendingChainToBlock( + leaf_hash, allowed_ancestry_len + 1)); + + for (size_t i = 1; i < hashes.size(); ++i) { + const auto &hash = hashes[i]; + OUTCOME_TRY(session, parachain_host->session_index_for_child(hash)); + + if (session == required_session) { + min = math::sat_sub_unsigned(min, BlockNumber(1)); + } else { + break; + } + } + + return min; } outcome::result ImplicitView::fetch_fresh_leaf_and_insert_ancestry(const Hash &leaf_hash) { - std::vector> min_relay_parents_raw = - prospective_parachains_->answerMinimumRelayParentsRequest(leaf_hash); + auto prospective_parachains = prospective_parachains_.lock(); + if (!prospective_parachains) { + return Error::NOT_INITIALIZED_WITH_PROSPECTIVE_PARACHAINS; + } + std::shared_ptr block_tree = - prospective_parachains_->getBlockTree(); + prospective_parachains->getBlockTree(); OUTCOME_TRY(leaf_header, block_tree->getBlockHeader(leaf_hash)); - BlockNumber min_min = min_relay_parents_raw.empty() - ? leaf_header.number - : min_relay_parents_raw[0].second; - std::vector relevant_paras; - relevant_paras.reserve(min_relay_parents_raw.size()); - for (auto &min_relay_parent : min_relay_parents_raw) { - min_min = std::min(min_relay_parent.second, min_min); - relevant_paras.emplace_back(min_relay_parent.first); + std::vector> min_relay_parents; + if (collating_for) { + OUTCOME_TRY( + mrp, + fetch_min_relay_parents_for_collator(leaf_hash, leaf_header.number)); + if (mrp) { + min_relay_parents.emplace_back(*collating_for, *mrp); + } + } else { + min_relay_parents = + prospective_parachains->answerMinimumRelayParentsRequest(leaf_hash); + } + + BlockNumber min_min = leaf_header.number; + if (!min_relay_parents.empty()) { + min_min = min_relay_parents.front().second; + for (const auto &[_, x] : min_relay_parents) { + min_min = std::min(x, min_min); + } } const size_t expected_ancestry_len = @@ -168,8 +278,8 @@ namespace kagome::parachain { .block_number = leaf_header.number, .maybe_allowed_relay_parents = AllowedRelayParents{ - .minimum_relay_parents = {min_relay_parents_raw.begin(), - min_relay_parents_raw.end()}, + .minimum_relay_parents = {min_relay_parents.begin(), + min_relay_parents.end()}, .allowed_relay_parents_contiguous = std::move(ancestry), }, .parent_hash = leaf_header.parent_hash, @@ -177,7 +287,6 @@ namespace kagome::parachain { return FetchSummary{ .minimum_ancestor_number = min_min, .leaf_number = leaf_header.number, - .relevant_paras = relevant_paras, }; } diff --git a/core/parachain/validator/backing_implicit_view.hpp b/core/parachain/validator/backing_implicit_view.hpp index f109fa7897..dbaa04f226 100644 --- a/core/parachain/validator/backing_implicit_view.hpp +++ b/core/parachain/validator/backing_implicit_view.hpp @@ -7,36 +7,102 @@ #pragma once #include +#include #include #include #include +#include "blockchain/block_tree.hpp" +#include "blockchain/block_tree_error.hpp" #include "parachain/types.hpp" -#include "parachain/validator/prospective_parachains.hpp" +#include "parachain/validator/prospective_parachains/common.hpp" #include "primitives/common.hpp" +#include "runtime/runtime_api/parachain_host.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" namespace kagome::parachain { + class ProspectiveParachains; + // Always aim to retain 1 block before the active leaves. constexpr BlockNumber MINIMUM_RETAIN_LENGTH = 2ull; struct ImplicitView { - enum Error { + enum Error : uint8_t { ALREADY_KNOWN, + NOT_INITIALIZED_WITH_PROSPECTIVE_PARACHAINS }; struct FetchSummary { BlockNumber minimum_ancestor_number; BlockNumber leaf_number; - std::vector relevant_paras; }; - std::span knownAllowedRelayParentsUnder( + /// Get the known, allowed relay-parents that are valid for parachain + /// candidates which could be backed in a child of a given block for a given + /// para ID. + /// + /// This is expressed as a contiguous slice of relay-chain block hashes + /// which may include the provided block hash itself. + /// + /// If `para_id` is `None`, this returns all valid relay-parents across all + /// paras for the leaf. + /// + /// `None` indicates that the block hash isn't part of the implicit view or + /// that there are no known allowed relay parents. + /// + /// This always returns `Some` for active leaves or for blocks that + /// previously were active leaves. + /// + /// This can return the empty slice, which indicates that no relay-parents + /// are allowed for the para, e.g. if the para is not scheduled at the given + /// block hash. + std::optional> known_allowed_relay_parents_under( const Hash &block_hash, const std::optional ¶_id) const; - outcome::result> activate_leaf( - const Hash &leaf_hash); + + /// Activate a leaf in the view. To be used by the prospective parachains + /// subsystem. + /// + /// This will not request any additional data, as prospective parachains + /// already provides all the required info. NOTE: using `activate_leaf` + /// instead of this function will result in a deadlock, as it calls + /// prospective-parachains under the hood. + /// + /// No-op for known leaves. + void activate_leaf_from_prospective_parachains( + fragment::BlockInfoProspectiveParachains leaf, + const std::vector &ancestors); + + /// Activate a leaf in the view. + /// This will request the minimum relay parents the leaf and will load + /// headers in the ancestry of the leaf as needed. These are the 'implicit + /// ancestors' of the leaf. + /// + /// To maximize reuse of outdated leaves, it's best to activate new leaves + /// before deactivating old ones. + /// + /// The allowed relay parents for the relevant paras under this leaf can be + /// queried with [`View::known_allowed_relay_parents_under`]. + /// + /// No-op for known leaves. + outcome::result activate_leaf(const Hash &leaf_hash); + + /// Deactivate a leaf in the view. This prunes any outdated implicit + /// ancestors as well. + /// + /// Returns hashes of blocks pruned from storage. std::vector deactivate_leaf(const Hash &leaf_hash); + + /// Get an iterator over all allowed relay-parents in the view with no + /// particular order. + /// + /// **Important**: not all blocks are guaranteed to be allowed for some + /// leaves, it may happen that a block info is only kept in the view storage + /// because of a retaining rule. + /// + /// For getting relay-parents that are valid for parachain candidates use + /// [`View::known_allowed_relay_parents_under`]. std::vector all_allowed_relay_parents() const { std::vector r; r.reserve(block_info_storage.size()); @@ -46,6 +112,9 @@ namespace kagome::parachain { return r; } + /// Trace print of all internal buffers. + /// + /// Usable for tracing memory consumption. void printStoragesLoad() { SL_TRACE(logger, "[Backing implicit view statistics]:" @@ -55,7 +124,10 @@ namespace kagome::parachain { block_info_storage.size()); } - ImplicitView(std::shared_ptr prospective_parachains); + ImplicitView(std::weak_ptr prospective_parachains, + std::shared_ptr parachain_host_, + std::shared_ptr block_tree, + std::optional collating_for_); private: struct ActiveLeafPruningInfo { @@ -77,13 +149,21 @@ namespace kagome::parachain { Hash parent_hash; }; + outcome::result fetch_fresh_leaf_and_insert_ancestry( + const Hash &leaf_hash); + + outcome::result> + fetch_min_relay_parents_for_collator(const Hash &leaf_hash, + BlockNumber leaf_number); + std::unordered_map leaves; std::unordered_map block_info_storage; - std::shared_ptr prospective_parachains_; - log::Logger logger = log::createLogger("BackingImplicitView", "parachain"); + std::shared_ptr parachain_host; + std::optional collating_for; - outcome::result fetch_fresh_leaf_and_insert_ancestry( - const Hash &leaf_hash); + std::weak_ptr prospective_parachains_; + std::shared_ptr block_tree_; + log::Logger logger = log::createLogger("BackingImplicitView", "parachain"); }; } // namespace kagome::parachain diff --git a/core/parachain/validator/collations.hpp b/core/parachain/validator/collations.hpp index 6032e3c6cc..216324d1bf 100644 --- a/core/parachain/validator/collations.hpp +++ b/core/parachain/validator/collations.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "crypto/type_hasher.hpp" #include "log/logger.hpp" @@ -32,18 +33,27 @@ namespace kagome::parachain { }; using ProspectiveParachainsModeOpt = std::optional; - struct ActiveLeafState { - ProspectiveParachainsModeOpt prospective_parachains_mode; - /// The candidates seconded at various depths under this active - /// leaf with respect to parachain id. A candidate can only be - /// seconded when its hypothetical frontier under every active leaf - /// has an empty entry in this map. - /// - /// When prospective parachains are disabled, the only depth - /// which is allowed is 0. - std::unordered_map> - seconded_at_depth; - }; + using SecondedList = std::unordered_set; + using ActiveLeafState = + boost::variant; + + inline ProspectiveParachainsModeOpt from(const ActiveLeafState &state) { + return visit_in_place( + state, + [](const ProspectiveParachainsMode &v) -> ProspectiveParachainsModeOpt { + return v; + }, + [](const SecondedList &) -> ProspectiveParachainsModeOpt { + return std::nullopt; + }); + } + + inline void add_seconded_candidate(ActiveLeafState &state, + ParachainId para_id) { + if (auto seconded = if_type(state)) { + seconded.value().get().insert(para_id); + } + } /// The status of the collations. enum struct CollationStatus { @@ -222,6 +232,109 @@ namespace kagome::parachain { using HypotheticalCandidate = boost::variant; + struct HypotheticalCandidateWrapper { + std::reference_wrapper candidate; + std::shared_ptr hasher; + + // `HypotheticalOrConcreteCandidate` impl + std::optional> + get_commitments() const { + return visit_in_place( + candidate.get(), + [&](const HypotheticalCandidateIncomplete &v) + -> std::optional< + std::reference_wrapper> { + return std::nullopt; + }, + [&](const HypotheticalCandidateComplete &v) + -> std::optional< + std::reference_wrapper> { + return std::cref(v.receipt.commitments); + }); + } + + std::optional< + std::reference_wrapper> + get_persisted_validation_data() const { + return visit_in_place( + candidate.get(), + [&](const HypotheticalCandidateIncomplete &v) + -> std::optional> { + return std::nullopt; + }, + [&](const HypotheticalCandidateComplete &v) + -> std::optional> { + return v.persisted_validation_data; + }); + } + + std::optional> + get_validation_code_hash() const { + return visit_in_place( + candidate.get(), + [&](const HypotheticalCandidateIncomplete &v) + -> std::optional< + std::reference_wrapper> { + return std::nullopt; + }, + [&](const HypotheticalCandidateComplete &v) + -> std::optional< + std::reference_wrapper> { + return v.receipt.descriptor.validation_code_hash; + }); + } + + std::optional get_output_head_data_hash() const { + return visit_in_place( + candidate.get(), + [&](const HypotheticalCandidateIncomplete &v) -> std::optional { + return std::nullopt; + }, + [&](const HypotheticalCandidateComplete &v) -> std::optional { + return v.receipt.descriptor.para_head_hash; + }); + } + + Hash get_parent_head_data_hash() const { + return visit_in_place( + candidate.get(), + [&](const HypotheticalCandidateIncomplete &v) -> Hash { + return v.parent_head_data_hash; + }, + [&](const HypotheticalCandidateComplete &v) -> Hash { + return hasher->blake2b_256(v.persisted_validation_data.parent_head); + }); + } + + Hash get_relay_parent() const { + return visit_in_place( + candidate.get(), + [&](const HypotheticalCandidateIncomplete &v) -> Hash { + return v.candidate_relay_parent; + }, + [&](const HypotheticalCandidateComplete &v) -> Hash { + return v.receipt.descriptor.relay_parent; + }); + } + + CandidateHash get_candidate_hash() const { + return visit_in_place(candidate.get(), [&](const auto &v) -> Hash { + return v.candidate_hash; + }); + } + }; + + inline HypotheticalCandidateWrapper into_wrapper( + const HypotheticalCandidate &candidate, + const std::shared_ptr &hasher) { + return HypotheticalCandidateWrapper{ + .candidate = candidate, + .hasher = hasher, + }; + } + inline std::reference_wrapper candidatePara( const HypotheticalCandidate &hc) { return visit_in_place( diff --git a/core/parachain/validator/fragment_tree.hpp b/core/parachain/validator/fragment_tree.hpp deleted file mode 100644 index 546bd6eb3e..0000000000 --- a/core/parachain/validator/fragment_tree.hpp +++ /dev/null @@ -1,1079 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "outcome/outcome.hpp" - -#include "crypto/hasher/hasher_impl.hpp" -#include "log/logger.hpp" -#include "network/types/collator_messages.hpp" -#include "network/types/collator_messages_vstaging.hpp" -#include "parachain/types.hpp" -#include "parachain/validator/collations.hpp" -#include "primitives/common.hpp" -#include "primitives/math.hpp" -#include "runtime/runtime_api/parachain_host_types.hpp" -#include "utils/map.hpp" - -namespace kagome::parachain::fragment { - - template - using HashMap = std::unordered_map; - template - using HashSet = std::unordered_set; - template - using Vec = std::vector; - using BitVec = scale::BitVec; - using ParaId = ParachainId; - template - using Option = std::optional; - template - using Map = std::map; - using FragmentTreeMembership = Vec>>; - - struct ProspectiveCandidate { - /// The commitments to the output of the execution. - network::CandidateCommitments commitments; - /// The collator that created the candidate. - CollatorId collator; - /// The signature of the collator on the payload. - runtime::CollatorSignature collator_signature; - /// The persisted validation data used to create the candidate. - runtime::PersistedValidationData persisted_validation_data; - /// The hash of the PoV. - Hash pov_hash; - /// The validation code hash used by the candidate. - ValidationCodeHash validation_code_hash; - }; - - /// The state of a candidate. - /// - /// Candidates aren't even considered until they've at least been seconded. - enum CandidateState { - /// The candidate has been introduced in a spam-protected way but - /// is not necessarily backed. - Introduced, - /// The candidate has been seconded. - Seconded, - /// The candidate has been completely backed by the group. - Backed, - }; - - struct CandidateEntry { - CandidateHash candidate_hash; - RelayHash relay_parent; - ProspectiveCandidate candidate; - CandidateState state; - }; - - struct CandidateStorage { - enum class Error { - CANDIDATE_ALREADY_KNOWN, - PERSISTED_VALIDATION_DATA_MISMATCH, - }; - - // Index from head data hash to candidate hashes with that head data as a - // parent. - HashMap> by_parent_head; - - // Index from head data hash to candidate hashes outputting that head data. - HashMap> by_output_head; - - // Index from candidate hash to fragment node. - HashMap by_candidate_hash; - - outcome::result addCandidate( - const CandidateHash &candidate_hash, - const network::CommittedCandidateReceipt &candidate, - const crypto::Hashed> - &persisted_validation_data, - const std::shared_ptr &hasher); - - Option> get( - const CandidateHash &candidate_hash) const { - if (auto it = by_candidate_hash.find(candidate_hash); - it != by_candidate_hash.end()) { - return {{it->second}}; - } - return std::nullopt; - } - - Option relayParentByCandidateHash( - const CandidateHash &candidate_hash) const { - if (auto it = by_candidate_hash.find(candidate_hash); - it != by_candidate_hash.end()) { - return it->second.relay_parent; - } - return std::nullopt; - } - - bool contains(const CandidateHash &candidate_hash) const { - return by_candidate_hash.find(candidate_hash) != by_candidate_hash.end(); - } - - template - void iterParaChildren(const Hash &parent_head_hash, F &&func) const { - if (auto it = by_parent_head.find(parent_head_hash); - it != by_parent_head.end()) { - for (const auto &h : it->second) { - if (auto c_it = by_candidate_hash.find(h); - c_it != by_candidate_hash.end()) { - std::forward(func)(c_it->second); - } - } - } - } - - Option> headDataByHash( - const Hash &hash) const { - auto search = [&](const auto &container) - -> Option> { - if (auto it = container.find(hash); it != container.end()) { - if (!it->second.empty()) { - const CandidateHash &a_candidate = *it->second.begin(); - return get(a_candidate); - } - } - return std::nullopt; - }; - - if (auto e = search(by_output_head)) { - return {{e->get().candidate.commitments.para_head}}; - } - if (auto e = search(by_parent_head)) { - return {{e->get().candidate.persisted_validation_data.parent_head}}; - } - return std::nullopt; - } - - void removeCandidate(const CandidateHash &candidate_hash, - const std::shared_ptr &hasher) { - if (auto it = by_candidate_hash.find(candidate_hash); - it != by_candidate_hash.end()) { - const auto parent_head_hash = hasher->blake2b_256( - it->second.candidate.persisted_validation_data.parent_head); - if (auto it_bph = by_parent_head.find(parent_head_hash); - it_bph != by_parent_head.end()) { - it_bph->second.erase(candidate_hash); - if (it_bph->second.empty()) { - by_parent_head.erase(it_bph); - } - } - by_candidate_hash.erase(it); - } - } - - template - void retain(F &&pred /*bool(CandidateHash)*/) { - for (auto it = by_candidate_hash.begin(); - it != by_candidate_hash.end();) { - if (pred(it->first)) { - ++it; - } else { - it = by_candidate_hash.erase(it); - } - } - - for (auto it = by_parent_head.begin(); it != by_parent_head.end();) { - auto &[_, children] = *it; - for (auto it_c = children.begin(); it_c != children.end();) { - if (pred(*it_c)) { - ++it_c; - } else { - it_c = children.erase(it_c); - } - } - if (children.empty()) { - it = by_parent_head.erase(it); - } else { - ++it; - } - } - - for (auto it = by_output_head.begin(); it != by_output_head.end();) { - auto &[_, candidates] = *it; - for (auto it_c = candidates.begin(); it_c != candidates.end();) { - if (pred(*it_c)) { - ++it_c; - } else { - it_c = candidates.erase(it_c); - } - } - if (candidates.empty()) { - it = by_output_head.erase(it); - } else { - ++it; - } - } - } - - void markSeconded(const CandidateHash &candidate_hash) { - if (auto it = by_candidate_hash.find(candidate_hash); - it != by_candidate_hash.end()) { - if (it->second.state != CandidateState::Backed) { - it->second.state = CandidateState::Seconded; - } - } - } - - void markBacked(const CandidateHash &candidate_hash) { - if (auto it = by_candidate_hash.find(candidate_hash); - it != by_candidate_hash.end()) { - it->second.state = CandidateState::Backed; - } - } - - bool isBacked(const CandidateHash &candidate_hash) const { - return by_candidate_hash.count(candidate_hash) > 0 - && by_candidate_hash.at(candidate_hash).state - == CandidateState::Backed; - } - - std::pair len() const { - return std::make_pair(by_parent_head.size(), by_candidate_hash.size()); - } - }; - - using NodePointerRoot = network::Empty; - using NodePointerStorage = size_t; - using NodePointer = boost::variant; - - struct RelayChainBlockInfo { - /// The hash of the relay-chain block. - Hash hash; - /// The number of the relay-chain block. - BlockNumber number; - /// The storage-root of the relay-chain block. - Hash storage_root; - }; - - inline bool validate_against_constraints( - const Constraints &constraints, - const RelayChainBlockInfo &relay_parent, - const ProspectiveCandidate &candidate, - const ConstraintModifications &modifications) { - runtime::PersistedValidationData expected_pvd{ - .parent_head = constraints.required_parent, - .relay_parent_number = relay_parent.number, - .relay_parent_storage_root = relay_parent.storage_root, - .max_pov_size = uint32_t(constraints.max_pov_size), - }; - - if (expected_pvd != candidate.persisted_validation_data) { - return false; - } - - if (constraints.validation_code_hash != candidate.validation_code_hash) { - return false; - } - - if (relay_parent.number < constraints.min_relay_parent_number) { - return false; - } - - size_t announced_code_size = 0ull; - if (candidate.commitments.opt_para_runtime) { - if (constraints.upgrade_restriction - && *constraints.upgrade_restriction == UpgradeRestriction::Present) { - return false; - } - announced_code_size = candidate.commitments.opt_para_runtime->size(); - } - - if (announced_code_size > constraints.max_code_size) { - return false; - } - - if (modifications.dmp_messages_processed == 0) { - if (!constraints.dmp_remaining_messages.empty() - && constraints.dmp_remaining_messages[0] <= relay_parent.number) { - return false; - } - } - - if (candidate.commitments.outbound_hor_msgs.size() - > constraints.max_hrmp_num_per_candidate) { - return false; - } - - if (candidate.commitments.upward_msgs.size() - > constraints.max_ump_num_per_candidate) { - return false; - } - - /// TODO(iceseer): do - /// add error-codes for each case - - return constraints.checkModifications(modifications); - } - - struct Fragment { - /// The new relay-parent. - RelayChainBlockInfo relay_parent; - /// The constraints this fragment is operating under. - Constraints operating_constraints; - /// The core information about the prospective candidate. - ProspectiveCandidate candidate; - /// Modifications to the constraints based on the outputs of - /// the candidate. - ConstraintModifications modifications; - - const RelayChainBlockInfo &relayParent() const { - return relay_parent; - } - - static Option create(const RelayChainBlockInfo &relay_parent, - const Constraints &operating_constraints, - const ProspectiveCandidate &candidate) { - const network::CandidateCommitments &commitments = candidate.commitments; - - std::unordered_map - outbound_hrmp; - std::optional last_recipient; - for (size_t i = 0; i < commitments.outbound_hor_msgs.size(); ++i) { - const network::OutboundHorizontal &message = - commitments.outbound_hor_msgs[i]; - if (last_recipient) { - if (*last_recipient >= message.para_id) { - return std::nullopt; - } - } - last_recipient = message.para_id; - OutboundHrmpChannelModification &record = - outbound_hrmp[message.para_id]; - - record.bytes_submitted += message.upward_msg.size(); - record.messages_submitted += 1; - } - - uint32_t ump_sent_bytes = 0ull; - for (const auto &m : commitments.upward_msgs) { - ump_sent_bytes += uint32_t(m.size()); - } - - ConstraintModifications modifications{ - .required_parent = commitments.para_head, - .hrmp_watermark = ((commitments.watermark == relay_parent.number) - ? HrmpWatermarkUpdate{HrmpWatermarkUpdateHead{ - .v = commitments.watermark}} - : HrmpWatermarkUpdate{HrmpWatermarkUpdateTrunk{ - .v = commitments.watermark}}), - .outbound_hrmp = outbound_hrmp, - .ump_messages_sent = uint32_t(commitments.upward_msgs.size()), - .ump_bytes_sent = ump_sent_bytes, - .dmp_messages_processed = commitments.downward_msgs_count, - .code_upgrade_applied = - operating_constraints.future_validation_code - ? (relay_parent.number - >= operating_constraints.future_validation_code->first) - : false, - }; - - if (!validate_against_constraints( - operating_constraints, relay_parent, candidate, modifications)) { - return std::nullopt; - } - - return Fragment{ - .relay_parent = relay_parent, - .operating_constraints = operating_constraints, - .candidate = candidate, - .modifications = modifications, - }; - } - - const ConstraintModifications &constraintModifications() const { - return modifications; - } - }; - - struct FragmentNode { - // A pointer to the parent node. - NodePointer parent; - Fragment fragment; - CandidateHash candidate_hash; - size_t depth; - ConstraintModifications cumulative_modifications; - Vec> children; - - const Hash &relayParent() const { - return fragment.relayParent().hash; - } - - Option candidateChild( - const CandidateHash &candidate_hash) const { - auto it = std::ranges::find_if( - children.begin(), - children.end(), - [&](const std::pair &p) { - return p.second == candidate_hash; - }); - if (it != children.end()) { - return it->first; - } - return std::nullopt; - } - }; - - struct PendingAvailability { - /// The candidate hash. - CandidateHash candidate_hash; - /// The block info of the relay parent. - RelayChainBlockInfo relay_parent; - }; - - struct Scope { - enum class Error { - UNEXPECTED_ANCESTOR, - }; - - ParaId para; - RelayChainBlockInfo relay_parent; - Map ancestors; - HashMap ancestors_by_hash; - Vec pending_availability; - Constraints base_constraints; - size_t max_depth; - - static outcome::result withAncestors( - ParachainId para, - const RelayChainBlockInfo &relay_parent, - const Constraints &base_constraints, - const Vec &pending_availability, - size_t max_depth, - const Vec &ancestors); - - const RelayChainBlockInfo &earliestRelayParent() const { - if (!ancestors.empty()) { - return ancestors.begin()->second; - } - return relay_parent; - } - - Option> - getPendingAvailability(const CandidateHash &candidate_hash) const { - auto it = - std::ranges::find_if(pending_availability.begin(), - pending_availability.end(), - [&](const PendingAvailability &c) { - return c.candidate_hash == candidate_hash; - }); - if (it != pending_availability.end()) { - return {{*it}}; - } - return std::nullopt; - } - - Option> ancestorByHash( - const Hash &hash) const { - if (hash == relay_parent.hash) { - return {{relay_parent}}; - } - if (auto it = ancestors_by_hash.find(hash); - it != ancestors_by_hash.end()) { - return {{it->second}}; - } - return std::nullopt; - } - }; - - /// This is a tree of candidates based on some underlying storage of - /// candidates and a scope. - /// - /// All nodes in the tree must be either pending availability or within the - /// scope. Within the scope means it's built off of the relay-parent or an - /// ancestor. - struct FragmentTree { - Scope scope; - - // Invariant: a contiguous prefix of the 'nodes' storage will contain - // the top-level children. - Vec nodes; - - // The candidates stored in this tree, mapped to a bitvec indicating the - // depths where the candidate is stored. - HashMap candidates; - - std::shared_ptr hasher_; - log::Logger logger = log::createLogger("parachain", "fragment_tree"); - - Option> candidate(const CandidateHash &hash) const { - if (auto it = candidates.find(hash); it != candidates.end()) { - Vec res; - for (size_t ix = 0; ix < it->second.bits.size(); ++ix) { - if (it->second.bits[ix]) { - res.emplace_back(ix); - } - } - return res; - } - return std::nullopt; - } - - std::vector getCandidates() const { - // doesn't compile in clang-15 due to a bug - // std::views::keys(candidates) - std::vector res; - res.reserve(candidates.size()); - for (auto &pair : candidates) { - res.push_back(pair.first); - } - return res; - } - - /** - * @brief Select `count` candidates after the given `required_path` which - * pass the predicate and have not already been backed on chain. - * Does an exhaustive search into the tree starting after `required_path`. - * If there are multiple possibilities of size `count`, this will select the - * first one. If there is no chain of size `count` that matches the - * criteria, this will return the largest chain it could find with the - * criteria. If there are no candidates meeting those criteria, - * returns an empty `Vec`. Cycles are accepted, see module docs for the - * `Cycles` section. The intention of the `required_path` is - * to allow queries on the basis of one or more candidates which were - * previously pending availability becoming available and opening up more - * room on the core. - */ - - template - std::vector selectChildren( - const std::vector &required_path, - uint32_t count, - Func &&pred) const { - NodePointer base_node{NodePointerRoot{}}; - for (const CandidateHash &required_step : required_path) { - if (auto node = nodeCandidateChild(base_node, required_step)) { - base_node = *node; - } else { - return {}; - } - } - - std::vector accum; - return selectChildrenInner( - std::move(base_node), count, count, std::forward(pred), accum); - } - - /** - * @brief Try finding a candidate chain starting from `base_node` of length - * `expected_count`. If not possible, return the longest one we - * could find. Does a depth-first search, since we're optimistic that - * there won't be more than one such chains (parachains shouldn't - * usually have forks). So in the usual case, this will conclude in - * `O(expected_count)`. Cycles are accepted, but this doesn't allow for - * infinite execution time, because the maximum depth we'll reach is - * `expected_count`. Worst case performance is `O(num_forks - * ^ expected_count)`. Although an exponential function, this is - * actually a constant that can only be altered via sudo/governance, - * because: 1. `num_forks` at a given level is at most `max_candidate_depth - * * max_validators_per_core` (because each validator in the - * assigned group can second `max_candidate_depth` candidates). The - * prospective-parachains subsystem assumes that the number of para forks is - * limited by collator-protocol and backing subsystems. In practice, this is - * a constant which can only be altered by sudo or governance. 2. - * `expected_count` is equal to the number of cores a para is scheduled on - * (in an elastic scaling scenario). For non-elastic-scaling, this is - * just 1. In practice, this should be a small number (1-3), capped - * by the total number of available cores (a constant alterable only - * via governance/sudo). - */ - template - std::vector selectChildrenInner( - NodePointer base_node, - uint32_t expected_count, - uint32_t remaining_count, - const Func &pred, - std::vector &accumulator) const { - if (remaining_count == 0) { - return accumulator; - } - - auto children = visit_in_place( - base_node, - [&](const NodePointerRoot &) - -> std::vector> { - std::vector> tmp; - for (size_t ptr = 0; ptr < nodes.size(); ++ptr) { - const FragmentNode &n = nodes[ptr]; - if (!is_type(n.parent)) { - continue; - } - if (scope.getPendingAvailability(n.candidate_hash)) { - continue; - } - if (!pred(n.candidate_hash)) { - continue; - } - tmp.emplace_back(NodePointerStorage{ptr}, n.candidate_hash); - } - return tmp; - }, - [&](const NodePointerStorage &base_node_ptr) - -> std::vector> { - std::vector> tmp; - const auto &bn = nodes[base_node_ptr]; - for (const auto &[ptr, hash] : bn.children) { - if (scope.getPendingAvailability(hash)) { - continue; - } - if (!pred(hash)) { - continue; - } - tmp.emplace_back(ptr, hash); - } - return tmp; - }); - - auto best_result = accumulator; - for (const auto &[child_ptr, child_hash] : children) { - accumulator.emplace_back(child_hash); - auto result = selectChildrenInner( - child_ptr, expected_count, remaining_count - 1, pred, accumulator); - accumulator.pop_back(); - - if (result.size() == size_t(expected_count)) { - return result; - } else if (best_result.size() < result.size()) { - best_result = result; - } - } - - return best_result; - } - - static FragmentTree populate(const std::shared_ptr &hasher, - const Scope &scope, - const CandidateStorage &storage) { - auto logger = log::createLogger("FragmentTree", "parachain"); - SL_TRACE(logger, - "Instantiating Fragment Tree. (relay parent={}, relay parent " - "num={}, para id={}, ancestors={})", - scope.relay_parent.hash, - scope.relay_parent.number, - scope.para, - scope.ancestors.size()); - - FragmentTree tree{ - .scope = scope, .nodes = {}, .candidates = {}, .hasher_ = hasher}; - - tree.populateFromBases(storage, {{NodePointerRoot{}}}); - return tree; - } - - void populateFromBases(const CandidateStorage &storage, - const std::vector &initial_bases) { - Option last_sweep_start{}; - do { - const auto sweep_start = nodes.size(); - if (last_sweep_start && *last_sweep_start == sweep_start) { - break; - } - - Vec parents; - if (last_sweep_start) { - parents.reserve(nodes.size() - *last_sweep_start); - for (size_t ix = *last_sweep_start; ix < nodes.size(); ++ix) { - parents.emplace_back(ix); - } - } else { - parents = initial_bases; - } - - for (const auto &parent_pointer : parents) { - const auto &[modifications, child_depth, earliest_rp] = - visit_in_place( - parent_pointer, - [&](const NodePointerRoot &) - -> std::tuple { - return std::make_tuple(ConstraintModifications{}, - size_t{0ull}, - scope.earliestRelayParent()); - }, - [&](const NodePointerStorage &ptr) - -> std::tuple { - const auto &node = nodes[ptr]; - if (auto opt_rcbi = - scope.ancestorByHash(node.relayParent())) { - return std::make_tuple(node.cumulative_modifications, - size_t(node.depth + 1), - opt_rcbi->get()); - } else { - if (auto c = scope.getPendingAvailability( - node.candidate_hash)) { - return std::make_tuple(node.cumulative_modifications, - size_t(node.depth + 1), - c->get().relay_parent); - } - UNREACHABLE; - } - }); - - if (child_depth > scope.max_depth) { - continue; - } - - auto child_constraints_res = - scope.base_constraints.applyModifications(modifications); - if (child_constraints_res.has_error()) { - SL_TRACE(logger, - "Failed to apply modifications. (error={})", - child_constraints_res.error()); - continue; - } - - const auto &child_constraints = child_constraints_res.value(); - const auto required_head_hash = - hasher_->blake2b_256(child_constraints.required_parent); - - storage.iterParaChildren( - required_head_hash, - // clang-15 doesn't like capturing structured bindings into - // lambdas - [&, - earliest_rp = earliest_rp, - child_depth = child_depth, - modifications = modifications](const CandidateEntry &candidate) { - auto pending = - scope.getPendingAvailability(candidate.candidate_hash); - Option relay_parent_opt; - if (pending) { - relay_parent_opt = pending->get().relay_parent; - } else { - relay_parent_opt = utils::fromRefToOwn( - scope.ancestorByHash(candidate.relay_parent)); - } - if (!relay_parent_opt) { - return; - } - auto &relay_parent = *relay_parent_opt; - - uint32_t min_relay_parent_number; - if (pending) { - min_relay_parent_number = visit_in_place( - parent_pointer, - [&](const NodePointerStorage &) { - return earliest_rp.number; - }, - [&](const NodePointerRoot &) { - return pending->get().relay_parent.number; - }); - } else { - min_relay_parent_number = std::max( - earliest_rp.number, scope.earliestRelayParent().number); - } - - if (relay_parent.number < min_relay_parent_number) { - return; - } - - if (nodeHasCandidateChild(parent_pointer, - candidate.candidate_hash)) { - return; - } - - auto constraints = child_constraints; - if (pending) { - constraints.min_relay_parent_number = - pending->get().relay_parent.number; - } - - Option f = Fragment::create( - relay_parent, constraints, candidate.candidate); - if (!f) { - SL_TRACE(logger, - "Failed to instantiate fragment. (relay parent={}, " - "candidate hash={})", - relay_parent.hash, - candidate.candidate_hash); - return; - } - - Fragment &fragment = *f; - ConstraintModifications cumulative_modifications = - modifications; - cumulative_modifications.stack( - fragment.constraintModifications()); - - insertNode(FragmentNode{ - .parent = parent_pointer, - .fragment = fragment, - .candidate_hash = candidate.candidate_hash, - .depth = child_depth, - .cumulative_modifications = cumulative_modifications, - .children = {}}); - }); - } - - last_sweep_start = sweep_start; - } while (true); - } - - void addAndPopulate(const CandidateHash &hash, - const CandidateStorage &storage) { - auto opt_candidate_entry = storage.get(hash); - if (!opt_candidate_entry) { - return; - } - - const auto &candidate_entry = opt_candidate_entry->get(); - const auto &candidate_parent = - candidate_entry.candidate.persisted_validation_data.parent_head; - - Vec bases{}; - if (scope.base_constraints.required_parent == candidate_parent) { - bases.emplace_back(NodePointerRoot{}); - } - - for (size_t ix = 0ull; ix < nodes.size(); ++ix) { - const auto &n = nodes[ix]; - if (n.cumulative_modifications.required_parent - && n.cumulative_modifications.required_parent.value() - == candidate_parent) { - bases.emplace_back(ix); - } - } - - populateFromBases(storage, bases); - } - - void insertNode(FragmentNode &&node) { - const NodePointerStorage pointer{nodes.size()}; - const auto parent_pointer = node.parent; - const auto &candidate_hash = node.candidate_hash; - const auto max_depth = scope.max_depth; - - auto &bv = candidates[candidate_hash]; - if (bv.bits.size() == 0ull) { - bv.bits.resize(max_depth + 1); - } - bv.bits[node.depth] = true; - - visit_in_place( - parent_pointer, - [&](const NodePointerRoot &) { - if (nodes.empty() - || is_type(nodes.back().parent)) { - nodes.emplace_back(std::move(node)); - } else { - nodes.insert(std::ranges::find_if( - nodes.begin(), - nodes.end(), - [](const auto &item) { - return !is_type(item.parent); - }), - std::move(node)); - } - }, - [&](const NodePointerStorage &ptr) { - nodes.emplace_back(std::move(node)); - nodes[ptr].children.emplace_back(pointer, candidate_hash); - }); - } - - Option nodeCandidateChild( - const NodePointer &pointer, const CandidateHash &candidate_hash) const { - return visit_in_place( - pointer, - [&](const NodePointerStorage &ptr) -> Option { - if (ptr < nodes.size()) { - return nodes[ptr].candidateChild(candidate_hash); - } - return std::nullopt; - }, - [&](const NodePointerRoot &) -> Option { - for (size_t ix = 0ull; ix < nodes.size(); ++ix) { - const FragmentNode &n = nodes[ix]; - if (!is_type(n.parent)) { - break; - } - if (n.candidate_hash != candidate_hash) { - continue; - } - return ix; - } - return std::nullopt; - }); - } - - bool nodeHasCandidateChild(const NodePointer &pointer, - const CandidateHash &candidate_hash) const { - return nodeCandidateChild(pointer, candidate_hash).has_value(); - } - - bool pathContainsBackedOnlyCandidates( - NodePointer parent_pointer, - const CandidateStorage &candidate_storage) const { - while (auto ptr = if_type(parent_pointer)) { - const auto &node = nodes[ptr->get()]; - const auto &candidate_hash = node.candidate_hash; - - auto opt_entry = candidate_storage.get(candidate_hash); - if (!opt_entry || opt_entry->get().state != CandidateState::Backed) { - return false; - } - parent_pointer = node.parent; - } - return true; - } - - Vec hypotheticalDepths(const CandidateHash &hash, - const HypotheticalCandidate &candidate, - const CandidateStorage &candidate_storage, - bool backed_in_path_only) const { - if (!backed_in_path_only) { - if (auto it = candidates.find(hash); it != candidates.end()) { - Vec res; - for (size_t ix = 0; ix < it->second.bits.size(); ++ix) { - if (it->second.bits[ix]) { - res.emplace_back(ix); - } - } - return res; - } - } - - const auto crp = relayParent(candidate); - auto candidate_relay_parent = - [&]() -> Option> { - if (scope.relay_parent.hash == crp.get()) { - return {{scope.relay_parent}}; - } - if (auto it = scope.ancestors_by_hash.find(crp); - it != scope.ancestors_by_hash.end()) { - return {{it->second}}; - } - return std::nullopt; - }(); - - if (!candidate_relay_parent) { - return {}; - } - - const auto max_depth = scope.max_depth; - BitVec depths; - depths.bits.resize(max_depth + 1); - - auto process_parent_pointer = [&](const NodePointer &parent_pointer) { - const auto [modifications, child_depth, earliest_rp] = visit_in_place( - parent_pointer, - [&](const NodePointerRoot &) - -> std::tuple< - ConstraintModifications, - size_t, - std::reference_wrapper> { - return std::make_tuple(ConstraintModifications{}, - size_t{0ull}, - std::cref(scope.earliestRelayParent())); - }, - [&](const NodePointerStorage &ptr) - -> std::tuple< - ConstraintModifications, - size_t, - std::reference_wrapper> { - const auto &node = nodes[ptr]; - if (auto opt_rcbi = scope.ancestorByHash(node.relayParent())) { - return std::make_tuple( - node.cumulative_modifications, node.depth + 1, *opt_rcbi); - } else { - if (auto r = - scope.getPendingAvailability(node.candidate_hash)) { - return std::make_tuple( - node.cumulative_modifications, - node.depth + 1, - std::cref(scope.earliestRelayParent())); - } - UNREACHABLE; - } - }); - - if (child_depth > max_depth) { - return; - } - - if (earliest_rp.get().number > candidate_relay_parent->get().number) { - return; - } - - auto child_constraints_res = - scope.base_constraints.applyModifications(modifications); - if (child_constraints_res.has_error()) { - SL_TRACE(logger, - "Failed to apply modifications. (error={})", - child_constraints_res.error()); - return; - } - - const auto &child_constraints = child_constraints_res.value(); - const auto parent_head_hash = parentHeadDataHash(*hasher_, candidate); - - /// TODO(iceseer): keep hashed object in constraints to avoid recalc - if (parent_head_hash - != hasher_->blake2b_256(child_constraints.required_parent)) { - return; - } - - if (auto const value = - if_type(candidate)) { - ProspectiveCandidate prospective_candidate{ - .commitments = value->get().receipt.commitments, - .collator = value->get().receipt.descriptor.collator_id, - .collator_signature = value->get().receipt.descriptor.signature, - .persisted_validation_data = - value->get().persisted_validation_data, - .pov_hash = value->get().receipt.descriptor.pov_hash, - .validation_code_hash = - value->get().receipt.descriptor.validation_code_hash, - }; - - if (!Fragment::create(candidate_relay_parent->get(), - child_constraints, - prospective_candidate)) { - return; - } - } - - if (!backed_in_path_only - || pathContainsBackedOnlyCandidates(parent_pointer, - candidate_storage)) { - depths.bits[child_depth] = true; - } - }; - - process_parent_pointer(NodePointerRoot{}); - for (size_t ix = 0; ix < nodes.size(); ++ix) { - process_parent_pointer(ix); - } - - Vec res; - for (size_t ix = 0; ix < depths.bits.size(); ++ix) { - if (depths.bits[ix]) { - res.emplace_back(ix); - } - } - return res; - } - }; - -} // namespace kagome::parachain::fragment - -OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, Constraints::Error); -OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, Scope::Error); -OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, CandidateStorage::Error); diff --git a/core/parachain/validator/impl/parachain_processor.cpp b/core/parachain/validator/impl/parachain_processor.cpp index 32188e3ab3..e23b1b6124 100644 --- a/core/parachain/validator/impl/parachain_processor.cpp +++ b/core/parachain/validator/impl/parachain_processor.cpp @@ -206,7 +206,8 @@ namespace kagome::parachain { BOOST_ASSERT(block_tree_); app_state_manager.takeControl(*this); - our_current_state_.implicit_view.emplace(prospective_parachains_); + our_current_state_.implicit_view.emplace( + prospective_parachains_, parachain_host_, block_tree_, std::nullopt); BOOST_ASSERT(our_current_state_.implicit_view); metrics_registry_->registerGaugeFamily( @@ -582,8 +583,9 @@ namespace kagome::parachain { void ParachainProcessorImpl::onViewUpdated(const network::ExView &event) { REINVOKE(*main_pool_handler_, onViewUpdated, event); CHECK_OR_RET(canProcessParachains().has_value()); - const auto &relay_parent = event.new_head.hash(); + + /// init `prospective_parachains` subsystem if (const auto r = prospective_parachains_->onActiveLeavesUpdate(network::ExViewRef{ .new_head = {event.new_head}, @@ -597,8 +599,13 @@ namespace kagome::parachain { r.error()); } + /// init `backing_store` subsystem backing_store_->onActivateLeaf(relay_parent); - createBackingTask(relay_parent, event.new_head); + + /// init `backing` subsystem + create_backing_task(relay_parent, event.new_head, event.lost); + + /// update our `view` on remote nodes SL_TRACE(logger_, "Update my view.(new head={}, finalized={}, leaves={})", relay_parent, @@ -607,6 +614,7 @@ namespace kagome::parachain { broadcastView(event.view); broadcastViewToGroup(relay_parent, event.view); + /// update `statements_distribution` subsystem { auto new_relay_parents = our_current_state_.implicit_view->all_allowed_relay_parents(); @@ -627,29 +635,12 @@ namespace kagome::parachain { } } } - new_leaf_fragment_tree_updates(relay_parent); + new_leaf_fragment_chain_updates(relay_parent); // need to lock removing session infoes - std::vector< - std::shared_ptr::RefObj>> - _keeper_; - _keeper_.reserve(event.lost.size()); - for (const auto &lost : event.lost) { - SL_TRACE(logger_, "Removed backing task.(relay parent={})", lost); - auto relay_parent_state = tryGetStateByRelayParent(lost); - if (relay_parent_state) { - _keeper_.emplace_back(relay_parent_state->get().per_session_state); - } - our_current_state_.active_leaves.erase(lost); - std::vector pruned = - our_current_state_.implicit_view->deactivate_leaf(lost); - for (const auto removed : pruned) { - our_current_state_.state_by_relay_parent.erase(removed); - } - { /// remove cancelations auto &container = our_current_state_.collation_requests_cancel_handles; for (auto pc = container.begin(); pc != container.end();) { @@ -672,97 +663,11 @@ namespace kagome::parachain { } av_store_->remove(lost); - our_current_state_.per_leaf.erase(lost); - our_current_state_.state_by_relay_parent.erase(lost); } + our_current_state_.active_leaves[relay_parent] = prospective_parachains_->prospectiveParachainsMode(relay_parent); - for (auto it = our_current_state_.per_candidate.begin(); - it != our_current_state_.per_candidate.end();) { - if (our_current_state_.state_by_relay_parent.find(it->second.relay_parent) - != our_current_state_.state_by_relay_parent.end()) { - ++it; - } else { - it = our_current_state_.per_candidate.erase(it); - } - } - - auto it_rp = our_current_state_.state_by_relay_parent.find(relay_parent); - if (it_rp == our_current_state_.state_by_relay_parent.end()) { - return; - } - - std::vector fresh_relay_parents; - if (!it_rp->second.prospective_parachains_mode) { - if (our_current_state_.per_leaf.find(relay_parent) - != our_current_state_.per_leaf.end()) { - return; - } - - our_current_state_.per_leaf.emplace( - relay_parent, - ActiveLeafState{ - .prospective_parachains_mode = std::nullopt, - .seconded_at_depth = {}, - }); - fresh_relay_parents.emplace_back(relay_parent); - } else { - auto frps = - our_current_state_.implicit_view->knownAllowedRelayParentsUnder( - relay_parent, std::nullopt); - - std::unordered_map> - seconded_at_depth; - for (const auto &[c_hash, cd] : our_current_state_.per_candidate) { - if (!cd.seconded_locally) { - continue; - } - - fragment::FragmentTreeMembership membership = - prospective_parachains_->answerTreeMembershipRequest(cd.para_id, - c_hash); - for (const auto &[h, depths] : membership) { - if (h == relay_parent) { - auto &mm = seconded_at_depth[cd.para_id]; - for (const auto depth : depths) { - mm.emplace(depth, c_hash); - } - } - } - } - - our_current_state_.per_leaf.emplace( - relay_parent, - ActiveLeafState{ - .prospective_parachains_mode = - it_rp->second.prospective_parachains_mode, - .seconded_at_depth = std::move(seconded_at_depth), - }); - - if (frps.empty()) { - SL_WARN(logger_, - "Implicit view gave no relay-parents. (leaf_hash={})", - relay_parent); - fresh_relay_parents.emplace_back(relay_parent); - } else { - fresh_relay_parents.insert( - fresh_relay_parents.end(), frps.begin(), frps.end()); - } - } - - for (const auto &maybe_new : fresh_relay_parents) { - if (our_current_state_.state_by_relay_parent.find(maybe_new) - != our_current_state_.state_by_relay_parent.end()) { - continue; - } - if (auto r = block_tree_->getBlockHeader(maybe_new); r.has_value()) { - createBackingTask(maybe_new, r.value()); - } else { - SL_ERROR(logger_, "No header found.(relay parent={})", maybe_new); - } - } - auto remove_if = [](bool eq, auto &it, auto &cont) { if (eq) { it = cont.erase(it); @@ -1018,8 +923,8 @@ namespace kagome::parachain { } outcome::result - ParachainProcessorImpl::getBabeRandomness( - const primitives::BlockHeader &block_header) { + ParachainProcessorImpl::getBabeRandomness(const RelayHash &relay_parent) { + OUTCOME_TRY(block_header, block_tree_->getBlockHeader(relay_parent)); OUTCOME_TRY(babe_header, consensus::babe::getBabeBlockHeader(block_header)); OUTCOME_TRY(epoch, slots_util_.get()->slotToEpoch(*block_header.parentInfo(), @@ -1031,9 +936,9 @@ namespace kagome::parachain { } outcome::result - ParachainProcessorImpl::initNewBackingTask( + ParachainProcessorImpl::construct_per_relay_parent_state( const primitives::BlockHash &relay_parent, - const network::HashedBlockHeader &block_header) { + const ProspectiveParachainsModeOpt &mode) { /** * It first checks if our node is a parachain validator for the relay * parent. If it is not, it returns an error. If the node is a validator, it @@ -1059,7 +964,7 @@ namespace kagome::parachain { parachain_host_->session_index_for_child(relay_parent)); OUTCOME_TRY(session_info, parachain_host_->session_info(relay_parent, session_index)); - OUTCOME_TRY(randomness, getBabeRandomness(block_header)); + OUTCOME_TRY(randomness, getBabeRandomness(relay_parent)); OUTCOME_TRY(disabled_validators_, parachain_host_->disabled_validators(relay_parent)); const auto &[validator_groups, group_rotation_info] = groups; @@ -1114,12 +1019,11 @@ namespace kagome::parachain { session_info->validator_groups, grid::shuffle(session_info->discovery_keys.size(), randomness), *global_v_index); - Groups groups{session_info->validator_groups, minimum_backing_votes}; return RefCache::RefObj( session_index, *session_info, - std::move(groups), + Groups{session_info->validator_groups, minimum_backing_votes}, std::move(grid_view), validator_index, peer_use_count_); @@ -1150,15 +1054,7 @@ namespace kagome::parachain { } } - auto mode = - prospective_parachains_->prospectiveParachainsMode(relay_parent); - BOOST_ASSERT(mode); - if (!mode) { - SL_ERROR(logger_, - "Prospective parachains are disabled. No sure for correctness"); - } const auto n_cores = cores.size(); - std::unordered_map> out_groups; std::optional assigned_core; std::optional assigned_para; @@ -1217,37 +1113,35 @@ namespace kagome::parachain { } std::optional statement_store; + std::optional local_validator; if (mode) { - [[maybe_unused]] const auto _ = - our_current_state_.implicit_view->activate_leaf(relay_parent); statement_store.emplace(per_session_state->value().groups); - } - - auto maybe_claim_queue = - [&]() -> std::optional { - auto r = fetch_claim_queue(relay_parent); - if (r.has_value()) { - return r.value(); - } - return std::nullopt; - }(); - - const auto seconding_limit = mode->max_candidate_depth + 1; - auto local_validator = [&]() -> std::optional { - if (!global_v_index) { + auto maybe_claim_queue = + [&]() -> std::optional { + auto r = fetch_claim_queue(relay_parent); + if (r.has_value()) { + return r.value(); + } return std::nullopt; - } - if (validator_index) { - return find_active_validator_state(*validator_index, - per_session_state->value().groups, - cores, - group_rotation_info, - maybe_claim_queue, - seconding_limit, - mode->max_candidate_depth); - } - return LocalValidatorState{}; - }(); + }(); + + const auto seconding_limit = mode->max_candidate_depth + 1; + local_validator = [&]() -> std::optional { + if (!global_v_index) { + return std::nullopt; + } + if (validator_index) { + return find_active_validator_state(*validator_index, + per_session_state->value().groups, + cores, + group_rotation_info, + maybe_claim_queue, + seconding_limit, + mode->max_candidate_depth); + } + return LocalValidatorState{}; + }(); + } std::unordered_set disabled_validators{ disabled_validators_.begin(), disabled_validators_.end()}; @@ -1357,19 +1251,131 @@ namespace kagome::parachain { }; } - void ParachainProcessorImpl::createBackingTask( + void ParachainProcessorImpl::create_backing_task( const primitives::BlockHash &relay_parent, - const network::HashedBlockHeader &block_header) { + const network::HashedBlockHeader &block_header, + const std::vector &lost) { BOOST_ASSERT(main_pool_handler_->isInCurrentThread()); - auto rps_result = initNewBackingTask(relay_parent, block_header); - if (rps_result.has_value()) { - storeStateByRelayParent(relay_parent, std::move(rps_result.value())); - } else if (rps_result.error() != Error::KEY_NOT_PRESENT) { + + using LeafHasProspectiveParachains = + std::optional>; + LeafHasProspectiveParachains res; + + if (auto mode = + prospective_parachains_->prospectiveParachainsMode(relay_parent)) { + if (auto r = + our_current_state_.implicit_view->activate_leaf(relay_parent); + r.has_error()) { + res = r.as_failure(); + } else { + res = *mode; + } + } else { + res = std::nullopt; + } + + for (const auto &deactivated : lost) { + our_current_state_.per_leaf.erase(deactivated); + our_current_state_.implicit_view->deactivate_leaf(deactivated); + } + + std::vector< + std::shared_ptr::RefObj>> + _keeper_; + _keeper_.reserve(lost.size()); + { + std::unordered_set remaining; + for (const auto &[h, _] : our_current_state_.per_leaf) { + remaining.emplace(h); + } + for (const auto &h : + our_current_state_.implicit_view->all_allowed_relay_parents()) { + remaining.emplace(h); + } + + for (auto it = our_current_state_.state_by_relay_parent.begin(); + it != our_current_state_.state_by_relay_parent.end();) { + if (remaining.contains(it->first)) { + ++it; + } else { + _keeper_.emplace_back(it->second.per_session_state); + it = our_current_state_.state_by_relay_parent.erase(it); + } + } + } + + for (auto it = our_current_state_.per_candidate.begin(); + it != our_current_state_.per_candidate.end();) { + if (our_current_state_.state_by_relay_parent.contains( + it->second.relay_parent)) { + ++it; + } else { + it = our_current_state_.per_candidate.erase(it); + } + } + + std::vector fresh_relay_parents; + ProspectiveParachainsModeOpt leaf_mode; + if (!res) { + if (our_current_state_.per_leaf.contains(relay_parent)) { + return; + } + + our_current_state_.per_leaf.insert_or_assign(relay_parent, + SecondedList{}); + fresh_relay_parents.emplace_back(relay_parent); + leaf_mode = std::nullopt; + } else if (res->has_value()) { + const ActiveLeafState active_leaf_state = res->value(); + our_current_state_.per_leaf.insert_or_assign(relay_parent, + active_leaf_state); + + if (auto f = our_current_state_.implicit_view + ->known_allowed_relay_parents_under(relay_parent, + std::nullopt)) { + fresh_relay_parents.insert( + fresh_relay_parents.end(), f->begin(), f->end()); + leaf_mode = res->value(); + } else { + SL_TRACE(logger_, + "Implicit view gave no relay-parents. (leaf_hash={})", + relay_parent); + fresh_relay_parents.emplace_back(relay_parent); + leaf_mode = res->value(); + } + } else { SL_TRACE( logger_, - "Relay parent state was not created. (relay parent={}, error={})", + "Failed to load implicit view for leaf. (leaf_hash={}, error={})", relay_parent, - rps_result.error()); + res->error()); + + return; + } + + for (const auto &maybe_new : fresh_relay_parents) { + if (our_current_state_.state_by_relay_parent.contains(maybe_new)) { + continue; + } + + ProspectiveParachainsModeOpt mode_; + if (auto l = utils::get(our_current_state_.per_leaf, maybe_new)) { + mode_ = from((*l)->second); + } else { + mode_ = leaf_mode; + } + + auto rps_result = construct_per_relay_parent_state(maybe_new, mode_); + if (rps_result.has_value()) { + our_current_state_.state_by_relay_parent.insert_or_assign( + relay_parent, std::move(rps_result.value())); + } else if (rps_result.error() != Error::KEY_NOT_PRESENT) { + SL_TRACE( + logger_, + "Relay parent state was not created. (relay parent={}, error={})", + relay_parent, + rps_result.error()); + } } } @@ -2799,25 +2805,25 @@ namespace kagome::parachain { candidate_hash); } - void ParachainProcessorImpl::new_confirmed_candidate_fragment_tree_updates( + void ParachainProcessorImpl::new_confirmed_candidate_fragment_chain_updates( const HypotheticalCandidate &candidate) { - fragment_tree_update_inner(std::nullopt, std::nullopt, {candidate}); + fragment_chain_update_inner(std::nullopt, std::nullopt, {candidate}); } - void ParachainProcessorImpl::new_leaf_fragment_tree_updates( + void ParachainProcessorImpl::new_leaf_fragment_chain_updates( const Hash &leaf_hash) { - fragment_tree_update_inner({leaf_hash}, std::nullopt, std::nullopt); + fragment_chain_update_inner({leaf_hash}, std::nullopt, std::nullopt); } - void - ParachainProcessorImpl::prospective_backed_notification_fragment_tree_updates( - ParachainId para_id, const Hash ¶_head) { + void ParachainProcessorImpl:: + prospective_backed_notification_fragment_chain_updates( + ParachainId para_id, const Hash ¶_head) { std::pair, ParachainId> p{{para_head}, para_id}; - fragment_tree_update_inner(std::nullopt, p, std::nullopt); + fragment_chain_update_inner(std::nullopt, p, std::nullopt); } - void ParachainProcessorImpl::fragment_tree_update_inner( + void ParachainProcessorImpl::fragment_chain_update_inner( std::optional> active_leaf_hash, std::optional, ParachainId>> required_parent_info, @@ -2830,14 +2836,15 @@ namespace kagome::parachain { hypotheticals.emplace_back(known_hypotheticals->get()); } - auto frontier = prospective_parachains_->answerHypotheticalFrontierRequest( - hypotheticals, active_leaf_hash, false); + auto frontier = + prospective_parachains_->answer_hypothetical_membership_request( + hypotheticals, active_leaf_hash); for (const auto &[hypo, membership] : frontier) { if (membership.empty()) { continue; } - for (const auto &[leaf_hash, _] : membership) { + for (const auto &leaf_hash : membership) { candidates_.note_importable_under(hypo, leaf_hash); } @@ -2932,7 +2939,7 @@ namespace kagome::parachain { send_cluster_candidate_statements( candidate_hash, relayParent(post_confirmation.hypothetical)); - new_confirmed_candidate_fragment_tree_updates( + new_confirmed_candidate_fragment_chain_updates( post_confirmation.hypothetical); } @@ -3213,7 +3220,7 @@ namespace kagome::parachain { auto &active = local_validator->active; std::optional validator_id; - bool is_cluster; // NOLINT(cppcoreguidelines-init-variables) + bool is_cluster = false; [&] { auto audi = query_audi_->get(peer_id); if (not audi.has_value()) { @@ -3699,7 +3706,7 @@ namespace kagome::parachain { provide_candidate_to_grid( candidate_hash, relay_parent_state_opt->get(), confirmed, session_info); - prospective_backed_notification_fragment_tree_updates( + prospective_backed_notification_fragment_chain_updates( confirmed.para_id(), confirmed.para_head()); } @@ -3832,7 +3839,7 @@ namespace kagome::parachain { continue; } - std::vector para_ancestors_vec( + std::unordered_set para_ancestors_vec( std::move_iterator(para_ancestors.begin()), std::move_iterator(para_ancestors.end())); auto response = prospective_parachains_->answerGetBackableCandidates( @@ -4086,22 +4093,16 @@ namespace kagome::parachain { == our_current_state_.per_candidate.end()) { auto &candidate = seconded->get().committed_receipt; if (rp_state.prospective_parachains_mode) { - fragment::FragmentTreeMembership membership = - prospective_parachains_->introduceCandidate( + if (!prospective_parachains_->introduce_seconded_candidate( candidate.descriptor.para_id, candidate, - crypto::Hashed>{ seconded->get().pvd}, - candidate_hash); - if (membership.empty()) { - SL_TRACE(logger_, "`membership` is empty."); + candidate_hash)) { return Error::REJECTED_BY_PROSPECTIVE_PARACHAINS; } - - prospective_parachains_->candidateSeconded(candidate.descriptor.para_id, - candidate_hash); } our_current_state_.per_candidate.insert( {candidate_hash, @@ -4392,8 +4393,8 @@ namespace kagome::parachain { summary->group_id, relay_parent); if (rp_state.prospective_parachains_mode) { - prospective_parachains_->candidateBacked(para_id, - summary->candidate); + prospective_parachains_->candidate_backed(para_id, + summary->candidate); unblockAdvertisements( rp_state, para_id, backed->candidate.descriptor.para_head_hash); statementDistributionBackedCandidate(summary->candidate); @@ -4775,10 +4776,8 @@ namespace kagome::parachain { .persisted_validation_data = validation_result.pvd, }; - fragment::FragmentTreeMembership fragment_tree_membership; - TRY_GET_OR_RET(seconding_allowed, - secondingSanityCheck(hypothetical_candidate, false)); - fragment_tree_membership = std::move(*seconding_allowed); + TRY_GET_OR_RET(hypothetical_membership, + seconding_sanity_check(hypothetical_candidate)); auto res = sign_import_and_distribute_statement( parachain_state, validation_result); @@ -4806,7 +4805,7 @@ namespace kagome::parachain { candidate_hash); } - for (const auto &[leaf, depths] : fragment_tree_membership) { + for (const auto &leaf : *hypothetical_membership) { auto it = our_current_state_.per_leaf.find(leaf); if (it == our_current_state_.per_leaf.end()) { SL_WARN(logger_, @@ -4816,13 +4815,8 @@ namespace kagome::parachain { } ActiveLeafState &leaf_data = it->second; - auto &seconded_at_depth = - leaf_data.seconded_at_depth[validation_result.candidate.descriptor - .para_id]; - - for (const auto &depth : depths) { - seconded_at_depth.emplace(depth, candidate_hash); - } + add_seconded_candidate(leaf_data, + validation_result.candidate.descriptor.para_id); } parachain_state.issued_statements.insert(candidate_hash); @@ -5237,10 +5231,12 @@ namespace kagome::parachain { for (const auto &[hash, mode] : active_leaves) { if (mode) { - for (const auto &h : - implicit_view.knownAllowedRelayParentsUnder(hash, para_id)) { - if (h == relay_parent) { - return true; + if (const auto k = implicit_view.known_allowed_relay_parents_under( + hash, para_id)) { + for (const auto &h : *k) { + if (h == relay_parent) { + return true; + } } } } @@ -5380,79 +5376,82 @@ namespace kagome::parachain { } ParachainProcessorImpl::SecondingAllowed - ParachainProcessorImpl::secondingSanityCheck( - const HypotheticalCandidate &hypothetical_candidate, - bool backed_in_path_only) { + ParachainProcessorImpl::seconding_sanity_check( + const HypotheticalCandidate &hypothetical_candidate) { const auto &active_leaves = our_current_state_.per_leaf; const auto &implicit_view = *our_current_state_.implicit_view; - fragment::FragmentTreeMembership membership; + std::vector leaves_for_seconding; const auto candidate_para = candidatePara(hypothetical_candidate); const auto candidate_relay_parent = relayParent(hypothetical_candidate); - [[maybe_unused]] const auto candidate_hash = - candidateHash(hypothetical_candidate); - - auto proc_response = [&](std::vector &&depths, - const Hash &head, - const ActiveLeafState &leaf_state) { - for (auto depth : depths) { - if (auto it = leaf_state.seconded_at_depth.find(candidate_para.get()); - it != leaf_state.seconded_at_depth.end() - && it->second.contains(depth)) { - return false; - } + const auto candidate_hash = candidateHash(hypothetical_candidate); + + auto proc_response = [&](bool is_member_or_potential, const Hash &head) { + if (!is_member_or_potential) { + SL_TRACE(logger_, + "Refusing to second candidate at leaf. Is not a potential " + "member. (candidate_hash={}, leaf_hash={})", + candidate_hash.get(), + head); + } else { + leaves_for_seconding.emplace_back(head); } - membership.emplace_back(head, std::move(depths)); - return true; }; for (const auto &[head, leaf_state] : active_leaves) { - if (leaf_state.prospective_parachains_mode) { + if (is_type(leaf_state)) { const auto allowed_parents_for_para = - implicit_view.knownAllowedRelayParentsUnder(head, - {candidate_para.get()}); - if (std::ranges::find(allowed_parents_for_para, - candidate_relay_parent.get()) - == allowed_parents_for_para.end()) { + implicit_view.known_allowed_relay_parents_under( + head, {candidate_para.get()}); + if (!allowed_parents_for_para + || std::find(allowed_parents_for_para->begin(), + allowed_parents_for_para->end(), + candidate_relay_parent.get()) + == allowed_parents_for_para->end()) { continue; } - std::vector r; - for (auto &&[candidate, memberships] : - prospective_parachains_->answerHypotheticalFrontierRequest( + bool is_member_or_potential = false; + for (auto &&[candidate, leaves] : + prospective_parachains_->answer_hypothetical_membership_request( std::span{&hypothetical_candidate, 1}, - {{head}}, - backed_in_path_only)) { - BOOST_ASSERT(candidateHash(candidate).get() == candidate_hash.get()); - for (auto &&[relay_parent, depths] : memberships) { - BOOST_ASSERT(relay_parent == head); - r.insert(r.end(), depths.begin(), depths.end()); + {{head}})) { + if (candidateHash(candidate).get() != candidate_hash.get()) { + continue; } - } - if (!proc_response(std::move(r), head, leaf_state)) { - return std::nullopt; + for (const auto &leaf : leaves) { + if (leaf == head) { + is_member_or_potential = true; + break; + } + } + + if (is_member_or_potential) { + break; + } } + + proc_response(is_member_or_potential, head); } else { if (head == candidate_relay_parent.get()) { - if (auto it = leaf_state.seconded_at_depth.find(candidate_para.get()); - it != leaf_state.seconded_at_depth.end() - && it->second.contains(0)) { - return std::nullopt; - } - if (!proc_response(std::vector{0ull}, head, leaf_state)) { - return std::nullopt; + if (auto seconded = if_type(leaf_state)) { + if (seconded->get().contains(candidate_para.get())) { + return std::nullopt; + } } + + proc_response(true, head); } } } - if (membership.empty()) { + if (leaves_for_seconding.empty()) { return std::nullopt; } - return membership; + return leaves_for_seconding; } bool ParachainProcessorImpl::canSecond(ParachainId candidate_para_id, @@ -5462,18 +5461,13 @@ namespace kagome::parachain { auto per_relay_parent = tryGetStateByRelayParent(relay_parent); if (per_relay_parent) { if (per_relay_parent->get().prospective_parachains_mode) { - if (auto seconding_allowed = secondingSanityCheck( - HypotheticalCandidateIncomplete{ + if (auto seconding_allowed = + seconding_sanity_check(HypotheticalCandidateIncomplete{ .candidate_hash = candidate_hash, .candidate_para = candidate_para_id, .parent_head_data_hash = parent_head_data_hash, - .candidate_relay_parent = relay_parent}, - true)) { - for (const auto &[_, m] : *seconding_allowed) { - if (!m.empty()) { - return true; - } - } + .candidate_relay_parent = relay_parent})) { + return !seconding_allowed->empty(); } } } diff --git a/core/parachain/validator/parachain_processor.hpp b/core/parachain/validator/parachain_processor.hpp index 88a7e3d56c..e7b51ed940 100644 --- a/core/parachain/validator/parachain_processor.hpp +++ b/core/parachain/validator/parachain_processor.hpp @@ -41,7 +41,7 @@ #include "parachain/validator/collations.hpp" #include "parachain/validator/impl/candidates.hpp" #include "parachain/validator/impl/statements_store.hpp" -#include "parachain/validator/prospective_parachains.hpp" +#include "parachain/validator/prospective_parachains/prospective_parachains.hpp" #include "parachain/validator/signer.hpp" #include "primitives/common.hpp" #include "primitives/event_types.hpp" @@ -298,7 +298,7 @@ namespace kagome::parachain { using WorkersContext = boost::asio::io_context; using WorkGuard = boost::asio::executor_work_guard< boost::asio::io_context::executor_type>; - using SecondingAllowed = std::optional; + using SecondingAllowed = std::optional>; struct ValidateAndSecondResult { outcome::result result; @@ -647,12 +647,12 @@ namespace kagome::parachain { ParachainId para_id) const; void send_cluster_candidate_statements(const CandidateHash &candidate_hash, const RelayHash &relay_parent); - void new_confirmed_candidate_fragment_tree_updates( + void new_confirmed_candidate_fragment_chain_updates( const HypotheticalCandidate &candidate); - void new_leaf_fragment_tree_updates(const Hash &leaf_hash); - void prospective_backed_notification_fragment_tree_updates( + void new_leaf_fragment_chain_updates(const Hash &leaf_hash); + void prospective_backed_notification_fragment_chain_updates( ParachainId para_id, const Hash ¶_head); - void fragment_tree_update_inner( + void fragment_chain_update_inner( std::optional> active_leaf_hash, std::optional, ParachainId>> required_parent_info, @@ -664,7 +664,7 @@ namespace kagome::parachain { const CandidateHash &candidate_hash, GroupIndex group_index); outcome::result getBabeRandomness( - const primitives::BlockHeader &block_header); + const RelayHash &relay_parent); outcome::result> fetch_claim_queue(const RelayHash &relay_parent); void request_attested_candidate(const libp2p::peer::PeerId &peer, @@ -1007,32 +1007,34 @@ namespace kagome::parachain { ParachainProcessorImpl::RelayParentState &relay_parent_state); /** - * The `createBackingTask` function is responsible for creating a new + * The `create_backing_task` function is responsible for creating a new * backing task for a given relay parent. It first asserts that the function * is running in the main thread context. Then, it initializes a new backing - * task for the relay parent by calling the `initNewBackingTask` function. - * If the initialization is successful, it stores the state of the relay - * parent by calling the `storeStateByRelayParent` function. If the - * initialization fails and the error is not due to the absence of a key, it - * logs an error message. + * task for the relay parent by calling the + * `construct_per_relay_parent_state` function. If the initialization is + * successful, it stores the state of the relay parent by calling the + * `storeStateByRelayParent` function. If the initialization fails and the + * error is not due to the absence of a key, it logs an error message. * * @param relay_parent The hash of the relay parent block for which the * backing task is to be created. */ - void createBackingTask(const primitives::BlockHash &relay_parent, - const network::HashedBlockHeader &block_header); + void create_backing_task( + const primitives::BlockHash &relay_parent, + const network::HashedBlockHeader &block_header, + const std::vector &deactivated); /** - * @brief The `initNewBackingTask` function is responsible for initializing - * a new backing task for a given relay parent. + * @brief The `construct_per_relay_parent_state` function is responsible for + * initializing a new backing task for a given relay parent. * @param relay_parent The hash of the relay parent block for which the * backing task is to be created. * @return A `RelayParentState` object that contains the assignment, * validator index, required collator, and table context. */ - outcome::result initNewBackingTask( + outcome::result construct_per_relay_parent_state( const primitives::BlockHash &relay_parent, - const network::HashedBlockHeader &block_header); + const ProspectiveParachainsModeOpt &mode); void spawn_and_update_peer( std::unordered_set &cache, @@ -1133,9 +1135,8 @@ namespace kagome::parachain { const CandidateHash &candidate_hash, const Hash &parent_head_data_hash); - ParachainProcessorImpl::SecondingAllowed secondingSanityCheck( - const HypotheticalCandidate &hypothetical_candidate, - bool backed_in_path_only); + ParachainProcessorImpl::SecondingAllowed seconding_sanity_check( + const HypotheticalCandidate &hypothetical_candidate); void printStoragesLoad(); diff --git a/core/parachain/validator/prospective_parachains.hpp b/core/parachain/validator/prospective_parachains.hpp deleted file mode 100644 index 53313a0b80..0000000000 --- a/core/parachain/validator/prospective_parachains.hpp +++ /dev/null @@ -1,816 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include "blockchain/block_tree.hpp" -#include "blockchain/block_tree_error.hpp" -#include "network/peer_view.hpp" -#include "network/types/collator_messages_vstaging.hpp" -#include "parachain/types.hpp" -#include "parachain/validator/collations.hpp" -#include "parachain/validator/fragment_tree.hpp" -#include "runtime/runtime_api/parachain_host.hpp" -#include "runtime/runtime_api/parachain_host_types.hpp" -#include "utils/map.hpp" - -namespace kagome::parachain { - - using ParentHeadData_OnlyHash = Hash; - using ParentHeadData_WithData = std::pair; - using ParentHeadData = - boost::variant; - - class ProspectiveParachains { -#ifdef CFG_TESTING - public: -#endif // CFG_TESTING - struct RelayBlockViewData { - // Scheduling info for paras and upcoming paras. - std::unordered_map fragment_trees; - std::unordered_set pending_availability; - }; - - struct View { - // Active or recent relay-chain blocks by block hash. - std::unordered_map active_leaves; - std::unordered_map - candidate_storage; - }; - - struct ImportablePendingAvailability { - network::CommittedCandidateReceipt candidate; - runtime::PersistedValidationData persisted_validation_data; - fragment::PendingAvailability compact; - }; - - View view; - std::shared_ptr hasher_; - std::shared_ptr parachain_host_; - std::shared_ptr block_tree_; - log::Logger logger = - log::createLogger("ProspectiveParachains", "parachain"); - - public: - ProspectiveParachains( - std::shared_ptr hasher, - std::shared_ptr parachain_host, - std::shared_ptr block_tree) - : hasher_{std::move(hasher)}, - parachain_host_{std::move(parachain_host)}, - block_tree_{std::move(block_tree)} { - BOOST_ASSERT(hasher_); - BOOST_ASSERT(parachain_host_); - BOOST_ASSERT(block_tree_); - } - - void printStoragesLoad() { - SL_TRACE(logger, - "[Prospective parachains storages statistics]:" - "\n\t-> view.active_leaves={}" - "\n\t-> view.candidate_storage={}", - view.active_leaves.size(), - view.candidate_storage.size()); - } - - std::shared_ptr getBlockTree() { - BOOST_ASSERT(block_tree_); - return block_tree_; - } - - std::vector> - answerMinimumRelayParentsRequest(const RelayHash &relay_parent) const { - std::vector> v; - SL_TRACE(logger, - "Search for minimum relay parents. (relay_parent={})", - relay_parent); - - auto it = view.active_leaves.find(relay_parent); - if (it != view.active_leaves.end()) { - const RelayBlockViewData &leaf_data = it->second; - SL_TRACE( - logger, - "Found active list. (relay_parent={}, fragment_trees_count={})", - relay_parent, - leaf_data.fragment_trees.size()); - - for (const auto &[para_id, fragment_tree] : leaf_data.fragment_trees) { - v.emplace_back(para_id, - fragment_tree.scope.earliestRelayParent().number); - } - } - return v; - } - - std::vector> answerGetBackableCandidates( - const RelayHash &relay_parent, - ParachainId para, - uint32_t count, - const std::vector &required_path) { - SL_TRACE(logger, - "Search for backable candidates. (para_id={}, " - "relay_parent={})", - para, - relay_parent); - auto data_it = view.active_leaves.find(relay_parent); - if (data_it == view.active_leaves.end()) { - SL_TRACE(logger, - "Requested backable candidate for inactive relay-parent. " - "(relay_parent={}, para_id={})", - relay_parent, - para); - return {}; - } - const RelayBlockViewData &data = data_it->second; - - auto tree_it = data.fragment_trees.find(para); - if (tree_it == data.fragment_trees.end()) { - SL_TRACE(logger, - "Requested backable candidate for inactive para. " - "(relay_parent={}, para_id={})", - relay_parent, - para); - return {}; - } - const fragment::FragmentTree &tree = tree_it->second; - - auto storage_it = view.candidate_storage.find(para); - if (storage_it == view.candidate_storage.end()) { - SL_WARN(logger, - "No candidate storage for active para. (relay_parent={}, " - "para_id={})", - relay_parent, - para); - return {}; - } - const fragment::CandidateStorage &storage = storage_it->second; - - std::vector> backable_candidates; - const auto children = tree.selectChildren( - required_path, count, [&](const CandidateHash &candidate) -> bool { - return storage.isBacked(candidate); - }); - for (const auto &child_hash : children) { - if (auto parent_hash_opt = - storage.relayParentByCandidateHash(child_hash)) { - backable_candidates.emplace_back(child_hash, *parent_hash_opt); - } else { - SL_ERROR(logger, - "Candidate is present in fragment tree but not in " - "candidate's storage! (child_hash={}, para_id={})", - child_hash, - para); - } - } - - if (backable_candidates.empty()) { - SL_TRACE(logger, - "Could not find any backable candidate. (relay_parent={}, " - "para_id={})", - relay_parent, - para); - } else { - SL_TRACE(logger, - "Found backable candidates. (relay_parent={}, count={})", - relay_parent, - backable_candidates.size()); - } - - return backable_candidates; - } - - fragment::FragmentTreeMembership answerTreeMembershipRequest( - ParachainId para, const CandidateHash &candidate) { - SL_TRACE(logger, - "Answer tree membership request. " - "(para_id={}, candidate_hash={})", - para, - candidate); - return fragmentTreeMembership(view.active_leaves, para, candidate); - } - - outcome::result> - answerProspectiveValidationDataRequest( - const RelayHash &candidate_relay_parent, - const ParentHeadData &parent_head_data, - ParachainId para_id) { - auto it = view.candidate_storage.find(para_id); - if (it == view.candidate_storage.end()) { - return std::nullopt; - } - - const auto &storage = it->second; - auto [head_data, parent_head_data_hash] = visit_in_place( - parent_head_data, - [&](const ParentHeadData_OnlyHash &parent_head_data_hash) - -> std::pair, - std::reference_wrapper> { - return {utils::fromRefToOwn( - storage.headDataByHash(parent_head_data_hash)), - parent_head_data_hash}; - }, - [&](const ParentHeadData_WithData &v) - -> std::pair, - std::reference_wrapper> { - const auto &[head_data, hash] = v; - return {head_data, hash}; - }); - - std::optional relay_parent_info{}; - std::optional max_pov_size{}; - - for (const auto &[_, x] : view.active_leaves) { - auto it = x.fragment_trees.find(para_id); - if (it == x.fragment_trees.end()) { - continue; - } - const fragment::FragmentTree &fragment_tree = it->second; - - if (head_data && relay_parent_info && max_pov_size) { - break; - } - if (!relay_parent_info) { - relay_parent_info = utils::fromRefToOwn( - fragment_tree.scope.ancestorByHash(candidate_relay_parent)); - } - if (!head_data) { - const auto &required_parent = - fragment_tree.scope.base_constraints.required_parent; - if (hasher_->blake2b_256(required_parent) - == parent_head_data_hash.get()) { - head_data = required_parent; - } - } - if (!max_pov_size) { - if (fragment_tree.scope.ancestorByHash(candidate_relay_parent)) { - max_pov_size = fragment_tree.scope.base_constraints.max_pov_size; - } - } - } - - if (head_data && relay_parent_info && max_pov_size) { - return runtime::PersistedValidationData{ - .parent_head = *head_data, - .relay_parent_number = relay_parent_info->number, - .relay_parent_storage_root = relay_parent_info->storage_root, - .max_pov_size = (uint32_t)*max_pov_size, - }; - } - - return std::nullopt; - } - - std::optional prospectiveParachainsMode( - const RelayHash &relay_parent) { - auto result = parachain_host_->staging_async_backing_params(relay_parent); - if (result.has_error()) { - SL_TRACE(logger, - "Prospective parachains are disabled, is not supported by the " - "current Runtime API. (relay parent={}, error={})", - relay_parent, - result.error()); - return std::nullopt; - } - - const parachain::fragment::AsyncBackingParams &vs = result.value(); - return ProspectiveParachainsMode{ - .max_candidate_depth = vs.max_candidate_depth, - .allowed_ancestry_len = vs.allowed_ancestry_len, - }; - } - - outcome::result>>> - fetchBackingState(const RelayHash &relay_parent, ParachainId para_id) { - auto result = - parachain_host_->staging_para_backing_state(relay_parent, para_id); - if (result.has_error()) { - SL_TRACE(logger, - "Staging para backing state failed. (relay parent={}, " - "para_id={}, error={})", - relay_parent, - para_id, - result.error()); - return result.as_failure(); - } - - auto &s = result.value(); - if (!s) { - return std::nullopt; - } - - return std::make_pair(std::move(s->constraints), - std::move(s->pending_availability)); - } - - outcome::result> - fetchBlockInfo(const RelayHash &relay_hash) { - /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 - /// cache for block header request and calculations - auto res_header = block_tree_->getBlockHeader(relay_hash); - if (res_header.has_error()) { - if (res_header.error() - == blockchain::BlockTreeError::HEADER_NOT_FOUND) { - return outcome::success(std::nullopt); - } - return res_header.error(); - } - - return fragment::RelayChainBlockInfo{ - .hash = relay_hash, - .number = res_header.value().number, - .storage_root = res_header.value().state_root, - }; - } - - outcome::result> fetchUpcomingParas( - const RelayHash &relay_parent, - std::unordered_set &pending_availability) { - OUTCOME_TRY(cores, parachain_host_->availability_cores(relay_parent)); - - std::unordered_set upcoming; - for (const auto &core : cores) { - visit_in_place( - core, - [&](const runtime::OccupiedCore &occupied) { - pending_availability.insert(occupied.candidate_hash); - if (occupied.next_up_on_available) { - upcoming.insert(occupied.next_up_on_available->para_id); - } - if (occupied.next_up_on_time_out) { - upcoming.insert(occupied.next_up_on_time_out->para_id); - } - }, - [&](const runtime::ScheduledCore &scheduled) { - upcoming.insert(scheduled.para_id); - }, - [](const auto &) {}); - } - return upcoming; - } - - outcome::result> fetchAncestry( - const RelayHash &relay_hash, size_t ancestors) { - std::vector block_info; - if (ancestors == 0) { - return block_info; - } - - OUTCOME_TRY( - hashes, - block_tree_->getDescendingChainToBlock(relay_hash, ancestors + 1)); - - if (logger->level() >= soralog::Level::TRACE) { - for (const auto &h : hashes) { - SL_TRACE(logger, - "Ancestor hash. " - "(relay_hash={}, ancestor_hash={})", - relay_hash, - h); - } - } - - OUTCOME_TRY(required_session, - parachain_host_->session_index_for_child(relay_hash)); - SL_TRACE(logger, - "Get ancestors. " - "(relay_hash={}, ancestors={}, hashes_len={})", - relay_hash, - ancestors, - hashes.size()); - - if (hashes.size() > 1) { - block_info.reserve(hashes.size() - 1); - } - for (size_t i = 1; i < hashes.size(); ++i) { - const auto &hash = hashes[i]; - OUTCOME_TRY(info, fetchBlockInfo(hash)); - if (!info) { - SL_WARN(logger, - "Failed to fetch info for hash returned from ancestry. " - "(relay_hash={})", - hash); - break; - } - OUTCOME_TRY(session, parachain_host_->session_index_for_child(hash)); - if (session == required_session) { - SL_TRACE(logger, - "Add block. " - "(relay_hash={}, hash={}, number={})", - relay_hash, - hash, - info->number); - block_info.emplace_back(*info); - } else { - SL_TRACE(logger, - "Skipped block. " - "(relay_hash={}, hash={}, number={})", - relay_hash, - hash, - info->number); - break; - } - } - return block_info; - } - - outcome::result> - preprocessCandidatesPendingAvailability( - const HeadData &required_parent, - const std::vector - &pending_availability) { - std::reference_wrapper required_parent_copy = - required_parent; - std::vector importable; - const size_t expected_count = pending_availability.size(); - - for (size_t i = 0; i < pending_availability.size(); i++) { - const auto &pending = pending_availability[i]; - OUTCOME_TRY(relay_parent, - fetchBlockInfo(pending.descriptor.relay_parent)); - if (!relay_parent) { - SL_DEBUG(logger, - "Had to stop processing pending candidates early due to " - "missing info. (candidate hash={}, parachain id={}, " - "index={}, expected count={})", - pending.candidate_hash, - pending.descriptor.para_id, - i, - expected_count); - break; - } - - const fragment::RelayChainBlockInfo &b = *relay_parent; - importable.push_back( - ImportablePendingAvailability{network::CommittedCandidateReceipt{ - pending.descriptor, - pending.commitments, - }, - runtime::PersistedValidationData{ - required_parent_copy.get(), - b.number, - b.storage_root, - pending.max_pov_size, - }, - fragment::PendingAvailability{ - pending.candidate_hash, - b, - }}); - required_parent_copy = pending.commitments.para_head; - } - return importable; - } - - outcome::result onActiveLeavesUpdate( - const network::ExViewRef &update) { - for (const auto &deactivated : update.lost) { - SL_TRACE(logger, - "Remove from active leaves. (relay_parent={})", - deactivated); - view.active_leaves.erase(deactivated); - } - - /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 - /// cache headers - [[maybe_unused]] std::unordered_map - temp_header_cache; - if (update.new_head) { - const auto &activated = update.new_head->get(); - const auto &hash = update.new_head->get().hash(); - const auto mode = prospectiveParachainsMode(hash); - if (!mode) { - SL_TRACE(logger, - "Skipping leaf activation since async backing is disabled. " - "(block_hash={})", - hash); - return outcome::success(); - } - std::unordered_set pending_availability{}; - OUTCOME_TRY(scheduled_paras, - fetchUpcomingParas(hash, pending_availability)); - - const fragment::RelayChainBlockInfo block_info{ - .hash = hash, - .number = activated.number, - .storage_root = activated.state_root, - }; - - OUTCOME_TRY(ancestry, fetchAncestry(hash, mode->allowed_ancestry_len)); - - std::unordered_map fragment_trees; - for (ParachainId para : scheduled_paras) { - auto &candidate_storage = view.candidate_storage[para]; - OUTCOME_TRY(backing_state, fetchBackingState(hash, para)); - - if (!backing_state) { - SL_TRACE(logger, - "Failed to get inclusion backing state. (para={}, relay " - "parent={})", - para, - hash); - continue; - } - const auto &[constraints, pe] = *backing_state; - OUTCOME_TRY(pending_availability, - preprocessCandidatesPendingAvailability( - constraints.required_parent, pe)); - - std::vector compact_pending; - compact_pending.reserve(pending_availability.size()); - - for (const ImportablePendingAvailability &c : pending_availability) { - const auto &candidate_hash = c.compact.candidate_hash; - auto res = candidate_storage.addCandidate( - candidate_hash, - c.candidate, - crypto::Hashed>{ - c.persisted_validation_data}, - hasher_); - compact_pending.emplace_back(c.compact); - - if (res.has_value() - || res.error() - == fragment::CandidateStorage::Error:: - CANDIDATE_ALREADY_KNOWN) { - candidate_storage.markBacked(candidate_hash); - } else { - SL_WARN(logger, - "Scraped invalid candidate pending availability. " - "(candidate_hash={}, para={}, error={})", - candidate_hash, - para, - res.error()); - } - } - - OUTCOME_TRY(scope, - fragment::Scope::withAncestors(para, - block_info, - constraints, - compact_pending, - mode->max_candidate_depth, - ancestry)); - - SL_TRACE(logger, - "Create fragment. " - "(relay_parent={}, para={}, min_relay_parent={})", - hash, - para, - scope.earliestRelayParent().number); - fragment_trees.emplace(para, - fragment::FragmentTree::populate( - hasher_, scope, candidate_storage)); - } - - SL_TRACE(logger, "Insert active leave. (relay parent={})", hash); - view.active_leaves.emplace( - hash, RelayBlockViewData{fragment_trees, pending_availability}); - } - - if (!update.lost.empty()) { - prune_view_candidate_storage(); - } - - return outcome::success(); - } - - void prune_view_candidate_storage() { - const auto &active_leaves = view.active_leaves; - std::unordered_set live_candidates; - std::unordered_set live_paras; - - for (const auto &[_, sub_view] : active_leaves) { - for (const auto &[para_id, fragment_tree] : sub_view.fragment_trees) { - for (const auto &[ch, _] : fragment_tree.candidates) { - live_candidates.insert(ch); - } - live_paras.insert(para_id); - } - - live_candidates.insert(sub_view.pending_availability.begin(), - sub_view.pending_availability.end()); - } - - for (auto it = view.candidate_storage.begin(); - it != view.candidate_storage.end();) { - auto &[para_id, storage] = *it; - if (live_paras.find(para_id) != live_paras.end()) { - storage.retain([&](const CandidateHash &h) { - return live_candidates.find(h) != live_candidates.end(); - }); - ++it; - } else { - it = view.candidate_storage.erase(it); - } - } - } - - /// @brief calculates hypothetical candidate and fragment tree membership - /// @param candidates Candidates, in arbitrary order, which should be - /// checked for possible membership in fragment trees. - /// @param fragment_tree_relay_parent Either a specific fragment tree to - /// check, otherwise all. - /// @param backed_in_path_only Only return membership if all candidates in - /// the path from the root are backed. - std::vector< - std::pair> - answerHypotheticalFrontierRequest( - const std::span &candidates, - const std::optional> - &fragment_tree_relay_parent, - bool backed_in_path_only) { - std::vector< - std::pair> - response; - response.reserve(candidates.size()); - std::ranges::transform( - candidates, - std::back_inserter(response), - [](const HypotheticalCandidate &candidate) - -> std::pair { - return {candidate, {}}; - }); - - const auto &required_active_leaf = fragment_tree_relay_parent; - for (const auto &[active_leaf, leaf_view] : view.active_leaves) { - if (required_active_leaf - && required_active_leaf->get() != active_leaf) { - continue; - } - - for (auto &[c, membership] : response) { - const ParachainId ¶_id = candidatePara(c); - auto it_fragment_tree = leaf_view.fragment_trees.find(para_id); - if (it_fragment_tree == leaf_view.fragment_trees.end()) { - continue; - } - - auto it_candidate_storage = view.candidate_storage.find(para_id); - if (it_candidate_storage == view.candidate_storage.end()) { - continue; - } - - const auto &fragment_tree = it_fragment_tree->second; - const auto &candidate_storage = it_candidate_storage->second; - const auto &candidate_hash = candidateHash(c); - const auto &hypothetical = c; - - std::vector depths = - fragment_tree.hypotheticalDepths(candidate_hash, - hypothetical, - candidate_storage, - backed_in_path_only); - - if (!depths.empty()) { - membership.emplace_back(active_leaf, std::move(depths)); - } - } - } - return response; - } - - fragment::FragmentTreeMembership fragmentTreeMembership( - const std::unordered_map &active_leaves, - ParachainId para, - const CandidateHash &candidate) const { - fragment::FragmentTreeMembership membership{}; - for (const auto &[relay_parent, view_data] : active_leaves) { - if (auto it = view_data.fragment_trees.find(para); - it != view_data.fragment_trees.end()) { - const auto &tree = it->second; - if (auto depths = tree.candidate(candidate)) { - membership.emplace_back(relay_parent, *depths); - } - } - } - return membership; - } - - void candidateSeconded(ParachainId para, - const CandidateHash &candidate_hash) { - auto it = view.candidate_storage.find(para); - if (it == view.candidate_storage.end()) { - SL_WARN(logger, - "Received instruction to second unknown candidate. (para " - "id={}, candidate hash={})", - para, - candidate_hash); - return; - } - - auto &storage = it->second; - if (!storage.contains(candidate_hash)) { - SL_WARN(logger, - "Received instruction to second unknown candidate in storage. " - "(para " - "id={}, candidate hash={})", - para, - candidate_hash); - return; - } - - storage.markSeconded(candidate_hash); - } - - void candidateBacked(ParachainId para, - const CandidateHash &candidate_hash) { - auto storage = view.candidate_storage.find(para); - if (storage == view.candidate_storage.end()) { - SL_WARN(logger, - "Received instruction to back unknown candidate. (para_id={}, " - "candidate_hash={})", - para, - candidate_hash); - return; - } - if (!storage->second.contains(candidate_hash)) { - SL_WARN(logger, - "Received instruction to back unknown candidate. (para_id={}, " - "candidate_hash={})", - para, - candidate_hash); - return; - } - if (storage->second.isBacked(candidate_hash)) { - SL_DEBUG(logger, - "Received redundant instruction to mark candidate as backed. " - "(para_id={}, candidate_hash={})", - para, - candidate_hash); - return; - } - storage->second.markBacked(candidate_hash); - } - - fragment::FragmentTreeMembership introduceCandidate( - ParachainId para, - const network::CommittedCandidateReceipt &candidate, - const crypto::Hashed> &pvd, - const CandidateHash &candidate_hash) { - auto it_storage = view.candidate_storage.find(para); - if (it_storage == view.candidate_storage.end()) { - SL_WARN(logger, - "Received seconded candidate for inactive para. (parachain " - "id={}, candidate hash={})", - para, - candidate_hash); - return {}; - } - - auto &storage = it_storage->second; - if (auto res = - storage.addCandidate(candidate_hash, candidate, pvd, hasher_); - res.has_error()) { - if (res.error() - == fragment::CandidateStorage::Error::CANDIDATE_ALREADY_KNOWN) { - return fragmentTreeMembership( - view.active_leaves, para, candidate_hash); - } - if (res.error() - == fragment::CandidateStorage::Error:: - PERSISTED_VALIDATION_DATA_MISMATCH) { - SL_WARN(logger, - "Received seconded candidate had mismatching validation " - "data. (parachain id={}, candidate hash={})", - para, - candidate_hash); - return {}; - } - } - - fragment::FragmentTreeMembership membership{}; - for (auto &[relay_parent, leaf_data] : view.active_leaves) { - if (auto it = leaf_data.fragment_trees.find(para); - it != leaf_data.fragment_trees.end()) { - auto &tree = it->second; - tree.addAndPopulate(candidate_hash, storage); - if (auto depths = tree.candidate(candidate_hash)) { - membership.emplace_back(relay_parent, *depths); - } - } - } - - if (membership.empty()) { - storage.removeCandidate(candidate_hash, hasher_); - } - - return membership; - } - }; - -} // namespace kagome::parachain diff --git a/core/parachain/validator/prospective_parachains/CMakeLists.txt b/core/parachain/validator/prospective_parachains/CMakeLists.txt new file mode 100644 index 0000000000..7b9a180133 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/CMakeLists.txt @@ -0,0 +1,26 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(prospective_parachains + candidate_storage.cpp + fragment.cpp + constraints.cpp + backed_chain.cpp + scope.cpp + fragment_chain.cpp + prospective_parachains.cpp + fragment_chain_errors.cpp + ) + +target_link_libraries(prospective_parachains + fmt::fmt + scale::scale + soralog::soralog + logger + Boost::boost + outcome + ) + diff --git a/core/parachain/validator/prospective_parachains/backed_chain.cpp b/core/parachain/validator/prospective_parachains/backed_chain.cpp new file mode 100644 index 0000000000..a7d3bab8ca --- /dev/null +++ b/core/parachain/validator/prospective_parachains/backed_chain.cpp @@ -0,0 +1,61 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/backed_chain.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT BackedChain +#define COMPONENT_NAME STRINGIFY(COMPONENT) + +namespace kagome::parachain::fragment { + + void BackedChain::push(FragmentNode candidate) { + candidates.emplace(candidate.candidate_hash); + by_parent_head.insert_or_assign(candidate.parent_head_data_hash, + candidate.candidate_hash); + by_output_head.insert_or_assign(candidate.output_head_data_hash, + candidate.candidate_hash); + chain.emplace_back(candidate); + } + + Vec BackedChain::clear() { + by_parent_head.clear(); + by_output_head.clear(); + candidates.clear(); + return std::move(chain); + } + + bool BackedChain::contains(const CandidateHash &hash) const { + return candidates.contains(hash); + } + + Vec BackedChain::revert_to_parent_hash( + const Hash &parent_head_data_hash) { + Option found_index; + for (size_t index = 0; index < chain.size(); ++index) { + const auto &node = chain[0]; + if (found_index) { + by_parent_head.erase(node.parent_head_data_hash); + by_output_head.erase(node.output_head_data_hash); + candidates.erase(node.candidate_hash); + } else if (node.output_head_data_hash == parent_head_data_hash) { + found_index = index; + } + } + if (found_index) { + auto it_from = chain.begin(); + std::advance(it_from, + std::min(*found_index + size_t(1ull), chain.size())); + + Vec removed{std::move_iterator(it_from), + std::move_iterator(chain.end())}; + chain.erase(it_from, chain.end()); + return removed; + } + return {}; + } + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/backed_chain.hpp b/core/parachain/validator/prospective_parachains/backed_chain.hpp new file mode 100644 index 0000000000..f83be98033 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/backed_chain.hpp @@ -0,0 +1,32 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/common.hpp" +#include "parachain/validator/prospective_parachains/fragment_node.hpp" + +namespace kagome::parachain::fragment { + + struct BackedChain { + // Holds the candidate chain. + Vec chain; + // Index from head data hash to the candidate hash with that head data as a + // parent. Only contains the candidates present in the `chain`. + HashMap by_parent_head; + // Index from head data hash to the candidate hash outputting that head + // data. Only contains the candidates present in the `chain`. + HashMap by_output_head; + // A set of the candidate hashes in the `chain`. + HashSet candidates; + + void push(FragmentNode candidate); + bool contains(const CandidateHash &hash) const; + Vec clear(); + Vec revert_to_parent_hash(const Hash &parent_head_data_hash); + }; + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/candidate_storage.cpp b/core/parachain/validator/prospective_parachains/candidate_storage.cpp new file mode 100644 index 0000000000..666c31c717 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/candidate_storage.cpp @@ -0,0 +1,243 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/candidate_storage.hpp" + +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, + CandidateStorage::Error, + e) { + using E = kagome::parachain::fragment::CandidateStorage::Error; + switch (e) { + case E::CANDIDATE_ALREADY_KNOWN: + return "Candidate already known"; + case E::PERSISTED_VALIDATION_DATA_MISMATCH: + return "Persisted validation data mismatch"; + case E::ZERO_LENGTH_CYCLE: + return "Zero length zycle"; + } + return "Unknown error"; +} + +namespace kagome::parachain::fragment { + + outcome::result CandidateEntry::create_seconded( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> + &persisted_validation_data, + const std::shared_ptr &hasher) { + return CandidateEntry::create(candidate_hash, + candidate, + persisted_validation_data, + CandidateState::Seconded, + hasher); + } + + outcome::result CandidateEntry::create( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> + &persisted_validation_data, + CandidateState state, + const std::shared_ptr &hasher) { + if (persisted_validation_data.getHash() + != candidate.descriptor.persisted_data_hash) { + return CandidateStorage::Error::PERSISTED_VALIDATION_DATA_MISMATCH; + } + + const auto parent_head_data_hash = + hasher->blake2b_256(persisted_validation_data.get().parent_head); + const auto output_head_data_hash = + hasher->blake2b_256(candidate.commitments.para_head); + + if (parent_head_data_hash == output_head_data_hash) { + return CandidateStorage::Error::ZERO_LENGTH_CYCLE; + } + + return CandidateEntry{ + .candidate_hash = candidate_hash, + .parent_head_data_hash = parent_head_data_hash, + .output_head_data_hash = output_head_data_hash, + .relay_parent = candidate.descriptor.relay_parent, + .candidate = std::make_shared( + candidate.commitments, + persisted_validation_data.get(), + candidate.descriptor.pov_hash, + candidate.descriptor.validation_code_hash), + .state = state, + }; + } + + Option> CandidateEntry::get_commitments() + const { + BOOST_ASSERT_MSG(candidate, "Candidate is undefined!"); + return std::cref(candidate->commitments); + } + + Option> + CandidateEntry::get_persisted_validation_data() const { + BOOST_ASSERT_MSG(candidate, "Candidate is undefined!"); + return std::cref(candidate->persisted_validation_data); + } + + Option> + CandidateEntry::get_validation_code_hash() const { + BOOST_ASSERT_MSG(candidate, "Candidate is undefined!"); + return std::cref(candidate->validation_code_hash); + } + + Hash CandidateEntry::get_parent_head_data_hash() const { + return parent_head_data_hash; + } + + Option CandidateEntry::get_output_head_data_hash() const { + return output_head_data_hash; + } + + Hash CandidateEntry::get_relay_parent() const { + return relay_parent; + } + + CandidateHash CandidateEntry::get_candidate_hash() const { + return candidate_hash; + } + + outcome::result CandidateStorage::add_candidate_entry( + CandidateEntry candidate) { + const auto candidate_hash = candidate.candidate_hash; + if (by_candidate_hash.contains(candidate_hash)) { + return Error::CANDIDATE_ALREADY_KNOWN; + } + + by_parent_head[candidate.parent_head_data_hash].emplace(candidate_hash); + by_output_head[candidate.output_head_data_hash].emplace(candidate_hash); + by_candidate_hash.emplace(candidate_hash, std::move(candidate)); + + return outcome::success(); + } + + outcome::result CandidateStorage::add_pending_availability_candidate( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> + &persisted_validation_data, + const std::shared_ptr &hasher) { + OUTCOME_TRY(entry, + CandidateEntry::create(candidate_hash, + candidate, + persisted_validation_data, + CandidateState::Backed, + hasher)); + return add_candidate_entry(std::move(entry)); + } + + bool CandidateStorage::contains(const CandidateHash &candidate_hash) const { + return by_candidate_hash.contains(candidate_hash); + } + + Option> CandidateStorage::get( + const CandidateHash &candidate_hash) const { + if (auto it = by_candidate_hash.find(candidate_hash); + it != by_candidate_hash.end()) { + return {{it->second}}; + } + return std::nullopt; + } + + Option> + CandidateStorage::head_data_by_hash(const Hash &hash) const { + auto search = [&](const auto &container) + -> Option> { + if (auto it = container.find(hash); it != container.end()) { + if (!it->second.empty()) { + const CandidateHash &a_candidate = *it->second.begin(); + return get(a_candidate); + } + } + return std::nullopt; + }; + + if (auto e = search(by_output_head)) { + return {{e->get().candidate->commitments.para_head}}; + } + if (auto e = search(by_parent_head)) { + return {{e->get().candidate->persisted_validation_data.parent_head}}; + } + return std::nullopt; + } + + void CandidateStorage::remove_candidate( + const CandidateHash &candidate_hash, + const std::shared_ptr &hasher) { + auto do_remove = [&](HashMap> &container, + const Hash &target) { + if (auto it_ = container.find(target); it_ != container.end()) { + it_->second.erase(candidate_hash); + if (it_->second.empty()) { + container.erase(it_); + } + } + }; + + auto it = by_candidate_hash.find(candidate_hash); + if (it != by_candidate_hash.end()) { + do_remove(by_parent_head, it->second.parent_head_data_hash); + do_remove(by_output_head, it->second.output_head_data_hash); + by_candidate_hash.erase(it); + } + } + + template + void CandidateStorage::retain(F &&pred /*bool(CandidateHash)*/) { + for (auto it = by_candidate_hash.begin(); it != by_candidate_hash.end();) { + if (std::forward(pred)(it->first)) { + ++it; + } else { + it = by_candidate_hash.erase(it); + } + } + + auto do_remove = [&](HashMap> &container) { + auto it = container.begin(); + for (; it != container.end();) { + auto &[_, c] = *it; + for (auto it_c = c.begin(); it_c != c.end();) { + if (std::forward(pred)(*it_c)) { + ++it_c; + } else { + it_c = c.erase(it_c); + } + } + if (c.empty()) { + it = container.erase(it); + } else { + ++it; + } + } + }; + + do_remove(by_parent_head); + do_remove(by_output_head); + } + + void CandidateStorage::mark_backed(const CandidateHash &candidate_hash) { + if (auto it = by_candidate_hash.find(candidate_hash); + it != by_candidate_hash.end()) { + it->second.state = CandidateState::Backed; + } + } + + size_t CandidateStorage::len() const { + return by_candidate_hash.size(); + } + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/candidate_storage.hpp b/core/parachain/validator/prospective_parachains/candidate_storage.hpp new file mode 100644 index 0000000000..ba6db34529 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/candidate_storage.hpp @@ -0,0 +1,145 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/common.hpp" + +#include "crypto/hasher/hasher_impl.hpp" +#include "log/logger.hpp" +#include "parachain/types.hpp" +#include "parachain/validator/collations.hpp" +#include "primitives/common.hpp" +#include "primitives/math.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" +#include "utils/map.hpp" + +namespace kagome::parachain::fragment { + + /// The state of a candidate. + /// + /// Candidates aren't even considered until they've at least been seconded. + enum CandidateState : uint8_t { + /// The candidate has been seconded. + Seconded, + /// The candidate has been completely backed by the group. + Backed, + }; + + /// Representation of a candidate into the [`CandidateStorage`]. + struct CandidateEntry { + CandidateHash candidate_hash; + Hash parent_head_data_hash; + Hash output_head_data_hash; + RelayHash relay_parent; + std::shared_ptr candidate; + CandidateState state; + + static outcome::result create_seconded( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> + &persisted_validation_data, + const std::shared_ptr &hasher); + + static outcome::result create( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> + &persisted_validation_data, + CandidateState state, + const std::shared_ptr &hasher); + + /// `HypotheticalOrConcreteCandidate` impl + Option> get_commitments() const; + Option> get_persisted_validation_data() + const; + Option> get_validation_code_hash() const; + Option get_output_head_data_hash() const; + Hash get_parent_head_data_hash() const; + Hash get_relay_parent() const; + CandidateHash get_candidate_hash() const; + }; + + struct CandidateStorage { + enum class Error : uint8_t { + CANDIDATE_ALREADY_KNOWN, + PERSISTED_VALIDATION_DATA_MISMATCH, + ZERO_LENGTH_CYCLE, + }; + + /// Introduce a new candidate entry. + outcome::result add_candidate_entry(CandidateEntry candidate); + outcome::result add_pending_availability_candidate( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> + &persisted_validation_data, + const std::shared_ptr &hasher); + + bool contains(const CandidateHash &candidate_hash) const; + + /// Returns the backed candidates which have the given head data hash as + /// parent. + template + void possible_backed_para_children( + const Hash &parent_head_hash, + F &&func /*void(const CandidateEntry &)*/) const { + if (auto it = by_parent_head.find(parent_head_hash); + it != by_parent_head.end()) { + for (const auto &h : it->second) { + if (auto c_it = by_candidate_hash.find(h); + c_it != by_candidate_hash.end() + && c_it->second.state == CandidateState::Backed) { + std::forward(func)(c_it->second); + } + } + } + } + + Option> get( + const CandidateHash &candidate_hash) const; + + Option> head_data_by_hash( + const Hash &hash) const; + + void remove_candidate(const CandidateHash &candidate_hash, + const std::shared_ptr &hasher); + + template + void retain(F &&pred /*bool(CandidateHash)*/); + + template + void candidates(F &&callback /*void(const CandidateEntry &)*/) const { + for (const auto &[_, entry] : by_candidate_hash) { + std::forward(callback)(entry); + } + } + + void mark_backed(const CandidateHash &candidate_hash); + + size_t len() const; + + // Index from head data hash to candidate hashes with that head data as a + // parent. + HashMap> by_parent_head; + + // Index from head data hash to candidate hashes outputting that head data. + HashMap> by_output_head; + + // Index from candidate hash to fragment node. + HashMap by_candidate_hash; + }; + +} // namespace kagome::parachain::fragment + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, CandidateStorage::Error); diff --git a/core/parachain/validator/prospective_parachains/common.hpp b/core/parachain/validator/prospective_parachains/common.hpp new file mode 100644 index 0000000000..d95c48a7fb --- /dev/null +++ b/core/parachain/validator/prospective_parachains/common.hpp @@ -0,0 +1,115 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "outcome/outcome.hpp" + +#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" +#include "parachain/types.hpp" + +namespace kagome::parachain::fragment { + + template + using HashMap = std::unordered_map; + template + using HashSet = std::unordered_set; + template + using Vec = std::vector; + using BitVec = scale::BitVec; + using ParaId = ParachainId; + template + using Option = std::optional; + template + using Map = std::map; + template + using Ref = std::reference_wrapper; + + using NodePointerRoot = network::Empty; + using NodePointerStorage = size_t; + using NodePointer = boost::variant; + + using CandidateCommitments = network::CandidateCommitments; + using PersistedValidationData = runtime::PersistedValidationData; + + /// Indicates the relay-parents whose fragment chain a candidate + /// is present in or can be added in (right now or in the future). + using HypotheticalMembership = Vec; + + /// A collection of ancestor candidates of a parachain. + using Ancestors = HashSet; + + struct RelayChainBlockInfo { + /// The hash of the relay-chain block. + Hash hash; + /// The number of the relay-chain block. + BlockNumber number = 0; + /// The storage-root of the relay-chain block. + Hash storage_root; + }; + + /// Information about a relay-chain block, to be used when calling this module + /// from prospective parachains. + struct BlockInfoProspectiveParachains { + /// The hash of the relay-chain block. + Hash hash; + /// The hash of the parent relay-chain block. + Hash parent_hash; + /// The number of the relay-chain block. + BlockNumber number = 0; + /// The storage-root of the relay-chain block. + Hash storage_root; + + RelayChainBlockInfo as_relay_chain_block_info() const { + return RelayChainBlockInfo{ + .hash = hash, + .number = number, + .storage_root = storage_root, + }; + } + }; + + struct ProspectiveCandidate { + /// The commitments to the output of the execution. + network::CandidateCommitments commitments; + /// The persisted validation data used to create the candidate. + runtime::PersistedValidationData persisted_validation_data; + /// The hash of the PoV. + Hash pov_hash; + /// The validation code hash used by the candidate. + ValidationCodeHash validation_code_hash; + + ProspectiveCandidate(network::CandidateCommitments c, + runtime::PersistedValidationData p, + Hash h, + ValidationCodeHash v) + : commitments{std::move(c)}, + persisted_validation_data{std::move(p)}, + pov_hash{h}, + validation_code_hash{v} {} + }; + + template + concept HypotheticalOrConcreteCandidate = requires(T v) { + v.get_commitments(); + v.get_persisted_validation_data(); + v.get_validation_code_hash(); + v.get_parent_head_data_hash(); + v.get_output_head_data_hash(); + v.get_relay_parent(); + v.get_candidate_hash(); + }; + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/impl/fragment_tree.cpp b/core/parachain/validator/prospective_parachains/constraints.cpp similarity index 50% rename from core/parachain/validator/impl/fragment_tree.cpp rename to core/parachain/validator/prospective_parachains/constraints.cpp index ad44105256..f325ebc806 100644 --- a/core/parachain/validator/impl/fragment_tree.cpp +++ b/core/parachain/validator/prospective_parachains/constraints.cpp @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "parachain/validator/fragment_tree.hpp" +#include "parachain/validator/prospective_parachains/common.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT Constraints +#define COMPONENT_NAME STRINGIFY(COMPONENT) OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, Constraints::Error, @@ -12,132 +16,28 @@ OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, using E = kagome::parachain::fragment::Constraints::Error; switch (e) { case E::DISALLOWED_HRMP_WATERMARK: - return "Disallowed HRMP watermark"; + return COMPONENT_NAME ": Disallowed HRMP watermark"; case E::NO_SUCH_HRMP_CHANNEL: - return "No such HRMP channel"; + return COMPONENT_NAME ": No such HRMP channel"; case E::HRMP_BYTES_OVERFLOW: - return "HRMP bytes overflow"; + return COMPONENT_NAME ": HRMP bytes overflow"; case E::HRMP_MESSAGE_OVERFLOW: - return "HRMP message overflow"; + return COMPONENT_NAME ": HRMP message overflow"; case E::UMP_MESSAGE_OVERFLOW: - return "UMP message overflow"; + return COMPONENT_NAME ": UMP message overflow"; case E::UMP_BYTES_OVERFLOW: - return "UMP bytes overflow"; + return COMPONENT_NAME ": UMP bytes overflow"; case E::DMP_MESSAGE_UNDERFLOW: - return "DMP message underflow"; + return COMPONENT_NAME ": DMP message underflow"; case E::APPLIED_NONEXISTENT_CODE_UPGRADE: - return "Applied nonexistent code upgrade"; - } - return "Unknown error"; -} - -OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, - CandidateStorage::Error, - e) { - using E = kagome::parachain::fragment::CandidateStorage::Error; - switch (e) { - case E::CANDIDATE_ALREADY_KNOWN: - return "Candidate already known"; - case E::PERSISTED_VALIDATION_DATA_MISMATCH: - return "Persisted validation data mismatch"; + return COMPONENT_NAME ": Applied nonexistent code upgrade"; } - return "Unknown error"; -} - -OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, Scope::Error, e) { - using E = kagome::parachain::fragment::Scope::Error; - switch (e) { - case E::UNEXPECTED_ANCESTOR: - return "Unexpected ancestor"; - } - return "Unknown error"; + return COMPONENT_NAME ": Unknown error"; } namespace kagome::parachain::fragment { - outcome::result Scope::withAncestors( - ParachainId para, - const fragment::RelayChainBlockInfo &relay_parent, - const Constraints &base_constraints, - const Vec &pending_availability, - size_t max_depth, - const Vec &ancestors) { - Map ancestors_map; - HashMap ancestors_by_hash; - - auto prev = relay_parent.number; - for (const auto &ancestor : ancestors) { - if (prev == 0) { - return Scope::Error::UNEXPECTED_ANCESTOR; - } - if (ancestor.number != prev - 1) { - return Scope::Error::UNEXPECTED_ANCESTOR; - } - if (prev == base_constraints.min_relay_parent_number) { - break; - } - prev = ancestor.number; - ancestors_by_hash.emplace(ancestor.hash, ancestor); - ancestors_map.emplace(ancestor.number, ancestor); - } - - return Scope{ - .para = para, - .relay_parent = relay_parent, - .ancestors = ancestors_map, - .ancestors_by_hash = ancestors_by_hash, - .pending_availability = pending_availability, - .base_constraints = base_constraints, - .max_depth = max_depth, - }; - } - - outcome::result CandidateStorage::addCandidate( - const CandidateHash &candidate_hash, - const network::CommittedCandidateReceipt &candidate, - const crypto::Hashed> - &persisted_validation_data, - const std::shared_ptr &hasher) { - if (by_candidate_hash.find(candidate_hash) != by_candidate_hash.end()) { - return Error::CANDIDATE_ALREADY_KNOWN; - } - - if (persisted_validation_data.getHash() - != candidate.descriptor.persisted_data_hash) { - return Error::PERSISTED_VALIDATION_DATA_MISMATCH; - } - - const auto parent_head_hash = - hasher->blake2b_256(persisted_validation_data.get().parent_head); - const auto output_head_hash = - hasher->blake2b_256(candidate.commitments.para_head); - - by_parent_head[parent_head_hash].insert(candidate_hash); - by_output_head[output_head_hash].insert(candidate_hash); - - by_candidate_hash.insert( - {candidate_hash, - CandidateEntry{ - .candidate_hash = candidate_hash, - .relay_parent = candidate.descriptor.relay_parent, - .candidate = - ProspectiveCandidate{ - .commitments = candidate.commitments, - .collator = candidate.descriptor.collator_id, - .collator_signature = candidate.descriptor.signature, - .persisted_validation_data = - persisted_validation_data.get(), - .pov_hash = candidate.descriptor.pov_hash, - .validation_code_hash = - candidate.descriptor.validation_code_hash}, - .state = CandidateState::Introduced, - }}); - return outcome::success(); - } - - bool Constraints::checkModifications( + outcome::result Constraints::check_modifications( const ConstraintModifications &modifications) const { if (modifications.hrmp_watermark) { if (auto hrmp_watermark = if_type( @@ -150,17 +50,17 @@ namespace kagome::parachain::fragment { } } if (!found) { - return false; + return Error::DISALLOWED_HRMP_WATERMARK; } } } /// TODO(iceseer): do /// implement - return true; + return outcome::success(); } - outcome::result Constraints::applyModifications( + outcome::result Constraints::apply_modifications( const ConstraintModifications &modifications) const { Constraints new_constraint{*this}; if (modifications.required_parent) { @@ -234,4 +134,5 @@ namespace kagome::parachain::fragment { return new_constraint; } + } // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/fragment.cpp b/core/parachain/validator/prospective_parachains/fragment.cpp new file mode 100644 index 0000000000..c04f4d727b --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment.cpp @@ -0,0 +1,193 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/fragment.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT Fragment +#define COMPONENT_NAME STRINGIFY(COMPONENT) + +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, Fragment::Error, e) { + using E = decltype(e); + switch (e) { + case E::HRMP_MESSAGE_DESCENDING_OR_DUPLICATE: + return COMPONENT_NAME + ": Horizontal message has descending order or duplicate"; + case E::PERSISTED_VALIDATION_DATA_MISMATCH: + return COMPONENT_NAME ": persisted validation data mismatch"; + case E::VALIDATION_CODE_MISMATCH: + return COMPONENT_NAME ": validation code mismatch by hash"; + case E::RELAY_PARENT_TOO_OLD: + return COMPONENT_NAME ": relay parent too old"; + case E::CODE_UPGRADE_RESTRICTED: + return COMPONENT_NAME ": code upgrade restricted"; + case E::CODE_SIZE_TOO_LARGE: + return COMPONENT_NAME ": code size too large"; + case E::DMP_ADVANCEMENT_RULE: + return COMPONENT_NAME ": dmp advancement rule"; + case E::HRMP_MESSAGES_PER_CANDIDATE_OVERFLOW: + return COMPONENT_NAME ": hrmp messages per candidate overflow"; + case E::UMP_MESSAGES_PER_CANDIDATE_OVERFLOW: + return COMPONENT_NAME ": ump messages per candidate overflow"; + } + return COMPONENT_NAME ": unknown error"; +} + +namespace kagome::parachain::fragment { + + outcome::result validate_against_constraints( + const Constraints &constraints, + const RelayChainBlockInfo &relay_parent, + const CandidateCommitments &commitments, + const PersistedValidationData &persisted_validation_data, + const ValidationCodeHash &validation_code_hash, + const ConstraintModifications &modifications) { + runtime::PersistedValidationData expected_pvd{ + .parent_head = constraints.required_parent, + .relay_parent_number = relay_parent.number, + .relay_parent_storage_root = relay_parent.storage_root, + .max_pov_size = uint32_t(constraints.max_pov_size), + }; + + if (expected_pvd != persisted_validation_data) { + return Fragment::Error::PERSISTED_VALIDATION_DATA_MISMATCH; + } + + if (constraints.validation_code_hash != validation_code_hash) { + return Fragment::Error::VALIDATION_CODE_MISMATCH; + } + + if (relay_parent.number < constraints.min_relay_parent_number) { + return Fragment::Error::RELAY_PARENT_TOO_OLD; + } + + if (commitments.opt_para_runtime && constraints.upgrade_restriction + && *constraints.upgrade_restriction == UpgradeRestriction::Present) { + return Fragment::Error::CODE_UPGRADE_RESTRICTED; + } + + const size_t announced_code_size = commitments.opt_para_runtime + ? commitments.opt_para_runtime->size() + : 0ull; + if (announced_code_size > constraints.max_code_size) { + return Fragment::Error::CODE_SIZE_TOO_LARGE; + } + + if (modifications.dmp_messages_processed == 0 + && !constraints.dmp_remaining_messages.empty() + && constraints.dmp_remaining_messages[0] <= relay_parent.number) { + return Fragment::Error::DMP_ADVANCEMENT_RULE; + } + + if (commitments.outbound_hor_msgs.size() + > constraints.max_hrmp_num_per_candidate) { + return Fragment::Error::HRMP_MESSAGES_PER_CANDIDATE_OVERFLOW; + } + + if (commitments.upward_msgs.size() + > constraints.max_ump_num_per_candidate) { + return Fragment::Error::UMP_MESSAGES_PER_CANDIDATE_OVERFLOW; + } + + return constraints.check_modifications(modifications); + } + + const RelayChainBlockInfo &Fragment::get_relay_parent() const { + return relay_parent; + } + + outcome::result Fragment::create( + const RelayChainBlockInfo &relay_parent, + const Constraints &operating_constraints, + const std::shared_ptr &candidate) { + OUTCOME_TRY( + modifications, + check_against_constraints(relay_parent, + operating_constraints, + candidate->commitments, + candidate->validation_code_hash, + candidate->persisted_validation_data)); + + return Fragment{ + .relay_parent = relay_parent, + .operating_constraints = operating_constraints, + .candidate = candidate, + .modifications = modifications, + }; + } + + outcome::result Fragment::check_against_constraints( + const RelayChainBlockInfo &relay_parent, + const Constraints &operating_constraints, + const CandidateCommitments &commitments, + const ValidationCodeHash &validation_code_hash, + const PersistedValidationData &persisted_validation_data) { + HashMap outbound_hrmp; + { + Option last_recipient; + for (const auto &message : commitments.outbound_hor_msgs) { + if (last_recipient && *last_recipient >= message.recipient) { + return Error::HRMP_MESSAGE_DESCENDING_OR_DUPLICATE; + } + last_recipient = message.recipient; + OutboundHrmpChannelModification &record = + outbound_hrmp[message.recipient]; + + record.bytes_submitted += message.data.size(); + record.messages_submitted += 1; + } + } + + uint32_t ump_sent_bytes = 0ull; + for (const auto &m : commitments.upward_msgs) { + ump_sent_bytes += uint32_t(m.size()); + } + + ConstraintModifications modifications{ + .required_parent = commitments.para_head, + .hrmp_watermark = ((commitments.watermark == relay_parent.number) + ? HrmpWatermarkUpdate{HrmpWatermarkUpdateHead{ + .v = commitments.watermark}} + : HrmpWatermarkUpdate{HrmpWatermarkUpdateTrunk{ + .v = commitments.watermark}}), + .outbound_hrmp = outbound_hrmp, + .ump_messages_sent = uint32_t(commitments.upward_msgs.size()), + .ump_bytes_sent = ump_sent_bytes, + .dmp_messages_processed = commitments.downward_msgs_count, + .code_upgrade_applied = + operating_constraints.future_validation_code + && relay_parent.number + >= operating_constraints.future_validation_code->first, + }; + + OUTCOME_TRY(validate_against_constraints(operating_constraints, + relay_parent, + commitments, + persisted_validation_data, + validation_code_hash, + modifications)); + return modifications; + } + + const ConstraintModifications &Fragment::constraint_modifications() const { + return modifications; + } + + const Constraints &Fragment::get_operating_constraints() const { + return operating_constraints; + } + + const ProspectiveCandidate &Fragment::get_candidate() const { + BOOST_ASSERT_MSG(candidate != nullptr, "Candidate is null"); + return *candidate; + } + + std::shared_ptr Fragment::get_candidate_clone() + const { + return candidate; + } + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/fragment.hpp b/core/parachain/validator/prospective_parachains/fragment.hpp new file mode 100644 index 0000000000..0983047927 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment.hpp @@ -0,0 +1,69 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/candidate_storage.hpp" +#include "parachain/validator/prospective_parachains/common.hpp" + +namespace kagome::parachain::fragment { + + struct Fragment { + enum Error : uint8_t { + HRMP_MESSAGE_DESCENDING_OR_DUPLICATE = 1, + PERSISTED_VALIDATION_DATA_MISMATCH, + VALIDATION_CODE_MISMATCH, + RELAY_PARENT_TOO_OLD, + CODE_UPGRADE_RESTRICTED, + CODE_SIZE_TOO_LARGE, + DMP_ADVANCEMENT_RULE, + HRMP_MESSAGES_PER_CANDIDATE_OVERFLOW, + UMP_MESSAGES_PER_CANDIDATE_OVERFLOW, + }; + + /// The new relay-parent. + RelayChainBlockInfo relay_parent; + /// The constraints this fragment is operating under. + Constraints operating_constraints; + /// The core information about the prospective candidate. + std::shared_ptr candidate; + /// Modifications to the constraints based on the outputs of + /// the candidate. + ConstraintModifications modifications; + + /// Access the relay parent information. + const RelayChainBlockInfo &get_relay_parent() const; + + /// Modifications to constraints based on the outputs of the candidate. + const ConstraintModifications &constraint_modifications() const; + + /// Access the operating constraints + const Constraints &get_operating_constraints() const; + + /// Access the underlying prospective candidate. + const ProspectiveCandidate &get_candidate() const; + + /// Get a cheap ref-counted copy of the underlying prospective candidate. + std::shared_ptr get_candidate_clone() const; + + static outcome::result create( + const RelayChainBlockInfo &relay_parent, + const Constraints &operating_constraints, + const std::shared_ptr &candidate); + + /// Check the candidate against the operating constrains and return the + /// constraint modifications + /// made by this candidate. + static outcome::result check_against_constraints( + const RelayChainBlockInfo &relay_parent, + const Constraints &operating_constraints, + const CandidateCommitments &commitments, + const ValidationCodeHash &validation_code_hash, + const PersistedValidationData &persisted_validation_data); + }; +} // namespace kagome::parachain::fragment + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, Fragment::Error) diff --git a/core/parachain/validator/prospective_parachains/fragment_chain.cpp b/core/parachain/validator/prospective_parachains/fragment_chain.cpp new file mode 100644 index 0000000000..7127f3be7e --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment_chain.cpp @@ -0,0 +1,482 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/fragment_chain.hpp" +#include "utils/stringify.hpp" + +namespace kagome::parachain::fragment { + + FragmentChain FragmentChain::init( + std::shared_ptr hasher, + const Scope &scope, + CandidateStorage candidates_pending_availability) { + FragmentChain fragment_chain{ + .scope = scope, + .best_chain = {}, + .unconnected = {}, + .hasher_ = std::move(hasher), + }; + + fragment_chain.populate_chain(candidates_pending_availability); + return fragment_chain; + } + + size_t FragmentChain::best_chain_len() const { + return best_chain.chain.size(); + } + + void FragmentChain::candidate_backed( + const CandidateHash &newly_backed_candidate) { + if (best_chain.candidates.contains(newly_backed_candidate)) { + return; + } + + auto it = unconnected.by_candidate_hash.find(newly_backed_candidate); + if (it == unconnected.by_candidate_hash.end()) { + return; + } + const auto parent_head_hash = it->second.parent_head_data_hash; + + unconnected.mark_backed(newly_backed_candidate); + if (!revert_to(parent_head_hash)) { + return; + } + + auto prev_storage{std::move(unconnected)}; + populate_chain(prev_storage); + + trim_uneligible_forks(prev_storage, parent_head_hash); + populate_unconnected_potential_candidates(std::move(prev_storage)); + } + + bool FragmentChain::is_candidate_backed(const CandidateHash &hash) const { + return best_chain.candidates.contains(hash) || [&]() { + auto it = unconnected.by_candidate_hash.find(hash); + return it != unconnected.by_candidate_hash.end() + && it->second.state == CandidateState::Backed; + }(); + } + + Vec FragmentChain::best_chain_vec() const { + Vec result; + result.reserve(best_chain.chain.size()); + + for (const auto &candidate : best_chain.chain) { + result.emplace_back(candidate.candidate_hash); + } + return result; + } + + bool FragmentChain::contains_unconnected_candidate( + const CandidateHash &candidate) const { + return unconnected.contains(candidate); + } + + size_t FragmentChain::unconnected_len() const { + return unconnected.len(); + } + + const Scope &FragmentChain::get_scope() const { + return scope; + } + + void FragmentChain::populate_from_previous( + const FragmentChain &prev_fragment_chain) { + auto prev_storage = prev_fragment_chain.unconnected; + for (const auto &candidate : prev_fragment_chain.best_chain.chain) { + if (!prev_fragment_chain.scope.get_pending_availability( + candidate.candidate_hash)) { + std::ignore = + prev_storage.add_candidate_entry(candidate.into_candidate_entry()); + } + } + + populate_chain(prev_storage); + trim_uneligible_forks(prev_storage, std::nullopt); + populate_unconnected_potential_candidates(std::move(prev_storage)); + } + + outcome::result FragmentChain::check_not_contains_candidate( + const CandidateHash &candidate_hash) const { + if (best_chain.contains(candidate_hash) + || unconnected.contains(candidate_hash)) { + return FragmentChainError::CANDIDATE_ALREADY_KNOWN; + } + return outcome::success(); + } + + outcome::result FragmentChain::check_cycles_or_invalid_tree( + const Hash &output_head_hash) const { + if (best_chain.by_parent_head.contains(output_head_hash)) { + return FragmentChainError::CYCLE; + } + + if (best_chain.by_output_head.contains(output_head_hash)) { + return FragmentChainError::MULTIPLE_PATH; + } + + return outcome::success(); + } + + Option FragmentChain::get_head_data_by_hash( + const Hash &head_data_hash) const { + const auto &required_parent = scope.get_base_constraints().required_parent; + if (hasher_->blake2b_256(required_parent) == head_data_hash) { + return required_parent; + } + + const auto has_head_data_in_chain = + best_chain.by_parent_head.contains(head_data_hash) + || best_chain.by_output_head.contains(head_data_hash); + if (has_head_data_in_chain) { + for (const auto &candidate : best_chain.chain) { + if (candidate.parent_head_data_hash == head_data_hash) { + return candidate.fragment.get_candidate() + .persisted_validation_data.parent_head; + } + + if (candidate.output_head_data_hash == head_data_hash) { + return candidate.fragment.get_candidate().commitments.para_head; + } + } + return std::nullopt; + } + + return utils::map(unconnected.head_data_by_hash(head_data_hash), + [](const auto &v) { return v.get(); }); + } + + void FragmentChain::populate_unconnected_potential_candidates( + CandidateStorage old_storage) { + for (auto &&[_, candidate] : old_storage.by_candidate_hash) { + if (scope.get_pending_availability(candidate.candidate_hash)) { + continue; + } + + if (can_add_candidate_as_potential(candidate).has_value()) { + std::ignore = unconnected.add_candidate_entry(std::move(candidate)); + } + } + } + + void FragmentChain::populate_chain(CandidateStorage &storage) { + auto cumulative_modifications = [&]() { + if (!best_chain.chain.empty()) { + const auto &last_candidate = best_chain.chain.back(); + return last_candidate.cumulative_modifications; + } + return ConstraintModifications{ + .required_parent = std::nullopt, + .hrmp_watermark = std::nullopt, + .outbound_hrmp = {}, + .ump_messages_sent = 0, + .ump_bytes_sent = 0, + .dmp_messages_processed = 0, + .code_upgrade_applied = false, + }; + }(); + + auto earliest_rp = earliest_relay_parent(); + if (!earliest_rp) { + return; + } + + for (;;) { + if (best_chain.chain.size() > scope.max_depth) { + break; + } + + Constraints child_constraints; + if (auto c = scope.base_constraints.apply_modifications( + cumulative_modifications); + c.has_value()) { + child_constraints = std::move(c.value()); + } else { + SL_TRACE( + logger, "Failed to apply modifications. (error={})", c.error()); + break; + } + + struct BestCandidate { + Fragment fragment; + CandidateHash candidate_hash; + Hash output_head_data_hash; + Hash parent_head_data_hash; + }; + + const auto required_head_hash = + hasher_->blake2b_256(child_constraints.required_parent); + Option best_candidate; + + storage.possible_backed_para_children( + required_head_hash, [&](const auto &candidate) { + auto pending = + scope.get_pending_availability(candidate.candidate_hash); + Option relay_parent = utils::map( + pending, [](const auto &v) { return v.get().relay_parent; }); + if (!relay_parent) { + relay_parent = scope.ancestor(candidate.relay_parent); + } + if (!relay_parent) { + return; + } + + if (check_cycles_or_invalid_tree(candidate.output_head_data_hash) + .has_error()) { + return; + } + + BlockNumber min_relay_parent_number = earliest_rp->number; + { + auto mrp = utils::map(pending, [&](const auto &p) { + if (best_chain.chain.empty()) { + return p.get().relay_parent.number; + } + return earliest_rp->number; + }); + if (mrp) { + min_relay_parent_number = *mrp; + } + } + + if (relay_parent->number < min_relay_parent_number) { + return; + } + + if (best_chain.contains(candidate.candidate_hash)) { + return; + } + + Fragment fragment; + { + auto constraints = child_constraints; + if (pending) { + constraints.min_relay_parent_number = + pending->get().relay_parent.number; + } + + if (auto f = Fragment::create( + *relay_parent, constraints, candidate.candidate); + f.has_value()) { + fragment = std::move(f.value()); + } else { + SL_TRACE(logger, + "Failed to instantiate fragment. (error={}, " + "candidate_hash={})", + f.error(), + candidate.candidate_hash); + return; + } + } + + if (!best_candidate + || scope.get_pending_availability(candidate.candidate_hash)) { + best_candidate = BestCandidate{ + .fragment = std::move(fragment), + .candidate_hash = candidate.candidate_hash, + .output_head_data_hash = candidate.output_head_data_hash, + .parent_head_data_hash = candidate.parent_head_data_hash, + }; + } else if (scope.get_pending_availability( + best_candidate->candidate_hash)) { + } else { + if (fork_selection_rule(candidate.candidate_hash, + best_candidate->candidate_hash)) { + best_candidate = BestCandidate{ + .fragment = std::move(fragment), + .candidate_hash = candidate.candidate_hash, + .output_head_data_hash = candidate.output_head_data_hash, + .parent_head_data_hash = candidate.parent_head_data_hash, + }; + } + } + }); + + if (!best_candidate) { + break; + } + + storage.remove_candidate(best_candidate->candidate_hash, hasher_); + cumulative_modifications.stack( + best_candidate->fragment.constraint_modifications()); + earliest_rp = best_candidate->fragment.get_relay_parent(); + + best_chain.push(FragmentNode{ + .fragment = std::move(best_candidate->fragment), + .candidate_hash = best_candidate->candidate_hash, + .cumulative_modifications = cumulative_modifications, + .parent_head_data_hash = best_candidate->parent_head_data_hash, + .output_head_data_hash = best_candidate->output_head_data_hash, + }); + } + } + + bool FragmentChain::fork_selection_rule(const CandidateHash &hash1, + const CandidateHash &hash2) { + return std::less<>()(hash1, hash2); + } + + bool FragmentChain::revert_to(const Hash &parent_head_hash) { + Option> removed_items; + if (hasher_->blake2b_256(scope.base_constraints.required_parent) + == parent_head_hash) { + removed_items = best_chain.clear(); + } + + if (!removed_items + && best_chain.by_output_head.contains(parent_head_hash)) { + removed_items = best_chain.revert_to_parent_hash(parent_head_hash); + } + + if (!removed_items) { + return false; + } + for (const auto &node : *removed_items) { + std::ignore = + unconnected.add_candidate_entry(node.into_candidate_entry()); + } + return true; + } + + void FragmentChain::trim_uneligible_forks(CandidateStorage &storage, + Option starting_point) const { + std::deque> queue; + if (starting_point) { + queue.emplace_back(*starting_point, true); + } else { + if (best_chain.chain.empty()) { + queue.emplace_back( + hasher_->blake2b_256(scope.base_constraints.required_parent), true); + } else { + for (const auto &c : best_chain.chain) { + queue.emplace_back(c.parent_head_data_hash, true); + } + } + } + + auto pop = [&]() { + Option> result; + if (!queue.empty()) { + result = std::move(queue.front()); + queue.pop_front(); + } + return result; + }; + + HashSet visited; + while (auto data = pop()) { + const auto &[parent, parent_has_potential] = *data; + visited.insert(parent); + + auto children = utils::get(storage.by_parent_head, parent); + if (!children) { + continue; + } + + Vec to_remove; + for (const auto &child_hash : (*children)->second) { + auto child = utils::get(storage.by_candidate_hash, child_hash); + if (!child) { + continue; + } + + if (visited.contains((*child)->second.output_head_data_hash)) { + continue; + } + + if (parent_has_potential + && check_potential((*child)->second).has_value()) { + queue.emplace_back((*child)->second.output_head_data_hash, true); + } else { + to_remove.emplace_back(child_hash); + queue.emplace_back((*child)->second.output_head_data_hash, false); + } + } + + for (const auto &hash : to_remove) { + storage.remove_candidate(hash, hasher_); + } + } + } + + RelayChainBlockInfo + FragmentChain::earliest_relay_parent_pending_availability() const { + for (const auto &candidate : std::ranges::reverse_view(best_chain.chain)) { + auto item = + utils::map(scope.get_pending_availability(candidate.candidate_hash), + [](const auto &v) { return v.get().relay_parent; }); + if (item) { + return *item; + } + } + return scope.earliest_relay_parent(); + } + + Option FragmentChain::earliest_relay_parent() const { + Option result; + if (!best_chain.chain.empty()) { + const auto &last_candidate = best_chain.chain.back(); + result = scope.ancestor(last_candidate.relay_parent()); + if (!result) { + result = utils::map( + scope.get_pending_availability(last_candidate.candidate_hash), + [](const auto &v) { return v.get().relay_parent; }); + } + } else { + result = scope.earliest_relay_parent(); + } + return result; + } + + size_t FragmentChain::find_ancestor_path(Ancestors ancestors) const { + if (best_chain.chain.empty()) { + return 0; + } + + for (size_t index = 0; index < best_chain.chain.size(); ++index) { + const auto &candidate = best_chain.chain[index]; + if (!ancestors.erase(candidate.candidate_hash)) { + return index; + } + } + return best_chain.chain.size(); + } + + Vec> FragmentChain::find_backable_chain( + Ancestors ancestors, uint32_t count) const { + if (count == 0) { + return {}; + } + + const auto base_pos = find_ancestor_path(std::move(ancestors)); + const auto actual_end_index = + std::min(base_pos + size_t(count), best_chain.chain.size()); + + Vec> res; + res.reserve(actual_end_index - base_pos); + + for (size_t ix = base_pos; ix < actual_end_index; ++ix) { + const auto &elem = best_chain.chain[ix]; + if (!scope.get_pending_availability(elem.candidate_hash)) { + res.emplace_back(elem.candidate_hash, elem.relay_parent()); + } else { + break; + } + } + return res; + } + + outcome::result FragmentChain::try_adding_seconded_candidate( + const CandidateEntry &candidate) { + if (candidate.state == CandidateState::Backed) { + return FragmentChainError::INTRODUCE_BACKED_CANDIDATE; + } + + OUTCOME_TRY(can_add_candidate_as_potential(candidate)); + return unconnected.add_candidate_entry(candidate); + } + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/fragment_chain.hpp b/core/parachain/validator/prospective_parachains/fragment_chain.hpp new file mode 100644 index 0000000000..ff2b694af3 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment_chain.hpp @@ -0,0 +1,268 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/backed_chain.hpp" +#include "parachain/validator/prospective_parachains/candidate_storage.hpp" +#include "parachain/validator/prospective_parachains/common.hpp" +#include "parachain/validator/prospective_parachains/fragment_chain_errors.hpp" +#include "parachain/validator/prospective_parachains/scope.hpp" + +namespace kagome::parachain::fragment { + + struct FragmentChain { + // The current scope, which dictates the on-chain operating constraints that + // all future candidates must adhere to. + Scope scope; + + // The current best chain of backable candidates. It only contains + // candidates which build on top of each other and which have reached the + // backing quorum. In the presence of potential forks, this chain will pick + // a fork according to the `fork_selection_rule`. + BackedChain best_chain; + + // The potential candidate storage. Contains candidates which are not yet + // part of the `chain` but may become in the future. These can form any tree + // shape as well as contain any unconnected candidates for which we don't + // know the parent. + CandidateStorage unconnected; + + // Hasher + std::shared_ptr hasher_; + + // Logger + log::Logger logger = log::createLogger("parachain", "fragment_chain"); + + /// Create a new [`FragmentChain`] with the given scope and populate it with + /// the candidates pending availability. + static FragmentChain init(std::shared_ptr hasher, + const Scope &scope, + CandidateStorage candidates_pending_availability); + + /// Populate the [`FragmentChain`] given the new candidates pending + /// availability and the optional previous fragment chain (of the previous + /// relay parent). + void populate_from_previous(const FragmentChain &prev_fragment_chain); + + /// Get the scope of the [`FragmentChain`]. + const Scope &get_scope() const; + + /// Returns the number of candidates in the best backable chain. + size_t best_chain_len() const; + + /// Returns the number of candidates in unconnected potential storage. + size_t unconnected_len() const; + + /// Whether the candidate exists as part of the unconnected potential + /// candidates. + bool contains_unconnected_candidate(const CandidateHash &candidate) const; + + /// Return a vector of the chain's candidate hashes, in-order. + Vec best_chain_vec() const; + + template + void get_unconnected(F &&callback /*void(const CandidateEntry &)*/) const { + unconnected.candidates(std::forward(callback)); + } + + /// Return whether this candidate is backed in this chain or the unconnected + /// storage. + bool is_candidate_backed(const CandidateHash &hash) const; + + /// Mark a candidate as backed. This can trigger a recreation of the best + /// backable chain. + void candidate_backed(const CandidateHash &newly_backed_candidate); + + /// Checks if this candidate could be added in the future to this chain. + /// This will return `Error::CandidateAlreadyKnown` if the candidate is + /// already in the chain or the unconnected candidate storage. + outcome::result can_add_candidate_as_potential( + const HypotheticalOrConcreteCandidate auto &candidate) const { + OUTCOME_TRY(check_not_contains_candidate(candidate.get_candidate_hash())); + return check_potential(candidate); + } + + outcome::result check_not_contains_candidate( + const CandidateHash &candidate_hash) const; + + /// Try adding a seconded candidate, if the candidate has potential. It will + /// never be added to the chain directly in the seconded state, it will only + /// be part of the unconnected storage. + outcome::result try_adding_seconded_candidate( + const CandidateEntry &candidate); + + /// Try getting the full head data associated with this hash. + Option get_head_data_by_hash(const Hash &head_data_hash) const; + + /// Select `count` candidates after the given `ancestors` which can be + /// backed on chain next. + /// + /// The intention of the `ancestors` is to allow queries on the basis of + /// one or more candidates which were previously pending availability + /// becoming available or candidates timing out. + Vec> find_backable_chain( + Ancestors ancestors, uint32_t count) const; + + // Tries to orders the ancestors into a viable path from root to the last + // one. Stops when the ancestors are all used or when a node in the chain is + // not present in the ancestor set. Returns the index in the chain were the + // search stopped. + size_t find_ancestor_path(Ancestors ancestors) const; + + // Return the earliest relay parent a new candidate can have in order to be + // added to the chain right now. This is the relay parent of the last + // candidate in the chain. The value returned may not be valid if we want to + // add a candidate pending availability, which may have a relay parent which + // is out of scope. Special handling is needed in that case. `None` is + // returned if the candidate's relay parent info cannot be found. + Option earliest_relay_parent() const; + + // Return the earliest relay parent a potential candidate may have for it to + // ever be added to the chain. This is the relay parent of the last + // candidate pending availability or the earliest relay parent in scope. + RelayChainBlockInfo earliest_relay_parent_pending_availability() const; + + // Populate the unconnected potential candidate storage starting from a + // previous storage. + void populate_unconnected_potential_candidates( + CandidateStorage old_storage); + + // Check whether a candidate outputting this head data would introduce a + // cycle or multiple paths to the same state. Trivial 0-length cycles are + // checked in `CandidateEntry::new`. + outcome::result check_cycles_or_invalid_tree( + const Hash &output_head_hash) const; + + // Checks the potential of a candidate to be added to the chain now or in + // the future. It works both with concrete candidates for which we have the + // full PVD and committed receipt, but also does some more basic checks for + // incomplete candidates (before even fetching them). + outcome::result check_potential( + const HypotheticalOrConcreteCandidate auto &candidate) const { + const auto parent_head_hash = candidate.get_parent_head_data_hash(); + + if (auto output_head_hash = candidate.get_output_head_data_hash()) { + if (parent_head_hash == *output_head_hash) { + return FragmentChainError::ZERO_LENGTH_CYCLE; + } + } + + auto relay_parent = scope.ancestor(candidate.get_relay_parent()); + if (!relay_parent) { + return FragmentChainError::RELAY_PARENT_NOT_IN_SCOPE; + } + + const auto earliest_rp_of_pending_availability = + earliest_relay_parent_pending_availability(); + if (relay_parent->number < earliest_rp_of_pending_availability.number) { + return FragmentChainError:: + RELAY_PARENT_PRECEDES_CANDIDATE_PENDING_AVAILABILITY; + } + + if (auto other_candidate = + utils::get(best_chain.by_parent_head, parent_head_hash)) { + if (scope.get_pending_availability((*other_candidate)->second)) { + return FragmentChainError::FORK_WITH_CANDIDATE_PENDING_AVAILABILITY; + } + + if (fork_selection_rule((*other_candidate)->second, + candidate.get_candidate_hash())) { + return FragmentChainError::FORK_CHOICE_RULE; + } + } + + std::pair> constraints_and_number; + if (auto parent_candidate_ = + utils::get(best_chain.by_output_head, parent_head_hash)) { + auto parent_candidate = std::ranges::find_if( + best_chain.chain.begin(), + best_chain.chain.end(), + [&](const auto &c) { + return c.candidate_hash == (*parent_candidate_)->second; + }); + if (parent_candidate == best_chain.chain.end()) { + return FragmentChainError::PARENT_CANDIDATE_NOT_FOUND; + } + + auto constraints = scope.base_constraints.apply_modifications( + parent_candidate->cumulative_modifications); + if (constraints.has_error()) { + return FragmentChainError::COMPUTE_CONSTRAINTS; + } + constraints_and_number = std::make_pair( + std::move(constraints.value()), + utils::map(scope.ancestor(parent_candidate->relay_parent()), + [](const auto &rp) { return rp.number; })); + } else if (hasher_->blake2b_256(scope.base_constraints.required_parent) + == parent_head_hash) { + constraints_and_number = + std::make_pair(scope.base_constraints, std::nullopt); + } else { + return outcome::success(); + } + const auto &[constraints, maybe_min_relay_parent_number] = + constraints_and_number; + + if (auto output_head_hash = candidate.get_output_head_data_hash()) { + OUTCOME_TRY(check_cycles_or_invalid_tree(*output_head_hash)); + } + + if (candidate.get_commitments() + && candidate.get_persisted_validation_data() + && candidate.get_validation_code_hash()) { + if (Fragment::check_against_constraints( + *relay_parent, + constraints, + candidate.get_commitments()->get(), + candidate.get_validation_code_hash()->get(), + candidate.get_persisted_validation_data()->get()) + .has_error()) { + return FragmentChainError::CHECK_AGAINST_CONSTRAINTS; + } + } + + if (relay_parent->number < constraints.min_relay_parent_number) { + return FragmentChainError::RELAY_PARENT_MOVED_BACKWARDS; + } + + if (maybe_min_relay_parent_number) { + if (relay_parent->number < *maybe_min_relay_parent_number) { + return FragmentChainError::RELAY_PARENT_MOVED_BACKWARDS; + } + } + + return outcome::success(); + } + + // Once the backable chain was populated, trim the forks generated by + // candidates which are not present in the best chain. Fan this out into a + // full breadth-first search. If `starting_point` is `Some()`, start the + // search from the candidates having this parent head hash. + void trim_uneligible_forks(CandidateStorage &storage, + Option starting_point) const; + + // Revert the best backable chain so that the last candidate will be one + // outputting the given `parent_head_hash`. If the `parent_head_hash` is + // exactly the required parent of the base constraints (builds on the latest + // included candidate), revert the entire chain. Return false if we couldn't + // find the parent head hash. + bool revert_to(const Hash &parent_head_hash); + + /// The rule for selecting between two backed candidate forks, when adding + /// to the chain. All validators should adhere to this rule, in order to not + /// lose out on rewards in case of forking parachains. + static bool fork_selection_rule(const CandidateHash &hash1, + const CandidateHash &hash2); + + // Populate the fragment chain with candidates from the supplied + // `CandidateStorage`. Can be called by the constructor or when backing a + // new candidate. When this is called, it may cause the previous chain to be + // completely erased or it may add more than one candidate. + void populate_chain(CandidateStorage &storage); + }; + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/fragment_chain_errors.cpp b/core/parachain/validator/prospective_parachains/fragment_chain_errors.cpp new file mode 100644 index 0000000000..3dd089f6e5 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment_chain_errors.cpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/fragment_chain_errors.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT FragmentChain +#define COMPONENT_NAME STRINGIFY(COMPONENT) + +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, + FragmentChainError, + e) { + using E = decltype(e); + switch (e) { + case E::CANDIDATE_ALREADY_KNOWN: + return COMPONENT_NAME ": Candidate already known"; + case E::INTRODUCE_BACKED_CANDIDATE: + return COMPONENT_NAME ": Introduce backed candidate"; + case E::CYCLE: + return COMPONENT_NAME ": Cycle"; + case E::MULTIPLE_PATH: + return COMPONENT_NAME ": Multiple path"; + case E::ZERO_LENGTH_CYCLE: + return COMPONENT_NAME ": Zero length cycle"; + case E::RELAY_PARENT_NOT_IN_SCOPE: + return COMPONENT_NAME ": Relay parent not in scope"; + case E::RELAY_PARENT_PRECEDES_CANDIDATE_PENDING_AVAILABILITY: + return COMPONENT_NAME + ": Relay parent precedes candidate pending availability"; + case E::FORK_WITH_CANDIDATE_PENDING_AVAILABILITY: + return COMPONENT_NAME ": Fork with candidate pending availability"; + case E::FORK_CHOICE_RULE: + return COMPONENT_NAME ": Fork choice rule"; + case E::PARENT_CANDIDATE_NOT_FOUND: + return COMPONENT_NAME ": Parent candidate not found"; + case E::COMPUTE_CONSTRAINTS: + return COMPONENT_NAME ": Compute constraints"; + case E::CHECK_AGAINST_CONSTRAINTS: + return COMPONENT_NAME ": Check against constraints"; + case E::RELAY_PARENT_MOVED_BACKWARDS: + return COMPONENT_NAME ": Relay parent moved backwards"; + } + return COMPONENT_NAME ": unknown error"; +} diff --git a/core/parachain/validator/prospective_parachains/fragment_chain_errors.hpp b/core/parachain/validator/prospective_parachains/fragment_chain_errors.hpp new file mode 100644 index 0000000000..d025d23c39 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment_chain_errors.hpp @@ -0,0 +1,31 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/common.hpp" + +namespace kagome::parachain::fragment { + + enum FragmentChainError : uint8_t { + CANDIDATE_ALREADY_KNOWN = 1, + INTRODUCE_BACKED_CANDIDATE, + CYCLE, + MULTIPLE_PATH, + ZERO_LENGTH_CYCLE, + RELAY_PARENT_NOT_IN_SCOPE, + RELAY_PARENT_PRECEDES_CANDIDATE_PENDING_AVAILABILITY, + FORK_WITH_CANDIDATE_PENDING_AVAILABILITY, + FORK_CHOICE_RULE, + PARENT_CANDIDATE_NOT_FOUND, + COMPUTE_CONSTRAINTS, + CHECK_AGAINST_CONSTRAINTS, + RELAY_PARENT_MOVED_BACKWARDS, + }; + +} // namespace kagome::parachain::fragment + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, FragmentChainError) diff --git a/core/parachain/validator/prospective_parachains/fragment_node.hpp b/core/parachain/validator/prospective_parachains/fragment_node.hpp new file mode 100644 index 0000000000..ccdbeff9b3 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/fragment_node.hpp @@ -0,0 +1,37 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/common.hpp" +#include "parachain/validator/prospective_parachains/fragment.hpp" + +namespace kagome::parachain::fragment { + + struct FragmentNode { + Fragment fragment; + CandidateHash candidate_hash; + ConstraintModifications cumulative_modifications; + Hash parent_head_data_hash; + Hash output_head_data_hash; + + const Hash &relay_parent() const { + return fragment.get_relay_parent().hash; + } + + CandidateEntry into_candidate_entry() const { + return CandidateEntry{ + .candidate_hash = this->candidate_hash, + .parent_head_data_hash = this->parent_head_data_hash, + .output_head_data_hash = this->output_head_data_hash, + .relay_parent = this->relay_parent(), + .candidate = this->fragment.get_candidate_clone(), + .state = CandidateState::Backed, + }; + } + }; + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/prospective_parachains.cpp b/core/parachain/validator/prospective_parachains/prospective_parachains.cpp new file mode 100644 index 0000000000..fce3b3672e --- /dev/null +++ b/core/parachain/validator/prospective_parachains/prospective_parachains.cpp @@ -0,0 +1,824 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/prospective_parachains.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT ProspectiveParachains +#define COMPONENT_NAME STRINGIFY(COMPONENT) + +template <> +struct fmt::formatter< + std::vector> { + constexpr auto parse(format_parse_context &ctx) + -> format_parse_context::iterator { + return ctx.end(); + } + + template + auto format( + const std::vector< + kagome::parachain::fragment::BlockInfoProspectiveParachains> &data, + FormatContext &ctx) const -> decltype(ctx.out()) { + auto out = fmt::format_to(ctx.out(), "[ "); + for (const auto &i : data) { + out = fmt::format_to( + out, + "BlockInfoProspectiveParachains {{ hash = {}, parent_hash = {}, " + "number = {}, storage_root = {} }}, ", + i.hash, + i.parent_hash, + i.number, + i.storage_root); + } + return fmt::format_to(ctx.out(), "]"); + } +}; + +namespace kagome::parachain { + ProspectiveParachains::ProspectiveParachains( + std::shared_ptr hasher, + std::shared_ptr parachain_host, + std::shared_ptr block_tree) + : hasher_{std::move(hasher)}, + parachain_host_{std::move(parachain_host)}, + block_tree_{std::move(block_tree)} { + BOOST_ASSERT(hasher_); + BOOST_ASSERT(parachain_host_); + BOOST_ASSERT(block_tree_); + } + + void ProspectiveParachains::printStoragesLoad() { + SL_TRACE(logger, + "[Prospective parachains storages statistics]:" + "\n\t-> view.per_relay_parent={}" + "\n\t-> view.active_leaves={}", + view().per_relay_parent.size(), + view().active_leaves.size()); + } + + std::shared_ptr ProspectiveParachains::getBlockTree() { + BOOST_ASSERT(block_tree_); + return block_tree_; + } + + std::vector> + ProspectiveParachains::answerMinimumRelayParentsRequest( + const RelayHash &relay_parent) { + std::vector> v; + if (view().active_leaves.contains(relay_parent)) { + if (auto leaf_data = utils::get(view().per_relay_parent, relay_parent)) { + for (const auto &[para_id, fragment_chain] : + (*leaf_data)->second.fragment_chains) { + v.emplace_back( + para_id, + fragment_chain.get_scope().earliest_relay_parent().number); + } + } + } + return v; + } + + std::vector> + ProspectiveParachains::answerGetBackableCandidates( + const RelayHash &relay_parent, + ParachainId para, + uint32_t count, + const fragment::Ancestors &ancestors) { + SL_TRACE(logger, + "Search for backable candidates. (para_id={}, " + "relay_parent={})", + para, + relay_parent); + if (!view().active_leaves.contains(relay_parent)) { + SL_TRACE(logger, + "Requested backable candidate for inactive relay-parent. " + "(relay_parent={}, para_id={})", + relay_parent, + para); + return {}; + } + + auto data = utils::get(view().per_relay_parent, relay_parent); + if (!data) { + SL_TRACE(logger, + "Requested backable candidate for inexistent relay-parent. " + "(relay_parent={}, para_id={})", + relay_parent, + para); + return {}; + } + + auto chain_it = utils::get((*data)->second.fragment_chains, para); + if (!chain_it) { + SL_TRACE(logger, + "Requested backable candidate for inactive para. " + "(relay_parent={}, para_id={})", + relay_parent, + para); + return {}; + } + + auto &chain = (*chain_it)->second; + SL_TRACE(logger, + "Candidate chain for para. " + "(relay_parent={}, para_id={}, best chain size={})", + relay_parent, + para, + chain.best_chain_len()); + + auto backable_candidates = chain.find_backable_chain(ancestors, count); + if (backable_candidates.empty()) { + SL_TRACE(logger, + "Could not find any backable candidate. " + "(relay_parent={}, para_id={})", + relay_parent, + para); + } else { + SL_TRACE(logger, + "Found backable candidates. " + "(relay_parent={}, para_id={}, backable_candidates size={})", + relay_parent, + para, + backable_candidates.size()); + } + return backable_candidates; + } + + std::optional + ProspectiveParachains::prospectiveParachainsMode( + const RelayHash &relay_parent) { + auto result = parachain_host_->staging_async_backing_params(relay_parent); + if (result.has_error()) { + SL_TRACE(logger, + "Prospective parachains are disabled, is not supported by the " + "current Runtime API. (relay parent={}, error={})", + relay_parent, + result.error()); + return std::nullopt; + } + + const parachain::fragment::AsyncBackingParams &vs = result.value(); + return ProspectiveParachainsMode{ + .max_candidate_depth = vs.max_candidate_depth, + .allowed_ancestry_len = vs.allowed_ancestry_len, + }; + } + + outcome::result> + ProspectiveParachains::fetchBlockInfo(const RelayHash &relay_hash) { + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// cache for block header request and calculations + auto res_header = block_tree_->getBlockHeader(relay_hash); + if (res_header.has_error()) { + if (res_header.error() == blockchain::BlockTreeError::HEADER_NOT_FOUND) { + return outcome::success(std::nullopt); + } + return res_header.error(); + } + + return fragment::BlockInfoProspectiveParachains{ + .hash = relay_hash, + .parent_hash = res_header.value().parent_hash, + .number = res_header.value().number, + .storage_root = res_header.value().state_root, + }; + } + + outcome::result>>> + ProspectiveParachains::fetchBackingState(const RelayHash &relay_parent, + ParachainId para_id) { + auto result = + parachain_host_->staging_para_backing_state(relay_parent, para_id); + if (result.has_error()) { + SL_TRACE(logger, + "Staging para backing state failed. (relay parent={}, " + "para_id={}, error={})", + relay_parent, + para_id, + result.error()); + return result.as_failure(); + } + + auto &s = result.value(); + if (!s) { + return std::nullopt; + } + + return std::make_pair(std::move(s->constraints), + std::move(s->pending_availability)); + } + + outcome::result> + ProspectiveParachains::answerProspectiveValidationDataRequest( + const RelayHash &candidate_relay_parent, + const ParentHeadData &parent_head_data, + ParachainId para_id) { + auto [head_data, parent_head_data_hash] = visit_in_place( + parent_head_data, + [&](const ParentHeadData_OnlyHash &parent_head_data_hash) + -> std::pair, + std::reference_wrapper> { + return {std::nullopt, parent_head_data_hash}; + }, + [&](const ParentHeadData_WithData &v) + -> std::pair, + std::reference_wrapper> { + const auto &[head_data, hash] = v; + return {head_data, hash}; + }); + + std::optional relay_parent_info{}; + std::optional max_pov_size{}; + + for (const auto &x : view().active_leaves) { + auto data = utils::get(view().per_relay_parent, x); + if (!data) { + continue; + } + + auto fragment_chain_it = + utils::get((*data)->second.fragment_chains, para_id); + if (!fragment_chain_it) { + continue; + } + + auto &fragment_chain = (*fragment_chain_it)->second; + if (head_data && relay_parent_info && max_pov_size) { + break; + } + + if (!relay_parent_info) { + relay_parent_info = + fragment_chain.get_scope().ancestor(candidate_relay_parent); + } + + if (!head_data) { + head_data = fragment_chain.get_head_data_by_hash(parent_head_data_hash); + } + + if (!max_pov_size) { + if (fragment_chain.get_scope().ancestor(candidate_relay_parent)) { + max_pov_size = + fragment_chain.get_scope().get_base_constraints().max_pov_size; + } + } + } + + if (head_data && relay_parent_info && max_pov_size) { + return runtime::PersistedValidationData{ + .parent_head = *head_data, + .relay_parent_number = relay_parent_info->number, + .relay_parent_storage_root = relay_parent_info->storage_root, + .max_pov_size = (uint32_t)*max_pov_size, + }; + } + + return std::nullopt; + } + + outcome::result> + ProspectiveParachains::fetchUpcomingParas( + const RelayHash &relay_parent, + std::unordered_set &pending_availability) { + OUTCOME_TRY(cores, parachain_host_->availability_cores(relay_parent)); + + std::unordered_set upcoming; + for (const auto &core : cores) { + visit_in_place( + core, + [&](const runtime::OccupiedCore &occupied) { + pending_availability.insert(occupied.candidate_hash); + if (occupied.next_up_on_available) { + upcoming.insert(occupied.next_up_on_available->para_id); + } + if (occupied.next_up_on_time_out) { + upcoming.insert(occupied.next_up_on_time_out->para_id); + } + }, + [&](const runtime::ScheduledCore &scheduled) { + upcoming.insert(scheduled.para_id); + }, + [](const auto &) {}); + } + return upcoming; + } + + outcome::result> + ProspectiveParachains::fetchAncestry(const RelayHash &relay_hash, + size_t ancestors) { + std::vector block_info; + if (ancestors == 0) { + return block_info; + } + + OUTCOME_TRY( + hashes, + block_tree_->getDescendingChainToBlock(relay_hash, ancestors + 1)); + + if (logger->level() >= soralog::Level::TRACE) { + for (const auto &h : hashes) { + SL_TRACE(logger, + "Ancestor hash. " + "(relay_hash={}, ancestor_hash={})", + relay_hash, + h); + } + } + + OUTCOME_TRY(required_session, + parachain_host_->session_index_for_child(relay_hash)); + SL_TRACE(logger, + "Get ancestors. " + "(relay_hash={}, ancestors={}, hashes_len={})", + relay_hash, + ancestors, + hashes.size()); + + if (hashes.size() > 1) { + block_info.reserve(hashes.size() - 1); + } + for (size_t i = 1; i < hashes.size(); ++i) { + const auto &hash = hashes[i]; + OUTCOME_TRY(info, fetchBlockInfo(hash)); + if (!info) { + SL_WARN(logger, + "Failed to fetch info for hash returned from ancestry. " + "(relay_hash={})", + hash); + break; + } + OUTCOME_TRY(session, parachain_host_->session_index_for_child(hash)); + if (session == required_session) { + SL_TRACE(logger, + "Add block. " + "(relay_hash={}, hash={}, number={})", + relay_hash, + hash, + info->number); + block_info.emplace_back(*info); + } else { + SL_TRACE(logger, + "Skipped block. " + "(relay_hash={}, hash={}, number={})", + relay_hash, + hash, + info->number); + break; + } + } + return block_info; + } + + outcome::result< + std::vector> + ProspectiveParachains::preprocessCandidatesPendingAvailability( + const HeadData &required_parent, + const std::vector + &pending_availability) { + std::reference_wrapper required_parent_copy = + required_parent; + std::vector importable; + const size_t expected_count = pending_availability.size(); + + for (size_t i = 0; i < pending_availability.size(); i++) { + const auto &pending = pending_availability[i]; + OUTCOME_TRY(relay_parent, + fetchBlockInfo(pending.descriptor.relay_parent)); + if (!relay_parent) { + SL_DEBUG(logger, + "Had to stop processing pending candidates early due to " + "missing info. (candidate hash={}, parachain id={}, " + "index={}, expected count={})", + pending.candidate_hash, + pending.descriptor.para_id, + i, + expected_count); + break; + } + + const auto &b = *relay_parent; + importable.push_back(ImportablePendingAvailability{ + .candidate = + network::CommittedCandidateReceipt{ + .descriptor = pending.descriptor, + .commitments = pending.commitments, + }, + .persisted_validation_data = + runtime::PersistedValidationData{ + .parent_head = required_parent_copy.get(), + .relay_parent_number = b.number, + .relay_parent_storage_root = b.storage_root, + .max_pov_size = pending.max_pov_size, + }, + .compact = fragment::PendingAvailability{ + .candidate_hash = pending.candidate_hash, + .relay_parent = b.as_relay_chain_block_info(), + }}); + required_parent_copy = pending.commitments.para_head; + } + return importable; + } + + outcome::result ProspectiveParachains::onActiveLeavesUpdate( + const network::ExViewRef &update) { + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// cache headers + [[maybe_unused]] std::unordered_map + temp_header_cache; + if (update.new_head) { + const auto &activated = update.new_head->get(); + const auto &hash = update.new_head->get().hash(); + const auto mode = prospectiveParachainsMode(hash); + if (!mode) { + SL_TRACE(logger, + "Skipping leaf activation since async backing is disabled. " + "(block_hash={})", + hash); + return outcome::success(); + } + std::unordered_set pending_availability{}; + OUTCOME_TRY(scheduled_paras, + fetchUpcomingParas(hash, pending_availability)); + + const fragment::BlockInfoProspectiveParachains block_info{ + .hash = hash, + .parent_hash = activated.parent_hash, + .number = activated.number, + .storage_root = activated.state_root, + }; + + OUTCOME_TRY(ancestry, fetchAncestry(hash, mode->allowed_ancestry_len)); + + std::optional>> + prev_fragment_chains; + if (!ancestry.empty()) { + const auto &prev_leaf = ancestry.front(); + prev_fragment_chains = view().get_fragment_chains(prev_leaf.hash); + } + + std::unordered_map fragment_chains; + for (ParachainId para : scheduled_paras) { + OUTCOME_TRY(backing_state, fetchBackingState(hash, para)); + if (!backing_state) { + SL_TRACE(logger, + "Failed to get inclusion backing state. (para={}, relay " + "parent={})", + para, + hash); + continue; + } + + const auto &[constraints, pe] = *backing_state; + OUTCOME_TRY(pending_availability, + preprocessCandidatesPendingAvailability( + constraints.required_parent, pe)); + + std::vector compact_pending; + compact_pending.reserve(pending_availability.size()); + + fragment::CandidateStorage pending_availability_storage; + for (const ImportablePendingAvailability &c : pending_availability) { + const auto &candidate_hash = c.compact.candidate_hash; + auto res = + pending_availability_storage.add_pending_availability_candidate( + candidate_hash, + c.candidate, + c.persisted_validation_data, + hasher_); + + if (res.has_error() + && res.error() + != fragment::CandidateStorage::Error:: + CANDIDATE_ALREADY_KNOWN) { + SL_WARN(logger, + "Scraped invalid candidate pending availability. " + "(candidate_hash={}, para={}, error={})", + candidate_hash, + para, + res.error()); + break; + } + compact_pending.emplace_back(c.compact); + } + + std::vector a; + a.reserve(ancestry.size()); + for (const auto &ancestor : ancestry) { + a.emplace_back(ancestor.as_relay_chain_block_info()); + } + + auto scope = fragment::Scope::with_ancestors( + block_info.as_relay_chain_block_info(), + constraints, + compact_pending, + mode->max_candidate_depth, + a); + if (scope.has_error()) { + SL_WARN(logger, + "Relay chain ancestors have wrong order. " + "(para={}, max_candidate_depth={}, leaf={}, error={})", + para, + mode->max_candidate_depth, + hash, + scope.error()); + continue; + } + + SL_TRACE( + logger, + "Creating fragment chain. " + "(relay_parent={}, para={}, min_relay_parent={}, ancestors={})", + hash, + para, + scope.value().earliest_relay_parent().number, + ancestry); + const auto number_of_pending_candidates = + pending_availability_storage.len(); + auto chain = fragment::FragmentChain::init( + hasher_, scope.value(), std::move(pending_availability_storage)); + + if (chain.best_chain_len() < number_of_pending_candidates) { + SL_WARN( + logger, + "Not all pending availability candidates could be introduced. " + "(para={}, relay_parent={}, best_chain_len={}, " + "number_of_pending_candidates={})", + para, + hash, + chain.best_chain_len(), + number_of_pending_candidates); + } + + if (prev_fragment_chains) { + if (auto prev_fragment_chain = + utils::get(prev_fragment_chains->get(), para)) { + chain.populate_from_previous((*prev_fragment_chain)->second); + } + } + + SL_TRACE( + logger, + "Populated fragment chain. " + "(relay_parent={}, para={}, best_chain_len={}, unconnected_len={})", + hash, + para, + chain.best_chain_len(), + chain.unconnected_len()); + + fragment_chains.insert_or_assign(para, std::move(chain)); + } + + view().per_relay_parent.insert_or_assign( + hash, + RelayBlockViewData{.fragment_chains = std::move(fragment_chains)}); + view().active_leaves.insert(hash); + view().implicit_view.activate_leaf_from_prospective_parachains(block_info, + ancestry); + } + + for (const auto &deactivated : update.lost) { + view().active_leaves.erase(deactivated); + view().implicit_view.deactivate_leaf(deactivated); + } + + auto r = view().implicit_view.all_allowed_relay_parents(); + std::unordered_set remaining{r.begin(), r.end()}; + + for (auto it = view().per_relay_parent.begin(); + it != view().per_relay_parent.end();) { + if (remaining.contains(it->first)) { + ++it; + } else { + it = view().per_relay_parent.erase(it); + } + } + + return outcome::success(); + } + + ProspectiveParachains::View &ProspectiveParachains::view() { + if (!view_) { + view_.emplace(View{ + .per_relay_parent = {}, + .active_leaves = {}, + .implicit_view = ImplicitView( + weak_from_this(), parachain_host_, block_tree_, std::nullopt), + }); + } + return *view_; + } + + std::vector< + std::pair> + ProspectiveParachains::answer_hypothetical_membership_request( + const std::span &candidates, + const std::optional> + &fragment_chain_relay_parent) { + std::vector< + std::pair> + response; + response.reserve(candidates.size()); + + std::ranges::transform(candidates, + std::back_inserter(response), + [](const HypotheticalCandidate &candidate) + -> std::pair { + return {candidate, {}}; + }); + + const auto &required_active_leaf = fragment_chain_relay_parent; + for (const auto &active_leaf : view().active_leaves) { + if (required_active_leaf && required_active_leaf->get() != active_leaf) { + continue; + } + + auto leaf_view = utils::get(view().per_relay_parent, active_leaf); + if (!leaf_view) { + continue; + } + + for (auto &[candidate, membership] : response) { + const ParachainId ¶_id = candidatePara(candidate); + + auto fragment_chain = + utils::get((*leaf_view)->second.fragment_chains, para_id); + if (!fragment_chain) { + continue; + } + + const auto res = (*fragment_chain) + ->second.can_add_candidate_as_potential( + into_wrapper(candidate, hasher_)); + if (res.has_value() + || res.error() + == fragment::FragmentChainError::CANDIDATE_ALREADY_KNOWN) { + membership.emplace_back(active_leaf); + } else { + SL_TRACE(logger, + "Candidate is not a hypothetical member. " + "(para " + "id={}, leaf={}, error={})", + para_id, + active_leaf, + res.error()); + } + } + } + return response; + } + + void ProspectiveParachains::candidate_backed( + ParachainId para, const CandidateHash &candidate_hash) { + bool found_candidate = false; + bool found_para = false; + + for (auto &[relay_parent, rp_data] : view().per_relay_parent) { + auto chain = utils::get(rp_data.fragment_chains, para); + if (!chain) { + continue; + } + + const auto is_active_leaf = view().active_leaves.contains(relay_parent); + + found_para = true; + if ((*chain)->second.is_candidate_backed(candidate_hash)) { + SL_TRACE( + logger, + "Received redundant instruction to mark as backed an already " + "backed candidate. (para={}, is_active_leaf={}, candidate_hash={})", + para, + is_active_leaf, + candidate_hash); + found_candidate = true; + } else if ((*chain)->second.contains_unconnected_candidate( + candidate_hash)) { + found_candidate = true; + (*chain)->second.candidate_backed(candidate_hash); + + SL_TRACE(logger, + "Candidate backed. Candidate chain for para. (para={}, " + "relay_parent={}, is_active_leaf={}, best_chain_len={})", + para, + relay_parent, + is_active_leaf, + (*chain)->second.best_chain_len()); + + SL_TRACE(logger, + "Potential candidate storage for para. (para={}, " + "relay_parent={}, is_active_leaf={}, unconnected_len={})", + para, + relay_parent, + is_active_leaf, + (*chain)->second.unconnected_len()); + } + } + + if (!found_para) { + SL_WARN(logger, + "Received instruction to back a candidate for unscheduled para. " + "(para={}, candidate_hash={})", + para, + candidate_hash); + return; + } + + if (!found_candidate) { + SL_TRACE(logger, + "Received instruction to back unknown candidate. (para={}, " + "candidate_hash={})", + para, + candidate_hash); + } + } + + bool ProspectiveParachains::introduce_seconded_candidate( + ParachainId para, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> &pvd, + const CandidateHash &candidate_hash) { + auto candidate_entry = fragment::CandidateEntry::create_seconded( + candidate_hash, candidate, pvd, hasher_); + if (candidate_entry.has_error()) { + SL_WARN(logger, + "Cannot add seconded candidate. (para={}, error={})", + para, + candidate_entry.error()); + return false; + } + + bool added = false; + bool para_scheduled = false; + for (auto &[relay_parent, rp_data] : view().per_relay_parent) { + auto chain = utils::get(rp_data.fragment_chains, para); + if (!chain) { + continue; + } + const auto is_active_leaf = view().active_leaves.contains(relay_parent); + + para_scheduled = true; + if (auto res = (*chain)->second.try_adding_seconded_candidate( + candidate_entry.value()); + res.has_value()) { + SL_TRACE(logger, + "Added seconded candidate. (para={}, relay_parent={}, " + "is_active_leaf={}, candidate_hash={})", + para, + relay_parent, + is_active_leaf, + candidate_hash); + added = true; + } else { + if (res.error() + == fragment::FragmentChainError::CANDIDATE_ALREADY_KNOWN) { + SL_TRACE( + logger, + "Attempting to introduce an already known candidate. (para={}, " + "relay_parent={}, is_active_leaf={}, candidate_hash={})", + para, + relay_parent, + is_active_leaf, + candidate_hash); + added = true; + } else { + SL_TRACE( + logger, + "Cannot introduce seconded candidate. (para={}, relay_parent={}, " + "is_active_leaf={}, candidate_hash={}, error={})", + para, + relay_parent, + is_active_leaf, + candidate_hash, + res.error()); + } + } + } + + if (!para_scheduled) { + SL_WARN(logger, + "Received seconded candidate for inactive para. (para={}, " + "candidate_hash={})", + para, + candidate_hash); + } + + if (!added) { + SL_TRACE(logger, + "Newly-seconded candidate cannot be kept under any relay " + "parent. (para={}, candidate_hash={})", + para, + candidate_hash); + } + return added; + } + +} // namespace kagome::parachain diff --git a/core/parachain/validator/prospective_parachains/prospective_parachains.hpp b/core/parachain/validator/prospective_parachains/prospective_parachains.hpp new file mode 100644 index 0000000000..a90a2a2e3b --- /dev/null +++ b/core/parachain/validator/prospective_parachains/prospective_parachains.hpp @@ -0,0 +1,161 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "blockchain/block_tree.hpp" +#include "blockchain/block_tree_error.hpp" +#include "network/peer_view.hpp" +#include "network/types/collator_messages_vstaging.hpp" +#include "parachain/types.hpp" +#include "parachain/validator/backing_implicit_view.hpp" +#include "parachain/validator/collations.hpp" +#include "parachain/validator/prospective_parachains/fragment_chain.hpp" +#include "parachain/validator/prospective_parachains/fragment_chain_errors.hpp" +#include "runtime/runtime_api/parachain_host.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" +#include "utils/map.hpp" + +namespace kagome::parachain { + + using ParentHeadData_OnlyHash = Hash; + using ParentHeadData_WithData = std::pair; + using ParentHeadData = + boost::variant; + + class ProspectiveParachains + : public std::enable_shared_from_this { +#ifdef CFG_TESTING + public: +#endif // CFG_TESTING + struct RelayBlockViewData { + // The fragment chains for current and upcoming scheduled paras. + std::unordered_map fragment_chains; + }; + + struct View { + // Per relay parent fragment chains. These includes all relay parents + // under the implicit view. + std::unordered_map per_relay_parent; + // The hashes of the currently active leaves. This is a subset of the keys + // in `per_relay_parent`. + std::unordered_set active_leaves; + // The backing implicit view. + ImplicitView implicit_view; + + // Get the fragment chains of this leaf. + std::optional>> + get_fragment_chains(const Hash &leaf) const { + auto view_data = utils::get(per_relay_parent, leaf); + if (view_data) { + return std::cref((*view_data)->second.fragment_chains); + } + return std::nullopt; + } + }; + + struct ImportablePendingAvailability { + network::CommittedCandidateReceipt candidate; + runtime::PersistedValidationData persisted_validation_data; + fragment::PendingAvailability compact; + }; + + std::optional view_; + std::shared_ptr hasher_; + std::shared_ptr parachain_host_; + std::shared_ptr block_tree_; + log::Logger logger = + log::createLogger("ProspectiveParachains", "parachain"); + + View &view(); + + public: + ProspectiveParachains( + std::shared_ptr hasher, + std::shared_ptr parachain_host, + std::shared_ptr block_tree); + + // Debug print of all internal buffers load. + void printStoragesLoad(); + + std::shared_ptr getBlockTree(); + + std::vector> + answerMinimumRelayParentsRequest(const RelayHash &relay_parent); + + std::vector> answerGetBackableCandidates( + const RelayHash &relay_parent, + ParachainId para, + uint32_t count, + const fragment::Ancestors &ancestors); + + outcome::result> + answerProspectiveValidationDataRequest( + const RelayHash &candidate_relay_parent, + const ParentHeadData &parent_head_data, + ParachainId para_id); + + std::optional prospectiveParachainsMode( + const RelayHash &relay_parent); + + outcome::result>>> + fetchBackingState(const RelayHash &relay_parent, ParachainId para_id); + + outcome::result> + fetchBlockInfo(const RelayHash &relay_hash); + + outcome::result> fetchUpcomingParas( + const RelayHash &relay_parent, + std::unordered_set &pending_availability); + + outcome::result> + fetchAncestry(const RelayHash &relay_hash, size_t ancestors); + + outcome::result> + preprocessCandidatesPendingAvailability( + const HeadData &required_parent, + const std::vector + &pending_availability); + + outcome::result onActiveLeavesUpdate( + const network::ExViewRef &update); + + /// @brief calculates hypothetical candidate and fragment tree membership + /// @param candidates Candidates, in arbitrary order, which should be + /// checked for possible membership in fragment trees. + /// @param fragment_tree_relay_parent Either a specific fragment tree to + /// check, otherwise all. + /// @param backed_in_path_only Only return membership if all candidates in + /// the path from the root are backed. + std::vector< + std::pair> + answer_hypothetical_membership_request( + const std::span &candidates, + const std::optional> + &fragment_tree_relay_parent); + + void candidate_backed(ParachainId para, + const CandidateHash &candidate_hash); + + bool introduce_seconded_candidate( + ParachainId para, + const network::CommittedCandidateReceipt &candidate, + const crypto::Hashed> &pvd, + const CandidateHash &candidate_hash); + }; + +} // namespace kagome::parachain diff --git a/core/parachain/validator/prospective_parachains/scope.cpp b/core/parachain/validator/prospective_parachains/scope.cpp new file mode 100644 index 0000000000..6568759122 --- /dev/null +++ b/core/parachain/validator/prospective_parachains/scope.cpp @@ -0,0 +1,94 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/scope.hpp" +#include "utils/stringify.hpp" + +#define COMPONENT Scope +#define COMPONENT_NAME STRINGIFY(COMPONENT) + +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, Scope::Error, e) { + using E = kagome::parachain::fragment::Scope::Error; + switch (e) { + case E::UNEXPECTED_ANCESTOR: + return COMPONENT_NAME ": Unexpected ancestor"; + } + return COMPONENT_NAME ": Unknown error"; +} + +namespace kagome::parachain::fragment { + + outcome::result Scope::with_ancestors( + const fragment::RelayChainBlockInfo &relay_parent, + const Constraints &base_constraints, + const Vec &pending_availability, + size_t max_depth, + const Vec &ancestors) { + Map ancestors_map; + HashMap ancestors_by_hash; + + auto prev = relay_parent.number; + for (const auto &ancestor : ancestors) { + if (prev == 0) { + return Scope::Error::UNEXPECTED_ANCESTOR; + } + if (ancestor.number != prev - 1) { + return Scope::Error::UNEXPECTED_ANCESTOR; + } + if (prev == base_constraints.min_relay_parent_number) { + break; + } + + prev = ancestor.number; + ancestors_by_hash[ancestor.hash] = ancestor; + ancestors_map[ancestor.number] = ancestor; + } + + return Scope{ + .relay_parent = relay_parent, + .ancestors = ancestors_map, + .ancestors_by_hash = ancestors_by_hash, + .pending_availability = pending_availability, + .base_constraints = base_constraints, + .max_depth = max_depth, + }; + } + + const Constraints &Scope::get_base_constraints() const { + return base_constraints; + } + + const RelayChainBlockInfo Scope::earliest_relay_parent() const { + if (!ancestors.empty()) { + return ancestors.begin()->second; + } + return relay_parent; + } + + Option> + Scope::get_pending_availability(const CandidateHash &candidate_hash) const { + auto it = std::ranges::find_if(pending_availability.begin(), + pending_availability.end(), + [&](const PendingAvailability &c) { + return c.candidate_hash == candidate_hash; + }); + if (it != pending_availability.end()) { + return {{*it}}; + } + return std::nullopt; + } + + Option Scope::ancestor(const Hash &hash) const { + if (hash == relay_parent.hash) { + return relay_parent; + } + if (auto it = ancestors_by_hash.find(hash); it != ancestors_by_hash.end()) { + return it->second; + } + return std::nullopt; + } + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/prospective_parachains/scope.hpp b/core/parachain/validator/prospective_parachains/scope.hpp new file mode 100644 index 0000000000..bec62d705e --- /dev/null +++ b/core/parachain/validator/prospective_parachains/scope.hpp @@ -0,0 +1,87 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "parachain/validator/prospective_parachains/common.hpp" + +namespace kagome::parachain::fragment { + + /// A candidate existing on-chain but pending availability, for special + /// treatment in the [`Scope`]. + struct PendingAvailability { + /// The candidate hash. + CandidateHash candidate_hash; + /// The block info of the relay parent. + RelayChainBlockInfo relay_parent; + }; + + /// The scope of a [`FragmentChain`]. + struct Scope { + enum class Error : uint8_t { + UNEXPECTED_ANCESTOR, + }; + + /// The relay parent we're currently building on top of. + RelayChainBlockInfo relay_parent; + + /// The other relay parents candidates are allowed to build upon, mapped by + /// the block number. + Map ancestors; + + /// The other relay parents candidates are allowed to build upon, mapped by + /// the block hash. + HashMap ancestors_by_hash; + + /// The candidates pending availability at this block. + Vec pending_availability; + + /// The base constraints derived from the latest included candidate. + Constraints base_constraints; + + /// Equal to `max_candidate_depth`. + size_t max_depth; + + /// Define a new [`Scope`]. + /// + /// Ancestors should be in reverse order, starting with the parent + /// of the `relay_parent`, and proceeding backwards in block number + /// increments of 1. Ancestors not following these conditions will be + /// rejected. + /// + /// This function will only consume ancestors up to the + /// `min_relay_parent_number` of the `base_constraints`. + /// + /// Only ancestors whose children have the same session as the + /// relay-parent's children should be provided. + /// + /// It is allowed to provide zero ancestors. + static outcome::result with_ancestors( + const RelayChainBlockInfo &relay_parent, + const Constraints &base_constraints, + const Vec &pending_availability, + size_t max_depth, + const Vec &ancestors); + + /// Get the base constraints of the scope + const Constraints &get_base_constraints() const; + + /// Get the earliest relay-parent allowed in the scope of the fragment + /// chain. + const RelayChainBlockInfo earliest_relay_parent() const; + + /// Whether the candidate in question is one pending availability in this + /// scope. + Option> + get_pending_availability(const CandidateHash &candidate_hash) const; + + /// Get the relay ancestor of the fragment chain by hash. + Option ancestor(const Hash &hash) const; + }; + +} // namespace kagome::parachain::fragment + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain::fragment, Scope::Error); diff --git a/test/core/parachain/CMakeLists.txt b/test/core/parachain/CMakeLists.txt index 006df192cd..52599c7d0b 100644 --- a/test/core/parachain/CMakeLists.txt +++ b/test/core/parachain/CMakeLists.txt @@ -6,10 +6,58 @@ add_subdirectory(availability) +addtest(candidate_storage_test + candidate_storage.cpp + ) + +target_link_libraries(candidate_storage_test + prospective_parachains + log_configurator + base_fs_test + key_store + logger + ) + +addtest(scope_test + scope.cpp + ) + +target_link_libraries(scope_test + prospective_parachains + log_configurator + base_fs_test + key_store + logger + ) + +addtest(fragment_chain_test + fragment_chain.cpp + ) + +target_link_libraries(fragment_chain_test + prospective_parachains + log_configurator + base_fs_test + key_store + logger + ) + +addtest(prospective_parachains_test + prospective_parachains.cpp + ) + +target_link_libraries(prospective_parachains_test + validator_parachain + prospective_parachains + log_configurator + base_fs_test + key_store + logger + ) + addtest(parachain_test pvf_test.cpp assignments.cpp - prospective_parachains.cpp cluster_test.cpp grid.cpp ) diff --git a/test/core/parachain/candidate_storage.cpp b/test/core/parachain/candidate_storage.cpp new file mode 100644 index 0000000000..8025b6f2a6 --- /dev/null +++ b/test/core/parachain/candidate_storage.cpp @@ -0,0 +1,153 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "core/parachain/parachain_test_harness.hpp" +#include "parachain/validator/prospective_parachains/fragment_chain.hpp" + +using namespace kagome::parachain::fragment; + +class CandidateStorageTest : public ProspectiveParachainsTestHarness { + void SetUp() override { + ProspectiveParachainsTestHarness::SetUp(); + } + + void TearDown() override { + ProspectiveParachainsTestHarness::TearDown(); + } +}; + +TEST_F(CandidateStorageTest, candidate_storage_methods) { + fragment::CandidateStorage storage{}; + Hash relay_parent(hashFromStrData("69")); + + const auto &[pvd, candidate] = + make_committed_candidate(5, relay_parent, 8, {4, 5, 6}, {1, 2, 3}, 7); + + const Hash candidate_hash = network::candidateHash(*hasher_, candidate); + const Hash parent_head_hash = hasher_->blake2b_256(pvd.get().parent_head); + + // Invalid pvd hash + auto wrong_pvd = pvd; + wrong_pvd.get_mut().max_pov_size = 0; + + ASSERT_EQ( + fragment::CandidateEntry::create(candidate_hash, + candidate, + wrong_pvd.get(), + fragment::CandidateState::Seconded, + hasher_) + .error(), + fragment::CandidateStorage::Error::PERSISTED_VALIDATION_DATA_MISMATCH); + ASSERT_EQ( + fragment::CandidateEntry::create_seconded( + candidate_hash, candidate, wrong_pvd.get(), hasher_) + .error(), + fragment::CandidateStorage::Error::PERSISTED_VALIDATION_DATA_MISMATCH); + + // Zero-length cycle. + { + auto candidate_2 = candidate; + candidate_2.commitments.para_head = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + auto pvd_2 = pvd; + pvd_2.get_mut().parent_head = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + candidate_2.descriptor.persisted_data_hash = pvd_2.getHash(); + ASSERT_EQ(fragment::CandidateEntry::create_seconded( + candidate_hash, candidate_2, pvd_2.get(), hasher_) + .error(), + fragment::CandidateStorage::Error::ZERO_LENGTH_CYCLE); + } + + auto calculate_count = [&](const Hash &h) { + size_t count = 0; + storage.possible_backed_para_children(h, [&](const auto &) { ++count; }); + return count; + }; + + auto aggregate_hashes = [&](const Hash &h) { + std::unordered_set data; + storage.possible_backed_para_children( + h, [&](const auto &c) { data.emplace(c.candidate_hash); }); + return data; + }; + + ASSERT_FALSE(storage.contains(candidate_hash)); + ASSERT_EQ(calculate_count(parent_head_hash), 0); + ASSERT_EQ(storage.head_data_by_hash(candidate.descriptor.para_head_hash), + std::nullopt); + ASSERT_EQ(storage.head_data_by_hash(parent_head_hash), std::nullopt); + + // Add a valid candidate. + EXPECT_OUTCOME_TRUE( + candidate_entry, + fragment::CandidateEntry::create(candidate_hash, + candidate, + pvd.get(), + fragment::CandidateState::Seconded, + hasher_)); + std::ignore = storage.add_candidate_entry(candidate_entry); + ASSERT_TRUE(storage.contains(candidate_hash)); + + ASSERT_EQ(calculate_count(parent_head_hash), 0); + ASSERT_EQ(calculate_count(candidate.descriptor.para_head_hash), 0); + ASSERT_EQ(storage.head_data_by_hash(candidate.descriptor.para_head_hash) + .value() + .get(), + candidate.commitments.para_head); + ASSERT_EQ(storage.head_data_by_hash(parent_head_hash).value().get(), + pvd.get().parent_head); + + // Now mark it as backed + storage.mark_backed(candidate_hash); + // Marking it twice is fine. + storage.mark_backed(candidate_hash); + ASSERT_EQ(aggregate_hashes(parent_head_hash), + std::unordered_set{candidate_hash}); + ASSERT_EQ(calculate_count(candidate.descriptor.para_head_hash), 0); + + // Re-adding a candidate fails. + ASSERT_EQ(storage.add_candidate_entry(candidate_entry).error(), + fragment::CandidateStorage::Error::CANDIDATE_ALREADY_KNOWN); + + // Remove candidate and re-add it later in backed state. + storage.remove_candidate(candidate_hash, hasher_); + ASSERT_FALSE(storage.contains(candidate_hash)); + + // Removing it twice is fine. + storage.remove_candidate(candidate_hash, hasher_); + ASSERT_FALSE(storage.contains(candidate_hash)); + ASSERT_EQ(calculate_count(parent_head_hash), 0); + ASSERT_EQ(storage.head_data_by_hash(candidate.descriptor.para_head_hash), + std::nullopt); + ASSERT_EQ(storage.head_data_by_hash(parent_head_hash), std::nullopt); + + std::ignore = storage.add_pending_availability_candidate( + candidate_hash, candidate, pvd.get(), hasher_); + ASSERT_TRUE(storage.contains(candidate_hash)); + + ASSERT_EQ(aggregate_hashes(parent_head_hash), + std::unordered_set{candidate_hash}); + ASSERT_EQ(calculate_count(candidate.descriptor.para_head_hash), 0); + + // Now add a second candidate in Seconded state. This will be a fork. + const auto &[pvd_2, candidate_2] = + make_committed_candidate(5, relay_parent, 8, {4, 5, 6}, {2, 3, 4}, 7); + + const Hash candidate_hash_2 = network::candidateHash(*hasher_, candidate_2); + EXPECT_OUTCOME_TRUE(candidate_entry_2, + fragment::CandidateEntry::create_seconded( + candidate_hash_2, candidate_2, pvd_2.get(), hasher_)); + + std::ignore = storage.add_candidate_entry(candidate_entry_2); + ASSERT_EQ(aggregate_hashes(parent_head_hash), + std::unordered_set{candidate_hash}); + + // Now mark it as backed. + storage.mark_backed(candidate_hash_2); + ASSERT_EQ(aggregate_hashes(parent_head_hash), + (std::unordered_set{candidate_hash, candidate_hash_2})); +} diff --git a/test/core/parachain/fragment_chain.cpp b/test/core/parachain/fragment_chain.cpp new file mode 100644 index 0000000000..094e1be714 --- /dev/null +++ b/test/core/parachain/fragment_chain.cpp @@ -0,0 +1,1259 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/fragment_chain.hpp" +#include +#include "core/parachain/parachain_test_harness.hpp" + +using namespace kagome::parachain::fragment; + +class FragmentChainTest : public ProspectiveParachainsTestHarness { + void SetUp() override { + ProspectiveParachainsTestHarness::SetUp(); + } + + void TearDown() override { + ProspectiveParachainsTestHarness::TearDown(); + } + + public: + FragmentChain populate_chain_from_previous_storage( + const Scope &scope, const CandidateStorage &storage) { + FragmentChain chain = + FragmentChain::init(hasher_, scope, CandidateStorage{}); + FragmentChain prev_chain = chain; + prev_chain.unconnected = storage; + + chain.populate_from_previous(prev_chain); + return chain; + } + + HashSet get_unconnected(const FragmentChain &chain) { + HashSet unconnected; + chain.get_unconnected( + [&](const auto &c) { unconnected.insert(c.candidate_hash); }); + return unconnected; + } +}; + +TEST_F(FragmentChainTest, init_and_populate_from_empty) { + const auto base_constraints = make_constraints(0, {0}, {0x0a}); + + EXPECT_OUTCOME_TRUE(scope, + Scope::with_ancestors( + RelayChainBlockInfo{ + .hash = fromNumber(1), + .number = 1, + .storage_root = fromNumber(2), + }, + base_constraints, + {}, + 4, + {})); + + auto chain = FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_EQ(chain.best_chain_len(), 0); + ASSERT_EQ(chain.unconnected_len(), 0); + + auto new_chain = FragmentChain::init(hasher_, scope, CandidateStorage{}); + new_chain.populate_from_previous(chain); + ASSERT_EQ(chain.best_chain_len(), 0); + ASSERT_EQ(chain.unconnected_len(), 0); +} + +TEST_F(FragmentChainTest, test_populate_and_check_potential) { + fragment::CandidateStorage storage{}; + + const ParachainId para_id{5}; + const auto relay_parent_x = fromNumber(1); + const auto relay_parent_y = fromNumber(2); + const auto relay_parent_z = fromNumber(3); + + RelayChainBlockInfo relay_parent_x_info{ + .hash = relay_parent_x, + .number = 0, + .storage_root = fromNumber(0), + }; + RelayChainBlockInfo relay_parent_y_info{ + .hash = relay_parent_y, + .number = 1, + .storage_root = fromNumber(0), + }; + RelayChainBlockInfo relay_parent_z_info{ + .hash = relay_parent_z, + .number = 2, + .storage_root = fromNumber(0), + }; + + Vec ancestors = {relay_parent_y_info, + relay_parent_x_info}; + + const auto base_constraints = make_constraints(0, {0}, {0x0a}); + + // Candidates A -> B -> C. They are all backed + const auto &[pvd_a, candidate_a] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0x0a}, + {0x0b}, + relay_parent_x_info.number); + const auto candidate_a_hash = network::candidateHash(*hasher_, candidate_a); + const auto candidate_a_entry = + CandidateEntry::create( + candidate_a_hash, candidate_a, pvd_a, CandidateState::Backed, hasher_) + .value(); + ASSERT_TRUE(storage.add_candidate_entry(candidate_a_entry).has_value()); + + const auto &[pvd_b, candidate_b] = + make_committed_candidate(para_id, + relay_parent_y_info.hash, + relay_parent_y_info.number, + {0x0b}, + {0x0c}, + relay_parent_y_info.number); + const auto candidate_b_hash = network::candidateHash(*hasher_, candidate_b); + const auto candidate_b_entry = + CandidateEntry::create( + candidate_b_hash, candidate_b, pvd_b, CandidateState::Backed, hasher_) + .value(); + ASSERT_TRUE(storage.add_candidate_entry(candidate_b_entry).has_value()); + + const auto &[pvd_c, candidate_c] = + make_committed_candidate(para_id, + relay_parent_z_info.hash, + relay_parent_z_info.number, + {0x0c}, + {0x0d}, + relay_parent_z_info.number); + const auto candidate_c_hash = network::candidateHash(*hasher_, candidate_c); + const auto candidate_c_entry = + CandidateEntry::create( + candidate_c_hash, candidate_c, pvd_c, CandidateState::Backed, hasher_) + .value(); + ASSERT_TRUE(storage.add_candidate_entry(candidate_c_entry).has_value()); + + // Candidate A doesn't adhere to the base constraints. + for (const auto &wrong_constraints : + {make_constraints( + relay_parent_x_info.number, {relay_parent_x_info.number}, {0x0e}), + make_constraints(relay_parent_y_info.number, {0}, {0x0a})}) { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, wrong_constraints, {}, 4, ancestors)); + auto chain = populate_chain_from_previous_storage(scope, storage); + ASSERT_TRUE(chain.best_chain_vec().empty()); + + if (wrong_constraints.min_relay_parent_number + == relay_parent_y_info.number) { + ASSERT_EQ(chain.unconnected_len(), 0); + ASSERT_EQ(chain.can_add_candidate_as_potential(candidate_a_entry).error(), + FragmentChainError::RELAY_PARENT_NOT_IN_SCOPE); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_b_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_c_entry).has_value()); + } else { + const HashSet ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + } + + // Various depths + { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 0, ancestors)); + { + const auto chain = + FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_a_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_b_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_c_entry).has_value()); + } + + { + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = {candidate_a_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { + const HashSet ref = {candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + } + } + { + // depth is 1, allows two candidates + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 1, ancestors)); + { + const auto chain = + FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_a_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_b_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_c_entry).has_value()); + } + { + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = {candidate_a_hash, candidate_b_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { + const HashSet ref = {candidate_c_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + } + } + + // depth is larger than 2, allows all three candidates + for (size_t depth = 2; depth < 6; ++depth) { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, depth, ancestors)); + { + const auto chain = + FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_a_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_b_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_c_entry).has_value()); + } + { + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { ASSERT_EQ(chain.unconnected_len(), 0); } + } + } + + // Relay parents out of scope + { + { + // Candidate A has relay parent out of scope. Candidates B and C will also + // be deleted since they form a chain with A. + Vec ancestors_without_x = { + relay_parent_y_info}; + EXPECT_OUTCOME_TRUE(scope, + Scope::with_ancestors(relay_parent_z_info, + base_constraints, + {}, + 4, + ancestors_without_x)); + + const auto chain = populate_chain_from_previous_storage(scope, storage); + ASSERT_TRUE(chain.best_chain_vec().empty()); + ASSERT_EQ(chain.unconnected_len(), 0); + + ASSERT_EQ(chain.can_add_candidate_as_potential(candidate_a_entry).error(), + FragmentChainError::RELAY_PARENT_NOT_IN_SCOPE); + + // However, if taken independently, both B and C still have potential, + // since we don't know that A doesn't. + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_b_entry).has_value()); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_c_entry).has_value()); + } + + { + // Candidates A and B have relay parents out of scope. Candidate C will + // also be deleted since it forms a chain with A and B. + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 4, {})); + + const auto chain = populate_chain_from_previous_storage(scope, storage); + ASSERT_TRUE(chain.best_chain_vec().empty()); + ASSERT_EQ(chain.unconnected_len(), 0); + + ASSERT_EQ(chain.can_add_candidate_as_potential(candidate_a_entry).error(), + FragmentChainError::RELAY_PARENT_NOT_IN_SCOPE); + ASSERT_EQ(chain.can_add_candidate_as_potential(candidate_b_entry).error(), + FragmentChainError::RELAY_PARENT_NOT_IN_SCOPE); + // However, if taken independently, C still has potential, since we + // don't know that A and B don't + ASSERT_TRUE( + chain.can_add_candidate_as_potential(candidate_c_entry).has_value()); + } + } + + // Parachain cycle is not allowed. Make C have the same parent as A. + { + fragment::CandidateStorage modified_storage = storage; + modified_storage.remove_candidate(candidate_c_hash, hasher_); + + const auto &[wrong_pvd_c, wrong_candidate_c] = + make_committed_candidate(para_id, + relay_parent_z_info.hash, + relay_parent_z_info.number, + {0x0c}, + {0x0a}, + relay_parent_z_info.number); + EXPECT_OUTCOME_TRUE(wrong_candidate_c_entry, + CandidateEntry::create( + network::candidateHash(*hasher_, wrong_candidate_c), + wrong_candidate_c, + wrong_pvd_c, + CandidateState::Backed, + hasher_)); + ASSERT_TRUE(modified_storage.add_candidate_entry(wrong_candidate_c_entry) + .has_value()); + + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 4, ancestors)); + + { + const auto chain = + populate_chain_from_previous_storage(scope, modified_storage); + { + const Vec ref = {candidate_a_hash, candidate_b_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + ASSERT_EQ(chain.unconnected_len(), 0); + + ASSERT_EQ( + chain.can_add_candidate_as_potential(wrong_candidate_c_entry).error(), + FragmentChainError::CYCLE); + } + + { + // However, if taken independently, C still has potential, since we don't + // know A and B. + const auto chain = + FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_TRUE(chain.can_add_candidate_as_potential(wrong_candidate_c_entry) + .has_value()); + } + } + + // Candidate C has the same relay parent as candidate A's parent. Relay parent + // not allowed to move backwards + { + fragment::CandidateStorage modified_storage = storage; + modified_storage.remove_candidate(candidate_c_hash, hasher_); + + const auto &[wrong_pvd_c, wrong_candidate_c] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0x0c}, + {0x0d}, + 0); + EXPECT_OUTCOME_TRUE(wrong_candidate_c_entry, + CandidateEntry::create( + network::candidateHash(*hasher_, wrong_candidate_c), + wrong_candidate_c, + wrong_pvd_c, + CandidateState::Backed, + hasher_)); + + ASSERT_TRUE(modified_storage.add_candidate_entry(wrong_candidate_c_entry) + .has_value()); + + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 4, ancestors)); + + const auto chain = + populate_chain_from_previous_storage(scope, modified_storage); + { + const Vec ref = {candidate_a_hash, candidate_b_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + ASSERT_EQ(chain.unconnected_len(), 0); + + ASSERT_EQ( + chain.can_add_candidate_as_potential(wrong_candidate_c_entry).error(), + FragmentChainError::RELAY_PARENT_MOVED_BACKWARDS); + } + + // Candidate C is an unconnected candidate. + // C's relay parent is allowed to move backwards from B's relay parent, + // because C may later on trigger a reorg and B may get removed. + { + fragment::CandidateStorage modified_storage = storage; + modified_storage.remove_candidate(candidate_c_hash, hasher_); + + const auto &[unconnected_pvd_c, unconnected_candidate_c] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0x0d}, + {0x0e}, + 0); + const auto unconnected_candidate_c_hash = + network::candidateHash(*hasher_, unconnected_candidate_c); + EXPECT_OUTCOME_TRUE(unconnected_candidate_c_entry, + CandidateEntry::create(unconnected_candidate_c_hash, + unconnected_candidate_c, + unconnected_pvd_c, + CandidateState::Backed, + hasher_)); + + ASSERT_TRUE( + modified_storage.add_candidate_entry(unconnected_candidate_c_entry) + .has_value()); + + { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 4, ancestors)); + { + const auto chain = + FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_TRUE( + chain.can_add_candidate_as_potential(unconnected_candidate_c_entry) + .has_value()); + } + + { + const auto chain = + populate_chain_from_previous_storage(scope, modified_storage); + { + const Vec ref = {candidate_a_hash, candidate_b_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { + const HashSet ref = {unconnected_candidate_c_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + } + } + + // Candidate A is a pending availability candidate and Candidate C is an + // unconnected candidate, C's relay parent is not allowed to move backwards + // from A's relay parent because we're sure A will not get removed in the + // future, as it's already on-chain (unless it times out availability, a + // case for which we don't care to optimise for) + modified_storage.remove_candidate(candidate_a_hash, hasher_); + const auto &[modified_pvd_a, modified_candidate_a] = + make_committed_candidate(para_id, + relay_parent_y_info.hash, + relay_parent_y_info.number, + {0x0a}, + {0x0b}, + relay_parent_y_info.number); + + const auto modified_candidate_a_hash = + network::candidateHash(*hasher_, modified_candidate_a); + EXPECT_OUTCOME_TRUE(modified_candidate_a_entry, + CandidateEntry::create(modified_candidate_a_hash, + modified_candidate_a, + modified_pvd_a, + CandidateState::Backed, + hasher_)); + ASSERT_TRUE(modified_storage.add_candidate_entry(modified_candidate_a_entry) + .has_value()); + { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors(relay_parent_z_info, + base_constraints, + {PendingAvailability{ + .candidate_hash = modified_candidate_a_hash, + .relay_parent = relay_parent_y_info, + }}, + 4, + ancestors)); + + const auto chain = + populate_chain_from_previous_storage(scope, modified_storage); + { + const Vec ref = {modified_candidate_a_hash, + candidate_b_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + ASSERT_EQ(chain.unconnected_len(), 0); + ASSERT_EQ( + chain.can_add_candidate_as_potential(unconnected_candidate_c_entry) + .error(), + FragmentChainError:: + RELAY_PARENT_PRECEDES_CANDIDATE_PENDING_AVAILABILITY); + } + + // Not allowed to fork from a candidate pending availability + const auto &[wrong_pvd_c, wrong_candidate_c] = + make_committed_candidate(para_id, + relay_parent_y_info.hash, + relay_parent_y_info.number, + {0x0a}, + {0x0b2}, + 0); + + const auto wrong_candidate_c_hash = + network::candidateHash(*hasher_, wrong_candidate_c); + EXPECT_OUTCOME_TRUE(wrong_candidate_c_entry, + CandidateEntry::create(wrong_candidate_c_hash, + wrong_candidate_c, + wrong_pvd_c, + CandidateState::Backed, + hasher_)); + ASSERT_TRUE(modified_storage.add_candidate_entry(wrong_candidate_c_entry) + .has_value()); + + // Does not even matter if the fork selection rule would have picked up the + // new candidate, as the other is already pending availability. + ASSERT_TRUE(FragmentChain::fork_selection_rule(wrong_candidate_c_hash, + modified_candidate_a_hash)); + { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors(relay_parent_z_info, + base_constraints, + {PendingAvailability{ + .candidate_hash = modified_candidate_a_hash, + .relay_parent = relay_parent_y_info, + }}, + 4, + ancestors)); + + const auto chain = + populate_chain_from_previous_storage(scope, modified_storage); + { + const Vec ref = {modified_candidate_a_hash, + candidate_b_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + ASSERT_EQ(chain.unconnected_len(), 0); + ASSERT_EQ( + chain.can_add_candidate_as_potential(wrong_candidate_c_entry).error(), + FragmentChainError::FORK_WITH_CANDIDATE_PENDING_AVAILABILITY); + } + } + + // Test with candidates pending availability + { + Vec test_case_0 = {PendingAvailability{ + .candidate_hash = candidate_a_hash, + .relay_parent = relay_parent_x_info, + }}; + Vec test_case_1 = { + PendingAvailability{ + .candidate_hash = candidate_a_hash, + .relay_parent = relay_parent_x_info, + }, + PendingAvailability{ + .candidate_hash = candidate_b_hash, + .relay_parent = relay_parent_y_info, + }}; + Vec test_case_2 = { + PendingAvailability{ + .candidate_hash = candidate_a_hash, + .relay_parent = relay_parent_x_info, + }, + PendingAvailability{ + .candidate_hash = candidate_b_hash, + .relay_parent = relay_parent_y_info, + }, + PendingAvailability{ + .candidate_hash = candidate_c_hash, + .relay_parent = relay_parent_z_info, + }}; + + for (const auto &pending : {test_case_0, test_case_1, test_case_2}) { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, pending, 3, ancestors)); + + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + ASSERT_EQ(chain.unconnected_len(), 0); + } + + // Relay parents of pending availability candidates can be out of scope + // Relay parent of candidate A is out of scope. + Vec ancestors_without_x = { + relay_parent_y_info}; + + { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors(relay_parent_z_info, + base_constraints, + {PendingAvailability{ + .candidate_hash = candidate_a_hash, + .relay_parent = relay_parent_x_info, + }}, + 4, + ancestors_without_x)); + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + ASSERT_EQ(chain.unconnected_len(), 0); + } + { + // Even relay parents of pending availability candidates which are out of + // scope cannot move backwards. + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, + base_constraints, + {PendingAvailability{ + .candidate_hash = candidate_a_hash, + .relay_parent = + RelayChainBlockInfo{ + .hash = relay_parent_x_info.hash, + .number = 1, + .storage_root = relay_parent_x_info.storage_root, + }, + }, + PendingAvailability{ + .candidate_hash = candidate_b_hash, + .relay_parent = + RelayChainBlockInfo{ + .hash = relay_parent_y_info.hash, + .number = 0, + .storage_root = relay_parent_y_info.storage_root, + }, + }}, + 4, + {})); + const auto chain = populate_chain_from_previous_storage(scope, storage); + ASSERT_TRUE(chain.best_chain_vec().empty()); + ASSERT_EQ(chain.unconnected_len(), 0); + } + } + + // More complex case: + // max_depth is 2 (a chain of max depth 3). + // A -> B -> C are the best backable chain. + // D is backed but would exceed the max depth. + // F is unconnected and seconded. + // A1 has same parent as A, is backed but has a higher candidate hash. It'll + // therefore be deleted. + // A1 has underneath a subtree that will all need to be trimmed. A1 -> B1. + // B1 -> C1 + // and B1 -> C2. (C1 is backed). + // A2 is seconded but is kept because it has a lower candidate hash than A. + // A2 points to B2, which is backed. + // + // Check that D, F, A2 and B2 are kept as unconnected potential candidates. + { + { + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_z_info, base_constraints, {}, 2, ancestors)); + + // Candidate D + const auto &[pvd_d, candidate_d] = + make_committed_candidate(para_id, + relay_parent_z_info.hash, + relay_parent_z_info.number, + {0x0d}, + {0x0e}, + relay_parent_z_info.number); + const auto candidate_d_hash = hash(candidate_d); + const auto candidate_d_entry = + CandidateEntry::create(candidate_d_hash, + candidate_d, + pvd_d, + CandidateState::Backed, + hasher_) + .value(); + + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_d_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_d_entry).has_value()); + + // Candidate F + const auto &[pvd_f, candidate_f] = + make_committed_candidate(para_id, + relay_parent_z_info.hash, + relay_parent_z_info.number, + {0x0f}, + {0xf1}, + 1000); + const auto candidate_f_hash = hash(candidate_f); + const auto candidate_f_entry = + CandidateEntry::create(candidate_f_hash, + candidate_f, + pvd_f, + CandidateState::Seconded, + hasher_) + .value(); + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_f_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_f_entry).has_value()); + + // Candidate A1 + const auto &[pvd_a1, candidate_a1] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0x0a}, + {0xb1}, + relay_parent_x_info.number); + const auto candidate_a1_hash = hash(candidate_a1); + const auto candidate_a1_entry = + CandidateEntry::create(candidate_a1_hash, + candidate_a1, + pvd_a1, + CandidateState::Backed, + hasher_) + .value(); + // Candidate A1 is created so that its hash is greater than the candidate + // A hash. + ASSERT_TRUE(FragmentChain::fork_selection_rule(candidate_a_hash, + candidate_a1_hash)); + ASSERT_EQ(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_a1_entry) + .error(), + FragmentChainError::FORK_CHOICE_RULE); + ASSERT_TRUE(storage.add_candidate_entry(candidate_a1_entry).has_value()); + + // Candidate B1. + const auto &[pvd_b1, candidate_b1] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0xb1}, + {0xc1}, + relay_parent_x_info.number); + const auto candidate_b1_hash = hash(candidate_b1); + const auto candidate_b1_entry = + CandidateEntry::create(candidate_b1_hash, + candidate_b1, + pvd_b1, + CandidateState::Seconded, + hasher_) + .value(); + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_b1_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_b1_entry).has_value()); + + // Candidate C1. + const auto &[pvd_c1, candidate_c1] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0xc1}, + {0xd1}, + relay_parent_x_info.number); + const auto candidate_c1_hash = hash(candidate_c1); + const auto candidate_c1_entry = + CandidateEntry::create(candidate_c1_hash, + candidate_c1, + pvd_c1, + CandidateState::Backed, + hasher_) + .value(); + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_c1_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_c1_entry).has_value()); + + // Candidate C2. + const auto &[pvd_c2, candidate_c2] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0xc1}, + {0xd2}, + relay_parent_x_info.number); + const auto candidate_c2_hash = hash(candidate_c2); + const auto candidate_c2_entry = + CandidateEntry::create(candidate_c2_hash, + candidate_c2, + pvd_c2, + CandidateState::Seconded, + hasher_) + .value(); + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_c2_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_c2_entry).has_value()); + + // Candidate A2. + const auto &[pvd_a2, candidate_a2] = + make_committed_candidate(para_id, + relay_parent_x_info.hash, + relay_parent_x_info.number, + {0x0a}, + {0xb3}, + relay_parent_x_info.number); + const auto candidate_a2_hash = hash(candidate_a2); + const auto candidate_a2_entry = + CandidateEntry::create(candidate_a2_hash, + candidate_a2, + pvd_a2, + CandidateState::Seconded, + hasher_) + .value(); + // Candidate A2 is created so that its hash is greater than the candidate + // A hash. + ASSERT_TRUE(FragmentChain::fork_selection_rule(candidate_a2_hash, + candidate_a_hash)); + + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_a2_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_a2_entry).has_value()); + + // Candidate B2. + const auto &[pvd_b2, candidate_b2] = + make_committed_candidate(para_id, + relay_parent_y_info.hash, + relay_parent_y_info.number, + {0xb3}, + {0xb4}, + relay_parent_y_info.number); + const auto candidate_b2_hash = hash(candidate_b2); + const auto candidate_b2_entry = + CandidateEntry::create(candidate_b2_hash, + candidate_b2, + pvd_b2, + CandidateState::Backed, + hasher_) + .value(); + ASSERT_TRUE(populate_chain_from_previous_storage(scope, storage) + .can_add_candidate_as_potential(candidate_b2_entry) + .has_value()); + ASSERT_TRUE(storage.add_candidate_entry(candidate_b2_entry).has_value()); + + { + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { + const HashSet ref = {candidate_d_hash, + candidate_f_hash, + candidate_a2_hash, + candidate_b2_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + + // Cannot add as potential an already present candidate (whether it's in + // the best chain or in unconnected storage) + ASSERT_EQ( + chain.can_add_candidate_as_potential(candidate_a_entry).error(), + FragmentChainError::CANDIDATE_ALREADY_KNOWN); + ASSERT_EQ( + chain.can_add_candidate_as_potential(candidate_f_entry).error(), + FragmentChainError::CANDIDATE_ALREADY_KNOWN); + + // Simulate a best chain reorg by backing a2. + { + FragmentChain chain_2 = chain; + chain_2.candidate_backed(candidate_a2_hash); + { + const Vec ref = {candidate_a2_hash, + candidate_b2_hash}; + ASSERT_EQ(chain_2.best_chain_vec(), ref); + } + { + // F is kept as it was truly unconnected. The rest will be trimmed. + const HashSet ref = {candidate_f_hash}; + ASSERT_EQ(get_unconnected(chain_2), ref); + } + // A and A1 will never have potential again. + ASSERT_EQ(chain_2.can_add_candidate_as_potential(candidate_a1_entry) + .error(), + FragmentChainError::FORK_CHOICE_RULE); + ASSERT_EQ( + chain_2.can_add_candidate_as_potential(candidate_a_entry).error(), + FragmentChainError::FORK_CHOICE_RULE); + } + } + // Candidate F has an invalid hrmp watermark. however, it was not checked + // beforehand as we don't have its parent yet. Add its parent now. This + // will not impact anything as E is not yet part of the best chain. + + const auto &[pvd_e, candidate_e] = + make_committed_candidate(para_id, + relay_parent_z_info.hash, + relay_parent_z_info.number, + {0x0e}, + {0x0f}, + relay_parent_z_info.number); + const auto candidate_e_hash = hash(candidate_e); + ASSERT_TRUE(storage + .add_candidate_entry( + CandidateEntry::create(candidate_e_hash, + candidate_e, + pvd_e, + CandidateState::Seconded, + hasher_) + .value()) + .has_value()); + + { + const auto chain = populate_chain_from_previous_storage(scope, storage); + { + const Vec ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { + const HashSet ref = {candidate_d_hash, + candidate_f_hash, + candidate_a2_hash, + candidate_b2_hash, + candidate_e_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + } + + // Simulate the fact that candidates A, B, C are now pending availability. + EXPECT_OUTCOME_TRUE( + scope2, + Scope::with_ancestors(relay_parent_z_info, + base_constraints, + {PendingAvailability{ + .candidate_hash = candidate_a_hash, + .relay_parent = relay_parent_x_info, + }, + PendingAvailability{ + .candidate_hash = candidate_b_hash, + .relay_parent = relay_parent_y_info, + }, + PendingAvailability{ + .candidate_hash = candidate_c_hash, + .relay_parent = relay_parent_z_info, + }}, + 2, + ancestors)); + + // A2 and B2 will now be trimmed + const auto chain = populate_chain_from_previous_storage(scope2, storage); + { + const Vec ref = { + candidate_a_hash, candidate_b_hash, candidate_c_hash}; + ASSERT_EQ(chain.best_chain_vec(), ref); + } + { + const HashSet ref = { + candidate_d_hash, candidate_f_hash, candidate_e_hash}; + ASSERT_EQ(get_unconnected(chain), ref); + } + + // Cannot add as potential an already pending availability candidate + ASSERT_EQ(chain.can_add_candidate_as_potential(candidate_a_entry).error(), + FragmentChainError::CANDIDATE_ALREADY_KNOWN); + + // Simulate the fact that candidates A, B and C have been included. + EXPECT_OUTCOME_TRUE( + scope3, + Scope::with_ancestors(relay_parent_z_info, + make_constraints(0, {0}, {0x0d}), + {}, + 2, + ancestors)); + + FragmentChain prev_chain = chain; + FragmentChain chain_new = + FragmentChain::init(hasher_, scope3, CandidateStorage{}); + chain_new.populate_from_previous(prev_chain); + { + const Vec ref = {candidate_d_hash}; + ASSERT_EQ(chain_new.best_chain_vec(), ref); + } + { + const HashSet ref = {candidate_e_hash, candidate_f_hash}; + ASSERT_EQ(get_unconnected(chain_new), ref); + } + + // Mark E as backed. F will be dropped for invalid watermark. No other + // unconnected candidates. + chain_new.candidate_backed(candidate_e_hash); + { + const Vec ref = {candidate_d_hash, candidate_e_hash}; + ASSERT_EQ(chain_new.best_chain_vec(), ref); + } + ASSERT_EQ(chain_new.unconnected_len(), 0); + + ASSERT_EQ( + chain_new.can_add_candidate_as_potential(candidate_f_entry).error(), + FragmentChainError::CHECK_AGAINST_CONSTRAINTS); + } + } +} + +TEST_F(FragmentChainTest, + test_find_ancestor_path_and_find_backable_chain_empty_best_chain) { + const auto relay_parent = fromNumber(1); + HeadData required_parent = {0xff}; + size_t max_depth = 10; + + // Empty chain + const auto base_constraints = make_constraints(0, {0}, required_parent); + + const RelayChainBlockInfo relay_parent_info{ + .hash = relay_parent, + .number = 0, + .storage_root = fromNumber(0), + }; + + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_info, base_constraints, {}, max_depth, {})); + const auto chain = FragmentChain::init(hasher_, scope, CandidateStorage{}); + ASSERT_EQ(chain.best_chain_len(), 0); + + Vec> ref; + ASSERT_EQ(chain.find_ancestor_path({}), 0); + ASSERT_EQ(chain.find_backable_chain({}, 2), ref); + + // Invalid candidate. + Ancestors ancestors = {CandidateHash{}}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 0); + ASSERT_EQ(chain.find_backable_chain(ancestors, 2), ref); +} + +TEST_F(FragmentChainTest, test_find_ancestor_path_and_find_backable_chain) { + const ParachainId para_id{5}; + const auto relay_parent = fromNumber(1); + HeadData required_parent = {0xff}; + size_t max_depth = 5; + BlockNumber relay_parent_number = 0; + auto relay_parent_storage_root = fromNumber(0); + + Vec>, + network::CommittedCandidateReceipt>> + candidates; + + // Candidate 0 + candidates.emplace_back(make_committed_candidate( + para_id, relay_parent, 0, required_parent, {0}, 0)); + + // Candidates 1..=5 + for (uint8_t index = 1; index <= 5; ++index) { + candidates.emplace_back(make_committed_candidate( + para_id, relay_parent, 0, {uint8_t(index - 1)}, {index}, 0)); + } + + CandidateStorage storage; + for (const auto &[pvd, candidate] : candidates) { + ASSERT_TRUE( + storage + .add_candidate_entry(CandidateEntry::create_seconded( + hash(candidate), candidate, pvd, hasher_) + .value()) + .has_value()); + } + + Vec candidate_hashes; + for (const auto &[_, candidate] : candidates) { + candidate_hashes.emplace_back(hash(candidate)); + } + + auto hashes = [&](size_t from, size_t to) { + Vec> result; + for (size_t ix = from; ix < to; ++ix) { + result.emplace_back(candidate_hashes[ix], relay_parent); + } + return result; + }; + + const RelayChainBlockInfo relay_parent_info{ + .hash = relay_parent, + .number = relay_parent_number, + .storage_root = relay_parent_storage_root, + }; + + const auto base_constraints = make_constraints(0, {0}, required_parent); + EXPECT_OUTCOME_TRUE( + scope, + Scope::with_ancestors( + relay_parent_info, base_constraints, {}, max_depth, {})); + auto chain = populate_chain_from_previous_storage(scope, storage); + + // For now, candidates are only seconded, not backed. So the best chain is + // empty and no candidate will be returned. + ASSERT_EQ(candidate_hashes.size(), 6); + ASSERT_EQ(chain.best_chain_len(), 0); + ASSERT_EQ(chain.unconnected_len(), 6); + + for (size_t count = 0; count < 10; ++count) { + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, count).size(), 0); + } + + // Do tests with only a couple of candidates being backed. + { + auto chain_new = chain; + chain_new.candidate_backed(candidate_hashes[5]); + ASSERT_EQ(chain_new.unconnected_len(), 6); + for (size_t count = 0; count < 10; ++count) { + ASSERT_EQ(chain_new.find_backable_chain(Ancestors{}, count).size(), 0); + } + + chain_new.candidate_backed(candidate_hashes[3]); + ASSERT_EQ(chain_new.unconnected_len(), 6); + chain_new.candidate_backed(candidate_hashes[4]); + ASSERT_EQ(chain_new.unconnected_len(), 6); + for (size_t count = 0; count < 10; ++count) { + ASSERT_EQ(chain_new.find_backable_chain(Ancestors{}, count).size(), 0); + } + + chain_new.candidate_backed(candidate_hashes[1]); + ASSERT_EQ(chain_new.unconnected_len(), 6); + for (size_t count = 0; count < 10; ++count) { + ASSERT_EQ(chain_new.find_backable_chain(Ancestors{}, count).size(), 0); + } + + chain_new.candidate_backed(candidate_hashes[0]); + ASSERT_EQ(chain_new.unconnected_len(), 4); + ASSERT_EQ(chain_new.find_backable_chain(Ancestors{}, 1), hashes(0, 1)); + for (size_t count = 2; count < 10; ++count) { + ASSERT_EQ(chain_new.find_backable_chain(Ancestors{}, count), + hashes(0, 2)); + } + + // Now back the missing piece. + chain_new.candidate_backed(candidate_hashes[2]); + ASSERT_EQ(chain_new.unconnected_len(), 0); + ASSERT_EQ(chain_new.best_chain_len(), 6); + + for (size_t count = 0; count < 10; ++count) { + ASSERT_EQ(chain_new.find_backable_chain(Ancestors{}, count), + hashes(0, std::min(count, size_t(6)))); + } + } + + // Now back all candidates. Back them in a random order. The result should + // always be the same. + auto candidates_shuffled = candidate_hashes; + std::default_random_engine random_; + std::shuffle(candidates_shuffled.begin(), candidates_shuffled.end(), random_); + for (const auto &candidate : candidates_shuffled) { + chain.candidate_backed(candidate); + storage.mark_backed(candidate); + } + + // No ancestors supplied. + ASSERT_EQ(chain.find_ancestor_path(Ancestors{}), 0); + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, 0), hashes(0, 0)); + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, 1), hashes(0, 1)); + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, 2), hashes(0, 2)); + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, 5), hashes(0, 5)); + + for (size_t count = 6; count < 10; ++count) { + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, count), hashes(0, 6)); + } + + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, 7), hashes(0, 6)); + ASSERT_EQ(chain.find_backable_chain(Ancestors{}, 10), hashes(0, 6)); + + // Ancestor which is not part of the chain. Will be ignored. + { + Ancestors ancestors = {CandidateHash{}}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 0); + ASSERT_EQ(chain.find_backable_chain(ancestors, 4), hashes(0, 4)); + } + + { + Ancestors ancestors = {candidate_hashes[1], CandidateHash{}}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 0); + ASSERT_EQ(chain.find_backable_chain(ancestors, 4), hashes(0, 4)); + } + + { + Ancestors ancestors = {candidate_hashes[0], CandidateHash{}}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 1); + ASSERT_EQ(chain.find_backable_chain(ancestors, 4), hashes(1, 5)); + } + + { + // Ancestors which are part of the chain but don't form a path from root. + // Will be ignored. + Ancestors ancestors = {candidate_hashes[1], candidate_hashes[2]}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 0); + ASSERT_EQ(chain.find_backable_chain(ancestors, 4), hashes(0, 4)); + } + + { + // Valid ancestors. + Ancestors ancestors = { + candidate_hashes[2], candidate_hashes[0], candidate_hashes[1]}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 3); + ASSERT_EQ(chain.find_backable_chain(ancestors, 2), hashes(3, 5)); + for (size_t count = 3; count < 10; ++count) { + ASSERT_EQ(chain.find_backable_chain(ancestors, count), hashes(3, 6)); + } + } + + { + // Valid ancestors with candidates which have been omitted due to timeouts + Ancestors ancestors = {candidate_hashes[0], candidate_hashes[2]}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 1); + ASSERT_EQ(chain.find_backable_chain(ancestors, 3), hashes(1, 4)); + ASSERT_EQ(chain.find_backable_chain(ancestors, 4), hashes(1, 5)); + for (size_t count = 5; count < 10; ++count) { + ASSERT_EQ(chain.find_backable_chain(ancestors, count), hashes(1, 6)); + } + } + + { + Ancestors ancestors = { + candidate_hashes[0], candidate_hashes[1], candidate_hashes[3]}; + ASSERT_EQ(chain.find_ancestor_path(ancestors), 2); + ASSERT_EQ(chain.find_backable_chain(ancestors, 4), hashes(2, 6)); + + // Requested count is 0. + ASSERT_EQ(chain.find_backable_chain(ancestors, 0), hashes(0, 0)); + } + + // Stop when we've found a candidate which is pending availability + { + EXPECT_OUTCOME_TRUE( + scope2, + Scope::with_ancestors(relay_parent_info, + base_constraints, + {PendingAvailability{ + .candidate_hash = candidate_hashes[3], + .relay_parent = relay_parent_info, + }}, + max_depth, + {})); + + auto chain = populate_chain_from_previous_storage(scope2, storage); + Ancestors ancestors = {candidate_hashes[0], candidate_hashes[1]}; + ASSERT_EQ(chain.find_backable_chain(ancestors, 3), hashes(2, 3)); + } +} diff --git a/test/core/parachain/parachain_test_harness.hpp b/test/core/parachain/parachain_test_harness.hpp new file mode 100644 index 0000000000..a2106770e3 --- /dev/null +++ b/test/core/parachain/parachain_test_harness.hpp @@ -0,0 +1,153 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "testutil/literals.hpp" +#include "testutil/outcome.hpp" +#include "testutil/prepare_loggers.hpp" + +#include "crypto/hasher/hasher_impl.hpp" +#include "crypto/random_generator/boost_generator.hpp" +#include "crypto/sr25519/sr25519_provider_impl.hpp" +#include "crypto/type_hasher.hpp" +#include "mock/core/blockchain/block_tree_mock.hpp" +#include "mock/core/runtime/parachain_host_mock.hpp" +#include "parachain/types.hpp" +#include "parachain/validator/prospective_parachains/candidate_storage.hpp" +#include "parachain/validator/signer.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" +#include "scale/kagome_scale.hpp" +#include "scale/scale.hpp" +#include "testutil/scale_test_comparator.hpp" + +using namespace kagome::primitives; +using namespace kagome::parachain; + +namespace network = kagome::network; +namespace runtime = kagome::runtime; +namespace common = kagome::common; +namespace crypto = kagome::crypto; + +using testing::Return; + +inline Hash ghashFromStrData( + const std::shared_ptr &hasher, + std::span data) { + return hasher->blake2b_256(data); +} + +class ProspectiveParachainsTestHarness : public testing::Test { + protected: + void SetUp() override { + testutil::prepareLoggers(); + hasher_ = std::make_shared(); + + block_tree_ = std::make_shared(); + sr25519_provider_ = std::make_shared(); + } + + void TearDown() override { + block_tree_.reset(); + } + + using CandidatesHashMap = std::unordered_map< + Hash, + std::unordered_map>>; + + std::shared_ptr hasher_; + std::shared_ptr block_tree_; + std::shared_ptr sr25519_provider_; + + static constexpr uint64_t ALLOWED_ANCESTRY_LEN = 3ull; + static constexpr uint32_t MAX_POV_SIZE = 1000000; + + Hash hashFromStrData(std::span data) { + return ghashFromStrData(hasher_, data); + } + + Hash hash(const network::CommittedCandidateReceipt &data) { + return network::candidateHash(*hasher_, data); + } + + fragment::Constraints make_constraints( + BlockNumber min_relay_parent_number, + std::vector valid_watermarks, + HeadData required_parent) { + return fragment::Constraints{ + .min_relay_parent_number = min_relay_parent_number, + .max_pov_size = 1000000, + .max_code_size = 1000000, + .ump_remaining = 10, + .ump_remaining_bytes = 1000, + .max_ump_num_per_candidate = 10, + .dmp_remaining_messages = std::vector(10, 0), + .hrmp_inbound = fragment::InboundHrmpLimitations{.valid_watermarks = + valid_watermarks}, + .hrmp_channels_out = {}, + .max_hrmp_num_per_candidate = 0, + .required_parent = required_parent, + .validation_code_hash = fromNumber(42), + .upgrade_restriction = std::nullopt, + .future_validation_code = std::nullopt, + }; + } + + std::pair>, + network::CommittedCandidateReceipt> + make_committed_candidate(ParachainId para_id, + const Hash &relay_parent, + BlockNumber relay_parent_number, + const HeadData &parent_head, + const HeadData ¶_head, + BlockNumber hrmp_watermark) { + crypto::Hashed> + persisted_validation_data(runtime::PersistedValidationData{ + .parent_head = parent_head, + .relay_parent_number = relay_parent_number, + .relay_parent_storage_root = fromNumber(0), + .max_pov_size = 1000000, + }); + + network::CommittedCandidateReceipt candidate{ + .descriptor = + network::CandidateDescriptor{ + .para_id = para_id, + .relay_parent = relay_parent, + .collator_id = {}, + .persisted_data_hash = persisted_validation_data.getHash(), + .pov_hash = fromNumber(1), + .erasure_encoding_root = fromNumber(1), + .signature = {}, + .para_head_hash = hasher_->blake2b_256(para_head), + .validation_code_hash = fromNumber(42), + }, + .commitments = + network::CandidateCommitments{ + .upward_msgs = {}, + .outbound_hor_msgs = {}, + .opt_para_runtime = std::nullopt, + .para_head = para_head, + .downward_msgs_count = 1, + .watermark = hrmp_watermark, + }, + }; + + return std::make_pair(std::move(persisted_validation_data), + std::move(candidate)); + } + + Hash fromNumber(uint64_t n) const { + assert(n <= 255); + Hash h{}; + memset(&h[0], n, 32); + return h; + } +}; diff --git a/test/core/parachain/prospective_parachains.cpp b/test/core/parachain/prospective_parachains.cpp index c6f8c7c298..743ae83563 100644 --- a/test/core/parachain/prospective_parachains.cpp +++ b/test/core/parachain/prospective_parachains.cpp @@ -1,3228 +1,8 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#include - -#include "testutil/literals.hpp" -#include "testutil/outcome.hpp" -#include "testutil/prepare_loggers.hpp" - -#include "crypto/hasher/hasher_impl.hpp" -#include "crypto/random_generator/boost_generator.hpp" -#include "crypto/sr25519/sr25519_provider_impl.hpp" -#include "crypto/type_hasher.hpp" -#include "mock/core/blockchain/block_tree_mock.hpp" -#include "mock/core/runtime/parachain_host_mock.hpp" -#include "parachain/types.hpp" -#include "parachain/validator/fragment_tree.hpp" -#include "parachain/validator/impl/candidates.hpp" -#include "parachain/validator/parachain_processor.hpp" -#include "parachain/validator/prospective_parachains.hpp" -#include "parachain/validator/signer.hpp" -#include "runtime/runtime_api/parachain_host_types.hpp" -#include "scale/kagome_scale.hpp" -#include "scale/scale.hpp" -#include "testutil/scale_test_comparator.hpp" - -using namespace kagome::primitives; -using namespace kagome::parachain; - -namespace network = kagome::network; -namespace runtime = kagome::runtime; -namespace common = kagome::common; -namespace crypto = kagome::crypto; - -using testing::Return; - -inline Hash ghashFromStrData( - const std::shared_ptr &hasher, - std::span data) { - return hasher->blake2b_256(data); -} - -struct PerParaData { - BlockNumber min_relay_parent; - HeadData head_data; - std::vector pending_availability; - - PerParaData(BlockNumber min_relay_parent_, const HeadData &head_data_) - : min_relay_parent{min_relay_parent_}, head_data{head_data_} {} - - PerParaData( - BlockNumber min_relay_parent_, - const HeadData &head_data_, - const std::vector &pending_) - : min_relay_parent{min_relay_parent_}, - head_data{head_data_}, - pending_availability{pending_} {} -}; - -struct TestState { - std::vector availability_cores; - ValidationCodeHash validation_code_hash; - - TestState(const std::shared_ptr &hasher) - : availability_cores{{runtime::ScheduledCore{.para_id = ParachainId{1}, - .collator = std::nullopt}, - runtime::ScheduledCore{.para_id = ParachainId{2}, - .collator = std::nullopt}}}, - validation_code_hash{ghashFromStrData(hasher, "42")} {} - - ParachainId byIndex(size_t ix) const { - assert(ix < availability_cores.size()); - const runtime::CoreState &cs = availability_cores[ix]; - if (const runtime::ScheduledCore *ptr = - std::get_if(&cs)) { - return ptr->para_id; - } - UNREACHABLE; - } -}; - -struct TestLeaf { - BlockNumber number; - Hash hash; - std::vector> para_data; - - std::reference_wrapper paraData( - ParachainId para_id) const { - for (const auto &[para, per_data] : para_data) { - if (para == para_id) { - return {per_data}; - } - } - UNREACHABLE; - } -}; - -class ProspectiveParachainsTest : public testing::Test { - void SetUp() override { - testutil::prepareLoggers(); - hasher_ = std::make_shared(); - - parachain_api_ = std::make_shared(); - block_tree_ = std::make_shared(); - prospective_parachain_ = std::make_shared( - hasher_, parachain_api_, block_tree_); - sr25519_provider_ = std::make_shared(); - } - - void TearDown() override { - prospective_parachain_.reset(); - block_tree_.reset(); - parachain_api_.reset(); - } - - protected: - using CandidatesHashMap = std::unordered_map< - Hash, - std::unordered_map>>; - - std::shared_ptr hasher_; - std::shared_ptr parachain_api_; - std::shared_ptr block_tree_; - std::shared_ptr prospective_parachain_; - std::shared_ptr sr25519_provider_; - - static constexpr uint64_t ALLOWED_ANCESTRY_LEN = 3ull; - static constexpr uint32_t MAX_POV_SIZE = 1000000; - - Hash hashFromStrData(std::span data) { - return ghashFromStrData(hasher_, data); - } - - fragment::Constraints make_constraints( - BlockNumber min_relay_parent_number, - std::vector valid_watermarks, - HeadData required_parent) { - return fragment::Constraints{ - .min_relay_parent_number = min_relay_parent_number, - .max_pov_size = 1000000, - .max_code_size = 1000000, - .ump_remaining = 10, - .ump_remaining_bytes = 1000, - .max_ump_num_per_candidate = 10, - .dmp_remaining_messages = std::vector(10, 0), - .hrmp_inbound = fragment::InboundHrmpLimitations{.valid_watermarks = - valid_watermarks}, - .hrmp_channels_out = {}, - .max_hrmp_num_per_candidate = 0, - .required_parent = required_parent, - .validation_code_hash = hashFromStrData("42"), - .upgrade_restriction = std::nullopt, - .future_validation_code = std::nullopt, - }; - } - - std::pair - make_and_back_candidate(const TestState &test_state, - const TestLeaf &leaf, - const network::CommittedCandidateReceipt &parent, - uint64_t index) { - auto tmp = make_candidate(leaf.hash, - leaf.number, - 1, - parent.commitments.para_head, - {uint8_t(index)}, - test_state.validation_code_hash); - - tmp.first.descriptor.para_head_hash = fromNumber(index); - const auto &[candidate, pvd] = tmp; - const Hash candidate_hash = network::candidateHash(*hasher_, candidate); - - introduce_candidate(candidate, pvd); - second_candidate(candidate); - back_candidate(candidate, candidate_hash); - - return {candidate, candidate_hash}; - } - - std::pair - make_candidate(const Hash &relay_parent_hash, - BlockNumber relay_parent_number, - ParachainId para_id, - const HeadData &parent_head, - const HeadData &head_data, - const ValidationCodeHash &validation_code_hash) { - runtime::PersistedValidationData pvd{ - .parent_head = parent_head, - .relay_parent_number = relay_parent_number, - .relay_parent_storage_root = {}, - .max_pov_size = 1'000'000, - }; - - network::CandidateCommitments commitments{ - .upward_msgs = {}, - .outbound_hor_msgs = {}, - .opt_para_runtime = std::nullopt, - .para_head = head_data, - .downward_msgs_count = 0, - .watermark = relay_parent_number, - }; - - network::CandidateReceipt candidate{}; - candidate.descriptor = network::CandidateDescriptor{ - .para_id = 0, - .relay_parent = relay_parent_hash, - .collator_id = {}, - .persisted_data_hash = {}, - .pov_hash = {}, - .erasure_encoding_root = {}, - .signature = {}, - .para_head_hash = {}, - .validation_code_hash = - hasher_->blake2b_256(std::vector{1, 2, 3}), - }; - candidate.commitments_hash = {}; - - candidate.commitments_hash = - crypto::Hashed>(commitments) - .getHash(); - candidate.descriptor.para_id = para_id; - candidate.descriptor.persisted_data_hash = - crypto::Hashed>(pvd) - .getHash(); - candidate.descriptor.validation_code_hash = validation_code_hash; - return std::make_pair( - network::CommittedCandidateReceipt{ - .descriptor = candidate.descriptor, - .commitments = commitments, - }, - pvd); - } - - std::pair>, - network::CommittedCandidateReceipt> - make_committed_candidate(ParachainId para_id, - const Hash &relay_parent, - BlockNumber relay_parent_number, - const HeadData &parent_head, - const HeadData ¶_head, - BlockNumber hrmp_watermark) { - crypto::Hashed> - persisted_validation_data(runtime::PersistedValidationData{ - .parent_head = parent_head, - .relay_parent_number = relay_parent_number, - .relay_parent_storage_root = hashFromStrData("69"), - .max_pov_size = 1000000, - }); - - network::CommittedCandidateReceipt candidate{ - .descriptor = - network::CandidateDescriptor{ - .para_id = para_id, - .relay_parent = relay_parent, - .collator_id = {}, - .persisted_data_hash = persisted_validation_data.getHash(), - .pov_hash = hashFromStrData("1"), - .erasure_encoding_root = hashFromStrData("1"), - .signature = {}, - .para_head_hash = hasher_->blake2b_256(para_head), - .validation_code_hash = hashFromStrData("42"), - }, - .commitments = - network::CandidateCommitments{ - .upward_msgs = {}, - .outbound_hor_msgs = {}, - .opt_para_runtime = std::nullopt, - .para_head = para_head, - .downward_msgs_count = 1, - .watermark = hrmp_watermark, - }, - }; - - return std::make_pair(std::move(persisted_validation_data), - std::move(candidate)); - } - - bool getNodePointerStorage(const fragment::NodePointer &p, size_t val) { - auto pa = kagome::if_type(p); - return pa && pa->get() == val; - } - - template - bool compareVectors(const std::vector &l, const std::vector &r) { - return l == r; - } - - bool compareMapsOfCandidates(const CandidatesHashMap &l, - const CandidatesHashMap &r) { - return l == r; - } - - Hash get_parent_hash(const Hash &parent) const { - Hash h{}; - *(uint64_t *)&h[0] = *(uint64_t *)&parent[0] + 1ull; - return h; - } - - Hash fromNumber(uint64_t n) const { - Hash h{}; - *(uint64_t *)&h[0] = n; - return h; - } - - void filterACByPara(TestState &test_state, ParachainId para_id) { - for (auto it = test_state.availability_cores.begin(); - it != test_state.availability_cores.end();) { - const runtime::CoreState &cs = *it; - auto p = visit_in_place( - cs, - [](const runtime::OccupiedCore &core) mutable - -> std::optional { - return core.candidate_descriptor.para_id; - }, - [](const runtime::ScheduledCore &core) mutable - -> std::optional { return core.para_id; }, - [](runtime::FreeCore) -> std::optional { - return std::nullopt; - }); - - if (p && *p == para_id) { - ++it; - } else { - it = test_state.availability_cores.erase(it); - } - } - ASSERT_EQ(test_state.availability_cores.size(), 1); - } - - fragment::Constraints dummy_constraints( - BlockNumber min_relay_parent_number, - std::vector valid_watermarks, - const HeadData &required_parent, - const ValidationCodeHash &validation_code_hash) { - return fragment::Constraints{ - .min_relay_parent_number = min_relay_parent_number, - .max_pov_size = MAX_POV_SIZE, - .max_code_size = 1000000, - .ump_remaining = 10, - .ump_remaining_bytes = 1000, - .max_ump_num_per_candidate = 10, - .dmp_remaining_messages = {}, - .hrmp_inbound = - fragment::InboundHrmpLimitations{ - .valid_watermarks = valid_watermarks, - }, - .hrmp_channels_out = {}, - .max_hrmp_num_per_candidate = 0, - .required_parent = required_parent, - .validation_code_hash = validation_code_hash, - .upgrade_restriction = {}, - .future_validation_code = {}, - }; - } - - void handle_leaf_activation_2( - const network::ExView &update, - const TestLeaf &leaf, - const TestState &test_state, - const fragment::AsyncBackingParams &async_backing_params) { - const auto &[number, hash, para_data] = leaf; - const auto &header = update.new_head; - - EXPECT_CALL(*parachain_api_, staging_async_backing_params(hash)) - .WillRepeatedly(Return(outcome::success(async_backing_params))); - - EXPECT_CALL(*parachain_api_, availability_cores(hash)) - .WillRepeatedly( - Return(outcome::success(test_state.availability_cores))); - - EXPECT_CALL(*block_tree_, getBlockHeader(hash)) - .WillRepeatedly(Return(header)); - - BlockNumber min_min = [&, number = number]() -> BlockNumber { - std::optional min_min; - for (const auto &[_, data] : leaf.para_data) { - min_min = min_min ? std::min(*min_min, data.min_relay_parent) - : data.min_relay_parent; - } - if (min_min) { - return *min_min; - } - return number; - }(); - const auto ancestry_len = number - min_min; - std::vector ancestry_hashes; - std::deque ancestry_numbers; - - Hash d = hash; - for (BlockNumber x = 0; x <= ancestry_len + 1; ++x) { - assert(number - x - 1 != 0); - ancestry_hashes.emplace_back(d); - ancestry_numbers.push_front(number - ancestry_len + x - 1); - d = get_parent_hash(d); - } - ASSERT_EQ(ancestry_hashes.size(), ancestry_numbers.size()); - - if (ancestry_len > 0) { - EXPECT_CALL(*block_tree_, - getDescendingChainToBlock(hash, ALLOWED_ANCESTRY_LEN + 1)) - .WillRepeatedly(Return(ancestry_hashes)); - EXPECT_CALL(*parachain_api_, session_index_for_child(hash)) - .WillRepeatedly(Return(1)); - } - - for (size_t i = 0; i < ancestry_hashes.size(); ++i) { - const auto &h_ = ancestry_hashes[i]; - const auto &n_ = ancestry_numbers[i]; - - ASSERT_TRUE(n_ > 0); - BlockHeader h{ - .number = n_, - .parent_hash = get_parent_hash(h_), - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - EXPECT_CALL(*block_tree_, getBlockHeader(h_)).WillRepeatedly(Return(h)); - EXPECT_CALL(*parachain_api_, session_index_for_child(h_)) - .WillRepeatedly(Return(outcome::success(1))); - } - - for (size_t i = 0; i < test_state.availability_cores.size(); ++i) { - const auto para_id = test_state.byIndex(i); - const auto &[min_relay_parent, head_data, pending_availability] = - leaf.paraData(para_id).get(); - fragment::BackingState backing_state{ - .constraints = dummy_constraints(min_relay_parent, - {number}, - head_data, - test_state.validation_code_hash), - .pending_availability = pending_availability, - }; - EXPECT_CALL(*parachain_api_, staging_para_backing_state(hash, para_id)) - .WillRepeatedly(Return(backing_state)); - - for (const auto &pending : pending_availability) { - BlockHeader h{ - .number = pending.relay_parent_number, - .parent_hash = get_parent_hash(pending.descriptor.relay_parent), - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - EXPECT_CALL(*block_tree_, - getBlockHeader(pending.descriptor.relay_parent)) - .WillRepeatedly(Return(h)); - } - } - - ASSERT_OUTCOME_SUCCESS_TRY( - prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {update.new_head}, - .lost = update.lost, - })); - auto resp = prospective_parachain_->answerMinimumRelayParentsRequest(hash); - std::sort(resp.begin(), resp.end(), [](const auto &l, const auto &r) { - return l.first < r.first; - }); - - std::vector> mrp_response; - for (const auto &[pid, ppd] : para_data) { - mrp_response.emplace_back(pid, ppd.min_relay_parent); - } - ASSERT_EQ(resp, mrp_response); - } - - void handle_leaf_activation( - const TestLeaf &leaf, - const TestState &test_state, - const fragment::AsyncBackingParams &async_backing_params) { - const auto &[number, hash, para_data] = leaf; - BlockHeader header{ - .number = number, - .parent_hash = get_parent_hash(hash), - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - - network::ExView update{ - .view = {}, - .new_head = header, - .lost = {}, - }; - update.new_head.hash_opt = hash; - handle_leaf_activation_2(update, leaf, test_state, async_backing_params); - } - - void activate_leaf(const TestLeaf &leaf, - const TestState &test_state, - const fragment::AsyncBackingParams &async_backing_params) { - handle_leaf_activation(leaf, test_state, async_backing_params); - } - - void introduce_candidate(const network::CommittedCandidateReceipt &candidate, - const runtime::PersistedValidationData &pvd) { - [[maybe_unused]] const auto _ = prospective_parachain_->introduceCandidate( - candidate.descriptor.para_id, - candidate, - crypto::Hashed>(pvd), - network::candidateHash(*hasher_, candidate)); - } - - auto get_backable_candidates( - const TestLeaf &leaf, - ParachainId para_id, - std::vector required_path, - uint32_t count, - const std::vector> &expected_result) { - auto resp = prospective_parachain_->answerGetBackableCandidates( - leaf.hash, para_id, count, required_path); - ASSERT_EQ(resp, expected_result); - } - - auto get_hypothetical_frontier( - const CandidateHash &candidate_hash, - const network::CommittedCandidateReceipt &receipt, - const runtime::PersistedValidationData &persisted_validation_data, - const Hash &fragment_tree_relay_parent, - bool backed_in_path_only, - const std::vector &expected_depths) { - HypotheticalCandidate hypothetical_candidate{HypotheticalCandidateComplete{ - .candidate_hash = candidate_hash, - .receipt = receipt, - .persisted_validation_data = persisted_validation_data, - }}; - auto resp = prospective_parachain_->answerHypotheticalFrontierRequest( - std::span{&hypothetical_candidate, 1}, - {{fragment_tree_relay_parent}}, - backed_in_path_only); - std::vector< - std::pair> - expected_frontier; - if (expected_depths.empty()) { - fragment::FragmentTreeMembership s{}; - expected_frontier.emplace_back(hypothetical_candidate, s); - } else { - fragment::FragmentTreeMembership s{ - {fragment_tree_relay_parent, expected_depths}}; - expected_frontier.emplace_back(hypothetical_candidate, s); - }; - ASSERT_EQ(resp.size(), expected_frontier.size()); - for (size_t i = 0; i < resp.size(); ++i) { - const auto &[ll, lr] = resp[i]; - const auto &[rl, rr] = expected_frontier[i]; - - ASSERT_TRUE(ll == rl); - ASSERT_EQ(lr, rr); - } - } - - void back_candidate(const network::CommittedCandidateReceipt &candidate, - const CandidateHash &candidate_hash) { - prospective_parachain_->candidateBacked(candidate.descriptor.para_id, - candidate_hash); - } - - void second_candidate(const network::CommittedCandidateReceipt &candidate) { - prospective_parachain_->candidateSeconded( - candidate.descriptor.para_id, - network::candidateHash(*hasher_, candidate)); - } - - auto get_membership(ParachainId para_id, - const CandidateHash &candidate_hash, - const std::vector>> - &expected_membership_response) { - const auto resp = prospective_parachain_->answerTreeMembershipRequest( - para_id, candidate_hash); - ASSERT_EQ(resp, expected_membership_response); - } - - void deactivate_leaf(const Hash &hash) { - network::ExView update{ - .view = {}, - .new_head = {}, - .lost = {hash}, - }; - std::ignore = - prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {}, - .lost = update.lost, - }); - } - - auto get_pvd( - ParachainId para_id, - const Hash &candidate_relay_parent, - const HeadData &parent_head_data, - const std::optional &expected_pvd) { - auto resp = prospective_parachain_->answerProspectiveValidationDataRequest( - candidate_relay_parent, - hasher_->blake2b_256(parent_head_data), - para_id); - ASSERT_TRUE(resp.has_value()); - ASSERT_EQ(resp.value(), expected_pvd); - } -}; - -TEST_F(ProspectiveParachainsTest, shouldDoNoWorkIfAsyncBackingDisabledForLeaf) { - network::ExView update{ - .view = {}, - .new_head = - BlockHeader{ - .number = 1, - .parent_hash = fromNumber(131), - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }, - .lost = {}, - }; - const auto hash = fromNumber(130); - update.new_head.hash_opt = hash; - - EXPECT_CALL(*parachain_api_, staging_async_backing_params(hash)) - .WillRepeatedly( - Return(outcome::failure(ParachainProcessorImpl::Error::NO_STATE))); - - std::ignore = prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {update.new_head}, - .lost = update.lost, - }); - ASSERT_TRUE(prospective_parachain_->view.active_leaves.empty()); - ASSERT_TRUE(prospective_parachain_->view.candidate_storage.empty()); -} - -TEST_F(ProspectiveParachainsTest, sendCandidatesAndCheckIfFound) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - TestLeaf leaf_b{ - .number = 101, - .hash = fromNumber(131), - .para_data = - { - {1, PerParaData(99, {3, 4, 5})}, - {2, PerParaData(101, {4, 5, 6})}, - }, - }; - TestLeaf leaf_c{ - .number = 102, - .hash = fromNumber(132), - .para_data = - { - {1, PerParaData(102, {5, 6, 7})}, - {2, PerParaData(98, {6, 7, 8})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - activate_leaf(leaf_b, test_state, async_backing_params); - activate_leaf(leaf_c, test_state, async_backing_params); - - const auto &[candidate_a1, pvd_a1] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a1 = network::candidateHash(*hasher_, candidate_a1); - std::vector>> response_a1 = { - {leaf_a.hash, {0}}}; - - const auto &[candidate_a2, pvd_a2] = - make_candidate(leaf_a.hash, - leaf_a.number, - 2, - {2, 3, 4}, - {2}, - test_state.validation_code_hash); - const Hash candidate_hash_a2 = network::candidateHash(*hasher_, candidate_a2); - std::vector>> response_a2 = { - {leaf_a.hash, {0}}}; - - const auto &[candidate_b, pvd_b] = - make_candidate(leaf_b.hash, - leaf_b.number, - 1, - {3, 4, 5}, - {3}, - test_state.validation_code_hash); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - std::vector>> response_b = { - {leaf_b.hash, {0}}}; - - const auto &[candidate_c, pvd_c] = - make_candidate(leaf_c.hash, - leaf_c.number, - 2, - {6, 7, 8}, - {4}, - test_state.validation_code_hash); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - std::vector>> response_c = { - {leaf_c.hash, {0}}}; - - introduce_candidate(candidate_a1, pvd_a1); - introduce_candidate(candidate_a2, pvd_a2); - introduce_candidate(candidate_b, pvd_b); - introduce_candidate(candidate_c, pvd_c); - - get_membership(1, candidate_hash_a1, response_a1); - get_membership(2, candidate_hash_a2, response_a2); - get_membership(1, candidate_hash_b, response_b); - get_membership(2, candidate_hash_c, response_c); - get_membership(2, candidate_hash_a1, {}); - get_membership(1, candidate_hash_a2, {}); - get_membership(2, candidate_hash_b, {}); - get_membership(1, candidate_hash_c, {}); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 3); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); - - { - auto it = prospective_parachain_->view.candidate_storage.find(1); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(2), size_t(2))); - } - { - auto it = prospective_parachain_->view.candidate_storage.find(2); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(2), size_t(2))); - } -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_checkCandidateParentLeavingView) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - TestLeaf leaf_b{ - .number = 101, - .hash = fromNumber(131), - .para_data = - { - {1, PerParaData(99, {3, 4, 5})}, - {2, PerParaData(101, {4, 5, 6})}, - }, - }; - TestLeaf leaf_c{ - .number = 102, - .hash = fromNumber(132), - .para_data = - { - {1, PerParaData(102, {5, 6, 7})}, - {2, PerParaData(98, {6, 7, 8})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - activate_leaf(leaf_b, test_state, async_backing_params); - activate_leaf(leaf_c, test_state, async_backing_params); - - const auto &[candidate_a1, pvd_a1] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a1 = network::candidateHash(*hasher_, candidate_a1); - - const auto &[candidate_a2, pvd_a2] = - make_candidate(leaf_a.hash, - leaf_a.number, - 2, - {2, 3, 4}, - {2}, - test_state.validation_code_hash); - const Hash candidate_hash_a2 = network::candidateHash(*hasher_, candidate_a2); - - const auto &[candidate_b, pvd_b] = - make_candidate(leaf_b.hash, - leaf_b.number, - 1, - {3, 4, 5}, - {3}, - test_state.validation_code_hash); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - std::vector>> response_b = { - {leaf_b.hash, {0}}}; - - const auto &[candidate_c, pvd_c] = - make_candidate(leaf_c.hash, - leaf_c.number, - 2, - {6, 7, 8}, - {4}, - test_state.validation_code_hash); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - std::vector>> response_c = { - {leaf_c.hash, {0}}}; - - introduce_candidate(candidate_a1, pvd_a1); - introduce_candidate(candidate_a2, pvd_a2); - introduce_candidate(candidate_b, pvd_b); - introduce_candidate(candidate_c, pvd_c); - - deactivate_leaf(leaf_a.hash); - - get_membership(1, candidate_hash_a1, {}); - get_membership(2, candidate_hash_a2, {}); - get_membership(1, candidate_hash_b, response_b); - get_membership(2, candidate_hash_c, response_c); - - deactivate_leaf(leaf_b.hash); - - get_membership(1, candidate_hash_a1, {}); - get_membership(2, candidate_hash_a2, {}); - get_membership(1, candidate_hash_b, {}); - get_membership(2, candidate_hash_c, response_c); - - deactivate_leaf(leaf_c.hash); - - get_membership(1, candidate_hash_a1, {}); - get_membership(2, candidate_hash_a2, {}); - get_membership(1, candidate_hash_b, {}); - get_membership(2, candidate_hash_c, {}); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 0); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 0); -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_checkCandidateOnMultipleForks) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - TestLeaf leaf_b{ - .number = 101, - .hash = fromNumber(131), - .para_data = - { - {1, PerParaData(99, {3, 4, 5})}, - {2, PerParaData(101, {4, 5, 6})}, - }, - }; - TestLeaf leaf_c{ - .number = 102, - .hash = fromNumber(132), - .para_data = - { - {1, PerParaData(102, {5, 6, 7})}, - {2, PerParaData(98, {6, 7, 8})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - activate_leaf(leaf_b, test_state, async_backing_params); - activate_leaf(leaf_c, test_state, async_backing_params); - - const auto &[candidate_a, pvd_a] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - std::vector>> response_a = { - {leaf_a.hash, {0}}}; - - const auto &[candidate_b, pvd_b] = - make_candidate(leaf_b.hash, - leaf_b.number, - 1, - {3, 4, 5}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - std::vector>> response_b = { - {leaf_b.hash, {0}}}; - - const auto &[candidate_c, pvd_c] = - make_candidate(leaf_c.hash, - leaf_c.number, - 1, - {5, 6, 7}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - std::vector>> response_c = { - {leaf_c.hash, {0}}}; - - introduce_candidate(candidate_a, pvd_a); - introduce_candidate(candidate_b, pvd_b); - introduce_candidate(candidate_c, pvd_c); - - get_membership(1, candidate_hash_a, response_a); - get_membership(1, candidate_hash_b, response_b); - get_membership(1, candidate_hash_c, response_c); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 3); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); - - { - auto it = prospective_parachain_->view.candidate_storage.find(1); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(3), size_t(3))); - } - { - auto it = prospective_parachain_->view.candidate_storage.find(2); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(0), size_t(0))); - } -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_checkBackableQuerySingleCandidate) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - - const auto &[candidate_a, pvd_a] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - - auto c_p = make_candidate( - leaf_a.hash, leaf_a.number, 1, {1}, {2}, test_state.validation_code_hash); - c_p.first.descriptor.para_head_hash = fromNumber(1000); - const auto &[candidate_b, pvd_b] = c_p; - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - - introduce_candidate(candidate_a, pvd_a); - introduce_candidate(candidate_b, pvd_b); - - get_backable_candidates(leaf_a, 1, {candidate_hash_a}, 1, {}); - get_backable_candidates(leaf_a, 1, {candidate_hash_a}, 0, {}); - get_backable_candidates(leaf_a, 1, {}, 0, {}); - - second_candidate(candidate_a); - second_candidate(candidate_b); - - get_backable_candidates(leaf_a, 1, {candidate_hash_a}, 1, {}); - - back_candidate(candidate_a, candidate_hash_a); - back_candidate(candidate_b, candidate_hash_b); - - // Should not get any backable candidates for the other para. - get_backable_candidates(leaf_a, 2, {}, 1, {}); - get_backable_candidates(leaf_a, 2, {candidate_hash_a}, 1, {}); - - // Get backable candidate. - get_backable_candidates(leaf_a, 1, {}, 1, {{candidate_hash_a, leaf_a.hash}}); - - get_backable_candidates( - leaf_a, 1, {candidate_hash_a}, 1, {{candidate_hash_b, leaf_a.hash}}); - - get_backable_candidates(leaf_a, 1, {candidate_hash_b}, 1, {}); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 1); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); - - { - auto it = prospective_parachain_->view.candidate_storage.find(1); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(2), size_t(2))); - } - { - auto it = prospective_parachain_->view.candidate_storage.find(2); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(0), size_t(0))); - } -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_checkBackableQueryMultipleCandidates_1) { - // Parachain 1 looks like this: - // +---A----+ - // | | - // +----B---+ C - // | | | | - // D E F H - // | | - // G I - // | - // J - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - - const auto &[candidate_a, pvd_a] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - introduce_candidate(candidate_a, pvd_a); - second_candidate(candidate_a); - back_candidate(candidate_a, candidate_hash_a); - - const auto &[candidate_b, candidate_hash_b] = - make_and_back_candidate(test_state, leaf_a, candidate_a, 2); - const auto &[candidate_c, candidate_hash_c] = - make_and_back_candidate(test_state, leaf_a, candidate_a, 3); - const auto &[_candidate_d, candidate_hash_d] = - make_and_back_candidate(test_state, leaf_a, candidate_b, 4); - const auto &[_candidate_e, candidate_hash_e] = - make_and_back_candidate(test_state, leaf_a, candidate_b, 5); - const auto &[candidate_f, candidate_hash_f] = - make_and_back_candidate(test_state, leaf_a, candidate_b, 6); - const auto &[_candidate_g, candidate_hash_g] = - make_and_back_candidate(test_state, leaf_a, candidate_f, 7); - const auto &[candidate_h, candidate_hash_h] = - make_and_back_candidate(test_state, leaf_a, candidate_c, 8); - const auto &[candidate_i, candidate_hash_i] = - make_and_back_candidate(test_state, leaf_a, candidate_h, 9); - const auto &[_candidate_j, candidate_hash_j] = - make_and_back_candidate(test_state, leaf_a, candidate_i, 10); - - get_backable_candidates(leaf_a, 2, {}, 1, {}); - get_backable_candidates(leaf_a, 2, {}, 5, {}); - get_backable_candidates(leaf_a, 2, {candidate_hash_a}, 1, {}); - - // empty required_path - get_backable_candidates(leaf_a, 1, {}, 1, {{candidate_hash_a, leaf_a.hash}}); - get_backable_candidates(leaf_a, - 1, - {}, - 4, - {{candidate_hash_a, leaf_a.hash}, - {candidate_hash_b, leaf_a.hash}, - {candidate_hash_f, leaf_a.hash}, - {candidate_hash_g, leaf_a.hash}}); - - // required path of 1 - get_backable_candidates( - leaf_a, 1, {candidate_hash_a}, 1, {{candidate_hash_b, leaf_a.hash}}); - get_backable_candidates( - leaf_a, - 1, - {candidate_hash_a}, - 2, - {{candidate_hash_b, leaf_a.hash}, {candidate_hash_d, leaf_a.hash}}); - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a}, - 3, - {{candidate_hash_b, leaf_a.hash}, - {candidate_hash_f, leaf_a.hash}, - {candidate_hash_g, leaf_a.hash}}); - - for (uint32_t count = 5; count < 10; ++count) { - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a}, - count, - {{candidate_hash_c, leaf_a.hash}, - {candidate_hash_h, leaf_a.hash}, - {candidate_hash_i, leaf_a.hash}, - {candidate_hash_j, leaf_a.hash}}); - } - - // required path of 2 - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a, candidate_hash_b}, - 1, - {{candidate_hash_d, leaf_a.hash}}); - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a, candidate_hash_c}, - 1, - {{candidate_hash_h, leaf_a.hash}}); - for (uint32_t count = 4; count < 10; ++count) { - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a, candidate_hash_c}, - count, - {{candidate_hash_h, leaf_a.hash}, - {candidate_hash_i, leaf_a.hash}, - {candidate_hash_j, leaf_a.hash}}); - } - - // No more candidates in any chain. - { - std::vector> required_paths = { - {candidate_hash_a, candidate_hash_b, candidate_hash_e}, - {candidate_hash_a, - candidate_hash_c, - candidate_hash_h, - candidate_hash_i, - candidate_hash_j}}; - - for (const auto &path : required_paths) { - for (uint32_t count = 1; count < 4; ++count) { - get_backable_candidates(leaf_a, 1, path, count, {}); - } - } - } - - // Should not get anything at the wrong path. - get_backable_candidates(leaf_a, 1, {candidate_hash_b}, 1, {}); - get_backable_candidates( - leaf_a, 1, {candidate_hash_b, candidate_hash_a}, 3, {}); - get_backable_candidates( - leaf_a, 1, {candidate_hash_a, candidate_hash_b, candidate_hash_c}, 3, {}); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 1); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); - - { - auto it = prospective_parachain_->view.candidate_storage.find(1); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(7), size_t(10))); - } - { - auto it = prospective_parachain_->view.candidate_storage.find(2); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(0), size_t(0))); - } -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_checkBackableQueryMultipleCandidates_2) { - // A tree with multiple roots. - // Parachain 1 looks like this: - // (imaginary root) - // | | - // +----B---+ A - // | | | | - // | | | C - // D E F | - // | H - // G | - // I - // | - // J - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - - const auto &[candidate_b, pvd_b] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {2}, - test_state.validation_code_hash); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - introduce_candidate(candidate_b, pvd_b); - second_candidate(candidate_b); - back_candidate(candidate_b, candidate_hash_b); - - const auto &[candidate_a, pvd_a] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - introduce_candidate(candidate_a, pvd_a); - second_candidate(candidate_a); - back_candidate(candidate_a, candidate_hash_a); - - const auto &[candidate_c, candidate_hash_c] = - make_and_back_candidate(test_state, leaf_a, candidate_a, 3); - const auto &[_candidate_d, candidate_hash_d] = - make_and_back_candidate(test_state, leaf_a, candidate_b, 4); - const auto &[_candidate_e, candidate_hash_e] = - make_and_back_candidate(test_state, leaf_a, candidate_b, 5); - const auto &[candidate_f, candidate_hash_f] = - make_and_back_candidate(test_state, leaf_a, candidate_b, 6); - const auto &[_candidate_g, candidate_hash_g] = - make_and_back_candidate(test_state, leaf_a, candidate_f, 7); - const auto &[candidate_h, candidate_hash_h] = - make_and_back_candidate(test_state, leaf_a, candidate_c, 8); - const auto &[candidate_i, candidate_hash_i] = - make_and_back_candidate(test_state, leaf_a, candidate_h, 9); - const auto &[_candidate_j, candidate_hash_j] = - make_and_back_candidate(test_state, leaf_a, candidate_i, 10); - - // Should not get any backable candidates for the other para. - get_backable_candidates(leaf_a, 2, {}, 1, {}); - get_backable_candidates(leaf_a, 2, {}, 5, {}); - get_backable_candidates(leaf_a, 2, {candidate_hash_a}, 1, {}); - - // empty required_path - get_backable_candidates(leaf_a, 1, {}, 1, {{candidate_hash_b, leaf_a.hash}}); - get_backable_candidates( - leaf_a, - 1, - {}, - 2, - {{candidate_hash_b, leaf_a.hash}, {candidate_hash_d, leaf_a.hash}}); - get_backable_candidates(leaf_a, - 1, - {}, - 4, - {{candidate_hash_a, leaf_a.hash}, - {candidate_hash_c, leaf_a.hash}, - {candidate_hash_h, leaf_a.hash}, - {candidate_hash_i, leaf_a.hash}}); - - // required path of 1 - get_backable_candidates( - leaf_a, 1, {candidate_hash_a}, 1, {{candidate_hash_c, leaf_a.hash}}); - get_backable_candidates( - leaf_a, 1, {candidate_hash_b}, 1, {{candidate_hash_d, leaf_a.hash}}); - get_backable_candidates( - leaf_a, - 1, - {candidate_hash_a}, - 2, - {{candidate_hash_c, leaf_a.hash}, {candidate_hash_h, leaf_a.hash}}); - - for (uint32_t count = 2; count < 10; ++count) { - get_backable_candidates( - leaf_a, - 1, - {candidate_hash_b}, - count, - {{candidate_hash_f, leaf_a.hash}, {candidate_hash_g, leaf_a.hash}}); - } - - // required path of 2 - get_backable_candidates(leaf_a, - 1, - {candidate_hash_b, candidate_hash_f}, - 1, - {{candidate_hash_g, leaf_a.hash}}); - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a, candidate_hash_c}, - 1, - {{candidate_hash_h, leaf_a.hash}}); - for (uint32_t count = 4; count < 10; ++count) { - get_backable_candidates(leaf_a, - 1, - {candidate_hash_a, candidate_hash_c}, - count, - {{candidate_hash_h, leaf_a.hash}, - {candidate_hash_i, leaf_a.hash}, - {candidate_hash_j, leaf_a.hash}}); - } - - // No more candidates in any chain. - { - std::vector> required_paths = { - {candidate_hash_b, candidate_hash_f, candidate_hash_g}, - {candidate_hash_b, candidate_hash_e}, - {candidate_hash_b, candidate_hash_d}, - { - candidate_hash_a, - candidate_hash_c, - candidate_hash_h, - candidate_hash_i, - candidate_hash_j, - }}; - - for (const auto &path : required_paths) { - for (uint32_t count = 1; count < 4; ++count) { - get_backable_candidates(leaf_a, 1, path, count, {}); - } - } - } - - // Should not get anything at the wrong path. - get_backable_candidates(leaf_a, 1, {candidate_hash_d}, 1, {}); - get_backable_candidates( - leaf_a, 1, {candidate_hash_b, candidate_hash_a}, 3, {}); - get_backable_candidates( - leaf_a, 1, {candidate_hash_a, candidate_hash_c, candidate_hash_d}, 3, {}); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 1); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); - - { - auto it = prospective_parachain_->view.candidate_storage.find(1); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(7), size_t(10))); - } - { - auto it = prospective_parachain_->view.candidate_storage.find(2); - ASSERT_TRUE(it != prospective_parachain_->view.candidate_storage.end()); - ASSERT_EQ(it->second.len(), std::make_pair(size_t(0), size_t(0))); - } -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_checkHypotheticalFrontierQuery) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - - const auto &[candidate_a, pvd_a] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - - const auto &[candidate_b, pvd_b] = make_candidate( - leaf_a.hash, leaf_a.number, 1, {1}, {2}, test_state.validation_code_hash); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - - const auto &[candidate_c, pvd_c] = make_candidate( - leaf_a.hash, leaf_a.number, 1, {2}, {3}, test_state.validation_code_hash); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - - get_hypothetical_frontier( - candidate_hash_a, candidate_a, pvd_a, leaf_a.hash, false, {0}); - get_hypothetical_frontier( - candidate_hash_a, candidate_a, pvd_a, leaf_a.hash, true, {0}); - - introduce_candidate(candidate_a, pvd_a); - - get_hypothetical_frontier( - candidate_hash_a, candidate_a, pvd_a, leaf_a.hash, false, {0}); - - get_hypothetical_frontier( - candidate_hash_b, candidate_b, pvd_b, leaf_a.hash, false, {1}); - - introduce_candidate(candidate_b, pvd_b); - - get_hypothetical_frontier( - candidate_hash_b, candidate_b, pvd_b, leaf_a.hash, false, {1}); - - get_hypothetical_frontier( - candidate_hash_c, candidate_c, pvd_c, leaf_a.hash, false, {2}); - get_hypothetical_frontier( - candidate_hash_c, candidate_c, pvd_c, leaf_a.hash, true, {}); - - introduce_candidate(candidate_c, pvd_c); - - get_hypothetical_frontier( - candidate_hash_c, candidate_c, pvd_c, leaf_a.hash, false, {2}); - get_hypothetical_frontier( - candidate_hash_c, candidate_c, pvd_c, leaf_a.hash, true, {}); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 1); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_checkPvdQuery) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - - const auto &[candidate_a, pvd_a] = - make_candidate(leaf_a.hash, - leaf_a.number, - 1, - {1, 2, 3}, - {1}, - test_state.validation_code_hash); - - const auto &[candidate_b, pvd_b] = make_candidate( - leaf_a.hash, leaf_a.number, 1, {1}, {2}, test_state.validation_code_hash); - - const auto &[candidate_c, pvd_c] = make_candidate( - leaf_a.hash, leaf_a.number, 1, {2}, {3}, test_state.validation_code_hash); - - get_pvd(1, leaf_a.hash, {1, 2, 3}, pvd_a); - - introduce_candidate(candidate_a, pvd_a); - back_candidate(candidate_a, network::candidateHash(*hasher_, candidate_a)); - - get_pvd(1, leaf_a.hash, {1, 2, 3}, pvd_a); - - get_pvd(1, leaf_a.hash, {1}, pvd_b); - - introduce_candidate(candidate_b, pvd_b); - - get_pvd(1, leaf_a.hash, {1}, pvd_b); - - get_pvd(1, leaf_a.hash, {2}, pvd_c); - - introduce_candidate(candidate_c, pvd_c); - - get_pvd(1, leaf_a.hash, {2}, pvd_c); - - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 1); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 2); -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_persistsPendingAvailabilityCandidate) { - TestState test_state(hasher_); - ParachainId para_id{1}; - filterACByPara(test_state, para_id); - - const HeadData para_head{1, 2, 3}; - const auto candidate_relay_parent = fromNumber(5); - const uint32_t candidate_relay_parent_number = 97; - - TestLeaf leaf_a{ - .number = candidate_relay_parent_number + ALLOWED_ANCESTRY_LEN, - .hash = fromNumber(2), - .para_data = - { - {para_id, PerParaData(candidate_relay_parent_number, para_head)}, - }, - }; - - const auto leaf_b_hash = fromNumber(1); - const BlockNumber leaf_b_number = leaf_a.number + 1; - - const fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - activate_leaf(leaf_a, test_state, async_backing_params); - - const auto &[candidate_a, pvd_a] = - make_candidate(candidate_relay_parent, - candidate_relay_parent_number, - para_id, - para_head, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - - const auto &[candidate_b, pvd_b] = - make_candidate(leaf_b_hash, - leaf_b_number, - para_id, - {1}, - {2}, - test_state.validation_code_hash); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - - introduce_candidate(candidate_a, pvd_a); - second_candidate(candidate_a); - back_candidate(candidate_a, candidate_hash_a); - - fragment::CandidatePendingAvailability candidate_a_pending_av{ - .candidate_hash = candidate_hash_a, - .descriptor = candidate_a.descriptor, - .commitments = candidate_a.commitments, - .relay_parent_number = candidate_relay_parent_number, - .max_pov_size = MAX_POV_SIZE, - }; - - TestLeaf leaf_b{ - .number = leaf_b_number, - .hash = leaf_b_hash, - .para_data = - { - {1, - PerParaData(candidate_relay_parent_number + 1, - para_head, - {candidate_a_pending_av})}, - }, - }; - - activate_leaf(leaf_b, test_state, async_backing_params); - - introduce_candidate(candidate_b, pvd_b); - second_candidate(candidate_b); - back_candidate(candidate_b, candidate_hash_b); - - get_backable_candidates(leaf_b, - para_id, - {candidate_hash_a}, - 1, - {{candidate_hash_b, leaf_b_hash}}); -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_backwardsCompatible) { - TestState test_state(hasher_); - ParachainId para_id{1}; - filterACByPara(test_state, para_id); - - const HeadData para_head{1, 2, 3}; - const auto leaf_b_hash = fromNumber(15); - const Hash candidate_relay_parent = get_parent_hash(leaf_b_hash); - const BlockNumber candidate_relay_parent_number = 100; - - TestLeaf leaf_a{ - .number = candidate_relay_parent_number, - .hash = candidate_relay_parent, - .para_data = - { - {para_id, PerParaData(candidate_relay_parent_number, para_head)}, - }, - }; - - activate_leaf(leaf_a, - test_state, - fragment::AsyncBackingParams{ - .max_candidate_depth = 0, - .allowed_ancestry_len = 0, - }); - - const auto &[candidate_a, pvd_a] = - make_candidate(candidate_relay_parent, - candidate_relay_parent_number, - para_id, - para_head, - {1}, - test_state.validation_code_hash); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - - introduce_candidate(candidate_a, pvd_a); - second_candidate(candidate_a); - back_candidate(candidate_a, candidate_hash_a); - - get_backable_candidates( - leaf_a, para_id, {}, 1, {{candidate_hash_a, candidate_relay_parent}}); - - TestLeaf leaf_b{ - .number = candidate_relay_parent_number + 1, - .hash = leaf_b_hash, - .para_data = - { - {para_id, - PerParaData(candidate_relay_parent_number + 1, para_head)}, - }, - }; - - activate_leaf(leaf_b, - test_state, - fragment::AsyncBackingParams{ - .max_candidate_depth = 0, - .allowed_ancestry_len = 0, - }); - - get_backable_candidates(leaf_b, para_id, {}, 1, {}); -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_usesAncestryOnlyWithinSession) { - std::vector ancestry_hashes{ - fromNumber(4), fromNumber(3), fromNumber(2), fromNumber(1)}; - const BlockNumber number = 5; - const Hash hash = fromNumber(5); - const uint32_t ancestry_len = 3; - const uint32_t session = 2; - - const Hash session_change_hash = fromNumber(3); - - BlockHeader header{ - .number = number, - .parent_hash = get_parent_hash(hash), - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - network::ExView update{ - .view = {}, - .new_head = header, - .lost = {}, - }; - update.new_head.hash_opt = hash; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 0, - .allowed_ancestry_len = ancestry_len, - }; - - std::vector empty{}; - - EXPECT_CALL(*parachain_api_, staging_async_backing_params(hash)) - .WillRepeatedly(Return(outcome::success(async_backing_params))); - - EXPECT_CALL(*parachain_api_, availability_cores(hash)) - .WillRepeatedly(Return(outcome::success(empty))); - - EXPECT_CALL(*block_tree_, getBlockHeader(hash)) - .WillRepeatedly(Return(header)); - - EXPECT_CALL(*block_tree_, getDescendingChainToBlock(hash, ancestry_len + 1)) - .WillRepeatedly(Return(ancestry_hashes)); - - EXPECT_CALL(*parachain_api_, session_index_for_child(hash)) - .WillRepeatedly(Return(session)); - - for (size_t i = 0; i < ancestry_hashes.size(); ++i) { - const Hash h = ancestry_hashes[i]; - const BlockNumber n = number - (i + 1); - - BlockHeader r{ - .number = n, - .parent_hash = get_parent_hash(h), - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - EXPECT_CALL(*block_tree_, getBlockHeader(h)).WillRepeatedly(Return(r)); - - if (h == session_change_hash) { - EXPECT_CALL(*parachain_api_, session_index_for_child(h)) - .WillRepeatedly(Return(session - 1)); - break; - } else { - EXPECT_CALL(*parachain_api_, session_index_for_child(h)) - .WillRepeatedly(Return(session)); - } - } - - std::ignore = prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {update.new_head}, - .lost = update.lost, - }); -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_correctlyUpdatesLeaves) { - TestState test_state(hasher_); - TestLeaf leaf_a{ - .number = 100, - .hash = fromNumber(130), - .para_data = - { - {1, PerParaData(97, {1, 2, 3})}, - {2, PerParaData(100, {2, 3, 4})}, - }, - }; - TestLeaf leaf_b{ - .number = 101, - .hash = fromNumber(131), - .para_data = - { - {1, PerParaData(99, {3, 4, 5})}, - {2, PerParaData(101, {4, 5, 6})}, - }, - }; - TestLeaf leaf_c{ - .number = 102, - .hash = fromNumber(132), - .para_data = - { - {1, PerParaData(102, {5, 6, 7})}, - {2, PerParaData(98, {6, 7, 8})}, - }, - }; - - fragment::AsyncBackingParams async_backing_params{ - .max_candidate_depth = 4, - .allowed_ancestry_len = ALLOWED_ANCESTRY_LEN, - }; - - activate_leaf(leaf_a, test_state, async_backing_params); - activate_leaf(leaf_b, test_state, async_backing_params); - activate_leaf(leaf_b, test_state, async_backing_params); - - std::ignore = prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {}, - .lost = {}, - }); - - { - BlockHeader header{ - .number = leaf_c.number, - .parent_hash = {}, - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - network::ExView update{ - .view = {}, - .new_head = header, - .lost = {leaf_b.hash}, - }; - update.new_head.hash_opt = leaf_c.hash; - - handle_leaf_activation_2(update, leaf_c, test_state, async_backing_params); - // prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - // .new_head = update.new_head, - // .lost = update.lost, - // }); - } - - { - network::ExView update2{ - .view = {}, - .new_head = {}, - .lost = {leaf_a.hash, leaf_c.hash}, - }; - // handle_leaf_activation_2(update2, leaf_c, test_state, - // async_backing_params); - std::ignore = - prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {}, - .lost = update2.lost, - }); - } - - { - BlockHeader header{ - .number = leaf_a.number, - .parent_hash = {}, - .state_root = {}, - .extrinsics_root = {}, - .digest = {}, - .hash_opt = {}, - }; - network::ExView update{ - .view = {}, - .new_head = header, - .lost = {leaf_a.hash}, - }; - update.new_head.hash_opt = leaf_a.hash; - handle_leaf_activation_2(update, leaf_a, test_state, async_backing_params); - // prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - // .new_head = update.new_head, - // .lost = update.lost, - // }); - } - - // handle_leaf_activation(leaf_a, test_state, async_backing_params); - - { - network::ExView update2{ - .view = {}, - .new_head = {}, - .lost = {leaf_a.hash, leaf_b.hash, leaf_c.hash}, - }; - std::ignore = - prospective_parachain_->onActiveLeavesUpdate(network::ExViewRef{ - .new_head = {}, - .lost = update2.lost, - }); - } - ASSERT_EQ(prospective_parachain_->view.active_leaves.size(), 0); - ASSERT_EQ(prospective_parachain_->view.candidate_storage.size(), 0); -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_scopeRejectsAncestorsThatSkipBlocks) { - ParachainId para_id{5}; - fragment::RelayChainBlockInfo relay_parent{ - .hash = hashFromStrData("10"), - .number = 10, - .storage_root = hashFromStrData("69"), - }; - - std::vector ancestors = { - fragment::RelayChainBlockInfo{ - .hash = hashFromStrData("8"), - .number = 8, - .storage_root = hashFromStrData("69"), - }}; - - const size_t max_depth = 2ull; - fragment::Constraints base_constraints( - make_constraints(8, {8, 9}, {1, 2, 3})); - ASSERT_EQ( - fragment::Scope::withAncestors( - para_id, relay_parent, base_constraints, {}, max_depth, ancestors) - .error(), - fragment::Scope::Error::UNEXPECTED_ANCESTOR); -} - -TEST_F(ProspectiveParachainsTest, - FragmentTree_scopeRejectsAncestorFor_0_Block) { - ParachainId para_id{5}; - fragment::RelayChainBlockInfo relay_parent{ - .hash = hashFromStrData("0"), - .number = 0, - .storage_root = hashFromStrData("69"), - }; - - std::vector ancestors = { - fragment::RelayChainBlockInfo{ - .hash = hashFromStrData("99"), - .number = 99999, - .storage_root = hashFromStrData("69"), - }}; - - const size_t max_depth = 2ull; - fragment::Constraints base_constraints(make_constraints(0, {}, {1, 2, 3})); - ASSERT_EQ( - fragment::Scope::withAncestors( - para_id, relay_parent, base_constraints, {}, max_depth, ancestors) - .error(), - fragment::Scope::Error::UNEXPECTED_ANCESTOR); -} - -TEST_F(ProspectiveParachainsTest, FragmentTree_scopeOnlyTakesAncestorsUpToMin) { - ParachainId para_id{5}; - fragment::RelayChainBlockInfo relay_parent{ - .hash = hashFromStrData("0"), - .number = 5, - .storage_root = hashFromStrData("69"), - }; - - std::vector ancestors = { - fragment::RelayChainBlockInfo{ - .hash = hashFromStrData("4"), - .number = 4, - .storage_root = hashFromStrData("69"), - }, - fragment::RelayChainBlockInfo{ - .hash = hashFromStrData("3"), - .number = 3, - .storage_root = hashFromStrData("69"), - }, - fragment::RelayChainBlockInfo{ - .hash = hashFromStrData("2"), - .number = 2, - .storage_root = hashFromStrData("69"), - }}; - - const size_t max_depth = 2ull; - fragment::Constraints base_constraints(make_constraints(3, {2}, {1, 2, 3})); - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent, base_constraints, {}, max_depth, ancestors) - .value(); - - ASSERT_EQ(scope.ancestors.size(), 2); - ASSERT_EQ(scope.ancestors_by_hash.size(), 2); -} - -TEST_F(ProspectiveParachainsTest, Storage_AddCandidate) { - fragment::CandidateStorage storage{}; - Hash relay_parent(hashFromStrData("69")); - - const auto &[pvd, candidate] = - make_committed_candidate(5, relay_parent, 8, {4, 5, 6}, {1, 2, 3}, 7); - - const Hash candidate_hash = network::candidateHash(*hasher_, candidate); - const Hash parent_head_hash = hasher_->blake2b_256(pvd.get().parent_head); - - ASSERT_TRUE( - storage.addCandidate(candidate_hash, candidate, pvd.get(), hasher_) - .has_value()); - ASSERT_TRUE(storage.contains(candidate_hash)); - - size_t counter = 0ull; - storage.iterParaChildren(parent_head_hash, [&](const auto &) { ++counter; }); - ASSERT_EQ(1, counter); - - auto h = storage.relayParentByCandidateHash(candidate_hash); - ASSERT_TRUE(h); - ASSERT_EQ(*h, relay_parent); -} - -TEST_F(ProspectiveParachainsTest, Storage_PopulateWorksRecursively) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - - Hash relay_parent_a(hashFromStrData("1")); - Hash relay_parent_b(hashFromStrData("2")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_b, 1, {0x0b}, {0x0c}, 1); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - std::vector ancestors = { - fragment::RelayChainBlockInfo{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }}; - - fragment::RelayChainBlockInfo relay_parent_b_info{ - .hash = relay_parent_b, - .number = pvd_b.get().relay_parent_number, - .storage_root = pvd_b.get().relay_parent_storage_root, - }; - - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_b_info, base_constraints, {}, 4ull, ancestors) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 2); - - ASSERT_TRUE(std::find(candidates.begin(), candidates.end(), candidate_a_hash) - != candidates.end()); - ASSERT_TRUE(std::find(candidates.begin(), candidates.end(), candidate_b_hash) - != candidates.end()); - - ASSERT_EQ(tree.nodes.size(), 2); - ASSERT_TRUE(kagome::is_type(tree.nodes[0].parent)); - ASSERT_EQ(tree.nodes[0].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[0].depth, 0); - - auto pa = kagome::if_type(tree.nodes[1].parent); - ASSERT_TRUE(pa && pa->get() == 0); - ASSERT_EQ(tree.nodes[1].candidate_hash, candidate_b_hash); - ASSERT_EQ(tree.nodes[1].depth, 1); -} - -TEST_F(ProspectiveParachainsTest, Storage_childrenOfRootAreContiguous) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - - Hash relay_parent_a(hashFromStrData("1")); - Hash relay_parent_b(hashFromStrData("2")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_b, 1, {0x0b}, {0x0c}, 1); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - const auto &[pvd_a2, candidate_a2] = make_committed_candidate( - para_id, relay_parent_a, 0, {0x0a}, {0x0b, 1}, 0); - const Hash candidate_a2_hash = network::candidateHash(*hasher_, candidate_a2); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - std::vector ancestors = { - fragment::RelayChainBlockInfo{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }}; - - fragment::RelayChainBlockInfo relay_parent_b_info{ - .hash = relay_parent_b, - .number = pvd_b.get().relay_parent_number, - .storage_root = pvd_b.get().relay_parent_storage_root, - }; - - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_b_info, base_constraints, {}, 4ull, ancestors) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - ASSERT_TRUE( - storage - .addCandidate(candidate_a2_hash, candidate_a2, pvd_a2.get(), hasher_) - .has_value()); - - tree.addAndPopulate(candidate_a2_hash, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 3); - ASSERT_TRUE(kagome::is_type(tree.nodes[0].parent)); - ASSERT_TRUE(kagome::is_type(tree.nodes[1].parent)); - - auto pa = kagome::if_type(tree.nodes[2].parent); - ASSERT_TRUE(pa && pa->get() == 0); -} - -TEST_F(ProspectiveParachainsTest, Storage_addCandidateChildOfRoot) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0c}, 0); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - - auto scope = fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, 4ull, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - - tree.addAndPopulate(candidate_b_hash, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 2); - ASSERT_TRUE(kagome::is_type(tree.nodes[0].parent)); - ASSERT_TRUE(kagome::is_type(tree.nodes[1].parent)); -} - -TEST_F(ProspectiveParachainsTest, Storage_addCandidateChildOfNonRoot) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0b}, {0x0c}, 0); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - - auto scope = fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, 4ull, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - - tree.addAndPopulate(candidate_b_hash, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 2); - ASSERT_TRUE(kagome::is_type(tree.nodes[0].parent)); - auto pa = kagome::if_type(tree.nodes[1].parent); - ASSERT_TRUE(pa && pa->get() == 0); -} - -TEST_F(ProspectiveParachainsTest, Storage_gracefulCycleOf_0) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0a}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - const size_t max_depth = 4ull; - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, max_depth, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 1); - ASSERT_EQ(tree.nodes.size(), max_depth + 1); - - ASSERT_TRUE(kagome::is_type(tree.nodes[0].parent)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[1].parent, 0)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[2].parent, 1)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[3].parent, 2)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[4].parent, 3)); - - ASSERT_EQ(tree.nodes[0].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[1].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[2].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[3].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[4].candidate_hash, candidate_a_hash); -} - -TEST_F(ProspectiveParachainsTest, Storage_gracefulCycleOf_1) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0b}, {0x0a}, 0); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - const size_t max_depth = 4ull; - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, max_depth, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 2); - ASSERT_EQ(tree.nodes.size(), max_depth + 1); - - ASSERT_TRUE(kagome::is_type(tree.nodes[0].parent)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[1].parent, 0)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[2].parent, 1)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[3].parent, 2)); - ASSERT_TRUE(getNodePointerStorage(tree.nodes[4].parent, 3)); - - ASSERT_EQ(tree.nodes[0].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[1].candidate_hash, candidate_b_hash); - ASSERT_EQ(tree.nodes[2].candidate_hash, candidate_a_hash); - ASSERT_EQ(tree.nodes[3].candidate_hash, candidate_b_hash); - ASSERT_EQ(tree.nodes[4].candidate_hash, candidate_a_hash); -} - -TEST_F(ProspectiveParachainsTest, Storage_hypotheticalDepthsKnownAndUnknown) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0b}, {0x0a}, 0); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - const size_t max_depth = 4ull; - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, max_depth, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 2); - ASSERT_EQ(tree.nodes.size(), max_depth + 1); - - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_a_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0a}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - false), - {0, 2, 4})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_b_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0b}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - false), - {1, 3})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(hashFromStrData("21"), - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0a}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - false), - {0, 2, 4})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(hashFromStrData("22"), - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0b}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - false), - {1, 3})); -} - -TEST_F(ProspectiveParachainsTest, - Storage_hypotheticalDepthsStricterOnComplete) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(fromNumber(1)); - - const auto &[pvd_a, candidate_a] = make_committed_candidate( - para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 1000); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - const size_t max_depth = 4ull; - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, max_depth, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_a_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0a}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - false), - {0})); - const auto tmp = - tree.hypotheticalDepths(candidate_a_hash, - HypotheticalCandidateComplete{ - .candidate_hash = {}, - .receipt = candidate_a, - .persisted_validation_data = pvd_a.get(), - }, - storage, - false); - ASSERT_TRUE(tmp.empty()); -} - -TEST_F(ProspectiveParachainsTest, Storage_hypotheticalDepthsBackedInPath) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0b}, {0x0c}, 0); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - const auto &[pvd_c, candidate_c] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0b}, {0x0d}, 0); - const Hash candidate_c_hash = network::candidateHash(*hasher_, candidate_c); - - fragment::Constraints base_constraints(make_constraints(0, {0}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - - const size_t max_depth = 4ull; - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - ASSERT_TRUE( - storage.addCandidate(candidate_c_hash, candidate_c, pvd_c.get(), hasher_) - .has_value()); - - storage.markBacked(candidate_a_hash); - storage.markBacked(candidate_b_hash); - - auto scope = - fragment::Scope::withAncestors( - para_id, relay_parent_a_info, base_constraints, {}, max_depth, {}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 3); - ASSERT_EQ(tree.nodes.size(), 3); - - Hash candidate_d_hash(hashFromStrData("AA")); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_d_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0a}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - true), - {0})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_d_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0c}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - true), - {2})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_d_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0d}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - true), - {})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_d_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0d}), - .candidate_relay_parent = relay_parent_a, - }, - storage, - false), - {2})); -} - -TEST_F(ProspectiveParachainsTest, Storage_pendingAvailabilityInScope) { - fragment::CandidateStorage storage{}; - ParachainId para_id{5}; - Hash relay_parent_a(hashFromStrData("1")); - Hash relay_parent_b(hashFromStrData("2")); - Hash relay_parent_c(hashFromStrData("3")); - - const auto &[pvd_a, candidate_a] = - make_committed_candidate(para_id, relay_parent_a, 0, {0x0a}, {0x0b}, 0); - const Hash candidate_a_hash = network::candidateHash(*hasher_, candidate_a); - - const auto &[pvd_b, candidate_b] = - make_committed_candidate(para_id, relay_parent_b, 1, {0x0b}, {0x0c}, 1); - const Hash candidate_b_hash = network::candidateHash(*hasher_, candidate_b); - - fragment::Constraints base_constraints(make_constraints(1, {}, {0x0a})); - fragment::RelayChainBlockInfo relay_parent_a_info{ - .hash = relay_parent_a, - .number = pvd_a.get().relay_parent_number, - .storage_root = pvd_a.get().relay_parent_storage_root, - }; - std::vector pending_availability = { - fragment::PendingAvailability{ - .candidate_hash = candidate_a_hash, - .relay_parent = relay_parent_a_info, - }}; - fragment::RelayChainBlockInfo relay_parent_b_info{ - .hash = relay_parent_b, - .number = pvd_b.get().relay_parent_number, - .storage_root = pvd_b.get().relay_parent_storage_root, - }; - fragment::RelayChainBlockInfo relay_parent_c_info{ - .hash = relay_parent_c, - .number = pvd_b.get().relay_parent_number + 1, - .storage_root = {}, - }; - - const size_t max_depth = 4ull; - ASSERT_TRUE( - storage.addCandidate(candidate_a_hash, candidate_a, pvd_a.get(), hasher_) - .has_value()); - ASSERT_TRUE( - storage.addCandidate(candidate_b_hash, candidate_b, pvd_b.get(), hasher_) - .has_value()); - - storage.markBacked(candidate_a_hash); - auto scope = fragment::Scope::withAncestors(para_id, - relay_parent_c_info, - base_constraints, - pending_availability, - max_depth, - {relay_parent_b_info}) - .value(); - - fragment::FragmentTree tree = - fragment::FragmentTree::populate(hasher_, scope, storage); - std::vector candidates = tree.getCandidates(); - - ASSERT_EQ(candidates.size(), 2); - ASSERT_EQ(tree.nodes.size(), 2); - - Hash candidate_d_hash(hashFromStrData("AA")); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_d_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0b}), - .candidate_relay_parent = relay_parent_c, - }, - storage, - false), - {1})); - ASSERT_TRUE(compareVectors( - tree.hypotheticalDepths(candidate_d_hash, - HypotheticalCandidateIncomplete{ - .candidate_hash = {}, - .candidate_para = 0, - .parent_head_data_hash = hasher_->blake2b_256( - std::vector{0x0c}), - .candidate_relay_parent = relay_parent_b, - }, - storage, - false), - {2})); -} - -TEST_F(ProspectiveParachainsTest, - Candidates_insertingUnconfirmedRejectsOnIncompatibleClaims) { - HeadData relay_head_data_a{{1, 2, 3}}; - HeadData relay_head_data_b{{4, 5, 6}}; - - const Hash relay_hash_a = hasher_->blake2b_256(relay_head_data_a); - const Hash relay_hash_b = hasher_->blake2b_256(relay_head_data_b); - - ParachainId para_id_a{1}; - ParachainId para_id_b{2}; - - const auto &[candidate_a, pvd_a] = make_candidate(relay_hash_a, - 1, - para_id_a, - relay_head_data_a, - {1}, - hashFromStrData("1000")); - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - const libp2p::peer::PeerId peer{"peer1"_peerid}; - - GroupIndex group_index_a = 100; - GroupIndex group_index_b = 200; - - Candidates candidates; - candidates.confirm_candidate( - candidate_hash_a, candidate_a, pvd_a, group_index_a, hasher_); - - ASSERT_FALSE( - candidates.insert_unconfirmed(peer, - candidate_hash_a, - relay_hash_b, - group_index_a, - std::make_pair(relay_hash_a, para_id_a))); - ASSERT_FALSE( - candidates.insert_unconfirmed(peer, - candidate_hash_a, - relay_hash_a, - group_index_b, - std::make_pair(relay_hash_a, para_id_a))); - ASSERT_FALSE( - candidates.insert_unconfirmed(peer, - candidate_hash_a, - relay_hash_a, - group_index_a, - std::make_pair(relay_hash_b, para_id_a))); - ASSERT_FALSE( - candidates.insert_unconfirmed(peer, - candidate_hash_a, - relay_hash_a, - group_index_a, - std::make_pair(relay_hash_a, para_id_b))); - ASSERT_TRUE( - candidates.insert_unconfirmed(peer, - candidate_hash_a, - relay_hash_a, - group_index_a, - std::make_pair(relay_hash_a, para_id_a))); -} - -TEST_F(ProspectiveParachainsTest, - Candidates_confirmingMaintainsParentHashIndex) { - HeadData relay_head_data{{1, 2, 3}}; - const Hash relay_hash = hasher_->blake2b_256(relay_head_data); - - HeadData candidate_head_data_a{1}; - HeadData candidate_head_data_b{2}; - HeadData candidate_head_data_c{3}; - HeadData candidate_head_data_d{4}; - - const Hash candidate_head_data_hash_a = - hasher_->blake2b_256(candidate_head_data_a); - const Hash candidate_head_data_hash_b = - hasher_->blake2b_256(candidate_head_data_b); - const Hash candidate_head_data_hash_c = - hasher_->blake2b_256(candidate_head_data_c); - - const auto &[candidate_a, pvd_a] = make_candidate(relay_hash, - 1, - 1, - relay_head_data, - candidate_head_data_a, - hashFromStrData("1000")); - const auto &[candidate_b, pvd_b] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_a, - candidate_head_data_b, - hashFromStrData("2000")); - const auto &[candidate_c, pvd_c] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_b, - candidate_head_data_c, - hashFromStrData("3000")); - const auto &[candidate_d, pvd_d] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_c, - candidate_head_data_d, - hashFromStrData("4000")); - - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - const Hash candidate_hash_d = network::candidateHash(*hasher_, candidate_d); - - const libp2p::peer::PeerId peer{"peer1"_peerid}; - GroupIndex group_index = 100; - - Candidates candidates; - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, candidate_hash_a, relay_hash, group_index, std::nullopt)); - ASSERT_TRUE(candidates.by_parent.empty()); - - ASSERT_TRUE(candidates.insert_unconfirmed(peer, - candidate_hash_a, - relay_hash, - group_index, - std::make_pair(relay_hash, 1))); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, {{relay_hash, {{1, {candidate_hash_a}}}}})); - - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, - candidate_hash_b, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, {{1, {candidate_hash_b}}}}})); - - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, - candidate_hash_c, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE( - compareMapsOfCandidates(candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c}}}}})); - - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, - candidate_hash_d, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c, candidate_hash_d}}}}})); - - candidates.confirm_candidate( - candidate_hash_a, candidate_a, pvd_a, group_index, hasher_); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c, candidate_hash_d}}}}})); - - candidates.confirm_candidate( - candidate_hash_b, candidate_b, pvd_b, group_index, hasher_); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c, candidate_hash_d}}}}})); - - candidates.confirm_candidate( - candidate_hash_d, candidate_d, pvd_d, group_index, hasher_); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c}}}}, - {candidate_head_data_hash_c, {{1, {candidate_hash_d}}}}})); - - const auto &[new_candidate_c, new_pvd_c] = - make_candidate(relay_hash, - 1, - 2, - candidate_head_data_b, - candidate_head_data_c, - hashFromStrData("3000")); - candidates.confirm_candidate( - candidate_hash_c, new_candidate_c, new_pvd_c, group_index, hasher_); - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, {{1, {candidate_hash_b}}}}, - {candidate_head_data_hash_b, {{2, {candidate_hash_c}}}}, - {candidate_head_data_hash_c, {{1, {candidate_hash_d}}}}})); -} - -TEST_F(ProspectiveParachainsTest, Candidates_testReturnedPostConfirmation) { - HeadData relay_head_data{{1, 2, 3}}; - const Hash relay_hash = hasher_->blake2b_256(relay_head_data); - - HeadData candidate_head_data_a{1}; - HeadData candidate_head_data_b{2}; - HeadData candidate_head_data_c{3}; - HeadData candidate_head_data_d{4}; - - const Hash candidate_head_data_hash_a = - hasher_->blake2b_256(candidate_head_data_a); - const Hash candidate_head_data_hash_b = - hasher_->blake2b_256(candidate_head_data_b); - - const auto &[candidate_a, pvd_a] = make_candidate(relay_hash, - 1, - 1, - relay_head_data, - candidate_head_data_a, - hashFromStrData("1000")); - const auto &[candidate_b, pvd_b] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_a, - candidate_head_data_b, - hashFromStrData("2000")); - const auto &[candidate_c, _] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_a, - candidate_head_data_c, - hashFromStrData("3000")); - const auto &[candidate_d, pvd_d] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_b, - candidate_head_data_d, - hashFromStrData("4000")); - - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - const Hash candidate_hash_d = network::candidateHash(*hasher_, candidate_d); - - const libp2p::peer::PeerId peer_a{"peer1"_peerid}; - const libp2p::peer::PeerId peer_b{"peer2"_peerid}; - const libp2p::peer::PeerId peer_c{"peer3"_peerid}; - const libp2p::peer::PeerId peer_d{"peer4"_peerid}; - - GroupIndex group_index = 100; - Candidates candidates; - - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_a, candidate_hash_a, relay_hash, group_index, std::nullopt)); - ASSERT_TRUE(candidates.insert_unconfirmed(peer_a, - candidate_hash_a, - relay_hash, - group_index, - std::make_pair(relay_hash, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_a, - candidate_hash_b, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_b, - candidate_hash_b, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_b, - candidate_hash_c, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_c, - candidate_hash_c, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_c, - candidate_hash_d, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_b, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer_d, - candidate_hash_d, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c, candidate_hash_d}}}}, - {candidate_head_data_hash_b, {{1, {candidate_hash_d}}}}})); - - { - auto post_confirmation = candidates.confirm_candidate( - candidate_hash_a, candidate_a, pvd_a, group_index, hasher_); - ASSERT_TRUE(post_confirmation); - PostConfirmation pc{ - .hypothetical = - HypotheticalCandidateComplete{ - .candidate_hash = candidate_hash_a, - .receipt = candidate_a, - .persisted_validation_data = pvd_a, - }, - .reckoning = - PostConfirmationReckoning{ - .correct = {peer_a}, - .incorrect = {}, - }, - }; - ASSERT_EQ(*post_confirmation, pc); - } - { - auto post_confirmation = candidates.confirm_candidate( - candidate_hash_b, candidate_b, pvd_b, group_index, hasher_); - ASSERT_TRUE(post_confirmation); - PostConfirmation pc{ - .hypothetical = - HypotheticalCandidateComplete{ - .candidate_hash = candidate_hash_b, - .receipt = candidate_b, - .persisted_validation_data = pvd_b, - }, - .reckoning = - PostConfirmationReckoning{ - .correct = {peer_a, peer_b}, - .incorrect = {}, - }, - }; - ASSERT_EQ(*post_confirmation, pc); - } - - const auto &[new_candidate_c, new_pvd_c] = - make_candidate(relay_hash, - 1, - 2, - candidate_head_data_b, - candidate_head_data_c, - hashFromStrData("3000")); - { - auto post_confirmation = candidates.confirm_candidate( - candidate_hash_c, new_candidate_c, new_pvd_c, group_index, hasher_); - ASSERT_TRUE(post_confirmation); - PostConfirmation pc{ - .hypothetical = - HypotheticalCandidateComplete{ - .candidate_hash = candidate_hash_c, - .receipt = new_candidate_c, - .persisted_validation_data = new_pvd_c, - }, - .reckoning = - PostConfirmationReckoning{ - .correct = {}, - .incorrect = {peer_b, peer_c}, - }, - }; - ASSERT_EQ(*post_confirmation, pc); - } - { - auto post_confirmation = candidates.confirm_candidate( - candidate_hash_d, candidate_d, pvd_d, group_index, hasher_); - ASSERT_TRUE(post_confirmation); - PostConfirmation pc{ - .hypothetical = - HypotheticalCandidateComplete{ - .candidate_hash = candidate_hash_d, - .receipt = candidate_d, - .persisted_validation_data = pvd_d, - }, - .reckoning = - PostConfirmationReckoning{ - .correct = {peer_c}, - .incorrect = {peer_d}, - }, - }; - ASSERT_EQ(*post_confirmation, pc); - } -} - -TEST_F(ProspectiveParachainsTest, Candidates_testHypotheticalFrontiers) { - HeadData relay_head_data{{1, 2, 3}}; - const Hash relay_hash = hasher_->blake2b_256(relay_head_data); - - HeadData candidate_head_data_a{1}; - HeadData candidate_head_data_b{2}; - HeadData candidate_head_data_c{3}; - HeadData candidate_head_data_d{4}; - - const Hash candidate_head_data_hash_a = - hasher_->blake2b_256(candidate_head_data_a); - const Hash candidate_head_data_hash_b = - hasher_->blake2b_256(candidate_head_data_b); - const Hash candidate_head_data_hash_d = - hasher_->blake2b_256(candidate_head_data_d); - - const auto &[candidate_a, pvd_a] = make_candidate(relay_hash, - 1, - 1, - relay_head_data, - candidate_head_data_a, - hashFromStrData("1000")); - const auto &[candidate_b, _] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_a, - candidate_head_data_b, - hashFromStrData("2000")); - const auto &[candidate_c, __] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_a, - candidate_head_data_c, - hashFromStrData("3000")); - const auto &[candidate_d, ___] = make_candidate(relay_hash, - 1, - 1, - candidate_head_data_b, - candidate_head_data_d, - hashFromStrData("4000")); - - const Hash candidate_hash_a = network::candidateHash(*hasher_, candidate_a); - const Hash candidate_hash_b = network::candidateHash(*hasher_, candidate_b); - const Hash candidate_hash_c = network::candidateHash(*hasher_, candidate_c); - const Hash candidate_hash_d = network::candidateHash(*hasher_, candidate_d); - - const libp2p::peer::PeerId peer{"peer1"_peerid}; - - GroupIndex group_index = 100; - Candidates candidates; - - candidates.confirm_candidate( - candidate_hash_a, candidate_a, pvd_a, group_index, hasher_); - - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, - candidate_hash_b, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, - candidate_hash_c, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_a, 1))); - ASSERT_TRUE(candidates.insert_unconfirmed( - peer, - candidate_hash_d, - relay_hash, - group_index, - std::make_pair(candidate_head_data_hash_b, 1))); - - ASSERT_TRUE(compareMapsOfCandidates( - candidates.by_parent, - {{relay_hash, {{1, {candidate_hash_a}}}}, - {candidate_head_data_hash_a, - {{1, {candidate_hash_b, candidate_hash_c}}}}, - {candidate_head_data_hash_b, {{1, {candidate_hash_d}}}}})); - - HypotheticalCandidateComplete hypothetical_a{ - .candidate_hash = candidate_hash_a, - .receipt = candidate_a, - .persisted_validation_data = pvd_a, - }; - HypotheticalCandidateIncomplete hypothetical_b{ - .candidate_hash = candidate_hash_b, - .candidate_para = 1, - .parent_head_data_hash = candidate_head_data_hash_a, - .candidate_relay_parent = relay_hash, - }; - HypotheticalCandidateIncomplete hypothetical_c{ - .candidate_hash = candidate_hash_c, - .candidate_para = 1, - .parent_head_data_hash = candidate_head_data_hash_a, - .candidate_relay_parent = relay_hash, - }; - HypotheticalCandidateIncomplete hypothetical_d{ - .candidate_hash = candidate_hash_d, - .candidate_para = 1, - .parent_head_data_hash = candidate_head_data_hash_b, - .candidate_relay_parent = relay_hash, - }; - - { - auto hypotheticals = - candidates.frontier_hypotheticals(std::make_pair(relay_hash, 1)); - ASSERT_EQ(hypotheticals.size(), 1); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_a}) - != hypotheticals.end()); - } - { - auto hypotheticals = candidates.frontier_hypotheticals( - std::make_pair(candidate_head_data_hash_a, 2)); - ASSERT_EQ(hypotheticals.size(), 0); - } - { - auto hypotheticals = candidates.frontier_hypotheticals( - std::make_pair(candidate_head_data_hash_a, 1)); - ASSERT_EQ(hypotheticals.size(), 2); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_b}) - != hypotheticals.end()); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_c}) - != hypotheticals.end()); - } - { - auto hypotheticals = candidates.frontier_hypotheticals( - std::make_pair(candidate_head_data_hash_d, 1)); - ASSERT_EQ(hypotheticals.size(), 0); - } - { - auto hypotheticals = candidates.frontier_hypotheticals(std::nullopt); - ASSERT_EQ(hypotheticals.size(), 4); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_a}) - != hypotheticals.end()); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_b}) - != hypotheticals.end()); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_c}) - != hypotheticals.end()); - ASSERT_TRUE(std::find(hypotheticals.begin(), - hypotheticals.end(), - HypotheticalCandidate{hypothetical_d}) - != hypotheticals.end()); - } -} - -/// polkadot/node/network/statement-distribution/src/v2/statement_store.rs -TEST_F(ProspectiveParachainsTest, - StatementsStore_always_provides_fresh_statements_in_order) { - const ValidatorIndex validator_a{1}; - const ValidatorIndex validator_b{2}; - const auto candidate_hash = fromNumber(42); - - // SecondedCandidateHash, ValidCandidateHash - const network::vstaging::CompactStatement valid_statement{ - network::vstaging::ValidCandidateHash{ - .hash = candidate_hash, - }}; - const network::vstaging::CompactStatement seconded_statement{ - network::vstaging::SecondedCandidateHash{ - .hash = candidate_hash, - }}; - - const Groups groups( - std::vector>{{validator_a, validator_b}}, 2); - StatementStore store(groups); - - kagome::parachain::IndexedAndSigned - signed_valid_by_a{ - .payload = - { - .payload = valid_statement, - .ix = validator_a, - }, - .signature = {}, - }; - store.insert(groups, signed_valid_by_a, StatementOrigin::Remote); - - kagome::parachain::IndexedAndSigned - signed_seconded_by_b{ - .payload = - { - .payload = seconded_statement, - .ix = validator_b, - }, - .signature = {}, - }; - store.insert(groups, signed_seconded_by_b, StatementOrigin::Remote); - - { - std::vector vals = {validator_a, validator_b}; - std::vector> - statements; - store.fresh_statements_for_backing( - vals, - candidate_hash, - [&](const kagome::parachain::IndexedAndSigned< - network::vstaging::CompactStatement> &statement) { - statements.emplace_back(statement); - }); - ASSERT_EQ(statements.size(), 2); - ASSERT_EQ(statements[0].payload.payload, seconded_statement); - ASSERT_EQ(statements[1].payload.payload, valid_statement); - } - - { - std::vector vals = {validator_b, validator_a}; - std::vector> - statements; - store.fresh_statements_for_backing( - vals, - candidate_hash, - [&](const kagome::parachain::IndexedAndSigned< - network::vstaging::CompactStatement> &statement) { - statements.emplace_back(statement); - }); - - ASSERT_EQ(statements.size(), 2); - ASSERT_EQ(statements[0].payload.payload, seconded_statement); - ASSERT_EQ(statements[1].payload.payload, valid_statement); - } -} +///** +// * Copyright Quadrivium LLC +// * All Rights Reserved +// * SPDX-License-Identifier: Apache-2.0 +// */ + +/// TODO(iceseer): Add prospective parachains +/// tests(https://github.com/qdrvm/kagome/issues/2224) diff --git a/test/core/parachain/scope.cpp b/test/core/parachain/scope.cpp new file mode 100644 index 0000000000..c23e512b67 --- /dev/null +++ b/test/core/parachain/scope.cpp @@ -0,0 +1,145 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/prospective_parachains/scope.hpp" +#include "core/parachain/parachain_test_harness.hpp" + +using namespace kagome::parachain::fragment; + +class ScopeTest : public ProspectiveParachainsTestHarness { + void SetUp() override { + ProspectiveParachainsTestHarness::SetUp(); + } + + void TearDown() override { + ProspectiveParachainsTestHarness::TearDown(); + } +}; + +TEST_F(ScopeTest, scope_only_takes_ancestors_up_to_min) { + RelayChainBlockInfo relay_parent{ + .hash = fromNumber(0), + .number = 5, + .storage_root = fromNumber(69), + }; + + Vec ancestors = {RelayChainBlockInfo{ + .hash = fromNumber(4), + .number = 4, + .storage_root = fromNumber(69), + }, + RelayChainBlockInfo{ + .hash = fromNumber(3), + .number = 3, + .storage_root = fromNumber(69), + }, + RelayChainBlockInfo{ + .hash = fromNumber(2), + .number = 2, + .storage_root = fromNumber(69), + }}; + + const size_t max_depth = 2; + const auto base_constraints = make_constraints(3, {2}, {1, 2, 3}); + + Vec pending_availability; + auto scope = Scope::with_ancestors(relay_parent, + base_constraints, + pending_availability, + max_depth, + ancestors); + ASSERT_TRUE(scope.has_value()); + ASSERT_EQ(scope.value().ancestors.size(), 2); + ASSERT_EQ(scope.value().ancestors_by_hash.size(), 2); +} + +TEST_F(ScopeTest, scope_rejects_unordered_ancestors) { + RelayChainBlockInfo relay_parent{ + .hash = fromNumber(0), + .number = 5, + .storage_root = fromNumber(69), + }; + + Vec ancestors = {RelayChainBlockInfo{ + .hash = fromNumber(4), + .number = 4, + .storage_root = fromNumber(69), + }, + RelayChainBlockInfo{ + .hash = fromNumber(2), + .number = 2, + .storage_root = fromNumber(69), + }, + RelayChainBlockInfo{ + .hash = fromNumber(3), + .number = 3, + .storage_root = fromNumber(69), + }}; + + const size_t max_depth = 2; + const auto base_constraints = make_constraints(0, {2}, {1, 2, 3}); + + Vec pending_availability; + ASSERT_EQ(Scope::with_ancestors(relay_parent, + base_constraints, + pending_availability, + max_depth, + ancestors) + .error(), + Scope::Error::UNEXPECTED_ANCESTOR); +} + +TEST_F(ScopeTest, scope_rejects_ancestor_for_0_block) { + RelayChainBlockInfo relay_parent{ + .hash = fromNumber(0), + .number = 0, + .storage_root = fromNumber(69), + }; + + Vec ancestors = {RelayChainBlockInfo{ + .hash = fromNumber(99), + .number = 99999, + .storage_root = fromNumber(69), + }}; + + const size_t max_depth = 2; + const auto base_constraints = make_constraints(0, {}, {1, 2, 3}); + + Vec pending_availability; + ASSERT_EQ(Scope::with_ancestors(relay_parent, + base_constraints, + pending_availability, + max_depth, + ancestors) + .error(), + Scope::Error::UNEXPECTED_ANCESTOR); +} + +TEST_F(ScopeTest, scope_rejects_ancestors_that_skip_blocks) { + RelayChainBlockInfo relay_parent{ + .hash = fromNumber(10), + .number = 10, + .storage_root = fromNumber(69), + }; + + Vec ancestors = {RelayChainBlockInfo{ + .hash = fromNumber(8), + .number = 8, + .storage_root = fromNumber(69), + }}; + + const size_t max_depth = 2; + const auto base_constraints = make_constraints(8, {8, 9}, {1, 2, 3}); + + Vec pending_availability; + ASSERT_EQ(Scope::with_ancestors(relay_parent, + base_constraints, + pending_availability, + max_depth, + ancestors) + .error(), + Scope::Error::UNEXPECTED_ANCESTOR); +}