From 5183aa62b9477a7436108ebf311dd449dd7d0843 Mon Sep 17 00:00:00 2001 From: Alexander Lednev <57529355+iceseer@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:33:43 +0300 Subject: [PATCH] Feature/async-backing (#1825) * Async-backing impl Signed-off-by: iceseer * Prospective parachains impl Signed-off-by: iceseer * remove validation protocol v1 Signed-off-by: iceseer --------- Signed-off-by: iceseer Co-authored-by: Harrm Co-authored-by: kamilsa --- CMakeLists.txt | 1 + core/consensus/babe/impl/babe.cpp | 13 +- core/consensus/babe/impl/babe.hpp | 7 +- core/consensus/beefy/types.hpp | 16 +- core/crypto/type_hasher.hpp | 11 +- .../dispute_coordinator/impl/storage_impl.cpp | 2 +- core/injector/application_injector.cpp | 3 +- core/macro/endianness_utils.hpp | 2 + core/network/collation_observer.hpp | 8 +- core/network/common.hpp | 8 + core/network/impl/peer_manager_impl.cpp | 104 +- core/network/impl/peer_manager_impl.hpp | 6 +- core/network/impl/peer_view.cpp | 4 +- .../protocols/fetch_attested_candidate.hpp | 84 + .../impl/protocols/grandpa_protocol.cpp | 2 +- .../impl/protocols/parachain_protocol.hpp | 25 +- .../impl/protocols/parachain_protocols.hpp | 65 +- .../protocol_fetch_available_data.hpp | 5 +- .../impl/protocols/protocol_req_collation.cpp | 84 +- .../impl/protocols/protocol_req_collation.hpp | 17 +- core/network/impl/router_libp2p.cpp | 36 +- core/network/impl/router_libp2p.hpp | 14 +- core/network/impl/stream_engine.hpp | 2 +- .../impl/sync_protocol_observer_impl.cpp | 2 +- core/network/peer_manager.hpp | 46 +- core/network/peer_view.hpp | 13 + .../protocols/req_collation_protocol.hpp | 9 +- core/network/req_collation_observer.hpp | 4 +- core/network/router.hpp | 7 + core/network/types/collator_messages.hpp | 102 +- .../types/collator_messages_vstaging.hpp | 239 ++ core/network/validation_observer.hpp | 7 +- core/parachain/CMakeLists.txt | 2 + .../approval/approval_distribution.cpp | 150 +- .../approval/approval_distribution.hpp | 4 +- .../availability/bitfield/store_impl.cpp | 8 +- core/parachain/backing/store.hpp | 24 +- core/parachain/backing/store_impl.cpp | 233 +- core/parachain/backing/store_impl.hpp | 72 +- core/parachain/pvf/pvf.hpp | 4 +- core/parachain/pvf/pvf_impl.cpp | 50 +- core/parachain/pvf/pvf_impl.hpp | 13 +- core/parachain/types.hpp | 261 ++ .../validator/backing_implicit_view.cpp | 184 + .../validator/backing_implicit_view.hpp | 73 + core/parachain/validator/collations.hpp | 208 + core/parachain/validator/fragment_tree.hpp | 988 +++++ core/parachain/validator/impl/candidates.hpp | 446 +++ .../validator/impl/fragment_tree.cpp | 236 ++ .../impl/parachain_observer_impl.cpp | 98 +- .../impl/parachain_observer_impl.hpp | 27 +- .../validator/impl/parachain_processor.cpp | 3446 ++++++++++++++--- .../validator/impl/statements_store.hpp | 298 ++ .../validator/parachain_processor.hpp | 423 +- .../validator/prospective_parachains.hpp | 718 ++++ core/parachain/validator/signer.hpp | 2 +- core/primitives/block_header.cpp | 7 +- core/primitives/block_header.hpp | 12 +- core/primitives/common.hpp | 7 + core/primitives/inherent_data.hpp | 4 +- core/primitives/math.hpp | 21 +- core/runtime/module_instance.hpp | 2 +- .../runtime_api/impl/parachain_host.cpp | 23 + .../runtime_api/impl/parachain_host.hpp | 10 + .../impl/transaction_payment_api.cpp | 1 + core/runtime/runtime_api/parachain_host.hpp | 18 + .../runtime_api/parachain_host_types.hpp | 4 +- core/scale/encoder/primitives.hpp | 10 +- core/scale/kagome_scale.hpp | 22 + core/utils/map.hpp | 38 + .../api/service/system/system_api_test.cpp | 6 +- test/core/blockchain/block_storage_test.cpp | 1 + test/core/consensus/babe/babe_test.cpp | 7 +- .../host_api/child_storage_extension_test.cpp | 5 +- test/core/host_api/storage_extension_test.cpp | 2 + test/core/network/rpc_libp2p_test.cpp | 8 +- test/core/parachain/CMakeLists.txt | 9 + test/core/parachain/assignments.cpp | 14 +- .../core/parachain/prospective_parachains.cpp | 2770 +++++++++++++ test/core/parachain/pvf_test.cpp | 1 + .../storage/trie_pruner/trie_pruner_test.cpp | 4 +- test/mock/core/network/peer_manager_mock.hpp | 5 + test/mock/core/network/router_mock.hpp | 15 + .../parachain/backed_candidates_source.hpp | 22 + .../core/parachain/backing_store_mock.hpp | 21 +- .../mock/core/runtime/parachain_host_mock.hpp | 16 + test/testutil/scale_test_comparator.hpp | 20 + 87 files changed, 10872 insertions(+), 1149 deletions(-) create mode 100644 core/network/impl/protocols/fetch_attested_candidate.hpp create mode 100644 core/network/types/collator_messages_vstaging.hpp create mode 100644 core/parachain/validator/backing_implicit_view.cpp create mode 100644 core/parachain/validator/backing_implicit_view.hpp create mode 100644 core/parachain/validator/collations.hpp create mode 100644 core/parachain/validator/fragment_tree.hpp create mode 100644 core/parachain/validator/impl/candidates.hpp create mode 100644 core/parachain/validator/impl/fragment_tree.cpp create mode 100644 core/parachain/validator/impl/statements_store.hpp create mode 100644 core/parachain/validator/prospective_parachains.hpp create mode 100644 core/utils/map.hpp create mode 100644 test/core/parachain/prospective_parachains.cpp create mode 100644 test/mock/core/parachain/backed_candidates_source.hpp create mode 100644 test/testutil/scale_test_comparator.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f1bd438f4c..64e184cd1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,6 +222,7 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kagomeConfig.cmake export(PACKAGE kagome) if(TESTING) + add_compile_definitions(CFG_TESTING) enable_testing() add_subdirectory(test) endif() diff --git a/core/consensus/babe/impl/babe.cpp b/core/consensus/babe/impl/babe.cpp index 466d9aa9bd..a465a1d357 100644 --- a/core/consensus/babe/impl/babe.cpp +++ b/core/consensus/babe/impl/babe.cpp @@ -31,6 +31,7 @@ #include "parachain/availability/bitfield/store.hpp" #include "parachain/backing/store.hpp" #include "parachain/parachain_inherent_data.hpp" +#include "parachain/validator/parachain_processor.hpp" #include "primitives/inherent_data.hpp" #include "runtime/runtime_api/offchain_worker_api.hpp" #include "storage/changes_trie/impl/storage_changes_tracker_impl.hpp" @@ -74,7 +75,7 @@ namespace kagome::consensus::babe { std::shared_ptr sr25519_provider, std::shared_ptr validating, std::shared_ptr bitfield_store, - std::shared_ptr backing_store, + std::shared_ptr candidates_source, std::shared_ptr dispute_coordinator, std::shared_ptr proposer, primitives::events::StorageSubscriptionEnginePtr storage_sub_engine, @@ -95,7 +96,7 @@ namespace kagome::consensus::babe { sr25519_provider_(std::move(sr25519_provider)), validating_(std::move(validating)), bitfield_store_(std::move(bitfield_store)), - backing_store_(std::move(backing_store)), + candidates_source_(std::move(candidates_source)), dispute_coordinator_(std::move(dispute_coordinator)), proposer_(std::move(proposer)), storage_sub_engine_(std::move(storage_sub_engine)), @@ -114,7 +115,7 @@ namespace kagome::consensus::babe { BOOST_ASSERT(sr25519_provider_); BOOST_ASSERT(validating_); BOOST_ASSERT(bitfield_store_); - BOOST_ASSERT(backing_store_); + BOOST_ASSERT(candidates_source_); BOOST_ASSERT(dispute_coordinator_); BOOST_ASSERT(proposer_); BOOST_ASSERT(chain_sub_engine_); @@ -329,9 +330,13 @@ namespace kagome::consensus::babe { auto &relay_parent = parent_.hash; parachain_inherent_data.bitfields = bitfield_store_->getBitfields(relay_parent); + SL_INFO(log_, + "Bitfields set for block.(count={}, relay_parent={})", + parachain_inherent_data.bitfields.size(), + relay_parent); parachain_inherent_data.backed_candidates = - backing_store_->get(relay_parent); + candidates_source_->getBackedCandidates(relay_parent); SL_TRACE(log_, "Get backed candidates from store.(count={}, relay_parent={})", parachain_inherent_data.backed_candidates.size(), diff --git a/core/consensus/babe/impl/babe.hpp b/core/consensus/babe/impl/babe.hpp index 4e6f992dc6..e04fb0aab1 100644 --- a/core/consensus/babe/impl/babe.hpp +++ b/core/consensus/babe/impl/babe.hpp @@ -57,7 +57,8 @@ namespace kagome::dispute { namespace kagome::parachain { class BitfieldStore; - class BackingStore; + struct ParachainProcessorImpl; + struct BackedCandidatesSource; } // namespace kagome::parachain namespace kagome::network { @@ -99,7 +100,7 @@ namespace kagome::consensus::babe { std::shared_ptr sr25519_provider, std::shared_ptr validating, std::shared_ptr bitfield_store, - std::shared_ptr backing_store, + std::shared_ptr candidates_source, std::shared_ptr dispute_coordinator, std::shared_ptr proposer, primitives::events::StorageSubscriptionEnginePtr storage_sub_engine, @@ -157,7 +158,7 @@ namespace kagome::consensus::babe { std::shared_ptr sr25519_provider_; std::shared_ptr validating_; std::shared_ptr bitfield_store_; - std::shared_ptr backing_store_; + std::shared_ptr candidates_source_; std::shared_ptr dispute_coordinator_; std::shared_ptr proposer_; primitives::events::StorageSubscriptionEnginePtr storage_sub_engine_; diff --git a/core/consensus/beefy/types.hpp b/core/consensus/beefy/types.hpp index 984638d490..61a8809104 100644 --- a/core/consensus/beefy/types.hpp +++ b/core/consensus/beefy/types.hpp @@ -66,8 +66,8 @@ namespace kagome::consensus::beefy { Commitment commitment; std::vector> signatures; }; - inline scale::ScaleEncoderStream &operator<<(scale::ScaleEncoderStream &s, - const SignedCommitment &v) { + inline ::scale::ScaleEncoderStream &operator<<(::scale::ScaleEncoderStream &s, + const SignedCommitment &v) { s << v.commitment; size_t count = 0; common::Buffer bits; @@ -83,7 +83,7 @@ namespace kagome::consensus::beefy { } s << bits; s << static_cast(v.signatures.size()); - s << scale::CompactInteger{count}; + s << ::scale::CompactInteger{count}; for (auto &sig : v.signatures) { if (sig) { s << *sig; @@ -91,8 +91,8 @@ namespace kagome::consensus::beefy { } return s; } - inline scale::ScaleDecoderStream &operator>>(scale::ScaleDecoderStream &s, - SignedCommitment &v) { + inline ::scale::ScaleDecoderStream &operator>>(::scale::ScaleDecoderStream &s, + SignedCommitment &v) { s >> v.commitment; common::Buffer bits; s >> bits; @@ -105,12 +105,12 @@ namespace kagome::consensus::beefy { uint32_t total = 0; s >> total; if (bits.size() * 8 < total) { - scale::raise(scale::DecodeError::NOT_ENOUGH_DATA); + ::scale::raise(::scale::DecodeError::NOT_ENOUGH_DATA); } - scale::CompactInteger actual_count; + ::scale::CompactInteger actual_count; s >> actual_count; if (actual_count != expected_count) { - scale::raise(scale::DecodeError::TOO_MANY_ITEMS); + ::scale::raise(::scale::DecodeError::TOO_MANY_ITEMS); } v.signatures.resize(total); for (size_t i = 0; i < total; ++i) { diff --git a/core/crypto/type_hasher.hpp b/core/crypto/type_hasher.hpp index 76a9058252..15adba3e1f 100644 --- a/core/crypto/type_hasher.hpp +++ b/core/crypto/type_hasher.hpp @@ -15,12 +15,8 @@ namespace kagome::crypto { template inline void hashTypes(H &hasher, common::Blob &out, T &&...t) { - kagome::scale::encode( - [&](const uint8_t *const val, size_t count) { - hasher.update({val, count}); - }, - std::forward(t)...); - + auto val = ::scale::encode(std::forward(t)...).value(); + hasher.update(val); hasher.get_final(out); } @@ -63,8 +59,9 @@ namespace kagome::crypto { } return *opt_hash_; } - +#ifndef CFG_TESTING private: +#endif // CFG_TESTING T type_; mutable std::optional opt_hash_{}; }; diff --git a/core/dispute_coordinator/impl/storage_impl.cpp b/core/dispute_coordinator/impl/storage_impl.cpp index 14ac841568..501c4aec14 100644 --- a/core/dispute_coordinator/impl/storage_impl.cpp +++ b/core/dispute_coordinator/impl/storage_impl.cpp @@ -106,7 +106,7 @@ namespace kagome::dispute { const StoredWindow &stored_window) { auto dispute_space = storage_->getSpace(storage::Space::kDisputeData); - OUTCOME_TRY(encoded, scale::encode(stored_window)); + OUTCOME_TRY(encoded, ::scale::encode(stored_window)); OUTCOME_TRY(dispute_space->put(storage::kSessionsWindowLookupKey, common::BufferView{encoded})); diff --git a/core/injector/application_injector.cpp b/core/injector/application_injector.cpp index a74a42add7..0c30fd2236 100644 --- a/core/injector/application_injector.cpp +++ b/core/injector/application_injector.cpp @@ -8,7 +8,7 @@ #define BOOST_DI_CFG_DIAGNOSTICS_LEVEL 2 #define BOOST_DI_CFG_CTOR_LIMIT_SIZE \ - 24 // TODO(Harrm): check how it influences on compilation time + 32 // TODO(Harrm): check how it influences on compilation time #include #include @@ -789,6 +789,7 @@ namespace { di::bind.template to(), di::bind.template to(), di::bind.template to(), + di::bind.template to(), di::bind.template to(), di::bind.template to(), di::bind.template to(), diff --git a/core/macro/endianness_utils.hpp b/core/macro/endianness_utils.hpp index bb1d2e9b8a..ed631cf969 100644 --- a/core/macro/endianness_utils.hpp +++ b/core/macro/endianness_utils.hpp @@ -7,9 +7,11 @@ #pragma once #ifdef _MSC_VER +#define LE_BE_SWAP16 _byteswap_ushort #define LE_BE_SWAP32 _byteswap_ulong #define LE_BE_SWAP64 _byteswap_uint64 #else //_MSC_VER +#define LE_BE_SWAP16 __builtin_bswap16 #define LE_BE_SWAP32 __builtin_bswap32 #define LE_BE_SWAP64 __builtin_bswap64 #endif //_MSC_VER diff --git a/core/network/collation_observer.hpp b/core/network/collation_observer.hpp index 29dcdce269..f6aa145bb3 100644 --- a/core/network/collation_observer.hpp +++ b/core/network/collation_observer.hpp @@ -8,7 +8,7 @@ #include -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "primitives/common.hpp" namespace kagome::network { @@ -21,11 +21,13 @@ namespace kagome::network { /// Handle incoming collation stream. virtual void onIncomingCollationStream( - const libp2p::peer::PeerId &peer_id) = 0; + const libp2p::peer::PeerId &peer_id, + network::CollationVersion version) = 0; /// Handle incoming collation message. virtual void onIncomingMessage( const libp2p::peer::PeerId &peer_id, - CollationProtocolMessage &&collation_message) = 0; + network::VersionedCollatorProtocolMessage &&msg) = 0; }; + } // namespace kagome::network diff --git a/core/network/common.hpp b/core/network/common.hpp index 53614c513e..a2c9df8621 100644 --- a/core/network/common.hpp +++ b/core/network/common.hpp @@ -29,7 +29,13 @@ namespace kagome::network { const libp2p::peer::ProtocolName kLightProtocol = "/{}/light/2"; const libp2p::peer::ProtocolName kCollationProtocol{"/{}/collation/1"}; const libp2p::peer::ProtocolName kValidationProtocol{"/{}/validation/1"}; + const libp2p::peer::ProtocolName kCollationProtocolVStaging{ + "/{}/collation/2"}; + const libp2p::peer::ProtocolName kValidationProtocolVStaging{ + "/{}/validation/2"}; const libp2p::peer::ProtocolName kReqCollationProtocol{"/{}/req_collation/1"}; + const libp2p::peer::ProtocolName kReqCollationVStagingProtocol{ + "/{}/req_collation/2"}; const libp2p::peer::ProtocolName kReqPovProtocol{"/{}/req_pov/1"}; const libp2p::peer::ProtocolName kFetchChunkProtocol{"/{}/req_chunk/1"}; const libp2p::peer::ProtocolName kFetchAvailableDataProtocol{ @@ -37,6 +43,8 @@ namespace kagome::network { const libp2p::peer::ProtocolName kFetchStatementProtocol{ "/{}/req_statement/1"}; const libp2p::peer::ProtocolName kSendDisputeProtocol{"/{}/send_dispute/1"}; + const libp2p::peer::ProtocolName kFetchAttestedCandidateProtocol{ + "/{}/req_attested_candidate/2"}; const libp2p::peer::ProtocolName kBeefyProtocol{"/{}/beefy/2"}; const libp2p::peer::ProtocolName kBeefyJustificationProtocol{ "/{}/beefy/justifications/1"}; diff --git a/core/network/impl/peer_manager_impl.cpp b/core/network/impl/peer_manager_impl.cpp index 0edb2cf5e6..197ca351ab 100644 --- a/core/network/impl/peer_manager_impl.cpp +++ b/core/network/impl/peer_manager_impl.cpp @@ -265,14 +265,16 @@ namespace kagome::network { const network::CollatorPublicKey &collator_id, network::ParachainId para_id) { if (auto it = peer_states_.find(peer_id); it != peer_states_.end()) { - it->second.collator_state = CollatorState{ - .parachain_id = para_id, + it->second.collator_state = CollatingPeerState{ + .para_id = para_id, .collator_id = collator_id, + .advertisements = {}, + .last_active = std::chrono::system_clock::now(), }; it->second.time = clock_->now(); } - auto proto_col = router_->getCollationProtocol(); + auto proto_col = router_->getCollationProtocolVStaging(); BOOST_ASSERT_MSG(proto_col, "Router did not provide collaction protocol"); stream_engine_->reserveStreams(peer_id, proto_col); } @@ -292,7 +294,7 @@ namespace kagome::network { return Error::UNDECLARED_COLLATOR; } return std::make_pair(peer_state.collator_state.value().collator_id, - peer_state.collator_state.value().parachain_id); + peer_state.collator_state.value().para_id); } void PeerManagerImpl::align() { @@ -534,6 +536,13 @@ namespace kagome::network { }); } + std::optional> + PeerManagerImpl::createDefaultPeerState(const PeerId &peer_id) { + auto &state = peer_states_[peer_id]; + state.time = clock_->now(); + return state; + } + void PeerManagerImpl::updatePeerState( const PeerId &peer_id, const BlockAnnounceHandshake &handshake) { auto &state = peer_states_[peer_id]; @@ -623,9 +632,9 @@ namespace kagome::network { if (not stream_res.has_value()) { self->log_->verbose("Unable to create stream {} with {}: {}", - protocol->protocolName(), - peer_id, - stream_res.error()); + protocol->protocolName(), + peer_id, + stream_res.error()); self->connecting_peers_.erase(peer_id); self->disconnectFromPeer(peer_id); return; @@ -697,39 +706,57 @@ namespace kagome::network { } } - void PeerManagerImpl::tryOpenValidationProtocol(const PeerInfo &peer_info, - PeerState &peer_state) { + void PeerManagerImpl::tryOpenValidationProtocol( + const PeerInfo &peer_info, + PeerState &peer_state, + network::CollationVersion + proto_version) { // network::CollationVersion::VStaging /// If validator start validation protocol if (peer_state.roles.flags.authority) { - auto validation_protocol = router_->getValidationProtocol(); + auto validation_protocol = [&]() -> std::shared_ptr { + return router_->getValidationProtocolVStaging(); + }(); + BOOST_ASSERT_MSG(validation_protocol, "Router did not provide validation protocol"); log_->trace("Try to open outgoing validation protocol.(peer={})", peer_info.id); - openOutgoing(stream_engine_, - validation_protocol, - peer_info, - [validation_protocol, peer_info, wptr{weak_from_this()}]( - outcome::result> stream_result) { - auto self = wptr.lock(); - if (not self) { - return; - } - - auto &peer_id = peer_info.id; - if (!stream_result.has_value()) { - self->log_->verbose( - "Unable to create stream {} with {}: {}", - validation_protocol->protocolName(), - peer_id, - stream_result.error().message()); - return; - } + openOutgoing( + stream_engine_, + validation_protocol, + peer_info, + [validation_protocol, peer_info, wptr{weak_from_this()}]( + outcome::result> stream_result) { + auto self = wptr.lock(); + if (not self) { + return; + } - self->stream_engine_->addOutgoing(stream_result.value(), - validation_protocol); - }); + auto &peer_id = peer_info.id; + if (!stream_result.has_value()) { + SL_TRACE(self->log_, + "Unable to create stream {} with {}: {}", + validation_protocol->protocolName(), + peer_id, + stream_result.error().message()); + auto ps = self->getPeerState(peer_info.id); + if (ps) { + self->tryOpenValidationProtocol( + peer_info, ps->get(), network::CollationVersion::V1); + } else { + SL_TRACE( + self->log_, + "No peer state to open V1 validation protocol {} with {}", + validation_protocol->protocolName(), + peer_id); + } + return; + } + + self->stream_engine_->addOutgoing(stream_result.value(), + validation_protocol); + }); } } @@ -784,8 +811,10 @@ namespace kagome::network { } self->tryOpenGrandpaProtocol(peer_info, peer_state.value().get()); - self->tryOpenValidationProtocol(peer_info, - peer_state.value().get()); + self->tryOpenValidationProtocol( + peer_info, + peer_state.value().get(), + network::CollationVersion::VStaging); openOutgoing(self->stream_engine_, self->router_->getBeefyProtocol(), peer_info, @@ -804,10 +833,11 @@ namespace kagome::network { } void PeerManagerImpl::reserveStatusStreams(const PeerId &peer_id) const { - auto proto_val = router_->getValidationProtocol(); - BOOST_ASSERT_MSG(proto_val, "Router did not provide validation protocol"); + auto proto_val_vstaging = router_->getValidationProtocolVStaging(); + BOOST_ASSERT_MSG(proto_val_vstaging, + "Router did not provide validation protocol vstaging"); - stream_engine_->reserveStreams(peer_id, proto_val); + stream_engine_->reserveStreams(peer_id, proto_val_vstaging); } void PeerManagerImpl::reserveStreams(const PeerId &peer_id) const { diff --git a/core/network/impl/peer_manager_impl.hpp b/core/network/impl/peer_manager_impl.hpp index daf3847aac..ef7b0bdb6d 100644 --- a/core/network/impl/peer_manager_impl.hpp +++ b/core/network/impl/peer_manager_impl.hpp @@ -128,6 +128,9 @@ namespace kagome::network { void updatePeerState(const PeerId &peer_id, const BlockAnnounce &announce) override; + std::optional> createDefaultPeerState( + const PeerId &peer_id) override; + /** @see PeerManager::updatePeerState */ void updatePeerState( const PeerId &peer_id, @@ -156,7 +159,8 @@ namespace kagome::network { void tryOpenGrandpaProtocol(const PeerInfo &peer_info, PeerState &peer_state); void tryOpenValidationProtocol(const PeerInfo &peer_info, - PeerState &peer_state); + PeerState &peer_state, + network::CollationVersion proto_version); /// Opens streams set for special peer (i.e. new-discovered) void connectToPeer(const PeerId &peer_id); diff --git a/core/network/impl/peer_view.cpp b/core/network/impl/peer_view.cpp index c2800c3795..c6b10e3945 100644 --- a/core/network/impl/peer_view.cpp +++ b/core/network/impl/peer_view.cpp @@ -48,7 +48,7 @@ namespace kagome::network { const primitives::events::ChainEventParams &event) { if (auto self = wptr.lock()) { - if (auto const value = + if (auto value = if_type(event)) { self->updateMyView(ExView{ .view = @@ -57,7 +57,7 @@ namespace kagome::network { .finalized_number_ = self->block_tree_.get()->getLastFinalized().number, }, - .new_head = (*value).get(), + .new_head = value->get().get(), .lost = {}, }); } diff --git a/core/network/impl/protocols/fetch_attested_candidate.hpp b/core/network/impl/protocols/fetch_attested_candidate.hpp new file mode 100644 index 0000000000..0d7b656197 --- /dev/null +++ b/core/network/impl/protocols/fetch_attested_candidate.hpp @@ -0,0 +1,84 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "network/protocol_base.hpp" + +#include + +#include +#include + +#include "blockchain/genesis_block_hash.hpp" +#include "log/logger.hpp" +#include "network/common.hpp" +#include "network/impl/protocols/request_response_protocol.hpp" +#include "network/impl/stream_engine.hpp" +#include "parachain/validator/parachain_processor.hpp" +#include "utils/non_copyable.hpp" + +namespace kagome::network { + + class FetchAttestedCandidateProtocol final + : public RequestResponseProtocol, + NonCopyable, + NonMovable { + public: + FetchAttestedCandidateProtocol() = delete; + ~FetchAttestedCandidateProtocol() override = default; + + FetchAttestedCandidateProtocol( + libp2p::Host &host, + const application::ChainSpec &chain_spec, + const blockchain::GenesisBlockHash &genesis_hash, + std::shared_ptr pp) + : RequestResponseProtocol< + vstaging::AttestedCandidateRequest, + vstaging::AttestedCandidateResponse, + ScaleMessageReadWriter>{kFetchAttestedCandidateProtocolName, + host, + make_protocols( + kFetchAttestedCandidateProtocol, + genesis_hash, + kProtocolPrefixPolkadot), + log::createLogger( + kFetchAttestedCandidateProtocolName, + "req_attested_candidate_protocol")}, + pp_{std::move(pp)} { + BOOST_ASSERT(pp_); + } + + private: + std::optional> onRxRequest( + RequestType request, std::shared_ptr /*stream*/) override { + base().logger()->info( + "Fetching attested candidate request.(candidate={})", + request.candidate_hash); + auto res = pp_->OnFetchAttestedCandidateRequest(std::move(request)); + if (res.has_error()) { + base().logger()->error( + "Fetching attested candidate response failed.(error={})", + res.error().message()); + } else { + base().logger()->trace("Fetching attested candidate response."); + } + return res; + } + + void onTxRequest(const RequestType &request) override { + base().logger()->debug("Fetching attested candidate. (candidate={})", + request.candidate_hash); + } + + inline static const auto kFetchAttestedCandidateProtocolName = + "FetchAttestedCandidateProtocol"s; + std::shared_ptr pp_; + }; + +} // namespace kagome::network diff --git a/core/network/impl/protocols/grandpa_protocol.cpp b/core/network/impl/protocols/grandpa_protocol.cpp index 97db15a86d..593b698c38 100644 --- a/core/network/impl/protocols/grandpa_protocol.cpp +++ b/core/network/impl/protocols/grandpa_protocol.cpp @@ -508,7 +508,7 @@ namespace kagome::network { common::Hash256 GrandpaProtocol::getHash( const GrandpaMessage &message) const { - return hasher_->twox_256(scale::encode(message).value()); + return hasher_->twox_256(::scale::encode(message).value()); } bool GrandpaProtocol::addKnown(const PeerId &peer, diff --git a/core/network/impl/protocols/parachain_protocol.hpp b/core/network/impl/protocols/parachain_protocol.hpp index 9ceac15801..2be2840561 100644 --- a/core/network/impl/protocols/parachain_protocol.hpp +++ b/core/network/impl/protocols/parachain_protocol.hpp @@ -36,17 +36,21 @@ namespace kagome::network { - template + template class ParachainProtocol : public ProtocolBase, public std::enable_shared_from_this< - ParachainProtocol>, + ParachainProtocol>, NonCopyable, NonMovable { public: using ObserverType = Observer; using MessageType = Message; - using Self = ParachainProtocol; + using Self = + ParachainProtocol; ParachainProtocol() = delete; ~ParachainProtocol() override = default; @@ -81,20 +85,17 @@ namespace kagome::network { auto on_message = [peer_id = stream->remotePeerId().value()]( std::shared_ptr self, WireMessage message) { - SL_VERBOSE(self->base_.logger(), - "Received collatsion message from {}", - peer_id); visit_in_place( std::move(message), [&](ViewUpdate &&msg) { - SL_VERBOSE( + SL_TRACE( self->base_.logger(), "Received ViewUpdate from {}", peer_id); self->peer_view_->updateRemoteView(peer_id, std::move(msg.view)); }, [&](MessageType &&p) { - SL_VERBOSE(self->base_.logger(), - "Received Collation/Validation message from {}", - peer_id); + SL_TRACE(self->base_.logger(), + "Received Collation/Validation message from {}", + peer_id); self->observer_->onIncomingMessage(peer_id, std::move(p)); }); return true; @@ -150,9 +151,9 @@ namespace kagome::network { private: void onHandshake(const PeerId &peer) { if constexpr (kCollation) { - observer_->onIncomingCollationStream(peer); + observer_->onIncomingCollationStream(peer, kProtoVersion); } else { - observer_->onIncomingValidationStream(peer); + observer_->onIncomingValidationStream(peer, kProtoVersion); } } diff --git a/core/network/impl/protocols/parachain_protocols.hpp b/core/network/impl/protocols/parachain_protocols.hpp index 96ce3cba00..268308d65b 100644 --- a/core/network/impl/protocols/parachain_protocols.hpp +++ b/core/network/impl/protocols/parachain_protocols.hpp @@ -25,16 +25,18 @@ #include "network/peer_manager.hpp" #include "network/types/block_announce.hpp" #include "network/types/block_announce_handshake.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "network/types/roles.hpp" #include "network/validation_observer.hpp" #include "utils/non_copyable.hpp" namespace kagome::network { - class CollationProtocol : public ParachainProtocol { + class CollationProtocol + : public ParachainProtocol { public: CollationProtocol(libp2p::Host &host, Roles roles, @@ -53,9 +55,34 @@ namespace kagome::network { log::createLogger("CollationProtocol", "collation_protocol")){}; }; - class ValidationProtocol : public ParachainProtocol { + class CollationProtocolVStaging + : public ParachainProtocol { + public: + CollationProtocolVStaging(libp2p::Host &host, + Roles roles, + const application::ChainSpec &chain_spec, + const blockchain::GenesisBlockHash &genesis_hash, + std::shared_ptr observer, + std::shared_ptr peer_view) + : ParachainProtocol(host, + roles, + chain_spec, + genesis_hash, + std::move(observer), + kCollationProtocolVStaging, + std::move(peer_view), + log::createLogger("CollationProtocolVStaging", + "collation_protocol_vstaging")){}; + }; + + class ValidationProtocol + : public ParachainProtocol { public: ValidationProtocol(libp2p::Host &host, Roles roles, @@ -74,4 +101,28 @@ namespace kagome::network { log::createLogger("ValidationProtocol", "validation_protocol")){}; }; + class ValidationProtocolVStaging + : public ParachainProtocol { + public: + ValidationProtocolVStaging(libp2p::Host &host, + Roles roles, + const application::ChainSpec &chain_spec, + const blockchain::GenesisBlockHash &genesis_hash, + std::shared_ptr observer, + std::shared_ptr peer_view) + : ParachainProtocol( + host, + roles, + chain_spec, + genesis_hash, + std::move(observer), + kValidationProtocolVStaging, + std::move(peer_view), + log::createLogger("ValidationProtocolVStaging", + "validation_protocol_vstaging")){}; + }; + } // namespace kagome::network diff --git a/core/network/impl/protocols/protocol_fetch_available_data.hpp b/core/network/impl/protocols/protocol_fetch_available_data.hpp index 2b7973d752..3c2b91f113 100644 --- a/core/network/impl/protocols/protocol_fetch_available_data.hpp +++ b/core/network/impl/protocols/protocol_fetch_available_data.hpp @@ -87,8 +87,9 @@ namespace kagome::network { req.relay_parent, req.candidate_hash); - if (auto res = backing_store_->get_candidate(req.candidate_hash)) { - return std::move(*res); + if (auto res = backing_store_->getCadidateInfo(req.relay_parent, + req.candidate_hash)) { + return res->get().candidate; } base().logger()->error("No fetch statement response."); diff --git a/core/network/impl/protocols/protocol_req_collation.cpp b/core/network/impl/protocols/protocol_req_collation.cpp index a0c71aae6b..4a0449b936 100644 --- a/core/network/impl/protocols/protocol_req_collation.cpp +++ b/core/network/impl/protocols/protocol_req_collation.cpp @@ -13,39 +13,39 @@ namespace kagome::network { + template struct ReqCollationProtocolImpl - : RequestResponseProtocol, + std::decay_t, ScaleMessageReadWriter>, NonCopyable, NonMovable { + using Base = RequestResponseProtocol, + std::decay_t, + ScaleMessageReadWriter>; + ReqCollationProtocolImpl(libp2p::Host &host, + const libp2p::peer::ProtocolName &protoname, const application::ChainSpec &chain_spec, const blockchain::GenesisBlockHash &genesis_hash, std::shared_ptr observer) - : RequestResponseProtocol< - CollationFetchingRequest, - CollationFetchingResponse, - ScaleMessageReadWriter>{kReqCollationProtocolName, - host, - make_protocols(kReqCollationProtocol, - genesis_hash, - kProtocolPrefixPolkadot), - log::createLogger( - kReqCollationProtocolName, - "req_collation_protocol")}, + : Base{kReqCollationProtocolName, + host, + make_protocols(protoname, genesis_hash, kProtocolPrefixPolkadot), + log::createLogger(kReqCollationProtocolName, + "req_collation_protocol")}, observer_{std::move(observer)} {} protected: - std::optional> onRxRequest( - CollationFetchingRequest request, + std::optional> onRxRequest( + typename Base::RequestType request, std::shared_ptr /*stream*/) override { BOOST_ASSERT(observer_); return observer_->OnCollationRequest(std::move(request)); } - void onTxRequest(const CollationFetchingRequest &request) override { - base().logger()->debug("Requesting collation"); + void onTxRequest(const typename Base::RequestType &request) override { + Base::base().logger()->debug("Requesting collation"); } private: @@ -60,27 +60,41 @@ namespace kagome::network { const application::ChainSpec &chain_spec, const blockchain::GenesisBlockHash &genesis_hash, std::shared_ptr observer) - : impl_{std::make_shared( - host, chain_spec, genesis_hash, std::move(observer))} {} + : v1_impl_{std::make_shared< + ReqCollationProtocolImpl>( + host, kReqCollationProtocol, chain_spec, genesis_hash, observer)}, + vstaging_impl_{std::make_shared< + ReqCollationProtocolImpl>( + host, + kReqCollationVStagingProtocol, + chain_spec, + genesis_hash, + observer)} { + BOOST_ASSERT(v1_impl_); + BOOST_ASSERT(vstaging_impl_); + } const Protocol &ReqCollationProtocol::protocolName() const { - BOOST_ASSERT(impl_ && !!"ReqCollationProtocolImpl must be initialized!"); - return impl_->protocolName(); + BOOST_ASSERT_MSG(v1_impl_, "ReqCollationProtocolImpl must be initialized!"); + return v1_impl_->protocolName(); } bool ReqCollationProtocol::start() { - BOOST_ASSERT(impl_ && !!"ReqCollationProtocolImpl must be initialized!"); - return impl_->start(); + BOOST_ASSERT_MSG(v1_impl_, + "v1 ReqCollationProtocolImpl must be initialized!"); + BOOST_ASSERT_MSG(vstaging_impl_, + "vstaging ReqCollationProtocolImpl must be initialized!"); + return v1_impl_->start() && vstaging_impl_->start(); } - void ReqCollationProtocol::onIncomingStream(std::shared_ptr stream) { - BOOST_ASSERT(!"Must not be called!"); - } + void ReqCollationProtocol::onIncomingStream(std::shared_ptr stream) {} void ReqCollationProtocol::newOutgoingStream( const PeerInfo &peer_info, std::function>)> &&cb) { - BOOST_ASSERT(!"Must not be called!"); + BOOST_ASSERT_MSG(false, "Must not be called!"); } void ReqCollationProtocol::request( @@ -88,8 +102,20 @@ namespace kagome::network { CollationFetchingRequest request, std::function)> &&response_handler) { - BOOST_ASSERT(impl_ && !!"ReqCollationProtocolImpl must be initialized!"); - return impl_->doRequest( + BOOST_ASSERT_MSG(v1_impl_, + "v1 ReqCollationProtocolImpl must be initialized!"); + return v1_impl_->doRequest( + peer_id, std::move(request), std::move(response_handler)); + } + + void ReqCollationProtocol::request( + const PeerId &peer_id, + vstaging::CollationFetchingRequest request, + std::function)> + &&response_handler) { + BOOST_ASSERT_MSG(vstaging_impl_, + "vstaging ReqCollationProtocolImpl must be initialized!"); + return vstaging_impl_->doRequest( peer_id, std::move(request), std::move(response_handler)); } diff --git a/core/network/impl/protocols/protocol_req_collation.hpp b/core/network/impl/protocols/protocol_req_collation.hpp index b2a070af33..9c7b0b7872 100644 --- a/core/network/impl/protocols/protocol_req_collation.hpp +++ b/core/network/impl/protocols/protocol_req_collation.hpp @@ -19,7 +19,7 @@ #include "network/impl/stream_engine.hpp" #include "network/peer_manager.hpp" #include "network/protocols/req_collation_protocol.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "network/types/roles.hpp" #include "utils/non_copyable.hpp" @@ -29,6 +29,7 @@ namespace kagome::blockchain { namespace kagome::network { + template struct ReqCollationProtocolImpl; class ReqCollationProtocol final : public IReqCollationProtocol, @@ -58,8 +59,20 @@ namespace kagome::network { std::function)> &&response_handler) override; + void request(const PeerId &peer_id, + vstaging::CollationFetchingRequest request, + std::function< + void(outcome::result)> + &&response_handler) override; + private: - std::shared_ptr impl_; + std::shared_ptr> + v1_impl_; + std::shared_ptr< + ReqCollationProtocolImpl> + vstaging_impl_; }; } // namespace kagome::network diff --git a/core/network/impl/router_libp2p.cpp b/core/network/impl/router_libp2p.cpp index d52eebc2ae..240918721d 100644 --- a/core/network/impl/router_libp2p.cpp +++ b/core/network/impl/router_libp2p.cpp @@ -24,13 +24,16 @@ namespace kagome::network { LazySPtr propagate_transactions_protocol, LazySPtr validation_protocol, LazySPtr collation_protocol, + LazySPtr collation_protocol_vstaging, + LazySPtr validation_protocol_vstaging, LazySPtr req_collation_protocol, LazySPtr req_pov_protocol, LazySPtr fetch_chunk_protocol, LazySPtr fetch_available_data_protocol, LazySPtr statement_fetching_protocol, LazySPtr send_dispute_protocol, - LazySPtr ping_protocol) + LazySPtr ping_protocol, + LazySPtr fetch_attested_candidate) : app_state_manager_{app_state_manager}, host_{host}, app_config_(app_config), @@ -48,6 +51,8 @@ namespace kagome::network { std::move(propagate_transactions_protocol)), validation_protocol_(std::move(validation_protocol)), collation_protocol_(std::move(collation_protocol)), + collation_protocol_vstaging_(std::move(collation_protocol_vstaging)), + validation_protocol_vstaging_(std::move(validation_protocol_vstaging)), req_collation_protocol_(std::move(req_collation_protocol)), req_pov_protocol_(std::move(req_pov_protocol)), fetch_chunk_protocol_(std::move(fetch_chunk_protocol)), @@ -56,6 +61,7 @@ namespace kagome::network { statement_fetching_protocol_(std::move(statement_fetching_protocol)), send_dispute_protocol_(std::move(send_dispute_protocol)), ping_protocol_{std::move(ping_protocol)}, + fetch_attested_candidate_{std::move(fetch_attested_candidate)}, log_{log::createLogger("RouterLibp2p", "network")} { BOOST_ASSERT(app_state_manager_ != nullptr); @@ -78,25 +84,28 @@ namespace kagome::network { bool RouterLibp2p::prepare() { app_state_manager_->takeControl(*block_announce_protocol_.get()); app_state_manager_->takeControl(*grandpa_protocol_.get()); - app_state_manager_->takeControl(*sync_protocol_.get()); app_state_manager_->takeControl(*state_protocol_.get()); app_state_manager_->takeControl(*warp_protocol_.get()); app_state_manager_->takeControl(*beefy_protocol_.get()); app_state_manager_->takeControl(*beefy_justifications_protocol_.get()); app_state_manager_->takeControl(*light_protocol_.get()); - app_state_manager_->takeControl(*propagate_transactions_protocol_.get()); - app_state_manager_->takeControl(*collation_protocol_.get()); - app_state_manager_->takeControl(*validation_protocol_.get()); + /// TODO(iceseer): https://github.com/qdrvm/kagome/issues/1989 + /// should be uncommented when this task will be implemented + /// app_state_manager_->takeControl(*collation_protocol_.get()); + /// app_state_manager_->takeControl(*validation_protocol_.get()); + + app_state_manager_->takeControl(*collation_protocol_vstaging_.get()); + app_state_manager_->takeControl(*validation_protocol_vstaging_.get()); app_state_manager_->takeControl(*req_collation_protocol_.get()); app_state_manager_->takeControl(*req_pov_protocol_.get()); app_state_manager_->takeControl(*fetch_chunk_protocol_.get()); app_state_manager_->takeControl(*fetch_available_data_protocol_.get()); app_state_manager_->takeControl(*statement_fetching_protocol_.get()); - app_state_manager_->takeControl(*send_dispute_protocol_.get()); + app_state_manager_->takeControl(*fetch_attested_candidate_.get()); host_.setProtocolHandler( {ping_protocol_.get()->getProtocolId()}, @@ -194,11 +203,21 @@ namespace kagome::network { return collation_protocol_.get(); } + std::shared_ptr + RouterLibp2p::getCollationProtocolVStaging() const { + return collation_protocol_vstaging_.get(); + } + std::shared_ptr RouterLibp2p::getValidationProtocol() const { return validation_protocol_.get(); } + std::shared_ptr + RouterLibp2p::getValidationProtocolVStaging() const { + return validation_protocol_vstaging_.get(); + } + std::shared_ptr RouterLibp2p::getReqCollationProtocol() const { return req_collation_protocol_.get(); @@ -213,6 +232,11 @@ namespace kagome::network { return fetch_chunk_protocol_.get(); } + std::shared_ptr + RouterLibp2p::getFetchAttestedCandidateProtocol() const { + return fetch_attested_candidate_.get(); + } + std::shared_ptr RouterLibp2p::getFetchAvailableDataProtocol() const { return fetch_available_data_protocol_.get(); diff --git a/core/network/impl/router_libp2p.hpp b/core/network/impl/router_libp2p.hpp index 7fd4c74244..5c8c997c2a 100644 --- a/core/network/impl/router_libp2p.hpp +++ b/core/network/impl/router_libp2p.hpp @@ -50,13 +50,16 @@ namespace kagome::network { LazySPtr propagate_transactions_protocol, LazySPtr validation_protocol, LazySPtr collation_protocol, + LazySPtr collation_protocol_vstaging, + LazySPtr validation_protocol_vstaging, LazySPtr req_collation_protocol, LazySPtr req_pov_protocol, LazySPtr fetch_chunk_protocol, LazySPtr fetch_available_data_protocol, LazySPtr statement_fetching_protocol, LazySPtr send_dispute_protocol, - LazySPtr ping_protocol); + LazySPtr ping_protocol, + LazySPtr fetch_attested_candidate); ~RouterLibp2p() override = default; @@ -80,11 +83,17 @@ namespace kagome::network { getPropagateTransactionsProtocol() const override; std::shared_ptr getCollationProtocol() const override; + std::shared_ptr getCollationProtocolVStaging() + const override; std::shared_ptr getValidationProtocol() const override; + std::shared_ptr getValidationProtocolVStaging() + const override; std::shared_ptr getReqCollationProtocol() const override; std::shared_ptr getReqPovProtocol() const override; std::shared_ptr getFetchChunkProtocol() const override; + std::shared_ptr + getFetchAttestedCandidateProtocol() const override; std::shared_ptr getFetchAvailableDataProtocol() const override; std::shared_ptr getFetchStatementProtocol() @@ -127,6 +136,8 @@ namespace kagome::network { LazySPtr validation_protocol_; LazySPtr collation_protocol_; + LazySPtr collation_protocol_vstaging_; + LazySPtr validation_protocol_vstaging_; LazySPtr req_collation_protocol_; LazySPtr req_pov_protocol_; LazySPtr fetch_chunk_protocol_; @@ -136,6 +147,7 @@ namespace kagome::network { LazySPtr send_dispute_protocol_; LazySPtr ping_protocol_; + LazySPtr fetch_attested_candidate_; log::Logger log_; }; diff --git a/core/network/impl/stream_engine.hpp b/core/network/impl/stream_engine.hpp index dee0a75a91..562ab1d174 100644 --- a/core/network/impl/stream_engine.hpp +++ b/core/network/impl/stream_engine.hpp @@ -303,7 +303,7 @@ namespace kagome::network { protocol->protocolName(), peer_id); } else { - SL_DEBUG(self->logger_, + SL_TRACE(self->logger_, "Could not send message to {} stream with {}: {}", protocol->protocolName(), peer_id, diff --git a/core/network/impl/sync_protocol_observer_impl.cpp b/core/network/impl/sync_protocol_observer_impl.cpp index d4a5346b43..ad80aa2cf6 100644 --- a/core/network/impl/sync_protocol_observer_impl.cpp +++ b/core/network/impl/sync_protocol_observer_impl.cpp @@ -201,7 +201,7 @@ namespace kagome::network { if (auto r = beefy_->getJustification(*number)) { if (auto &opt = r.value()) { new_block.beefy_justification = primitives::Justification{ - common::Buffer{scale::encode(*opt).value()}, + common::Buffer{::scale::encode(*opt).value()}, }; } } diff --git a/core/network/peer_manager.hpp b/core/network/peer_manager.hpp index c3fc71dfbf..eb4559ef5c 100644 --- a/core/network/peer_manager.hpp +++ b/core/network/peer_manager.hpp @@ -14,7 +14,7 @@ #include "network/types/block_announce.hpp" #include "network/types/block_announce_handshake.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "network/types/grandpa_message.hpp" #include "outcome/outcome.hpp" #include "primitives/common.hpp" @@ -25,21 +25,21 @@ namespace kagome::network { constexpr size_t kPeerStateMaxKnownBlocks = 1024; constexpr size_t kPeerStateMaxKnownGrandpaMessages = 8192; - struct CollatorState { - network::ParachainId parachain_id; - network::CollatorPublicKey collator_id; - }; - - struct PendingCollation { - RelayHash relay_parent; + struct CollatingPeerState { network::ParachainId para_id; - libp2p::peer::PeerId peer_id; - std::optional commitments_hash{}; + network::CollatorPublicKey collator_id; + std::unordered_map> advertisements; + std::chrono::system_clock::time_point last_active; }; struct CollationEvent { CollatorId collator_id; - PendingCollation pending_collation; + struct { + RelayHash relay_parent; + network::ParachainId para_id; + libp2p::peer::PeerId peer_id; + std::optional commitments_hash; + } pending_collation; }; using OurView = network::View; @@ -57,13 +57,32 @@ namespace kagome::network { std::optional round_number = std::nullopt; std::optional set_id = std::nullopt; BlockNumber last_finalized = 0; - std::optional collator_state = std::nullopt; + std::optional collator_state = std::nullopt; std::optional view; + CollationVersion version; LruSet known_blocks{kPeerStateMaxKnownBlocks}; LruSet known_grandpa_messages{ kPeerStateMaxKnownGrandpaMessages, }; + bool hasAdvertised( + const RelayHash &relay_parent, + const std::optional &maybe_candidate_hash) const { + if (!collator_state) { + return false; + } + + const auto &collating_state = *collator_state; + if (maybe_candidate_hash) { + if (auto it = collating_state.advertisements.find(relay_parent); + it != collating_state.advertisements.end()) { + return it->second.contains(*maybe_candidate_hash); + } + return false; + } + return collating_state.advertisements.contains(relay_parent); + } + PeerStateCompact compact() const { return PeerStateCompact{ .round_number = round_number, @@ -136,6 +155,9 @@ namespace kagome::network { virtual void updatePeerState(const PeerId &peer_id, const BlockAnnounceHandshake &handshake) = 0; + virtual std::optional> + createDefaultPeerState(const PeerId &peer_id) = 0; + /** * Updates known data about peer with {@param peer_id} by {@param announce} */ diff --git a/core/network/peer_view.hpp b/core/network/peer_view.hpp index 9dea350c45..247ee308ad 100644 --- a/core/network/peer_view.hpp +++ b/core/network/peer_view.hpp @@ -12,6 +12,7 @@ #include "application/app_state_manager.hpp" #include "blockchain/block_tree.hpp" +#include "crypto/type_hasher.hpp" #include "injector/lazy.hpp" #include "network/types/collator_messages.hpp" #include "outcome/outcome.hpp" @@ -26,6 +27,18 @@ namespace kagome::blockchain { namespace kagome::network { + using HashedBlockHeader = primitives::BlockHeader; + struct ExView { + View view; + HashedBlockHeader new_head; + std::vector lost; + }; + + struct ExViewRef { + std::optional> new_head; + const std::vector &lost; + }; + /** * Observable class for current heads and finalized block number tracking. */ diff --git a/core/network/protocols/req_collation_protocol.hpp b/core/network/protocols/req_collation_protocol.hpp index 1b80376000..0356b4ca1c 100644 --- a/core/network/protocols/req_collation_protocol.hpp +++ b/core/network/protocols/req_collation_protocol.hpp @@ -13,7 +13,7 @@ #include "application/chain_spec.hpp" #include "log/logger.hpp" #include "network/req_collation_observer.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" namespace kagome::network { @@ -24,6 +24,13 @@ namespace kagome::network { CollationFetchingRequest request, std::function)> &&response_handler) = 0; + + virtual void request( + const PeerId &peer_id, + vstaging::CollationFetchingRequest request, + std::function< + void(outcome::result)> + &&response_handler) = 0; }; } // namespace kagome::network diff --git a/core/network/req_collation_observer.hpp b/core/network/req_collation_observer.hpp index 9cf48969a6..8871c9abd7 100644 --- a/core/network/req_collation_observer.hpp +++ b/core/network/req_collation_observer.hpp @@ -9,7 +9,7 @@ #include #include "consensus/grandpa/common.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "primitives/common.hpp" namespace kagome::network { @@ -26,5 +26,7 @@ namespace kagome::network { */ virtual outcome::result OnCollationRequest( CollationFetchingRequest request) = 0; + virtual outcome::result + OnCollationRequest(vstaging::CollationFetchingRequest request) = 0; }; } // namespace kagome::network diff --git a/core/network/router.hpp b/core/network/router.hpp index c96ad95505..ac54aa2fb3 100644 --- a/core/network/router.hpp +++ b/core/network/router.hpp @@ -10,6 +10,7 @@ #include #include "network/impl/protocols/block_announce_protocol.hpp" +#include "network/impl/protocols/fetch_attested_candidate.hpp" #include "network/impl/protocols/grandpa_protocol.hpp" #include "network/impl/protocols/parachain_protocols.hpp" #include "network/impl/protocols/propagate_transactions_protocol.hpp" @@ -35,13 +36,19 @@ namespace kagome::network { virtual std::shared_ptr getBlockAnnounceProtocol() const = 0; virtual std::shared_ptr getCollationProtocol() const = 0; + virtual std::shared_ptr + getCollationProtocolVStaging() const = 0; virtual std::shared_ptr getValidationProtocol() const = 0; + virtual std::shared_ptr + getValidationProtocolVStaging() const = 0; virtual std::shared_ptr getReqCollationProtocol() const = 0; virtual std::shared_ptr getReqPovProtocol() const = 0; virtual std::shared_ptr getFetchChunkProtocol() const = 0; + virtual std::shared_ptr + getFetchAttestedCandidateProtocol() const = 0; virtual std::shared_ptr getFetchAvailableDataProtocol() const = 0; virtual std::shared_ptr diff --git a/core/network/types/collator_messages.hpp b/core/network/types/collator_messages.hpp index 112450dbb3..7079179232 100644 --- a/core/network/types/collator_messages.hpp +++ b/core/network/types/collator_messages.hpp @@ -86,41 +86,6 @@ namespace kagome::network { using RequestPov = CandidateHash; using ResponsePov = boost::variant; - /** - * Unique descriptor of a candidate receipt. - */ - struct CandidateDescriptor { - SCALE_TIE(9); - - ParachainId para_id; /// Parachain Id - primitives::BlockHash - relay_parent; /// Hash of the relay chain block the candidate is - /// executed in the context of - CollatorPublicKey collator_id; /// Collators public key. - primitives::BlockHash - persisted_data_hash; /// Hash of the persisted validation data - primitives::BlockHash pov_hash; /// Hash of the PoV block. - storage::trie::RootHash - erasure_encoding_root; /// Root of the block’s erasure encoding Merkle - /// tree. - Signature signature; /// Collator signature of the concatenated components - primitives::BlockHash - para_head_hash; /// Hash of the parachain head data of this candidate. - primitives::BlockHash - validation_code_hash; /// Hash of the parachain Runtime. - - common::Buffer signable() const { - return common::Buffer{ - ::scale::encode(relay_parent, - para_id, - persisted_data_hash, - pov_hash, - validation_code_hash) - .value(), - }; - } - }; - /** * Contains information about the candidate and a proof of the results of its * execution. @@ -209,48 +174,6 @@ namespace kagome::network { using FetchAvailableDataResponse = boost::variant; - struct OutboundHorizontal { - SCALE_TIE(2); - - ParachainId para_id; /// Parachain Id is recepient id - UpwardMessage upward_msg; /// upward message for parallel parachain - }; - - struct InboundDownwardMessage { - SCALE_TIE(2); - /// The block number at which these messages were put into the downward - /// message queue. - BlockNumber sent_at; - /// The actual downward message to processes. - DownwardMessage msg; - }; - - struct InboundHrmpMessage { - SCALE_TIE(2); - /// The block number at which this message was sent. - /// Specifically, it is the block number at which the candidate that sends - /// this message was enacted. - BlockNumber sent_at; - /// The message payload. - common::Buffer data; - }; - - struct CandidateCommitments { - SCALE_TIE(6); - - std::vector upward_msgs; /// upward messages - std::vector - outbound_hor_msgs; /// outbound horizontal messages - std::optional - opt_para_runtime; /// new parachain runtime if present - HeadData para_head; /// parachain head data - uint32_t downward_msgs_count; /// number of downward messages that were - /// processed by the parachain - BlockNumber watermark; /// watermark which specifies the relay chain block - /// number up to which all inbound horizontal messages - /// have been processed - }; - struct CommittedCandidateReceipt { SCALE_TIE(2); @@ -296,7 +219,8 @@ namespace kagome::network { struct Statement { SCALE_TIE(1); - + Statement() = default; + Statement(CandidateState &&c) : candidate_state{std::move(c)} {} CandidateState candidate_state{Unused<0>{}}; }; using SignedStatement = IndexedAndSigned; @@ -353,12 +277,6 @@ namespace kagome::network { } }; - struct ExView { - View view; - primitives::BlockHeader new_head; - std::vector lost; - }; - using LargeStatement = parachain::IndexedAndSigned; using StatementDistributionMessage = boost::variant; @@ -505,25 +423,13 @@ namespace kagome::network { std::optional collator; }; - /** - * Common WireMessage that represents messages in NetworkBridge. - */ - template - using WireMessage = boost::variant< - Dummy, /// not used - std::enable_if_t::allowed, - T>, /// protocol message - ViewUpdate /// view update message - >; - inline CandidateHash candidateHash(const crypto::Hasher &hasher, const CommittedCandidateReceipt &receipt) { auto commitments_hash = hasher.blake2b_256(scale::encode(receipt.commitments).value()); return hasher.blake2b_256( - ::scale::encode(std::tie(receipt.descriptor, commitments_hash)).value()); + ::scale::encode(std::tie(receipt.descriptor, commitments_hash)) + .value()); } inline CandidateHash candidateHash(const crypto::Hasher &hasher, diff --git a/core/network/types/collator_messages_vstaging.hpp b/core/network/types/collator_messages_vstaging.hpp new file mode 100644 index 0000000000..9d1a47b428 --- /dev/null +++ b/core/network/types/collator_messages_vstaging.hpp @@ -0,0 +1,239 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "common/blob.hpp" +#include "consensus/grandpa/common.hpp" +#include "crypto/hasher.hpp" +#include "crypto/sr25519_types.hpp" +#include "network/types/collator_messages.hpp" +#include "parachain/approval/approval.hpp" +#include "parachain/types.hpp" +#include "primitives/block_header.hpp" +#include "primitives/common.hpp" +#include "primitives/compact_integer.hpp" +#include "primitives/digest.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" +#include "scale/tie.hpp" +#include "storage/trie/types.hpp" + +namespace kagome::network::vstaging { + using Dummy = network::Dummy; + using Empty = network::Empty; + + using CollatorProtocolMessageDeclare = network::CollatorDeclaration; + using CollatorProtocolMessageCollationSeconded = network::Seconded; + using BitfieldDistributionMessage = network::BitfieldDistributionMessage; + using BitfieldDistribution = network::BitfieldDistribution; + using ApprovalDistributionMessage = network::ApprovalDistributionMessage; + using ViewUpdate = network::ViewUpdate; + + struct CollatorProtocolMessageAdvertiseCollation { + SCALE_TIE(3); + /// Hash of the relay parent advertised collation is based on. + RelayHash relay_parent; + /// Candidate hash. + CandidateHash candidate_hash; + /// Parachain head data hash before candidate execution. + Hash parent_head_data_hash; + }; + + using CollationMessage = + boost::variant; + + using CollatorProtocolMessage = boost::variant; + + struct SecondedCandidateHash { + SCALE_TIE(1); + CandidateHash hash; + }; + + struct ValidCandidateHash { + SCALE_TIE(1); + CandidateHash hash; + }; + + struct CompactStatement { + std::array header = {'B', 'K', 'N', 'G'}; + boost::variant + inner_value{}; + + CompactStatement( + boost::variant &&val) + : inner_value{std::move(val)} {} + CompactStatement(ValidCandidateHash &&val) : inner_value{std::move(val)} {} + CompactStatement(SecondedCandidateHash &&val) + : inner_value{std::move(val)} {} + CompactStatement() = default; + + friend inline ::scale::ScaleEncoderStream &operator<<( + ::scale::ScaleEncoderStream &s, const CompactStatement &c) { + s << c.header << c.inner_value; + return s; + } + + friend inline ::scale::ScaleDecoderStream &operator>>( + ::scale::ScaleDecoderStream &s, CompactStatement &c) { + s >> c.header >> c.inner_value; + return s; + } + }; + + inline const CandidateHash &candidateHash(const CompactStatement &val) { + auto p = visit_in_place( + val.inner_value, + [&](const auto &v) + -> std::optional> { + return {{v.hash}}; + }, + [](const Empty &) + -> std::optional> { + return {}; + }); + if (p) { + return p->get(); + } + UNREACHABLE; + } + + struct StatementDistributionMessageStatement { + SCALE_TIE(2); + + RelayHash relay_parent; + IndexedAndSigned compact; + }; + + using v1StatementDistributionMessage = network::StatementDistributionMessage; + + struct StatementFilter { + SCALE_TIE(2); + + /// Seconded statements. '1' is known or undesired. + scale::BitVec seconded_in_group; + /// Valid statements. '1' is known or undesired. + scale::BitVec validated_in_group; + + StatementFilter() = default; + StatementFilter(size_t len) { + seconded_in_group.bits.assign(len, false); + validated_in_group.bits.assign(len, false); + } + }; + + struct BackedCandidateManifest { + SCALE_TIE(6); + RelayHash relay_parent; + CandidateHash candidate_hash; + GroupIndex group_index; + ParachainId para_id; + Hash parent_head_data_hash; + /// A statement filter which indicates which validators in the + /// para's group at the relay-parent have validated this candidate + /// and issued statements about it, to the advertiser's knowledge. + /// + /// This MUST have exactly the minimum amount of bytes + /// necessary to represent the number of validators in the assigned + /// backing group as-of the relay-parent. + StatementFilter statement_knowledge; + }; + + struct AttestedCandidateRequest { + SCALE_TIE(2); + CandidateHash candidate_hash; + StatementFilter mask; + }; + + struct AttestedCandidateResponse { + SCALE_TIE(3); + CommittedCandidateReceipt candidate_receipt; + runtime::PersistedValidationData persisted_validation_data; + std::vector> statements; + }; + + struct BackedCandidateAcknowledgement { + SCALE_TIE(2); + CandidateHash candidate_hash; + StatementFilter statement_knowledge; + }; + + using StatementDistributionMessage = + boost::variant; + + struct CollationFetchingRequest { + SCALE_TIE(3); + /// Relay parent collation is built on top of. + RelayHash relay_parent; + /// The `ParaId` of the collation. + ParachainId para_id; + /// Candidate hash. + CandidateHash candidate_hash; + }; + + using CollationFetchingResponse = network::CollationFetchingResponse; + + using ValidatorProtocolMessage = boost::variant< + Dummy, /// NU + BitfieldDistributionMessage, /// bitfield distribution message + Dummy, /// NU + StatementDistributionMessage, /// statement distribution message + ApprovalDistributionMessage /// approval distribution message + >; + +} // namespace kagome::network::vstaging + +namespace kagome::network { + + enum CollationVersion { + /// The first version. + V1 = 1, + /// The staging version. + VStaging = 2, + }; + + /** + * Common WireMessage that represents messages in NetworkBridge. + */ + template + using WireMessage = boost::variant< + Dummy, /// not used + std::enable_if_t< + AllowerTypeChecker::allowed, + T>, /// protocol message + ViewUpdate /// view update message + >; + + template + using Versioned = boost::variant; + + using VersionedCollatorProtocolMessage = + Versioned; + using VersionedValidatorProtocolMessage = + Versioned; + using VersionedStatementDistributionMessage = + Versioned; +} // namespace kagome::network diff --git a/core/network/validation_observer.hpp b/core/network/validation_observer.hpp index 5da0d26f9c..51d3593149 100644 --- a/core/network/validation_observer.hpp +++ b/core/network/validation_observer.hpp @@ -8,7 +8,7 @@ #include -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "primitives/common.hpp" namespace kagome::network { @@ -21,11 +21,12 @@ namespace kagome::network { /// Handle incoming validation stream. virtual void onIncomingValidationStream( - const libp2p::peer::PeerId &peer_id) = 0; + const libp2p::peer::PeerId &peer_id, + network::CollationVersion version) = 0; /// Handle incoming collation message. virtual void onIncomingMessage( const libp2p::peer::PeerId &peer_id, - ValidatorProtocolMessage &&validation_message) = 0; + network::VersionedValidatorProtocolMessage &&validation_message) = 0; }; } // namespace kagome::network diff --git a/core/parachain/CMakeLists.txt b/core/parachain/CMakeLists.txt index f6ec94a5e3..4645830516 100644 --- a/core/parachain/CMakeLists.txt +++ b/core/parachain/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(validator_parachain approval/approval_distribution_error.cpp approval/approval.cpp backing/store_impl.cpp + validator/impl/fragment_tree.cpp + validator/backing_implicit_view.cpp ) target_link_libraries(validator_parachain diff --git a/core/parachain/approval/approval_distribution.cpp b/core/parachain/approval/approval_distribution.cpp index 062f19511f..a9619409b1 100644 --- a/core/parachain/approval/approval_distribution.cpp +++ b/core/parachain/approval/approval_distribution.cpp @@ -1210,7 +1210,7 @@ namespace kagome::parachain { std::move(block_info.value()))); }}); - imported_block_info(head, std::move(updated.new_head)); + imported_block_info(head, updated.new_head); } void ApprovalDistribution::on_active_leaves_update( @@ -2023,83 +2023,91 @@ namespace kagome::parachain { void ApprovalDistribution::onValidationProtocolMsg( const libp2p::peer::PeerId &peer_id, - const network::ValidatorProtocolMessage &message) { + const network::VersionedValidatorProtocolMessage &message) { REINVOKE( *approval_thread_handler_, onValidationProtocolMsg, peer_id, message); if (!parachain_processor_->canProcessParachains()) { return; } - if (auto m{boost::get(&message)}) { - visit_in_place( - *m, - [&](const network::Assignments &assignments) { - SL_TRACE(logger_, - "Received assignments.(peer_id={}, count={})", - peer_id, - assignments.assignments.size()); - DeferedSender assignment_defered_sender{ - [wself{weak_from_this()}](auto &&msgs) { - if (auto self = wself.lock()) { - self->runDistributeAssignment(std::move(msgs)); - } - }}; - for (auto const &assignment : assignments.assignments) { - if (auto it = pending_known_.find( - assignment.indirect_assignment_cert.block_hash); - it != pending_known_.end()) { - SL_TRACE(logger_, - "Pending assignment.(block hash={}, claimed index={}, " - "validator={}, peer={})", - assignment.indirect_assignment_cert.block_hash, - assignment.candidate_ix, - assignment.indirect_assignment_cert.validator, - peer_id); - it->second.emplace_back( - std::make_pair(peer_id, PendingMessage{assignment})); - continue; - } - import_and_circulate_assignment( - peer_id, - assignment_defered_sender, - assignment.indirect_assignment_cert, - assignment.candidate_ix); + std::optional< + std::reference_wrapper> + m = visit_in_place(message, [](const auto &val) { + return if_type(val); + }); + + if (!m) { + return; + } + + visit_in_place( + m->get(), + [&](const network::Assignments &assignments) { + SL_TRACE(logger_, + "Received assignments.(peer_id={}, count={})", + peer_id, + assignments.assignments.size()); + DeferedSender assignment_defered_sender{ + [wself{weak_from_this()}](auto &&msgs) { + if (auto self = wself.lock()) { + self->runDistributeAssignment(std::move(msgs)); + } + }}; + for (auto const &assignment : assignments.assignments) { + if (auto it = pending_known_.find( + assignment.indirect_assignment_cert.block_hash); + it != pending_known_.end()) { + SL_TRACE(logger_, + "Pending assignment.(block hash={}, claimed index={}, " + "validator={}, peer={})", + assignment.indirect_assignment_cert.block_hash, + assignment.candidate_ix, + assignment.indirect_assignment_cert.validator, + peer_id); + it->second.emplace_back( + std::make_pair(peer_id, PendingMessage{assignment})); + continue; } - }, - [&](const network::Approvals &approvals) { - SL_TRACE(logger_, - "Received approvals.(peer_id={}, count={})", - peer_id, - approvals.approvals.size()); - DeferedSender - approval_defered_sender{[wself{weak_from_this()}](auto &&msgs) { - if (auto self = wself.lock()) { - self->runDistributeApproval(std::move(msgs)); - } - }}; - for (auto const &approval_vote : approvals.approvals) { - if (auto it = pending_known_.find( - approval_vote.payload.payload.block_hash); - it != pending_known_.end()) { - SL_TRACE(logger_, - "Pending approval.(block hash={}, candidate index={}, " - "validator={}, peer={})", - approval_vote.payload.payload.block_hash, - approval_vote.payload.payload.candidate_index, - approval_vote.payload.ix, - peer_id); - it->second.emplace_back( - std::make_pair(peer_id, PendingMessage{approval_vote})); - continue; - } - import_and_circulate_approval( - peer_id, approval_defered_sender, approval_vote); + import_and_circulate_assignment(peer_id, + assignment_defered_sender, + assignment.indirect_assignment_cert, + assignment.candidate_ix); + } + }, + [&](const network::Approvals &approvals) { + SL_TRACE(logger_, + "Received approvals.(peer_id={}, count={})", + peer_id, + approvals.approvals.size()); + DeferedSender + approval_defered_sender{[wself{weak_from_this()}](auto &&msgs) { + if (auto self = wself.lock()) { + self->runDistributeApproval(std::move(msgs)); + } + }}; + for (auto const &approval_vote : approvals.approvals) { + if (auto it = pending_known_.find( + approval_vote.payload.payload.block_hash); + it != pending_known_.end()) { + SL_TRACE(logger_, + "Pending approval.(block hash={}, candidate index={}, " + "validator={}, peer={})", + approval_vote.payload.payload.block_hash, + approval_vote.payload.payload.candidate_index, + approval_vote.payload.ix, + peer_id); + it->second.emplace_back( + std::make_pair(peer_id, PendingMessage{approval_vote})); + continue; } - }, - [&](const auto &) { UNREACHABLE; }); - } + + import_and_circulate_approval( + peer_id, approval_defered_sender, approval_vote); + } + }, + [&](const auto &) { UNREACHABLE; }); } void ApprovalDistribution::runDistributeAssignment( @@ -2153,7 +2161,7 @@ namespace kagome::parachain { .assignments = std::vector(begin, end), }}); - se->send(peer_id, router_->getValidationProtocol(), msg); + se->send(peer_id, router_->getValidationProtocolVStaging(), msg); assignments.erase(begin, end); } } @@ -2201,7 +2209,7 @@ namespace kagome::parachain { std::vector(begin, end), }}); - se->send(peer_id, router_->getValidationProtocol(), msg); + se->send(peer_id, router_->getValidationProtocolVStaging(), msg); approvals.erase(begin, end); } } @@ -2555,7 +2563,6 @@ namespace kagome::parachain { if (is_block_approved && !was_block_approved) { notifyApproved(block_hash); } - /// TODO(iceseer): store in database if needed } _ = std::make_pair(is_approved, std::move(status)); ae = approval_entry; @@ -2585,7 +2592,6 @@ namespace kagome::parachain { status.required_tranches); if (approval::is_local_approval(transition) || newly_approved || (already_approved_by && !*already_approved_by)) { - /// TODO(iceseer): store in database if needed BOOST_ASSERT(storedCandidateEntries().get(candidate_hash)->get() == candidate_entry); } diff --git a/core/parachain/approval/approval_distribution.hpp b/core/parachain/approval/approval_distribution.hpp index dd69cd1cc8..18b68b727f 100644 --- a/core/parachain/approval/approval_distribution.hpp +++ b/core/parachain/approval/approval_distribution.hpp @@ -27,7 +27,7 @@ #include "dispute_coordinator/dispute_coordinator.hpp" #include "injector/lazy.hpp" #include "network/peer_view.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "parachain/approval/approved_ancestor.hpp" #include "parachain/approval/knowledge.hpp" #include "parachain/approval/store.hpp" @@ -302,7 +302,7 @@ namespace kagome::parachain { void onValidationProtocolMsg( const libp2p::peer::PeerId &peer_id, - const network::ValidatorProtocolMessage &message); + const network::VersionedValidatorProtocolMessage &message); using SignaturesForCandidate = std::unordered_map; diff --git a/core/parachain/availability/bitfield/store_impl.cpp b/core/parachain/availability/bitfield/store_impl.cpp index 270c5a9e6d..3bba125540 100644 --- a/core/parachain/availability/bitfield/store_impl.cpp +++ b/core/parachain/availability/bitfield/store_impl.cpp @@ -64,10 +64,10 @@ namespace kagome::parachain { } if (bf.payload.payload.bits[ix]) { - SL_DEBUG(logger_, - "dropping invalid bitfield - bit is set for an unoccupied " - "core.(relay_parent={})", - relay_parent); + SL_INFO(logger_, + "dropping invalid bitfield - bit is set for an unoccupied " + "core.(relay_parent={})", + relay_parent); skip = true; break; } diff --git a/core/parachain/backing/store.hpp b/core/parachain/backing/store.hpp index 7bf01a7b1b..476d5c5589 100644 --- a/core/parachain/backing/store.hpp +++ b/core/parachain/backing/store.hpp @@ -29,32 +29,36 @@ namespace kagome::parachain { uint64_t validity_votes; }; - using ValidityVoteIssued = Tagged; - using ValidityVoteValid = Tagged; + using ValidityVoteIssued = Tagged; + using ValidityVoteValid = Tagged; using ValidityVote = boost::variant; - using StatementInfo = - std::pair>; + struct StatementInfo { + network::ParachainId group_id; + network::CommittedCandidateReceipt candidate; + std::map validity_votes; + }; virtual ~BackingStore() = default; virtual std::optional put( + const RelayHash &relay_parent, const std::unordered_map> &groups, - Statement statement) = 0; + Statement statement, + bool allow_multiple_seconded) = 0; virtual std::vector get( const BlockHash &relay_parent) const = 0; - virtual void remove(const BlockHash &relay_parent) = 0; + virtual void onActivateLeaf(const RelayHash &relay_parent) = 0; + virtual void onDeactivateLeaf(const RelayHash &relay_parent) = 0; virtual void add(const BlockHash &relay_parent, BackedCandidate &&candidate) = 0; - virtual std::optional get_candidate( - const network::CandidateHash &candidate_hash) const = 0; - virtual std::optional> - get_validity_votes(const network::CandidateHash &candidate_hash) const = 0; + getCadidateInfo(const RelayHash &relay_parent, + const network::CandidateHash &candidate_hash) const = 0; }; } // namespace kagome::parachain diff --git a/core/parachain/backing/store_impl.cpp b/core/parachain/backing/store_impl.cpp index e814481f2e..2d9ad05309 100644 --- a/core/parachain/backing/store_impl.cpp +++ b/core/parachain/backing/store_impl.cpp @@ -6,6 +6,21 @@ #include "parachain/backing/store_impl.hpp" +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain, BackingStoreImpl::Error, e) { + using E = decltype(e); + switch (e) { + case E::UNAUTHORIZED_STATEMENT: + return "Unauthorized statement"; + case E::DOUBLE_VOTE: + return "Double vote"; + case E::MULTIPLE_CANDIDATES: + return "Multiple candidates"; + case E::CRITICAL_ERROR: + return "Critical error"; + } + return "unknown error (invalid BackingStoreImpl::Error"; +} + namespace kagome::parachain { using network::BackedCandidate; using network::CommittedCandidateReceipt; @@ -14,14 +29,12 @@ namespace kagome::parachain { BackingStoreImpl::BackingStoreImpl(std::shared_ptr hasher) : hasher_{std::move(hasher)} {} - void BackingStoreImpl::remove(const BlockHash &relay_parent) { - backed_candidates_.erase(relay_parent); - if (auto it = candidates_.find(relay_parent); it != candidates_.end()) { - for (const auto &candidate : it->second) { - statements_.erase(candidate); - } - candidates_.erase(it); - } + void BackingStoreImpl::onDeactivateLeaf(const BlockHash &relay_parent) { + per_relay_parent_.erase(relay_parent); + } + + void BackingStoreImpl::onActivateLeaf(const BlockHash &relay_parent) { + std::ignore = per_relay_parent_[relay_parent]; } bool BackingStoreImpl::is_in_group( @@ -39,85 +52,171 @@ namespace kagome::parachain { return false; } - std::optional BackingStoreImpl::put( + outcome::result> + BackingStoreImpl::validity_vote( + PerRelayParent &state, const std::unordered_map> &groups, - Statement statement) { - auto candidate_hash = - candidateHash(*hasher_, statement.payload.payload.candidate_state); - StatementInfo *s{nullptr}; - if (auto s_it = statements_.find(candidate_hash); - s_it != statements_.end()) { - if (!is_in_group(groups, s_it->second.first, statement.payload.ix)) { - return std::nullopt; - } - s = &s_it->second; - s->second.emplace(statement.payload.ix, - ValidityVoteValid{std::move(statement)}); - } else if (auto seconded{boost::get( - &statement.payload.payload.candidate_state)}) { - const auto group = seconded->descriptor.para_id; - if (!is_in_group(groups, group, statement.payload.ix)) { - return std::nullopt; - } + ValidatorIndex from, + const CandidateHash &digest, + const ValidityVote &vote) { + auto it = state.candidate_votes_.find(digest); + if (it == state.candidate_votes_.end()) { + return std::nullopt; + } + BackingStore::StatementInfo &votes = it->second; - s = &statements_[candidate_hash]; - s->first = seconded->descriptor.para_id; - candidates_[seconded->descriptor.relay_parent].emplace(candidate_hash); - s->second.emplace(statement.payload.ix, - ValidityVoteIssued{std::move(statement)}); - } else { + if (!is_in_group(groups, votes.group_id, from)) { + return Error::UNAUTHORIZED_STATEMENT; + } + + auto i = votes.validity_votes.find(from); + if (i != votes.validity_votes.end()) { + if (i->second != vote) { + return Error::DOUBLE_VOTE; + } return std::nullopt; } + votes.validity_votes[from] = vote; return BackingStore::ImportResult{ - .candidate = candidate_hash, - .group_id = s->first, - .validity_votes = s->second.size(), + .candidate = digest, + .group_id = votes.group_id, + .validity_votes = votes.validity_votes.size(), }; } - std::optional> - BackingStoreImpl::get_validity_votes( - const network::CandidateHash &candidate_hash) const { - if (auto it = statements_.find(candidate_hash); it != statements_.end()) { - return {{it->second}}; + outcome::result> + BackingStoreImpl::import_candidate( + PerRelayParent &state, + const std::unordered_map> + &groups, + ValidatorIndex authority, + const network::CommittedCandidateReceipt &candidate, + const ValidatorSignature &signature, + bool allow_multiple_seconded) { + const auto group = candidate.descriptor.para_id; + if (auto it = groups.find(group); + it == groups.end() + || std::find(it->second.begin(), it->second.end(), authority) + == it->second.end()) { + return Error::UNAUTHORIZED_STATEMENT; + } + + const CandidateHash digest = candidateHash(*hasher_, candidate); + bool new_proposal; + if (auto it = state.authority_data_.find(authority); + it != state.authority_data_.end()) { + auto &existing = it->second; + if (!allow_multiple_seconded && existing.proposals.size() == 1) { + const auto &[old_digest, old_sig] = existing.proposals[0]; + if (old_digest != digest) { + return Error::MULTIPLE_CANDIDATES; + } + new_proposal = false; + } else if (allow_multiple_seconded + && std::find_if(existing.proposals.begin(), + existing.proposals.end(), + [&digest](const auto &hash_and_sig) { + const auto &[h, _] = hash_and_sig; + return h == digest; + }) + != existing.proposals.end()) { + new_proposal = false; + } else { + existing.proposals.emplace_back(digest, signature); + new_proposal = true; + } + } else { + auto &ad = state.authority_data_[authority]; + ad.proposals.emplace_back(digest, signature); + new_proposal = true; } - return std::nullopt; + + if (new_proposal) { + auto &cv = state.candidate_votes_[digest]; + cv.candidate = candidate; + cv.group_id = group; + } + + return validity_vote( + state, groups, authority, digest, ValidityVoteIssued{signature}); } - void BackingStoreImpl::add(const BlockHash &relay_parent, - BackedCandidate &&candidate) { - backed_candidates_[relay_parent].emplace_back(std::move(candidate)); + std::optional BackingStoreImpl::put( + const RelayHash &relay_parent, + const std::unordered_map> + &groups, + Statement stm, + bool allow_multiple_seconded) { + std::optional> per_rp_state; + forRelayState(relay_parent, + [&](PerRelayParent &state) { per_rp_state = state; }); + + if (!per_rp_state) { + return std::nullopt; + } + + const auto &signer = stm.payload.ix; + const auto &signature = stm.signature; + const auto &statement = stm.payload.payload; + + auto res = visit_in_place( + statement.candidate_state, + [&](const network::CommittedCandidateReceipt &candidate) { + return import_candidate(per_rp_state->get(), + groups, + signer, + candidate, + signature, + allow_multiple_seconded); + }, + [&](const CandidateHash &digest) { + return validity_vote(per_rp_state->get(), + groups, + signer, + digest, + ValidityVoteValid{signature}); + }, + [](const auto &) { + UNREACHABLE; + return Error::CRITICAL_ERROR; + }); + + if (res.has_error()) { + return std::nullopt; + } + return res.value(); } - std::optional - BackingStoreImpl::get_candidate( + std::optional> + BackingStoreImpl::getCadidateInfo( + const RelayHash &relay_parent, const network::CandidateHash &candidate_hash) const { - if (auto it = statements_.find(candidate_hash); it != statements_.end()) { - for (auto &[_, validity_vote] : it->second.second) { - const auto statement = visit_in_place( - validity_vote, - [](const auto &val) -> std::reference_wrapper { - return {(Statement &)val}; - }); - - if (auto seconded = - boost::get( - &statement.get().payload.payload.candidate_state)) { - return *seconded; - } + std::optional> + out; + forRelayState(relay_parent, [&](const PerRelayParent &state) { + if (auto it = state.candidate_votes_.find(candidate_hash); + it != state.candidate_votes_.end()) { + out = it->second; } - } - return std::nullopt; + }); + return out; + } + + void BackingStoreImpl::add(const BlockHash &relay_parent, + BackedCandidate &&candidate) { + forRelayState(relay_parent, [&](PerRelayParent &state) { + state.backed_candidates_.emplace_back(std::move(candidate)); + }); } std::vector BackingStoreImpl::get( const BlockHash &relay_parent) const { - if (auto it = backed_candidates_.find(relay_parent); - it != backed_candidates_.end()) { - return it->second; - } - return {}; + std::vector out; + forRelayState(relay_parent, [&](const PerRelayParent &state) { + out = state.backed_candidates_; + }); + return out; } } // namespace kagome::parachain diff --git a/core/parachain/backing/store_impl.hpp b/core/parachain/backing/store_impl.hpp index 8529a3ac3c..c490a0cf97 100644 --- a/core/parachain/backing/store_impl.hpp +++ b/core/parachain/backing/store_impl.hpp @@ -17,37 +17,85 @@ namespace kagome::parachain { using ValidatorIndex = network::ValidatorIndex; public: + enum Error { + UNAUTHORIZED_STATEMENT = 1, + DOUBLE_VOTE, + MULTIPLE_CANDIDATES, + CRITICAL_ERROR, + }; + BackingStoreImpl(std::shared_ptr hasher); std::optional put( + const RelayHash &relay_parent, const std::unordered_map> &groups, - Statement statement) override; + Statement statement, + bool allow_multiple_seconded) override; std::vector get( const BlockHash &relay_parent) const override; void add(const BlockHash &relay_parent, BackedCandidate &&candidate) override; - std::optional get_candidate( - const network::CandidateHash &candidate_hash) const override; - - std::optional> - get_validity_votes( + std::optional> getCadidateInfo( + const RelayHash &relay_parent, const network::CandidateHash &candidate_hash) const override; - void remove(const BlockHash &relay_parent) override; + void onActivateLeaf(const RelayHash &relay_parent) override; + void onDeactivateLeaf(const RelayHash &relay_parent) override; private: + struct AuthorityData { + std::deque> proposals; + }; + + struct PerRelayParent { + std::vector backed_candidates_; + std::unordered_map authority_data_; + std::unordered_map candidate_votes_; + }; + + template + void forRelayState(const RelayHash &relay_parent, F &&f) { + if (auto it = per_relay_parent_.find(relay_parent); + it != per_relay_parent_.end()) { + std::forward(f)(it->second); + } + } + + template + void forRelayState(const RelayHash &relay_parent, F &&f) const { + if (auto it = per_relay_parent_.find(relay_parent); + it != per_relay_parent_.end()) { + std::forward(f)(it->second); + } + } + bool is_in_group( const std::unordered_map> &groups, GroupIndex group, ValidatorIndex authority); - std::shared_ptr hasher_; - std::unordered_map> candidates_; - std::unordered_map statements_; - std::unordered_map> - backed_candidates_; + outcome::result> validity_vote( + PerRelayParent &state, + const std::unordered_map> + &groups, + ValidatorIndex from, + const CandidateHash &digest, + const ValidityVote &vote); + outcome::result> import_candidate( + PerRelayParent &state, + const std::unordered_map> + &groups, + ValidatorIndex authority, + const network::CommittedCandidateReceipt &candidate, + const ValidatorSignature &signature, + bool allow_multiple_seconded); + + std::shared_ptr hasher_; + std::unordered_map per_relay_parent_; }; } // namespace kagome::parachain + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain, BackingStoreImpl::Error) diff --git a/core/parachain/pvf/pvf.hpp b/core/parachain/pvf/pvf.hpp index 4c006d676a..9128122708 100644 --- a/core/parachain/pvf/pvf.hpp +++ b/core/parachain/pvf/pvf.hpp @@ -23,7 +23,9 @@ namespace kagome::parachain { /// Execute pvf synchronously virtual outcome::result pvfSync( - const CandidateReceipt &receipt, const ParachainBlock &pov) const = 0; + const CandidateReceipt &receipt, + const ParachainBlock &pov, + const runtime::PersistedValidationData &pvd) const = 0; virtual outcome::result pvfValidate( const PersistedValidationData &data, diff --git a/core/parachain/pvf/pvf_impl.cpp b/core/parachain/pvf/pvf_impl.cpp index b4e596c061..1b5114487e 100644 --- a/core/parachain/pvf/pvf_impl.cpp +++ b/core/parachain/pvf/pvf_impl.cpp @@ -30,6 +30,10 @@ OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain, PvfError, e) { using kagome::parachain::PvfError; switch (e) { + case PvfError::PERSISTED_DATA_HASH: + return "Incorrect Perssted Data hash"; + case PvfError::NO_CODE: + return "No code"; case PvfError::NO_PERSISTED_DATA: return "PersistedValidationData was not found"; case PvfError::POV_SIZE: @@ -254,59 +258,51 @@ namespace kagome::parachain { timer.reset(); OUTCOME_TRY(commitments, fromOutputs(receipt, std::move(result))); - return std::make_pair(std::move(commitments), std::move(data)); + return std::make_pair(std::move(commitments), data); } outcome::result PvfImpl::pvfSync( - const CandidateReceipt &receipt, const ParachainBlock &pov) const { + const CandidateReceipt &receipt, + const ParachainBlock &pov, + const runtime::PersistedValidationData &pvd) const { SL_DEBUG(log_, "pvfSync relay_parent={} para_id={}", receipt.descriptor.relay_parent, receipt.descriptor.para_id); - OUTCOME_TRY(data_code, findData(receipt.descriptor)); - auto &[data, code] = data_code; - return pvfValidate(data, pov, receipt, code); + + auto data_hash = hasher_->blake2b_256(::scale::encode(pvd).value()); + if (receipt.descriptor.persisted_data_hash != data_hash) { + return PvfError::PERSISTED_DATA_HASH; + } + + OUTCOME_TRY(code, getCode(receipt.descriptor)); + return pvfValidate(pvd, pov, receipt, code); } - outcome::result> - PvfImpl::findData(const CandidateDescriptor &descriptor) const { + outcome::result PvfImpl::getCode( + const CandidateDescriptor &descriptor) const { for (auto assumption : { runtime::OccupiedCoreAssumption::Included, runtime::OccupiedCoreAssumption::TimedOut, }) { - OUTCOME_TRY(data, - parachain_api_->persisted_validation_data( - descriptor.relay_parent, descriptor.para_id, assumption)); - if (!data) { - SL_VERBOSE(log_, - "findData relay_parent={} para_id={}: not found " - "(persisted_validation_data)", - descriptor.relay_parent, - descriptor.para_id); - return PvfError::NO_PERSISTED_DATA; - } - auto data_hash = hasher_->blake2b_256(scale::encode(*data).value()); - if (descriptor.persisted_data_hash != data_hash) { - continue; - } OUTCOME_TRY(code, parachain_api_->validation_code( descriptor.relay_parent, descriptor.para_id, assumption)); if (!code) { SL_VERBOSE( log_, - "findData relay_parent={} para_id={}: not found (validation_code)", + "getCode relay_parent={} para_id={}: not found (validation_code)", descriptor.relay_parent, descriptor.para_id); - return PvfError::NO_PERSISTED_DATA; + return PvfError::NO_CODE; } - return std::make_pair(*data, *code); + return *code; } SL_VERBOSE(log_, - "findData relay_parent={} para_id={}: not found", + "getCode relay_parent={} para_id={}: not found", descriptor.relay_parent, descriptor.para_id); - return PvfError::NO_PERSISTED_DATA; + return PvfError::NO_CODE; } outcome::result PvfImpl::callWasm( diff --git a/core/parachain/pvf/pvf_impl.hpp b/core/parachain/pvf/pvf_impl.hpp index 8690a5ce2c..a1d09d5b3f 100644 --- a/core/parachain/pvf/pvf_impl.hpp +++ b/core/parachain/pvf/pvf_impl.hpp @@ -51,6 +51,8 @@ namespace kagome::parachain { HEAD_HASH, COMMITMENTS_HASH, OUTPUTS, + PERSISTED_DATA_HASH, + NO_CODE, }; } // namespace kagome::parachain @@ -99,8 +101,10 @@ namespace kagome::parachain { bool prepare(); - outcome::result pvfSync(const CandidateReceipt &receipt, - const ParachainBlock &pov) const override; + outcome::result pvfSync( + const CandidateReceipt &receipt, + const ParachainBlock &pov, + const runtime::PersistedValidationData &pvd) const override; outcome::result pvfValidate( const PersistedValidationData &data, const ParachainBlock &pov, @@ -111,9 +115,8 @@ namespace kagome::parachain { using CandidateDescriptor = network::CandidateDescriptor; using ParachainRuntime = network::ParachainRuntime; - outcome::result> - findData(const CandidateDescriptor &descriptor) const; - + outcome::result getCode( + const CandidateDescriptor &descriptor) const; outcome::result callWasm( const CandidateReceipt &receipt, const common::Hash256 &code_hash, diff --git a/core/parachain/types.hpp b/core/parachain/types.hpp index 90fc897378..497c161a22 100644 --- a/core/parachain/types.hpp +++ b/core/parachain/types.hpp @@ -13,6 +13,7 @@ #include #include "common/blob.hpp" +#include "common/visitor.hpp" #include "consensus/grandpa/common.hpp" #include "crypto/sr25519_types.hpp" #include "primitives/block_header.hpp" @@ -92,3 +93,263 @@ namespace kagome::parachain { } }; } // namespace kagome::parachain + +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 + }; + + struct InboundDownwardMessage { + SCALE_TIE(2); + /// The block number at which these messages were put into the downward + /// message queue. + parachain::BlockNumber sent_at; + /// The actual downward message to processes. + parachain::DownwardMessage msg; + }; + + struct InboundHrmpMessage { + SCALE_TIE(2); + /// The block number at which this message was sent. + /// Specifically, it is the block number at which the candidate that sends + /// this message was enacted. + parachain::BlockNumber sent_at; + /// The message payload. + common::Buffer data; + }; + + struct CandidateCommitments { + SCALE_TIE(6); + + std::vector upward_msgs; /// upward messages + std::vector + outbound_hor_msgs; /// outbound horizontal messages + std::optional + opt_para_runtime; /// new parachain runtime if present + parachain::HeadData para_head; /// parachain head data + uint32_t downward_msgs_count; /// number of downward messages that were + /// processed by the parachain + parachain::BlockNumber + watermark; /// watermark which specifies the relay chain block + /// number up to which all inbound horizontal messages + /// have been processed + }; + + /** + * Unique descriptor of a candidate receipt. + */ + struct CandidateDescriptor { + SCALE_TIE(9); + + parachain::ParachainId para_id; /// Parachain Id + primitives::BlockHash + relay_parent; /// Hash of the relay chain block the candidate is + /// executed in the context of + parachain::CollatorPublicKey collator_id; /// Collators public key. + primitives::BlockHash + persisted_data_hash; /// Hash of the persisted validation data + primitives::BlockHash pov_hash; /// Hash of the PoV block. + storage::trie::RootHash + erasure_encoding_root; /// Root of the block’s erasure encoding Merkle + /// tree. + parachain::Signature + signature; /// Collator signature of the concatenated components + primitives::BlockHash + para_head_hash; /// Hash of the parachain head data of this candidate. + primitives::BlockHash + validation_code_hash; /// Hash of the parachain Runtime. + + common::Buffer signable() const { + return common::Buffer{ + ::scale::encode(relay_parent, + para_id, + persisted_data_hash, + pov_hash, + validation_code_hash) + .value(), + }; + } + }; +} // namespace kagome::network + +namespace kagome::parachain::fragment { + enum UpgradeRestriction { + /// There is an upgrade restriction and there are no details about its + /// specifics nor how long + /// it could last. + Present = 0, + }; + + struct CandidatePendingAvailability { + SCALE_TIE(5); + /// The hash of the candidate. + CandidateHash candidate_hash; + /// The candidate's descriptor. + network::CandidateDescriptor descriptor; + /// The commitments of the candidate. + network::CandidateCommitments commitments; + /// The candidate's relay parent's number. + BlockNumber relay_parent_number; + /// The maximum Proof-of-Validity size allowed, in bytes. + uint32_t max_pov_size; + }; + + /// Constraints on inbound HRMP channels. + struct InboundHrmpLimitations { + SCALE_TIE(1); + /// An exhaustive set of all valid watermarks, sorted ascending + std::vector valid_watermarks; + }; + + /// Constraints on outbound HRMP channels. + struct OutboundHrmpChannelLimitations { + SCALE_TIE(2); + /// The maximum bytes that can be written to the channel. + uint32_t bytes_remaining; + /// The maximum messages that can be written to the channel. + uint32_t messages_remaining; + }; + + struct HrmpWatermarkUpdateHead { + SCALE_TIE(1); + BlockNumber v; + }; + struct HrmpWatermarkUpdateTrunk { + SCALE_TIE(1); + BlockNumber v; + }; + using HrmpWatermarkUpdate = + boost::variant; + inline BlockNumber fromHrmpWatermarkUpdate(const HrmpWatermarkUpdate &value) { + return visit_in_place(value, [](const auto &val) { return val.v; }); + } + + struct OutboundHrmpChannelModification { + SCALE_TIE(2); + /// The number of bytes submitted to the channel. + uint32_t bytes_submitted; + /// The number of messages submitted to the channel. + uint32_t messages_submitted; + }; + + struct ConstraintModifications { + /// The required parent head to build upon. + std::optional required_parent{}; + /// The new HRMP watermark + std::optional hrmp_watermark{}; + /// Outbound HRMP channel modifications. + std::unordered_map + outbound_hrmp{}; + /// The amount of UMP messages sent. + uint32_t ump_messages_sent{0ull}; + /// The amount of UMP bytes sent. + uint32_t ump_bytes_sent{0ull}; + /// The amount of DMP messages processed. + uint32_t dmp_messages_processed{0ull}; + /// Whether a pending code upgrade has been applied. + bool code_upgrade_applied{false}; + + void stack(const ConstraintModifications &other) { + if (other.required_parent) { + required_parent = other.required_parent; + } + if (other.hrmp_watermark) { + hrmp_watermark = other.hrmp_watermark; + } + + for (const auto &[id, mods] : other.outbound_hrmp) { + auto &record = outbound_hrmp[id]; + record.messages_submitted += mods.messages_submitted; + record.bytes_submitted += mods.bytes_submitted; + } + + ump_messages_sent += other.ump_messages_sent; + ump_bytes_sent += other.ump_bytes_sent; + dmp_messages_processed += other.dmp_messages_processed; + code_upgrade_applied |= other.code_upgrade_applied; + } + }; + + struct Constraints { + SCALE_TIE(14); + enum class Error { + DISALLOWED_HRMP_WATERMARK, + NO_SUCH_HRMP_CHANNEL, + HRMP_BYTES_OVERFLOW, + HRMP_MESSAGE_OVERFLOW, + UMP_MESSAGE_OVERFLOW, + UMP_BYTES_OVERFLOW, + DMP_MESSAGE_UNDERFLOW, + APPLIED_NONEXISTENT_CODE_UPGRADE, + }; + + /// The minimum relay-parent number accepted under these constraints. + BlockNumber min_relay_parent_number; + /// The maximum Proof-of-Validity size allowed, in bytes. + uint32_t max_pov_size; + /// The maximum new validation code size allowed, in bytes. + uint32_t max_code_size; + /// The amount of UMP messages remaining. + uint32_t ump_remaining; + /// The amount of UMP bytes remaining. + uint32_t ump_remaining_bytes; + /// The maximum number of UMP messages allowed per candidate. + uint32_t max_ump_num_per_candidate; + /// Remaining DMP queue. Only includes sent-at block numbers. + std::vector dmp_remaining_messages; + /// The limitations of all registered inbound HRMP channels. + InboundHrmpLimitations hrmp_inbound; + /// The limitations of all registered outbound HRMP channels. + std::unordered_map + hrmp_channels_out; + /// The maximum number of HRMP messages allowed per candidate. + uint32_t max_hrmp_num_per_candidate; + /// The required parent head-data of the parachain. + HeadData required_parent; + /// The expected validation-code-hash of this parachain. + ValidationCodeHash validation_code_hash; + /// The code upgrade restriction signal as-of this parachain. + std::optional upgrade_restriction; + /// The future validation code hash, if any, and at what relay-parent + /// number the upgrade would be minimally applied. + std::optional> + future_validation_code; + + outcome::result applyModifications( + const ConstraintModifications &modifications) const; + + bool checkModifications(const ConstraintModifications &modifications) const; + }; + + struct BackingState { + SCALE_TIE(2); + /// The state-machine constraints of the parachain. + Constraints constraints; + /// The candidates pending availability. These should be ordered, i.e. they + /// should form a sub-chain, where the first candidate builds on top of the + /// required parent of the constraints and each subsequent builds on top of + /// the previous head-data. + std::vector pending_availability; + }; + + struct AsyncBackingParams { + SCALE_TIE(2); + /// The maximum number of para blocks between the para head in a relay + /// parent and a new candidate. Restricts nodes from building arbitrary long + /// chains and spamming other validators. + /// + /// When async backing is disabled, the only valid value is 0. + uint32_t max_candidate_depth; + /// How many ancestors of a relay parent are allowed to build candidates on + /// top of. + /// + /// When async backing is disabled, the only valid value is 0. + uint32_t allowed_ancestry_len; + }; + +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/backing_implicit_view.cpp b/core/parachain/validator/backing_implicit_view.cpp new file mode 100644 index 0000000000..bc614c69c4 --- /dev/null +++ b/core/parachain/validator/backing_implicit_view.cpp @@ -0,0 +1,184 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/backing_implicit_view.hpp" + +#include + +#include "parachain/types.hpp" +#include "primitives/math.hpp" + +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain, ImplicitView::Error, e) { + using E = decltype(e); + switch (e) { + case E::ALREADY_KNOWN: + return "Already known leaf"; + } + return "ImplicitView failed."; +} + +namespace kagome::parachain { + + ImplicitView::ImplicitView( + std::shared_ptr prospective_parachains) + : prospective_parachains_{std::move(prospective_parachains)} { + BOOST_ASSERT(prospective_parachains_); + } + + std::span + ImplicitView::AllowedRelayParents::allowedRelayParentsFor( + const std::optional ¶_id, + const BlockNumber &base_number) const { + if (!para_id) { + return {allowed_relay_parents_contiguous}; + } + + if (auto it = minimum_relay_parents.find(*para_id); + it != minimum_relay_parents.end()) { + const BlockNumber ¶_min = it->second; + if (base_number >= para_min) { + const auto diff = base_number - para_min; + const size_t slice_len = + std::min(size_t(diff + 1), allowed_relay_parents_contiguous.size()); + return std::span{allowed_relay_parents_contiguous}.first(slice_len); + } + } + return {}; + } + + std::span ImplicitView::knownAllowedRelayParentsUnder( + const Hash &block_hash, const std::optional ¶_id) const { + if (auto it = block_info_storage.find(block_hash); + it != block_info_storage.end()) { + const BlockInfo &block_info = it->second; + if (block_info.maybe_allowed_relay_parents) { + return block_info.maybe_allowed_relay_parents->allowedRelayParentsFor( + para_id, block_info.block_number); + } + } + return {}; + } + + std::vector ImplicitView::deactivate_leaf(const Hash &leaf_hash) { + std::vector removed; + if (leaves.erase(leaf_hash) == 0ull) { + return removed; + } + + std::optional minimum; + for (const auto &[_, l] : leaves) { + minimum = + minimum ? std::min(*minimum, l.retain_minimum) : l.retain_minimum; + } + + for (auto it = block_info_storage.begin(); + it != block_info_storage.end();) { + const auto &[hash, i] = *it; + const bool keep = minimum && i.block_number >= *minimum; + if (keep) { + ++it; + } else { + removed.emplace_back(hash); + it = block_info_storage.erase(it); + } + } + return removed; + } + + outcome::result> ImplicitView::activate_leaf( + const Hash &leaf_hash) { + if (leaves.contains(leaf_hash)) { + return Error::ALREADY_KNOWN; + } + + OUTCOME_TRY(fetched, fetch_fresh_leaf_and_insert_ancestry(leaf_hash)); + const uint32_t retain_minimum = std::min( + 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; + } + + outcome::result + ImplicitView::fetch_fresh_leaf_and_insert_ancestry(const Hash &leaf_hash) { + std::vector> min_relay_parents_raw = + prospective_parachains_->answerMinimumRelayParentsRequest(leaf_hash); + std::shared_ptr block_tree = + 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 (size_t i = 0; i < min_relay_parents_raw.size(); ++i) { + min_min = std::min(min_relay_parents_raw[i].second, min_min); + relevant_paras.emplace_back(min_relay_parents_raw[i].first); + } + + const size_t expected_ancestry_len = + math::sat_sub_unsigned(leaf_header.number, min_min) + 1ull; + std::vector ancestry; + if (leaf_header.number > 0) { + BlockNumber next_ancestor_number = leaf_header.number - 1; + Hash next_ancestor_hash = leaf_header.parent_hash; + + ancestry.reserve(expected_ancestry_len); + ancestry.emplace_back(leaf_hash); + + while (next_ancestor_number >= min_min) { + auto it = block_info_storage.find(next_ancestor_hash); + std::optional parent_hash; + if (it != block_info_storage.end()) { + parent_hash = it->second.parent_hash; + } else { + OUTCOME_TRY(header, block_tree->getBlockHeader(next_ancestor_hash)); + block_info_storage.emplace( + next_ancestor_hash, + BlockInfo{ + .block_number = next_ancestor_number, + .maybe_allowed_relay_parents = std::nullopt, + .parent_hash = header.parent_hash, + }); + parent_hash = header.parent_hash; + } + + ancestry.emplace_back(next_ancestor_hash); + if (next_ancestor_number == 0) { + break; + } + + next_ancestor_number -= 1; + next_ancestor_hash = *parent_hash; + } + } else { + ancestry.emplace_back(leaf_hash); + } + + block_info_storage.emplace( + leaf_hash, + BlockInfo{ + .block_number = leaf_header.number, + .maybe_allowed_relay_parents = + AllowedRelayParents{ + .minimum_relay_parents = {min_relay_parents_raw.begin(), + min_relay_parents_raw.end()}, + .allowed_relay_parents_contiguous = std::move(ancestry), + }, + .parent_hash = leaf_header.parent_hash, + }); + return FetchSummary{ + .minimum_ancestor_number = min_min, + .leaf_number = leaf_header.number, + .relevant_paras = relevant_paras, + }; + } + +} // namespace kagome::parachain diff --git a/core/parachain/validator/backing_implicit_view.hpp b/core/parachain/validator/backing_implicit_view.hpp new file mode 100644 index 0000000000..a5d3fb6ddc --- /dev/null +++ b/core/parachain/validator/backing_implicit_view.hpp @@ -0,0 +1,73 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#include "parachain/types.hpp" +#include "parachain/validator/prospective_parachains.hpp" +#include "primitives/common.hpp" + +namespace kagome::parachain { + + // Always aim to retain 1 block before the active leaves. + constexpr BlockNumber MINIMUM_RETAIN_LENGTH = 2ull; + + struct ImplicitView { + enum Error { + ALREADY_KNOWN, + }; + + struct FetchSummary { + BlockNumber minimum_ancestor_number; + BlockNumber leaf_number; + std::vector relevant_paras; + }; + + std::span knownAllowedRelayParentsUnder( + const Hash &block_hash, + const std::optional ¶_id) const; + outcome::result> activate_leaf( + const Hash &leaf_hash); + std::vector deactivate_leaf(const Hash &leaf_hash); + + ImplicitView(std::shared_ptr prospective_parachains); + + private: + struct ActiveLeafPruningInfo { + BlockNumber retain_minimum; + }; + + struct AllowedRelayParents { + std::unordered_map minimum_relay_parents; + std::vector allowed_relay_parents_contiguous; + + std::span allowedRelayParentsFor( + const std::optional ¶_id, + const BlockNumber &base_number) const; + }; + + struct BlockInfo { + BlockNumber block_number; + std::optional maybe_allowed_relay_parents; + Hash parent_hash; + }; + + std::unordered_map leaves; + std::unordered_map block_info_storage; + std::shared_ptr prospective_parachains_; + + outcome::result fetch_fresh_leaf_and_insert_ancestry( + const Hash &leaf_hash); + }; + +} // namespace kagome::parachain + +OUTCOME_HPP_DECLARE_ERROR(kagome::parachain, ImplicitView::Error) diff --git a/core/parachain/validator/collations.hpp b/core/parachain/validator/collations.hpp new file mode 100644 index 0000000000..992f89f6d2 --- /dev/null +++ b/core/parachain/validator/collations.hpp @@ -0,0 +1,208 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#include "crypto/type_hasher.hpp" +#include "network/types/collator_messages.hpp" +#include "parachain/types.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" + +namespace kagome::parachain { + + struct ProspectiveParachainsMode { + /// The maximum number of para blocks between the para head in a relay + /// parent and a new candidate. Restricts nodes from building arbitrary + /// long chains and spamming other validators. + size_t max_candidate_depth; + + /// How many ancestors of a relay parent are allowed to build candidates + /// on top of. + size_t allowed_ancestry_len; + }; + 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; + }; + + /// The status of the collations. + enum struct CollationStatus { + /// We are waiting for a collation to be advertised to us. + Waiting, + /// We are currently fetching a collation. + Fetching, + /// We are waiting that a collation is being validated. + WaitingOnValidation, + /// We have seconded a collation. + Seconded, + }; + + using ProspectiveCandidate = std::pair; + + struct PendingCollation { + RelayHash relay_parent; + network::ParachainId para_id; + libp2p::peer::PeerId peer_id; + std::optional commitments_hash; + std::optional prospective_candidate; + }; + + struct PendingCollationHash { + size_t operator()(const PendingCollation &val) const noexcept { + size_t r{0ull}; + boost::hash_combine(r, std::hash()(val.relay_parent)); + boost::hash_combine(r, std::hash()(val.para_id)); + if (val.prospective_candidate) { + boost::hash_combine( + r, std::hash()(val.prospective_candidate->first)); + } + return r; + } + }; + + struct PendingCollationEq { + bool operator()(const PendingCollation &__x, + const PendingCollation &__y) const { + return __x.relay_parent == __y.relay_parent && __x.para_id == __y.para_id + && __x.prospective_candidate == __y.prospective_candidate; + } + }; + + struct Collations { + /// How many collations have been seconded. + size_t seconded_count{0ull}; + /// What is the current status in regards to a collation for this relay + /// parent? + CollationStatus status{CollationStatus::Waiting}; + /// Collation that were advertised to us, but we did not yet fetch. + std::deque> waiting_queue{}; + /// Collator we're fetching from, optionally which candidate was requested. + /// + /// This is the currently last started fetch, which did not exceed + /// `MAX_UNSHARED_DOWNLOAD_TIME` yet. + std::optional>> + fetching_from{}; + + bool hasSecondedSpace( + const ProspectiveParachainsModeOpt &relay_parent_mode) { + const auto seconded_limit = + relay_parent_mode ? relay_parent_mode->max_candidate_depth + 1 : 1; + return seconded_count < seconded_limit; + } + }; + + struct HypotheticalCandidateComplete { + /// The hash of the candidate. + CandidateHash candidate_hash; + /// The receipt of the candidate. + network::CommittedCandidateReceipt receipt; + /// The persisted validation data of the candidate. + runtime::PersistedValidationData persisted_validation_data; + + bool operator==(const HypotheticalCandidateComplete &r) const = default; + }; + + struct HypotheticalCandidateIncomplete { + /// The claimed hash of the candidate. + CandidateHash candidate_hash; + /// The claimed para-ID of the candidate. + ParachainId candidate_para; + /// The claimed head-data hash of the candidate. + Hash parent_head_data_hash; + /// The claimed relay parent of the candidate. + Hash candidate_relay_parent; + + bool operator==(const HypotheticalCandidateIncomplete &r) const = default; + }; + + struct BlockedAdvertisement { + /// Peer that advertised the collation. + libp2p::peer::PeerId peer_id; + /// Collator id. + CollatorId collator_id; + /// The relay-parent of the candidate. + Hash candidate_relay_parent; + /// Hash of the candidate. + CandidateHash candidate_hash; + }; + + /// A hypothetical candidate to be evaluated for frontier membership + /// in the prospective parachains subsystem. + /// + /// Hypothetical candidates are either complete or incomplete. + /// Complete candidates have already had their (potentially heavy) + /// candidate receipt fetched, while incomplete candidates are simply + /// claims about properties that a fetched candidate would have. + /// + /// Complete candidates can be evaluated more strictly than incomplete + /// candidates. + using HypotheticalCandidate = boost::variant; + + inline std::reference_wrapper candidatePara( + const HypotheticalCandidate &hc) { + return visit_in_place( + hc, + [](const HypotheticalCandidateComplete &val) + -> std::reference_wrapper { + return val.receipt.descriptor.para_id; + }, + [](const HypotheticalCandidateIncomplete &val) + -> std::reference_wrapper { + return val.candidate_para; + }); + } + + inline Hash parentHeadDataHash(const crypto::Hasher &hasher, + const HypotheticalCandidate &hc) { + return visit_in_place( + hc, + [&](const HypotheticalCandidateComplete &val) { + return hasher.blake2b_256(val.persisted_validation_data.parent_head); + }, + [](const HypotheticalCandidateIncomplete &val) { + return val.parent_head_data_hash; + }); + } + + inline std::reference_wrapper relayParent( + const HypotheticalCandidate &hc) { + return visit_in_place( + hc, + [](const HypotheticalCandidateComplete &val) + -> std::reference_wrapper { + return val.receipt.descriptor.relay_parent; + }, + [](const HypotheticalCandidateIncomplete &val) + -> std::reference_wrapper { + return val.candidate_relay_parent; + }); + } + + inline std::reference_wrapper candidateHash( + const HypotheticalCandidate &hc) { + return visit_in_place( + hc, [](const auto &val) -> std::reference_wrapper { + return val.candidate_hash; + }); + } + +} // namespace kagome::parachain diff --git a/core/parachain/validator/fragment_tree.hpp b/core/parachain/validator/fragment_tree.hpp new file mode 100644 index 0000000000..294c697dac --- /dev/null +++ b/core/parachain/validator/fragment_tree.hpp @@ -0,0 +1,988 @@ +/** + * 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::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::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 { + auto kv = std::views::keys(candidates); + return {kv.begin(), kv.end()}; + } + + template + std::optional selectChild( + const std::vector &required_path, 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::nullopt; + } + } + + return visit_in_place( + base_node, + [&](const NodePointerRoot &) -> std::optional { + for (const FragmentNode &n : nodes) { + if (!is_type(n.parent)) { + return std::nullopt; + } + if (scope.getPendingAvailability(n.candidate_hash)) { + return std::nullopt; + } + if (!pred(n.candidate_hash)) { + return std::nullopt; + } + return n.candidate_hash; + } + return std::nullopt; + }, + [&](const NodePointerStorage &ptr) -> std::optional { + for (const auto &[_, h] : nodes[ptr].children) { + if (scope.getPendingAvailability(h)) { + return std::nullopt; + } + if (!pred(h)) { + return std::nullopt; + } + return h; + } + return std::nullopt; + }); + } + + static FragmentTree populate(const std::shared_ptr &hasher, + const Scope &scope, + const CandidateStorage &storage) { + auto logger = log::createLogger("parachain", "fragment_tree"); + 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().message()); + 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, [&](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::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}, + 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, + size_t(node.depth + 1), + opt_rcbi->get()); + } else { + if (auto r = + scope.getPendingAvailability(node.candidate_hash)) { + return std::make_tuple(node.cumulative_modifications, + size_t(node.depth + 1), + 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/candidates.hpp b/core/parachain/validator/impl/candidates.hpp new file mode 100644 index 0000000000..6add3563b2 --- /dev/null +++ b/core/parachain/validator/impl/candidates.hpp @@ -0,0 +1,446 @@ +/** + * Copyright Quadrivium Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include "parachain/validator/collations.hpp" +#include "primitives/common.hpp" + +namespace kagome::parachain { + + struct PostConfirmationReckoning { + std::unordered_set correct; + std::unordered_set incorrect; + bool operator==(const PostConfirmationReckoning &r) const = default; + }; + + struct PostConfirmation { + HypotheticalCandidate hypothetical; + PostConfirmationReckoning reckoning; + bool operator==(const PostConfirmation &r) const = default; + }; + + struct CandidateClaims { + RelayHash relay_parent; + GroupIndex group_index; + std::optional> parent_hash_and_id; + + bool check(const RelayHash &rp, + GroupIndex gi, + const Hash &ph, + ParachainId pi) const { + bool ch = true; + if (parent_hash_and_id) { + ch = + parent_hash_and_id->first == ph && parent_hash_and_id->second == pi; + } + + return relay_parent == rp && group_index == gi && ch; + } + }; + + struct UnconfirmedImportable { + RelayHash relay_parent; + Hash parent_hash; + ParachainId para_id; + + bool operator==(const UnconfirmedImportable &rhs) const { + return para_id == rhs.para_id && relay_parent == rhs.relay_parent + && parent_hash == rhs.parent_hash; + } + }; + + struct UnconfiredImportablePair { + Hash hash; + UnconfirmedImportable ui; + + size_t inner_hash() const { + size_t r{0ull}; + boost::hash_combine(r, std::hash()(hash)); + boost::hash_combine(r, std::hash()(ui.relay_parent)); + boost::hash_combine(r, std::hash()(ui.parent_hash)); + boost::hash_combine(r, std::hash()(ui.para_id)); + return r; + } + + bool operator==(const UnconfiredImportablePair &rhs) const { + return hash == rhs.hash && ui == rhs.ui; + } + }; + + struct UnconfirmedCandidate { + std::vector> claims; + std::unordered_map< + Hash, + std::unordered_map>>> + parent_claims; + std::unordered_set> + unconfirmed_importable_under; + + void note_maybe_importable_under( + const Hash &active_leaf, + UnconfirmedImportable &&unconfirmed_importable) { + unconfirmed_importable_under.emplace(UnconfiredImportablePair{ + .hash = active_leaf, + .ui = std::move(unconfirmed_importable), + }); + } + + void extend_hypotheticals( + const CandidateHash &candidate_hash, + std::vector &v, + const std::optional, + ParachainId>> &required_parent) const { + auto extend_hypotheticals_inner = + [&](const Hash &parent_head_hash, + ParachainId para_id, + const std::vector> + &possible_relay_parents) { + for (const auto &[relay_parent, _] : possible_relay_parents) { + v.emplace_back(HypotheticalCandidateIncomplete{ + .candidate_hash = candidate_hash, + .candidate_para = para_id, + .parent_head_data_hash = parent_head_hash, + .candidate_relay_parent = relay_parent, + }); + } + }; + + if (required_parent) { + if (auto h_it = parent_claims.find(required_parent->first.get()); + h_it != parent_claims.end()) { + if (auto p_it = h_it->second.find(required_parent->second); + p_it != h_it->second.end()) { + extend_hypotheticals_inner(h_it->first, p_it->first, p_it->second); + } + } + } else { + for (const auto &[h, m] : parent_claims) { + for (const auto &[p, d] : m) { + extend_hypotheticals_inner(h, p, d); + } + } + } + } + + void add_claims(const libp2p::peer::PeerId &peer, + const CandidateClaims &c) { + if (c.parent_hash_and_id) { + const auto &pc = *c.parent_hash_and_id; + auto &sub_claims = parent_claims[pc.first][pc.second]; + + bool found = false; + for (size_t p = 0; p < sub_claims.size(); ++p) { + if (sub_claims[p].first == c.relay_parent) { + sub_claims[p].second += 1; + found = true; + break; + } + } + if (!found) { + sub_claims.emplace_back(c.relay_parent, 1); + } + } + claims.emplace_back(peer, c); + } + }; + + struct ConfirmedCandidate { + network::CommittedCandidateReceipt receipt; + runtime::PersistedValidationData persisted_validation_data; + GroupIndex assigned_group; + RelayHash parent_hash; + std::unordered_set importable_under; + + HypotheticalCandidate to_hypothetical( + const CandidateHash &candidate_hash) const { + return HypotheticalCandidateComplete{ + .candidate_hash = candidate_hash, + .receipt = receipt, + .persisted_validation_data = persisted_validation_data, + }; + } + + ParachainId para_id() const { + return receipt.descriptor.para_id; + } + + GroupIndex group_index() const { + return assigned_group; + } + + const RelayHash &parent_head_data_hash() const { + return parent_hash; + } + + const Hash ¶_head() const { + return receipt.descriptor.para_head_hash; + } + + const RelayHash &relay_parent() const { + return receipt.descriptor.relay_parent; + } + + bool is_importable(std::optional> + under_active_leaf) const { + if (!under_active_leaf) { + return !importable_under.empty(); + } + return importable_under.find(under_active_leaf->get()) + != importable_under.end(); + } + }; + + using CandidateState = + boost::variant; + + struct Candidates { + std::unordered_map candidates; + std::unordered_map< + Hash, + std::unordered_map>> + by_parent; + + std::vector frontier_hypotheticals( + const std::optional, + ParachainId>> &parent) const { + std::vector v; + auto extend_hypotheticals = + [&](const CandidateHash &ch, + const CandidateState &cs, + const std::optional< + std::pair, ParachainId>> + &maybe_required_parent) { + visit_in_place( + cs, + [&](const UnconfirmedCandidate &u) { + u.extend_hypotheticals(ch, v, maybe_required_parent); + }, + [&](const ConfirmedCandidate &c) { + v.emplace_back(c.to_hypothetical(ch)); + }); + }; + + if (parent) { + if (auto h_it = by_parent.find(parent->first); + h_it != by_parent.end()) { + if (auto p_it = h_it->second.find(parent->second); + p_it != h_it->second.end()) { + for (const auto &ch : p_it->second) { + if (auto it = candidates.find(ch); it != candidates.end()) { + extend_hypotheticals(ch, it->second, parent); + } + } + } + } + } else { + for (const auto &[ch, cs] : candidates) { + extend_hypotheticals(ch, cs, std::nullopt); + } + } + + return v; + } + + bool is_confirmed(const CandidateHash &candidate_hash) const { + auto it = candidates.find(candidate_hash); + if (it != candidates.end()) { + return is_type(it->second); + } + return false; + } + + bool is_importable(const CandidateHash &candidate_hash) const { + auto opt_confirmed = get_confirmed(candidate_hash); + if (!opt_confirmed) { + return false; + } + return opt_confirmed->get().is_importable(std::nullopt); + } + + std::optional> + get_confirmed(const CandidateHash &candidate_hash) const { + auto it = candidates.find(candidate_hash); + if (it != candidates.end()) { + return if_type(it->second); + } + return std::nullopt; + } + + bool insert_unconfirmed(const libp2p::peer::PeerId &peer, + const CandidateHash &candidate_hash, + const Hash &claimed_relay_parent, + GroupIndex claimed_group_index, + const std::optional> + &claimed_parent_hash_and_id) { + const auto &[it, inserted] = + candidates.emplace(candidate_hash, UnconfirmedCandidate{}); + return visit_in_place( + it->second, + [&](UnconfirmedCandidate &c) { + c.add_claims(peer, + CandidateClaims{ + .relay_parent = claimed_relay_parent, + .group_index = claimed_group_index, + .parent_hash_and_id = claimed_parent_hash_and_id, + }); + if (claimed_parent_hash_and_id) { + by_parent[claimed_parent_hash_and_id->first] + [claimed_parent_hash_and_id->second] + .insert(candidate_hash); + } + return true; + }, + [&](ConfirmedCandidate &c) { + if (c.receipt.descriptor.relay_parent != claimed_relay_parent) { + return false; + } + if (c.assigned_group != claimed_group_index) { + return false; + } + if (claimed_parent_hash_and_id) { + const auto &[claimed_parent_hash, claimed_id] = + *claimed_parent_hash_and_id; + if (c.parent_hash != claimed_parent_hash) { + return false; + } + if (c.receipt.descriptor.para_id != claimed_id) { + return false; + } + } + return true; + }); + } + + void note_importable_under(const HypotheticalCandidate &candidate, + const Hash &leaf_hash) { + visit_in_place( + candidate, + [&](const HypotheticalCandidateIncomplete &v) { + if (auto it = candidates.find(v.candidate_hash); + it != candidates.end()) { + if (auto c = if_type(it->second)) { + c->get().note_maybe_importable_under( + leaf_hash, + UnconfirmedImportable{ + .relay_parent = v.candidate_relay_parent, + .parent_hash = v.parent_head_data_hash, + .para_id = v.candidate_para, + }); + } + } + }, + [&](const HypotheticalCandidateComplete &v) { + if (auto it = candidates.find(v.candidate_hash); + it != candidates.end()) { + if (auto c = if_type(it->second)) { + c->get().importable_under.insert(leaf_hash); + } + } + }); + } + + std::optional confirm_candidate( + const CandidateHash &candidate_hash, + const network::CommittedCandidateReceipt &candidate_receipt, + const runtime::PersistedValidationData &persisted_validation_data, + GroupIndex assigned_group, + const std::shared_ptr &hasher) { + const auto parent_hash = + hasher->blake2b_256(persisted_validation_data.parent_head); + const auto &relay_parent = candidate_receipt.descriptor.relay_parent; + const auto para_id = candidate_receipt.descriptor.para_id; + + std::optional prev_state; + if (auto it = candidates.find(candidate_hash); it != candidates.end()) { + prev_state = it->second; + } + + ConfirmedCandidate &new_confirmed = + [&]() -> std::reference_wrapper { + auto [it, _] = candidates.insert_or_assign( + candidate_hash, + ConfirmedCandidate{ + .receipt = candidate_receipt, + .persisted_validation_data = persisted_validation_data, + .assigned_group = assigned_group, + .parent_hash = parent_hash, + .importable_under = {}, + }); + auto n{boost::get(&it->second)}; + return {*n}; + }() + .get(); + by_parent[parent_hash][para_id].insert(candidate_hash); + + if (!prev_state) { + return PostConfirmation{ + .hypothetical = new_confirmed.to_hypothetical(candidate_hash), + .reckoning = {}, + }; + } + + if (auto ps = if_type(*prev_state)) { + return std::nullopt; + } + + auto u = if_type(*prev_state); + PostConfirmationReckoning reckoning{}; + + for (const UnconfiredImportablePair &d : + u->get().unconfirmed_importable_under) { + const auto &leaf_hash = d.hash; + const auto &x = d.ui; + + if (x.relay_parent == relay_parent && x.parent_hash == parent_hash + && x.para_id == para_id) { + new_confirmed.importable_under.insert(leaf_hash); + } + } + + for (const auto &[peer, claims] : u->get().claims) { + if (claims.parent_hash_and_id) { + const auto &[claimed_parent_hash, claimed_id] = + *claims.parent_hash_and_id; + if (claimed_parent_hash != parent_hash || claimed_id != para_id) { + if (auto it_1 = by_parent.find(claimed_parent_hash); + it_1 != by_parent.end()) { + if (auto it_2 = it_1->second.find(claimed_id); + it_2 != it_1->second.end()) { + it_2->second.erase(candidate_hash); + if (it_2->second.empty()) { + it_1->second.erase(it_2); + if (it_1->second.empty()) { + by_parent.erase(it_1); + } + } + } + } + } + } + + if (claims.check(relay_parent, assigned_group, parent_hash, para_id)) { + reckoning.correct.insert(peer); + } else { + reckoning.incorrect.insert(peer); + } + } + + return PostConfirmation{ + .hypothetical = new_confirmed.to_hypothetical(candidate_hash), + .reckoning = reckoning, + }; + } + }; + +} // namespace kagome::parachain diff --git a/core/parachain/validator/impl/fragment_tree.cpp b/core/parachain/validator/impl/fragment_tree.cpp new file mode 100644 index 0000000000..06efe17f65 --- /dev/null +++ b/core/parachain/validator/impl/fragment_tree.cpp @@ -0,0 +1,236 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "parachain/validator/fragment_tree.hpp" + +OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain::fragment, + Constraints::Error, + e) { + using E = kagome::parachain::fragment::Constraints::Error; + switch (e) { + case E::DISALLOWED_HRMP_WATERMARK: + return "Disallowed HRMP watermark"; + case E::NO_SUCH_HRMP_CHANNEL: + return "No such HRMP channel"; + case E::HRMP_BYTES_OVERFLOW: + return "HRMP bytes overflow"; + case E::HRMP_MESSAGE_OVERFLOW: + return "HRMP message overflow"; + case E::UMP_MESSAGE_OVERFLOW: + return "UMP message overflow"; + case E::UMP_BYTES_OVERFLOW: + return "UMP bytes overflow"; + case E::DMP_MESSAGE_UNDERFLOW: + return "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 "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"; +} + +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; + } else if (ancestor.number != prev - 1) { + return Scope::Error::UNEXPECTED_ANCESTOR; + } else if (prev == base_constraints.min_relay_parent_number) { + break; + } else { + prev = ancestor.number; + ancestors_by_hash.emplace(ancestor.hash, ancestor); + ancestors_map.emplace(ancestor.number, ancestor); + } + } + + return Scope{ + para, + relay_parent, + ancestors_map, + ancestors_by_hash, + pending_availability, + base_constraints, + 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( + const ConstraintModifications &modifications) const { + if (modifications.hrmp_watermark) { + if (auto hrmp_watermark = if_type( + *modifications.hrmp_watermark)) { + bool found = false; + for (const BlockNumber &w : hrmp_inbound.valid_watermarks) { + if (w == hrmp_watermark->get().v) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + } + + /// TODO(iceseer): do + /// implement + return true; + } + + outcome::result Constraints::applyModifications( + const ConstraintModifications &modifications) const { + Constraints new_constraint{*this}; + if (modifications.required_parent) { + new_constraint.required_parent = *modifications.required_parent; + } + + if (modifications.hrmp_watermark) { + const auto &hrmp_watermark = *modifications.hrmp_watermark; + const auto hrmp_watermark_val = fromHrmpWatermarkUpdate(hrmp_watermark); + + auto it_b = new_constraint.hrmp_inbound.valid_watermarks.begin(); + auto it_e = new_constraint.hrmp_inbound.valid_watermarks.end(); + if (auto it = std::lower_bound(it_b, it_e, hrmp_watermark_val); + !(it == it_e) && !(hrmp_watermark_val < *it)) { + BOOST_ASSERT(it != it_e); + new_constraint.hrmp_inbound.valid_watermarks.erase(it_b, it + 1); + } else { + const bool process = visit_in_place( + hrmp_watermark, + [&](const HrmpWatermarkUpdateHead &) { + new_constraint.hrmp_inbound.valid_watermarks.erase(it_b, it); + return true; + }, + [&](const HrmpWatermarkUpdateTrunk &val) { return false; }); + if (!process) { + return Error::DISALLOWED_HRMP_WATERMARK; + } + } + } + + for (const auto &[id, outbound_hrmp_mod] : modifications.outbound_hrmp) { + if (auto it = new_constraint.hrmp_channels_out.find(id); + it != new_constraint.hrmp_channels_out.end()) { + auto &outbound = it->second; + OUTCOME_TRY(math::checked_sub(outbound.bytes_remaining, + outbound_hrmp_mod.bytes_submitted, + Error::HRMP_BYTES_OVERFLOW)); + OUTCOME_TRY(math::checked_sub(outbound.messages_remaining, + outbound_hrmp_mod.messages_submitted, + Error::HRMP_MESSAGE_OVERFLOW)); + } else { + return Error::NO_SUCH_HRMP_CHANNEL; + } + } + + OUTCOME_TRY(math::checked_sub(new_constraint.ump_remaining, + modifications.ump_messages_sent, + Error::UMP_MESSAGE_OVERFLOW)); + OUTCOME_TRY(math::checked_sub(new_constraint.ump_remaining_bytes, + modifications.ump_bytes_sent, + Error::UMP_BYTES_OVERFLOW)); + + if (modifications.dmp_messages_processed + > new_constraint.dmp_remaining_messages.size()) { + return Error::DMP_MESSAGE_UNDERFLOW; + } else { + new_constraint.dmp_remaining_messages.erase( + new_constraint.dmp_remaining_messages.begin(), + new_constraint.dmp_remaining_messages.begin() + + modifications.dmp_messages_processed); + } + + if (modifications.code_upgrade_applied) { + if (auto new_code = std::move(new_constraint.future_validation_code)) { + BOOST_ASSERT(!new_constraint.future_validation_code); + new_constraint.validation_code_hash = std::move(new_code->second); + } else { + return Error::APPLIED_NONEXISTENT_CODE_UPGRADE; + } + } + + return new_constraint; + } +} // namespace kagome::parachain::fragment diff --git a/core/parachain/validator/impl/parachain_observer_impl.cpp b/core/parachain/validator/impl/parachain_observer_impl.cpp index b927721d4b..b90ab7aa72 100644 --- a/core/parachain/validator/impl/parachain_observer_impl.cpp +++ b/core/parachain/validator/impl/parachain_observer_impl.cpp @@ -38,10 +38,10 @@ namespace kagome::parachain { void ParachainObserverImpl::onIncomingMessage( const libp2p::peer::PeerId &peer_id, - network::CollationProtocolMessage &&collation_message) { + network::VersionedCollatorProtocolMessage &&msg) { visit_in_place( - std::move(collation_message), - [&](network::CollationMessage &&collation_msg) { + std::move(msg), + [&](kagome::network::CollationMessage &&collation_msg) { visit_in_place( std::move(collation_msg), [&](network::CollatorDeclaration &&collation_decl) { @@ -51,30 +51,64 @@ namespace kagome::parachain { std::move(collation_decl.signature)); }, [&](network::CollatorAdvertisement &&collation_adv) { - onAdvertise(peer_id, std::move(collation_adv.relay_parent)); + onAdvertise(peer_id, + std::move(collation_adv.relay_parent), + std::nullopt); }, [&](auto &&) { - SL_WARN(logger_, "Unexpected collation message from."); + SL_WARN(logger_, "Unexpected V1 collation message from."); }); }, + [&](kagome::network::vstaging::CollatorProtocolMessage + &&collation_msg) { + if (auto m = + if_type(collation_msg)) { + visit_in_place( + std::move(m->get()), + [&](kagome::network::vstaging::CollatorProtocolMessageDeclare + &&collation_decl) { + onDeclare(peer_id, + std::move(collation_decl.collator_id), + std::move(collation_decl.para_id), + std::move(collation_decl.signature)); + }, + [&](kagome::network::vstaging:: + CollatorProtocolMessageAdvertiseCollation + &&collation_adv) { + onAdvertise( + peer_id, + std::move(collation_adv.relay_parent), + std::make_pair( + std::move(collation_adv.candidate_hash), + std::move(collation_adv.parent_head_data_hash))); + }, + [&](auto &&) { + SL_WARN(logger_, + "Unexpected VStaging collation message from."); + }); + } else { + SL_WARN(logger_, + "Unexpected VStaging collation protocol message from."); + } + }, [&](auto &&) { - SL_WARN(logger_, "Unexpected collation message from."); + SL_WARN(logger_, "Unexpected versioned collation message from."); }); } void ParachainObserverImpl::onIncomingCollationStream( - const libp2p::peer::PeerId &peer_id) { - processor_->onIncomingCollationStream(peer_id); + const libp2p::peer::PeerId &peer_id, network::CollationVersion version) { + processor_->onIncomingCollationStream(peer_id, version); } void ParachainObserverImpl::onIncomingValidationStream( - const libp2p::peer::PeerId &peer_id) { - processor_->onIncomingValidationStream(peer_id); + const libp2p::peer::PeerId &peer_id, network::CollationVersion version) { + processor_->onIncomingValidationStream(peer_id, version); } void ParachainObserverImpl::onIncomingMessage( const libp2p::peer::PeerId &peer_id, - network::ValidatorProtocolMessage &&message) { + network::VersionedValidatorProtocolMessage &&message) { processor_->onValidationProtocolMsg(peer_id, message); approval_distribution_->onValidationProtocolMsg(peer_id, message); } @@ -91,8 +125,17 @@ namespace kagome::parachain { return network::ProtocolError::PROTOCOL_NOT_IMPLEMENTED; } - void ParachainObserverImpl::onAdvertise(const libp2p::peer::PeerId &peer_id, - primitives::BlockHash relay_parent) { + outcome::result + ParachainObserverImpl::OnCollationRequest( + network::vstaging::CollationFetchingRequest request) { + /// Need to decrease rank of the peer and return error. + return network::ProtocolError::PROTOCOL_NOT_IMPLEMENTED; + } + + void ParachainObserverImpl::onAdvertise( + const libp2p::peer::PeerId &peer_id, + primitives::BlockHash relay_parent, + std::optional> &&prospective_candidate) { const auto peer_state = pm_->getPeerState(peer_id); if (!peer_state) { logger_->warn("Received collation advertisement from unknown peer {}", @@ -107,23 +150,18 @@ namespace kagome::parachain { return; } - if (auto check_res = processor_->advCanBeProcessed(relay_parent, peer_id); - !check_res) { - logger_->warn("Insert advertisement from {} failed: {}", - peer_id, - check_res.error().message()); - return; - } - - logger_->info( - "Got advertisement from: {}, relay parent: {}", peer_id, relay_parent); - processor_->requestCollations(network::CollationEvent{ - .collator_id = result.value().first, - .pending_collation = - network::PendingCollation{.relay_parent = relay_parent, - .para_id = result.value().second, - .peer_id = peer_id}, - }); + processor_->handleAdvertisement( + network::CollationEvent{ + .collator_id = result.value().first, + .pending_collation = + { + .relay_parent = relay_parent, + .para_id = result.value().second, + .peer_id = peer_id, + .commitments_hash = {}, + }, + }, + std::move(prospective_candidate)); } void ParachainObserverImpl::onDeclare(const libp2p::peer::PeerId &peer_id, diff --git a/core/parachain/validator/impl/parachain_observer_impl.hpp b/core/parachain/validator/impl/parachain_observer_impl.hpp index b97138c1c8..294eaab418 100644 --- a/core/parachain/validator/impl/parachain_observer_impl.hpp +++ b/core/parachain/validator/impl/parachain_observer_impl.hpp @@ -9,7 +9,7 @@ #include "parachain/validator/parachain_observer.hpp" #include "log/logger.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" namespace kagome::network { class PeerManager; @@ -39,28 +39,33 @@ namespace kagome::parachain { /// collation protocol observer void onIncomingMessage( const libp2p::peer::PeerId &peer_id, - network::CollationProtocolMessage &&collation_message) override; - void onIncomingCollationStream( - const libp2p::peer::PeerId &peer_id) override; + network::VersionedCollatorProtocolMessage &&msg) override; + void onIncomingCollationStream(const libp2p::peer::PeerId &peer_id, + network::CollationVersion version) override; /// validation protocol observer - void onIncomingMessage( - const libp2p::peer::PeerId &peer_id, - network::ValidatorProtocolMessage &&validation_message) override; - void onIncomingValidationStream( - const libp2p::peer::PeerId &peer_id) override; + void onIncomingMessage(const libp2p::peer::PeerId &peer_id, + network::VersionedValidatorProtocolMessage + &&validation_message) override; + void onIncomingValidationStream(const libp2p::peer::PeerId &peer_id, + network::CollationVersion version) override; /// fetch collation protocol observer outcome::result OnCollationRequest( network::CollationFetchingRequest request) override; + outcome::result + OnCollationRequest( + network::vstaging::CollationFetchingRequest request) override; /// We should response with PoV block if we seconded this candidate outcome::result OnPovRequest( network::RequestPov request) override; private: - void onAdvertise(const libp2p::peer::PeerId &peer_id, - primitives::BlockHash relay_parent); + void onAdvertise( + const libp2p::peer::PeerId &peer_id, + primitives::BlockHash relay_parent, + std::optional> &&prospective_candidate); void onDeclare(const libp2p::peer::PeerId &peer_id, network::CollatorPublicKey pubkey, diff --git a/core/parachain/validator/impl/parachain_processor.cpp b/core/parachain/validator/impl/parachain_processor.cpp index f706f586c4..378506b368 100644 --- a/core/parachain/validator/impl/parachain_processor.cpp +++ b/core/parachain/validator/impl/parachain_processor.cpp @@ -7,6 +7,7 @@ #include "parachain/validator/parachain_processor.hpp" #include +#include #include #include @@ -25,6 +26,7 @@ #include "parachain/peer_relay_parent_knowledge.hpp" #include "scale/scale.hpp" #include "utils/async_sequence.hpp" +#include "utils/map.hpp" #include "utils/profiler.hpp" #include "utils/thread_handler.hpp" @@ -35,8 +37,12 @@ OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain, switch (e) { case E::RESPONSE_ALREADY_RECEIVED: return "Response already present"; + case E::REJECTED_BY_PROSPECTIVE_PARACHAINS: + return "Rejected by prospective parachains"; case E::COLLATION_NOT_FOUND: return "Collation not found"; + case E::UNDECLARED_COLLATOR: + return "Undeclared collator"; case E::KEY_NOT_PRESENT: return "Private key is not present"; case E::VALIDATION_FAILED: @@ -53,6 +59,18 @@ OUTCOME_CPP_DEFINE_CATEGORY(kagome::parachain, return "Node is not a validator"; case E::NOT_SYNCHRONIZED: return "Node not synchronized"; + case E::PEER_LIMIT_REACHED: + return "Peer limit reached"; + case E::PROTOCOL_MISMATCH: + return "Protocol mismatch"; + case E::NOT_CONFIRMED: + return "Candidate not confirmed"; + case E::NO_STATE: + return "No parachain state"; + case E::NO_SESSION_INFO: + return "No session info"; + case E::OUT_OF_BOUND: + return "Index out of bound"; } return "Unknown parachain processor error"; } @@ -85,7 +103,8 @@ namespace kagome::parachain { const application::AppConfiguration &app_config, std::shared_ptr app_state_manager, primitives::events::BabeStateSubscriptionEnginePtr babe_status_observable, - std::shared_ptr query_audi) + std::shared_ptr query_audi, + std::shared_ptr prospective_parachains) : pm_(std::move(pm)), runtime_info_(std::move(runtime_info)), crypto_provider_(std::move(crypto_provider)), @@ -107,7 +126,8 @@ namespace kagome::parachain { worker_thread_context_{[&] { BOOST_ASSERT(worker_thread_pool); return worker_thread_pool->io_context(); - }()} { + }()}, + prospective_parachains_{std::move(prospective_parachains)} { BOOST_ASSERT(pm_); BOOST_ASSERT(peer_view_); BOOST_ASSERT(crypto_provider_); @@ -123,8 +143,12 @@ namespace kagome::parachain { BOOST_ASSERT(signer_factory_); BOOST_ASSERT(babe_status_observable_); BOOST_ASSERT(query_audi_); + BOOST_ASSERT(prospective_parachains_); app_state_manager->takeControl(*this); + our_current_state_.implicit_view.emplace(prospective_parachains_); + BOOST_ASSERT(our_current_state_.implicit_view); + metrics_registry_->registerGaugeFamily( kIsParachainValidator, "Tracks if the validator participates in parachain consensus. " @@ -136,19 +160,38 @@ namespace kagome::parachain { metric_is_parachain_validator_->set(false); } + void ParachainProcessorImpl::OnBroadcastBitfields( + const primitives::BlockHash &relay_parent, + const network::SignedBitfield &bitfield) { + REINVOKE( + main_thread_context_, OnBroadcastBitfields, relay_parent, bitfield); + + SL_TRACE(logger_, "Distribute bitfield on {}", relay_parent); + auto relay_parent_state = tryGetStateByRelayParent(relay_parent); + if (!relay_parent_state) { + SL_TRACE(logger_, + "After `OnBroadcastBitfields` no parachain state on " + "relay_parent. (relay " + "parent={})", + relay_parent); + return; + } + + send_to_validators_group( + relay_parent, + {network::VersionedValidatorProtocolMessage{ + network::vstaging::ValidatorProtocolMessage{ + network::vstaging::BitfieldDistributionMessage{ + network::vstaging::BitfieldDistribution{relay_parent, + bitfield}}}}}); + } + bool ParachainProcessorImpl::prepare() { bitfield_signer_->setBroadcastCallback( - [log{logger_}, wptr_self{weak_from_this()}]( - const primitives::BlockHash &relay_parent, - const network::SignedBitfield &bitfield) { - SL_VERBOSE(log, "Distribute bitfield on {}", relay_parent); + [wptr_self{weak_from_this()}](const primitives::BlockHash &relay_parent, + const network::SignedBitfield &bitfield) { if (auto self = wptr_self.lock()) { - auto msg = std::make_shared< - network::WireMessage>( - network::BitfieldDistribution{relay_parent, bitfield}); - - self->pm_->getStreamEngine()->broadcast( - self->router_->getValidationProtocol(), msg); + self->OnBroadcastBitfields(relay_parent, bitfield); } }); @@ -205,23 +248,7 @@ namespace kagome::parachain { auto /*event_type*/, const primitives::events::ChainEventParams &event) { if (auto self = wptr.lock()) { - if (auto const value = if_type< - const primitives::events::RemoveAfterFinalizationParams>( - event)) { - self->our_current_state_.active_leaves.exclusiveAccess( - [&](auto &active_leaves) { - for (auto const &lost : value->get()) { - SL_TRACE(self->logger_, - "Remove from storages.(relay parent={})", - lost); - - self->backing_store_->remove(lost); - self->av_store_->remove(lost); - self->bitfield_store_->remove(lost); - active_leaves.erase(lost); - } - }); - } + self->onDeactivateBlocks(event); } }); @@ -235,56 +262,328 @@ namespace kagome::parachain { auto /*event_type*/, const network::ExView &event) { if (auto self = wptr.lock()) { - /// clear caches - BOOST_ASSERT(runningInThisThread(self->main_thread_context_)); - - self->our_current_state_.active_leaves.exclusiveAccess( - [&](auto &active_leaves) { - for (auto const &lost : event.lost) { - SL_TRACE(self->logger_, - "Removed backing task.(relay parent={})", - lost); - - self->our_current_state_.state_by_relay_parent.erase(lost); - self->pending_candidates.exclusiveAccess( - [&](auto &container) { container.erase(lost); }); - active_leaves.erase(lost); - } - active_leaves.insert(event.new_head.hash()); - }); - if (auto r = self->canProcessParachains(); r.has_error()) { - return; - } + self->onViewUpdated(event); + } + }); - self->createBackingTask(event.new_head.hash()); - SL_TRACE(self->logger_, - "Update my view.(new head={}, finalized={}, leaves={})", - event.new_head.hash(), - event.view.finalized_number_, - event.view.heads_.size()); - self->broadcastView(event.view); + remote_view_sub_ = std::make_shared( + peer_view_->getRemoteViewObservable(), false); + remote_view_sub_->subscribe(remote_view_sub_->generateSubscriptionSetId(), + network::PeerView::EventType::kViewUpdated); + remote_view_sub_->setCallback( + [wptr{weak_from_this()}](auto /*set_id*/, + auto && /*internal_obj*/, + auto /*event_type*/, + const libp2p::peer::PeerId &peer_id, + const network::View &view) { + if (auto self = wptr.lock()) { + self->onUpdatePeerView(peer_id, view); } }); + return true; } + void ParachainProcessorImpl::onUpdatePeerView( + const libp2p::peer::PeerId &peer_id, const network::View &view) { + REINVOKE(main_thread_context_, onUpdatePeerView, peer_id, view); + + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// `handle_peer_view_update` keep peer view to send only + /// perfect messages + for (const auto &h : view.heads_) { + send_peer_messages_for_relay_parent({{peer_id}}, h); + } + } + + void ParachainProcessorImpl::send_peer_messages_for_relay_parent( + std::optional> peer_id, + const RelayHash &relay_parent) { + auto parachain_state = tryGetStateByRelayParent(relay_parent); + if (!parachain_state) { + SL_WARN(logger_, + "After `send_peer_messages_for_relay_parent` no parachain state " + "on relay_parent. (relay_parent={})", + relay_parent); + return; + } + + auto opt_session_info = retrieveSessionInfo(relay_parent); + if (!opt_session_info) { + SL_WARN(logger_, "No session info. (relay_parent={})", relay_parent); + return; + } + + Groups groups{opt_session_info->validator_groups}; + std::deque messages; + + for (const auto &candidate_hash : + parachain_state->get().issued_statements) { + if (auto confirmed_candidate = + candidates_.get_confirmed(candidate_hash)) { + const auto group_index = confirmed_candidate->get().group_index(); + const auto group_size = groups.groups[group_index].size(); + + auto local_knowledge = + local_knowledge_filter(group_size, + group_index, + candidate_hash, + *parachain_state->get().statement_store); + network::VersionedValidatorProtocolMessage manifest{ + kagome::network::vstaging::ValidatorProtocolMessage{ + kagome::network::vstaging::StatementDistributionMessage{ + kagome::network::vstaging::BackedCandidateManifest{ + .relay_parent = relay_parent, + .candidate_hash = candidate_hash, + .group_index = group_index, + .para_id = confirmed_candidate->get().para_id(), + .parent_head_data_hash = + confirmed_candidate->get().parent_head_data_hash(), + .statement_knowledge = local_knowledge, + }}}}; + + auto m = acknowledgement_and_statement_messages( + *parachain_state->get().statement_store, + groups.groups[group_index], + local_knowledge, + candidate_hash, + relay_parent); + + messages.emplace_back(std::move(manifest)); + messages.insert(messages.end(), + std::move_iterator(m.begin()), + std::move_iterator(m.end())); + } + } + + if (peer_id) { + auto se = pm_->getStreamEngine(); + BOOST_ASSERT(se); + + for (auto &msg : messages) { + if (auto m = + if_type(msg)) { + auto message = std::make_shared>( + std::move(m->get())); + se->send(peer_id->get(), + router_->getValidationProtocolVStaging(), + message); + } + } + } else { + send_to_validators_group(relay_parent, std::move(messages)); + } + } + + void ParachainProcessorImpl::onViewUpdated(const network::ExView &event) { + REINVOKE(main_thread_context_, onViewUpdated, event); + + const auto &relay_parent = event.new_head.hash(); + if (auto r = canProcessParachains(); r.has_error()) { + return; + } + + [[maybe_unused]] const auto _ = + prospective_parachains_->onActiveLeavesUpdate(network::ExViewRef{ + .new_head = {event.new_head}, + .lost = event.lost, + }); + backing_store_->onActivateLeaf(relay_parent); + createBackingTask(relay_parent); + SL_TRACE(logger_, + "Update my view.(new head={}, finalized={}, leaves={})", + relay_parent, + event.view.finalized_number_, + event.view.heads_.size()); + broadcastView(event.view); + broadcastViewToGroup(relay_parent, event.view); + for (const auto &h : event.view.heads_) { + send_peer_messages_for_relay_parent(std::nullopt, h); + } + new_leaf_fragment_tree_updates(relay_parent); + + for (const auto &lost : event.lost) { + SL_TRACE(logger_, "Removed backing task.(relay parent={})", lost); + + our_current_state_.per_leaf.erase(lost); + our_current_state_.implicit_view->deactivate_leaf(lost); + our_current_state_.state_by_relay_parent.erase(lost); + pending_candidates.erase(lost); + our_current_state_.active_leaves.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; + } + createBackingTask(maybe_new); + } + } + + void ParachainProcessorImpl::onDeactivateBlocks( + const primitives::events::ChainEventParams &event) { + REINVOKE(main_thread_context_, onDeactivateBlocks, event); + + if (const auto value = + if_type( + event)) { + for (const auto &lost : value->get()) { + SL_TRACE(logger_, "Remove from storages.(relay parent={})", lost); + + backing_store_->onDeactivateLeaf(lost); + av_store_->remove(lost); + bitfield_store_->remove(lost); + our_current_state_.active_leaves.erase(lost); + } + } + } + void ParachainProcessorImpl::broadcastViewExcept( const libp2p::peer::PeerId &peer_id, const network::View &view) const { auto msg = std::make_shared< network::WireMessage>( network::ViewUpdate{.view = view}); pm_->getStreamEngine()->broadcast( - router_->getValidationProtocol(), + router_->getValidationProtocolVStaging(), msg, [&](const libp2p::peer::PeerId &p) { return peer_id != p; }); } + void ParachainProcessorImpl::broadcastViewToGroup( + const primitives::BlockHash &relay_parent, const network::View &view) { + auto opt_parachain_state = tryGetStateByRelayParent(relay_parent); + if (!opt_parachain_state) { + SL_ERROR( + logger_, "Relay state should exist. (relay_parent)", relay_parent); + return; + } + + std::deque group; + if (auto r = runtime_info_->get_session_info(relay_parent)) { + auto &[session, info] = r.value(); + if (info.our_group) { + for (auto &i : session.validator_groups[*info.our_group]) { + if (auto peer = query_audi_->get(session.discovery_keys[i])) { + group.emplace_back(peer->id); + } + } + } + } + + auto protocol = [&]() -> std::shared_ptr { + return router_->getValidationProtocolVStaging(); + }(); + + auto make_send = [&]( + const Msg &msg, + const std::shared_ptr + &protocol) { + auto se = pm_->getStreamEngine(); + BOOST_ASSERT(se); + + auto message = std::make_shared< + network::WireMessage>(msg); + SL_TRACE( + logger_, + "Broadcasting view update to group.(relay_parent={}, group_size={})", + relay_parent, + group.size()); + + for (const auto &peer : group) { + SL_TRACE(logger_, "Send to peer from group. (peer={})", peer); + se->send(peer, protocol, message); + } + }; + + make_send(network::vstaging::ViewUpdate{view}, + router_->getValidationProtocolVStaging()); + } + void ParachainProcessorImpl::broadcastView(const network::View &view) const { auto msg = std::make_shared< network::WireMessage>( network::ViewUpdate{.view = view}); - pm_->getStreamEngine()->broadcast(router_->getCollationProtocol(), msg); - pm_->getStreamEngine()->broadcast(router_->getValidationProtocol(), msg); + pm_->getStreamEngine()->broadcast(router_->getCollationProtocolVStaging(), + msg); + pm_->getStreamEngine()->broadcast(router_->getValidationProtocolVStaging(), + msg); } outcome::result> @@ -319,6 +618,10 @@ namespace kagome::parachain { OUTCOME_TRY(groups, parachain_host_->validator_groups(relay_parent)); OUTCOME_TRY(cores, parachain_host_->availability_cores(relay_parent)); OUTCOME_TRY(validator, isParachainValidator(relay_parent)); + OUTCOME_TRY(session_index, + parachain_host_->session_index_for_child(relay_parent)); + OUTCOME_TRY(session_info, + parachain_host_->session_info(relay_parent, session_index)); auto &[validator_groups, group_rotation_info] = groups; if (!validator) { @@ -350,24 +653,49 @@ namespace kagome::parachain { } } + std::optional statement_store; + auto mode = + prospective_parachains_->prospectiveParachainsMode(relay_parent); + if (mode) { + [[maybe_unused]] const auto _ = + our_current_state_.implicit_view->activate_leaf(relay_parent); + if (session_info) { + std::unordered_map> groups; + for (size_t g = 0; g < session_info->validator_groups.size(); ++g) { + groups[g] = std::move(session_info->validator_groups[g]); + } + statement_store.emplace(Groups{std::move(groups)}); + } + } + SL_VERBOSE(logger_, - "Inited new backing task.(assignment={}, our index={}, relay " + "Inited new backing task v2.(assignment={}, our index={}, relay " "parent={})", assignment, validator->validatorIndex(), relay_parent); + OUTCOME_TRY( + minimum_backing_votes, + parachain_host_->minimum_backing_votes(relay_parent, session_index)); + return RelayParentState{ + .prospective_parachains_mode = mode, .assignment = assignment, .seconded = {}, .our_index = validator->validatorIndex(), .required_collator = required_collator, + .collations = {}, .table_context = TableContext{ .validator = std::move(validator), .groups = std::move(out_groups), .validators = std::move(validators), }, + .statement_store = std::move(statement_store), + .availability_cores = cores, + .group_rotation_info = group_rotation_info, + .minimum_backing_votes = minimum_backing_votes, .awaiting_validation = {}, .issued_statements = {}, .peers_advertised = {}, @@ -390,225 +718,1207 @@ namespace kagome::parachain { } void ParachainProcessorImpl::handleFetchedCollation( - network::CollationEvent &&pending_collation, + PendingCollation &&pending_collation, network::CollationFetchingResponse &&response) { - logger_->trace( - "Processing collation from {}, relay parent: {}, para id: {}", - pending_collation.pending_collation.peer_id, - pending_collation.pending_collation.relay_parent, - pending_collation.pending_collation.para_id); - - auto opt_parachain_state = tryGetStateByRelayParent( - pending_collation.pending_collation.relay_parent); + REINVOKE(main_thread_context_, + handleFetchedCollation, + std::move(pending_collation), + std::move(response)); + + SL_TRACE(logger_, + "Processing collation from {}, relay parent: {}, para id: {}", + pending_collation.peer_id, + pending_collation.relay_parent, + pending_collation.para_id); + + our_current_state_.collation_requests_cancel_handles.erase( + pending_collation); + + auto collation_response = + if_type(response.response_data); + if (!collation_response) { + SL_WARN(logger_, + "Not a CollationResponse message from {}.", + pending_collation.peer_id); + return; + } + + auto opt_parachain_state = + tryGetStateByRelayParent(pending_collation.relay_parent); if (!opt_parachain_state) { - logger_->trace("Fetched collation from {}:{} out of view", - pending_collation.pending_collation.peer_id, - pending_collation.pending_collation.relay_parent); + SL_TRACE(logger_, + "Fetched collation from {}:{} out of view", + pending_collation.peer_id, + pending_collation.relay_parent); return; } + auto &per_relay_parent = opt_parachain_state->get(); + auto &collations = per_relay_parent.collations; + const auto &relay_parent_mode = + per_relay_parent.prospective_parachains_mode; + + network::CandidateReceipt receipt( + std::move(collation_response->get().receipt)); + network::ParachainBlock pov(std::move(collation_response->get().pov)); + const network::CandidateDescriptor &descriptor = receipt.descriptor; + + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// fetched_candidates ??? + auto ¶chain_state = opt_parachain_state->get(); auto &assignment = parachain_state.assignment; - auto &seconded = parachain_state.seconded; + // auto &seconded = parachain_state.seconded; auto &issued_statements = parachain_state.issued_statements; - const network::CandidateDescriptor &descriptor = - candidateDescriptorFrom(response); - primitives::BlockHash const candidate_hash = candidateHashFrom(response); - if (parachain_state.required_collator && *parachain_state.required_collator != descriptor.collator_id) { - logger_->warn( - "Fetched collation from wrong collator: received {} from {}", - descriptor.collator_id, - pending_collation.pending_collation.peer_id); + SL_WARN(logger_, + "Fetched collation from wrong collator: received {} from {}", + descriptor.collator_id, + pending_collation.peer_id); return; } - auto *collation_response = - boost::get(&response.response_data); - if (nullptr == collation_response) { - logger_->warn("Not a CollationResponse message from {}.", - pending_collation.pending_collation.peer_id); + const auto candidate_para_id = descriptor.para_id; + if (candidate_para_id != assignment) { + SL_WARN(logger_, + "Try to second for para_id {} out of our assignment {}.", + candidate_para_id, + assignment ? std::to_string(*assignment) : "{no assignment}"); return; } - const auto candidate_para_id = descriptor.para_id; - if (candidate_para_id != assignment) { - logger_->warn( - "Try to second for para_id {} out of our assignment {}.", - candidate_para_id, - assignment ? std::to_string(*assignment) : "{no assignment}"); + if (issued_statements.count(receipt.hash(*hasher_)) != 0) { + SL_DEBUG( + logger_, "Statement of {} already issued.", receipt.hash(*hasher_)); + return; + } + + if (auto it = pending_candidates.find(pending_collation.relay_parent); + it != pending_candidates.end()) { + SL_WARN(logger_, + "Trying to insert a pending candidate on {} failed, because " + "there is already one.", + pending_collation.relay_parent); return; } - if (seconded) { - logger_->debug("Already have seconded block {} instead of {}.", - seconded->toString(), - candidate_hash); + pending_collation.commitments_hash = receipt.commitments_hash; + + std::optional pvd; + if (relay_parent_mode && pending_collation.prospective_candidate) { + pvd = requestProspectiveValidationData( + pending_collation.relay_parent, + pending_collation.prospective_candidate->second, + pending_collation.para_id); + } else if (!relay_parent_mode) { + pvd = requestPersistedValidationData(receipt.descriptor.relay_parent, + receipt.descriptor.para_id); + } else { return; } - if (issued_statements.count(candidate_hash) != 0) { - logger_->debug("Statement of {} already issued.", candidate_hash); + if (!pvd) { + SL_ERROR( + logger_, + "Persisted validation data not found. (relay parent={}, para={})", + pending_collation.relay_parent, + pending_collation.para_id); return; } - const auto can_process = - pending_candidates.exclusiveAccess([&](auto &container) { - auto it = - container.find(pending_collation.pending_collation.relay_parent); - if (it != container.end()) { - logger_->warn( - "Trying to insert a pending candidate on {} failed, because " - "there is already one.", - pending_collation.pending_collation.relay_parent); - return false; - } - pending_collation.pending_collation.commitments_hash = - collation_response->receipt.commitments_hash; - container.insert( - std::make_pair(pending_collation.pending_collation.relay_parent, - pending_collation)); - return true; - }); + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// fetched_collation_sanity_check - if (can_process) { - appendAsyncValidationTask( - std::move(collation_response->receipt), - std::move(collation_response->pov), - pending_collation.pending_collation.relay_parent, - pending_collation.pending_collation.peer_id, - parachain_state, - candidate_hash, - parachain_state.table_context.validators.size()); + pending_candidates.insert( + std::make_pair(pending_collation.relay_parent, pending_collation)); + + collations.status = CollationStatus::WaitingOnValidation; + validateAsync( + std::move(receipt), + std::move(pov), + std::move(*pvd), + pending_collation.peer_id, + pending_collation.relay_parent, + parachain_state.table_context.validators.size()); + } + + std::optional + ParachainProcessorImpl::requestProspectiveValidationData( + const RelayHash &relay_parent, + const Hash &parent_head_data_hash, + ParachainId para_id) { + return prospective_parachains_->answerProspectiveValidationDataRequest( + relay_parent, parent_head_data_hash, para_id); + } + + std::optional + ParachainProcessorImpl::fetchPersistedValidationData( + const RelayHash &relay_parent, ParachainId para_id) { + return requestPersistedValidationData(relay_parent, para_id); + } + + std::optional + ParachainProcessorImpl::requestPersistedValidationData( + const RelayHash &relay_parent, ParachainId para_id) { + auto res_data = parachain_host_->persisted_validation_data( + relay_parent, para_id, runtime::OccupiedCoreAssumption::Free); + if (res_data.has_error()) { + SL_VERBOSE(logger_, + "PersistedValidationData not found. (error={}, " + "relay_parent={} para_id={})", + res_data.error().message(), + relay_parent, + para_id); + return std::nullopt; } + return std::move(res_data.value()); } - void ParachainProcessorImpl::onValidationProtocolMsg( - const libp2p::peer::PeerId &peer_id, - const network::ValidatorProtocolMessage &message) { - if (auto m{boost::get(&message)}) { - auto bd{boost::get(m)}; - BOOST_ASSERT_MSG( - bd, "BitfieldDistribution is not present. Check message format."); + void ParachainProcessorImpl::process_bitfield_distribution( + const network::BitfieldDistributionMessage &val) { + BOOST_ASSERT(runningInThisThread(main_thread_context_)); + auto bd{boost::get(&val)}; + BOOST_ASSERT_MSG( + bd, "BitfieldDistribution is not present. Check message format."); + auto opt_session_info = retrieveSessionInfo(bd->relay_parent); + if (!opt_session_info) { SL_TRACE(logger_, - "Imported bitfield {} {}", + "Unexpected relay parent. No session info. (validator index={}, " + "relay_parent={})", bd->data.payload.ix, bd->relay_parent); - bitfield_store_->putBitfield(bd->relay_parent, bd->data); return; } - if (auto msg{boost::get(&message)}) { - if (auto statement_msg{boost::get(msg)}) { - if (auto r = canProcessParachains(); r.has_error()) { - return; - } - if (auto r = isParachainValidator(statement_msg->relay_parent); - r.has_error() || !r.value()) { - return; - } - SL_TRACE( - logger_, "Imported statement on {}", statement_msg->relay_parent); - handleStatement( - peer_id, statement_msg->relay_parent, statement_msg->statement); - } else { - auto &large = boost::get(*msg); - // TODO(turuslan): #1757, LargeStatement - SL_ERROR(logger_, - "Ignoring LargeStatement about {} from {}", - large.payload.payload.candidate_hash, - peer_id); - } + if (bd->data.payload.ix >= opt_session_info->validators.size()) { + SL_TRACE( + logger_, + "Validator index out of bound. (validator index={}, relay_parent={})", + bd->data.payload.ix, + bd->relay_parent); return; } - } - template - void ParachainProcessorImpl::requestPoV( - const libp2p::peer::PeerInfo &peer_info, - const CandidateHash &candidate_hash, - F &&callback) { - /// TODO(iceseer): request PoV from validator, who seconded candidate - /// But now we can assume, that if we received either `seconded` or `valid` - /// from some peer, than we expect this peer has valid PoV, which we can - /// request. + auto res_sc = SigningContext::make(parachain_host_, bd->relay_parent); + if (res_sc.has_error()) { + SL_TRACE(logger_, + "Create signing context failed. (validator index={}, " + "relay_parent={})", + bd->data.payload.ix, + bd->relay_parent); + return; + } + SigningContext &context = res_sc.value(); + const auto buffer = context.signable(*hasher_, bd->data.payload.payload); + + auto res = crypto_provider_->verify( + bd->data.signature, + buffer, + opt_session_info->validators[bd->data.payload.ix]); + if (res.has_error() || !res.value()) { + SL_TRACE( + logger_, + "Signature validation failed. (validator index={}, relay_parent={})", + bd->data.payload.ix, + bd->relay_parent); + return; + } - logger_->info("Requesting PoV.(candidate hash={}, peer={})", - candidate_hash, - peer_info.id); + SL_TRACE(logger_, + "Imported bitfield {} {}", + bd->data.payload.ix, + bd->relay_parent); + bitfield_store_->putBitfield(bd->relay_parent, bd->data); + } - auto protocol = router_->getReqPovProtocol(); - BOOST_ASSERT(protocol); + ParachainProcessorImpl::ManifestImportSuccessOpt + ParachainProcessorImpl::handle_incoming_manifest_common( + const libp2p::peer::PeerId &peer_id, + const CandidateHash &candidate_hash, + const RelayHash &relay_parent, + const ManifestSummary &manifest_summary, + ParachainId para_id) { + if (!candidates_.insert_unconfirmed( + peer_id, + candidate_hash, + relay_parent, + manifest_summary.claimed_group_index, + {{manifest_summary.claimed_parent_hash, para_id}})) { + SL_TRACE(logger_, + "Insert unconfirmed candidate failed. (candidate hash={}, relay " + "parent={}, para id={}, claimed parent={})", + candidate_hash, + relay_parent, + para_id, + manifest_summary.claimed_parent_hash); + return std::nullopt; + } - protocol->request(peer_info, candidate_hash, std::forward(callback)); + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// `grid_topology` and `local_validator` + return ManifestImportSuccess{ + .acknowledge = false, + .sender_index = 0, + }; } - std::optional - ParachainProcessorImpl::retrieveSessionInfo(const RelayHash &relay_parent) { - if (auto session_index = - parachain_host_->session_index_for_child(relay_parent); - session_index.has_value()) { - if (auto session_info = parachain_host_->session_info( - relay_parent, session_index.value()); - session_info.has_value()) { - return session_info.value(); - } - } - return std::nullopt; + network::vstaging::StatementFilter + ParachainProcessorImpl::local_knowledge_filter( + size_t group_size, + GroupIndex group_index, + const CandidateHash &candidate_hash, + const StatementStore &statement_store) { + network::vstaging::StatementFilter f{group_size}; + statement_store.fill_statement_filter(group_index, candidate_hash, f); + return f; } - void ParachainProcessorImpl::kickOffValidationWork( + void ParachainProcessorImpl::send_to_validators_group( const RelayHash &relay_parent, - AttestingData &attesting_data, - RelayParentState ¶chain_state) { - const auto candidate_hash{candidateHashFrom(attesting_data.candidate)}; - + const std::deque &messages) { BOOST_ASSERT(runningInThisThread(main_thread_context_)); - if (!parachain_state.awaiting_validation.insert(candidate_hash).second) { - return; - } - const auto &collator_id = - collatorIdFromDescriptor(attesting_data.candidate.descriptor); - if (parachain_state.required_collator - && collator_id != *parachain_state.required_collator) { - parachain_state.issued_statements.insert(candidate_hash); + auto relay_parent_state = tryGetStateByRelayParent(relay_parent); + if (!relay_parent_state) { + SL_TRACE(logger_, + "After `send_to_validators_group` no parachain state on " + "relay_parent. (relay " + "parent={})", + relay_parent); return; } - auto session_info = retrieveSessionInfo(relay_parent); - if (!session_info) { - SL_WARN(logger_, "No session info.(relay_parent={})", relay_parent); - return; + auto se = pm_->getStreamEngine(); + BOOST_ASSERT(se); + + std::unordered_set group_set; + if (auto r = runtime_info_->get_session_info(relay_parent)) { + auto &[session, info] = r.value(); + if (info.our_group) { + for (auto &i : session.validator_groups[*info.our_group]) { + if (auto peer = query_audi_->get(session.discovery_keys[i])) { + group_set.emplace(peer->id); + } + } + } } - if (session_info->discovery_keys.size() <= attesting_data.from_validator) { - SL_ERROR(logger_, - "Invalid validator index.(relay_parent={}, validator_index={})", - relay_parent, - attesting_data.from_validator); - return; + std::deque group, any; + for (const auto &p : group_set) { + group.emplace_back(p); } - const auto &authority_id = - session_info->discovery_keys[attesting_data.from_validator]; - if (auto peer = query_audi_->get(authority_id)) { - requestPoV( - *peer, - candidate_hash, - [candidate{attesting_data.candidate}, - candidate_hash, - wself{weak_from_this()}, + auto protocol = [&]() -> std::shared_ptr { + return router_->getValidationProtocolVStaging(); + }(); + + se->forEachPeer(protocol, [&](const network::PeerId &peer) { + if (group_set.count(peer) == 0) { + any.emplace_back(peer); + } + }); + auto lucky = kMinGossipPeers - std::min(group.size(), kMinGossipPeers); + if (lucky != 0) { + std::shuffle(any.begin(), any.end(), random_); + any.erase(any.begin() + std::min(any.size(), lucky), any.end()); + } else { + any.clear(); + } + + auto make_send = [&]( + const Msg &msg, + const std::shared_ptr + &protocol) { + auto se = pm_->getStreamEngine(); + BOOST_ASSERT(se); + + auto message = + std::make_shared>>( + msg); + logger_->trace( + "Broadcasting messages.(relay_parent={}, group_size={}, " + "lucky_size={})", + relay_parent, + group.size(), + any.size()); + + for (auto &peer : group) { + SL_TRACE(logger_, "Send to peer from group. (peer={})", peer); + se->send(peer, protocol, message); + } + + for (auto &peer : any) { + SL_TRACE(logger_, "Send to peer from any. (peer={})", peer); + se->send(peer, protocol, message); + } + }; + + for (const network::VersionedValidatorProtocolMessage &msg : messages) { + visit_in_place( + msg, + [&](const kagome::network::vstaging::ValidatorProtocolMessage &m) { + make_send(m, router_->getValidationProtocolVStaging()); + }, + [&](const kagome::network::ValidatorProtocolMessage &m) { + make_send(m, router_->getValidationProtocol()); + }); + } + } + + std::deque + ParachainProcessorImpl::acknowledgement_and_statement_messages( + StatementStore &statement_store, + const std::vector &group, + const network::vstaging::StatementFilter &local_knowledge, + const CandidateHash &candidate_hash, + const RelayHash &relay_parent) { + std::deque messages; + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// Will sent to the whole group. Optimize when `grid_view` will be + /// implemented + messages.emplace_back(network::VersionedValidatorProtocolMessage{ + network::vstaging::ValidatorProtocolMessage{ + network::vstaging::StatementDistributionMessage{ + network::vstaging::BackedCandidateAcknowledgement{ + .candidate_hash = candidate_hash, + .statement_knowledge = local_knowledge, + }}}}); + statement_store.groupStatements( + group, + candidate_hash, + local_knowledge, + [&](const IndexedAndSigned + &statement) { + messages.emplace_back(network::VersionedValidatorProtocolMessage{ + network::vstaging::ValidatorProtocolMessage{ + network::vstaging::StatementDistributionMessage{ + network::vstaging::StatementDistributionMessageStatement{ + .relay_parent = relay_parent, + .compact = statement, + }}}}); + }); + return messages; + } + + std::deque + ParachainProcessorImpl::post_acknowledgement_statement_messages( + const RelayHash &relay_parent, + const StatementStore &statement_store, + const std::vector &group, + const CandidateHash &candidate_hash) { + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// fill data from grid tracker + network::vstaging::StatementFilter sending_filter{group.size()}; + + std::deque messages; + statement_store.groupStatements( + group, + candidate_hash, + sending_filter, + [&](const IndexedAndSigned + &statement) { + messages.emplace_back(network::VersionedValidatorProtocolMessage{ + network::vstaging::ValidatorProtocolMessage{ + network::vstaging::StatementDistributionMessage{ + network::vstaging::StatementDistributionMessageStatement{ + .relay_parent = relay_parent, + .compact = statement, + }}}}); + }); + return messages; + } + + void ParachainProcessorImpl::process_vstaging_statement( + const libp2p::peer::PeerId &peer_id, + const network::vstaging::StatementDistributionMessage &msg) { + BOOST_ASSERT(runningInThisThread(main_thread_context_)); + SL_TRACE( + logger_, "Incoming `StatementDistributionMessage`. (peer={})", peer_id); + if (auto inner = + if_type( + msg)) { + SL_TRACE(logger_, + "`BackedCandidateAcknowledgement`. (candidate_hash={})", + inner->get().candidate_hash); + const network::vstaging::BackedCandidateAcknowledgement &acknowledgement = + inner->get(); + const auto &candidate_hash = acknowledgement.candidate_hash; + SL_TRACE( + logger_, + "Received incoming acknowledgement. (peer={}, candidate hash={})", + peer_id, + candidate_hash); + + auto c = candidates_.get_confirmed(candidate_hash); + if (!c) { + return; + } + const RelayHash &relay_parent = c->get().relay_parent(); + const Hash &parent_head_data_hash = c->get().parent_head_data_hash(); + GroupIndex group_index = c->get().group_index(); + ParachainId para_id = c->get().para_id(); + + auto opt_parachain_state = tryGetStateByRelayParent(relay_parent); + if (!opt_parachain_state) { + SL_TRACE( + logger_, "Handled statement from {} out of view", relay_parent); + return; + } + auto &relay_parent_state = opt_parachain_state->get(); + BOOST_ASSERT(relay_parent_state.statement_store); + + std::optional opt_session_info = + retrieveSessionInfo(relay_parent); + if (!opt_session_info) { + SL_WARN(logger_, + "No session info for current parrent. (relay parent={})", + relay_parent); + return; + } + if (group_index >= opt_session_info->validator_groups.size()) { + SL_WARN(logger_, + "Group index out of bound. (relay parent={}, group={})", + relay_parent, + group_index); + return; + } + const auto &group = opt_session_info->validator_groups[group_index]; + + ManifestImportSuccessOpt x = handle_incoming_manifest_common( + peer_id, + candidate_hash, + relay_parent, + ManifestSummary{ + .claimed_parent_hash = parent_head_data_hash, + .claimed_group_index = group_index, + .statement_knowledge = acknowledgement.statement_knowledge, + }, + para_id); + if (!x) { + return; + } + + auto messages = post_acknowledgement_statement_messages( + relay_parent, + *relay_parent_state.statement_store, + group, + candidate_hash); + if (!messages.empty()) { + send_to_validators_group(relay_parent, messages); + } + return; + } + + if (auto manifest = + if_type(msg)) { + SL_TRACE(logger_, + "`BackedCandidateManifest`. (relay_parent={}, " + "candidate_hash={}, para_id={}, parent_head_data_hash={})", + manifest->get().relay_parent, + manifest->get().candidate_hash, + manifest->get().para_id, + manifest->get().parent_head_data_hash); + auto relay_parent_state = + tryGetStateByRelayParent(manifest->get().relay_parent); + if (!relay_parent_state) { + SL_WARN(logger_, + "After BackedCandidateManifest no parachain state on " + "relay_parent. (relay " + "parent={})", + manifest->get().relay_parent); + return; + } + + if (!relay_parent_state->get().statement_store) { + SL_ERROR(logger_, + "Statement store is not initialized. (relay parent={})", + manifest->get().relay_parent); + return; + } + + ManifestImportSuccessOpt x = handle_incoming_manifest_common( + peer_id, + manifest->get().candidate_hash, + manifest->get().relay_parent, + ManifestSummary{ + .claimed_parent_hash = manifest->get().parent_head_data_hash, + .claimed_group_index = manifest->get().group_index, + .statement_knowledge = manifest->get().statement_knowledge, + }, + manifest->get().para_id); + if (!x) { + return; + } + + std::optional opt_session_info = + retrieveSessionInfo(manifest->get().relay_parent); + if (!opt_session_info) { + SL_WARN(logger_, + "No session info for current parrent. (relay parent={})", + manifest->get().relay_parent); + return; + } + const auto &group = + opt_session_info->validator_groups[manifest->get().group_index]; + + if (x->acknowledge) { + SL_TRACE( + logger_, + "Known candidate - acknowledging manifest. (candidate hash={})", + manifest->get().candidate_hash); + network::vstaging::StatementFilter local_knowledge = + local_knowledge_filter(group.size(), + manifest->get().group_index, + manifest->get().candidate_hash, + *relay_parent_state->get().statement_store); + auto messages = acknowledgement_and_statement_messages( + *relay_parent_state->get().statement_store, + group, + local_knowledge, + manifest->get().candidate_hash, + manifest->get().relay_parent); + send_to_validators_group(manifest->get().relay_parent, messages); + } else if (!candidates_.is_confirmed(manifest->get().candidate_hash)) { + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// not used because of `acknowledge` = true. Implement `grid_view` to + /// retrieve real `acknowledge`. + + network::vstaging::StatementFilter unwanted_mask{group.size()}; + router_->getFetchAttestedCandidateProtocol()->doRequest( + peer_id, + network::vstaging::AttestedCandidateRequest{ + .candidate_hash = manifest->get().candidate_hash, + .mask = std::move(unwanted_mask), + }, + [wptr{weak_from_this()}, + relay_parent{manifest->get().relay_parent}, + candidate_hash{manifest->get().candidate_hash}, + groups{Groups{opt_session_info->validator_groups}}, + group_index{manifest->get().group_index}]( + outcome::result + r) mutable { + if (auto self = wptr.lock()) { + self->handleFetchedStatementResponse(std::move(r), + relay_parent, + candidate_hash, + std::move(groups), + group_index); + } + }); + } + return; + } + + if (auto stm = if_type< + const network::vstaging::StatementDistributionMessageStatement>( + msg)) { + SL_TRACE(logger_, + "`StatementDistributionMessageStatement`. (relay_parent={}, " + "candidate_hash={})", + stm->get().relay_parent, + candidateHash(getPayload(stm->get().compact))); + auto parachain_state = tryGetStateByRelayParent(stm->get().relay_parent); + if (!parachain_state) { + SL_TRACE(logger_, + "After request pov no parachain state on relay_parent. (relay " + "parent={})", + stm->get().relay_parent); + return; + } + + auto opt_session_info = retrieveSessionInfo(stm->get().relay_parent); + if (!opt_session_info) { + SL_WARN(logger_, + "No session info for current parrent. (relay parent={})", + stm->get().relay_parent); + return; + } + + std::optional originator_group = + [&]() -> std::optional { + for (GroupIndex g = 0; g < opt_session_info->validator_groups.size(); + ++g) { + const auto &group = opt_session_info->validator_groups[g]; + for (const auto &v : group) { + if (v == stm->get().compact.payload.ix) { + return g; + } + } + } + return std::nullopt; + }(); + if (!originator_group) { + SL_TRACE(logger_, + "No correct validator index in statement. (relay parent={}, " + "validator={})", + stm->get().relay_parent, + stm->get().compact.payload.ix); + return; + } + + const auto &candidate_hash = + candidateHash(getPayload(stm->get().compact)); + const bool res = candidates_.insert_unconfirmed(peer_id, + candidate_hash, + stm->get().relay_parent, + *originator_group, + std::nullopt); + if (!res) { + return; + } + + const auto confirmed = candidates_.get_confirmed(candidate_hash); + const auto is_confirmed = candidates_.is_confirmed(candidate_hash); + const auto &group = opt_session_info->validator_groups[*originator_group]; + + if (!is_confirmed) { + network::vstaging::StatementFilter unwanted_mask{group.size()}; + + if (!parachain_state->get().statement_store) { + SL_ERROR(logger_, "Statement store is not initialized."); + return; + } + if (!parachain_state->get().prospective_parachains_mode) { + SL_ERROR(logger_, "No prospective parachains."); + return; + } + + const auto seconding_limit = + parachain_state->get() + .prospective_parachains_mode->max_candidate_depth + + 1; + for (size_t i = 0; i < group.size(); ++i) { + const auto &v = group[i]; + if (parachain_state->get().statement_store->seconded_count(v) + >= seconding_limit) { + unwanted_mask.seconded_in_group.bits[i] = true; + } + } + + router_->getFetchAttestedCandidateProtocol()->doRequest( + peer_id, + network::vstaging::AttestedCandidateRequest{ + .candidate_hash = candidate_hash, + .mask = std::move(unwanted_mask), + }, + [wptr{weak_from_this()}, + relay_parent{stm->get().relay_parent}, + candidate_hash, + groups{Groups{opt_session_info->validator_groups}}, + group_index{*originator_group}]( + outcome::result + r) mutable { + if (auto self = wptr.lock()) { + self->handleFetchedStatementResponse(std::move(r), + relay_parent, + candidate_hash, + std::move(groups), + group_index); + } + }); + } + + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// check statement signature + + Groups groups{opt_session_info->validator_groups}; + const auto was_fresh_opt = parachain_state->get().statement_store->insert( + groups, stm->get().compact, StatementOrigin::Remote); + if (!was_fresh_opt) { + SL_WARN(logger_, + "Accepted message from unknown validator. (relay parent={}, " + "validator={})", + stm->get().relay_parent, + stm->get().compact.payload.ix); + return; + } + + if (!*was_fresh_opt) { + SL_TRACE(logger_, + "Statement was not fresh. (relay parent={}, validator={})", + stm->get().relay_parent, + stm->get().compact.payload.ix); + return; + } + + const auto is_importable = candidates_.is_importable(candidate_hash); + if (is_importable && confirmed) { + send_backing_fresh_statements(confirmed->get(), + stm->get().relay_parent, + parachain_state->get(), + group, + candidate_hash); + } + + circulate_statement(stm->get().relay_parent, stm->get().compact); + return; + } + + SL_INFO(logger_, "Skipped message."); + } + + void ParachainProcessorImpl::circulate_statement( + const RelayHash &relay_parent, + const IndexedAndSigned &statement) { + send_to_validators_group( + relay_parent, + {network::VersionedValidatorProtocolMessage{ + kagome::network::vstaging::ValidatorProtocolMessage{ + kagome::network::vstaging::StatementDistributionMessage{ + kagome::network::vstaging:: + StatementDistributionMessageStatement{ + .relay_parent = relay_parent, + .compact = statement, + }}}}}); + } + + void ParachainProcessorImpl::handleFetchedStatementResponse( + outcome::result &&r, + const RelayHash &relay_parent, + const CandidateHash &candidate_hash, + Groups &&groups, + GroupIndex group_index) { + REINVOKE(main_thread_context_, + handleFetchedStatementResponse, + std::move(r), + relay_parent, + candidate_hash, + std::move(groups), + group_index); + + if (r.has_error()) { + SL_INFO(logger_, + "Fetch attested candidate returned an error. (relay parent={}, " + "candidate={}, group index={}, error={})", + relay_parent, + candidate_hash, + group_index, + r.error()); + return; + } + + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// validate response + + auto parachain_state = tryGetStateByRelayParent(relay_parent); + if (!parachain_state) { + SL_TRACE( + logger_, + "No relay parent data on fetch attested candidate response. (relay " + "parent={})", + relay_parent); + return; + } + + if (!parachain_state->get().statement_store) { + SL_WARN(logger_, + "No statement store. (relay parent={}, candidate={})", + relay_parent, + candidate_hash); + return; + } + + const network::vstaging::AttestedCandidateResponse &response = r.value(); + for (const auto &statement : response.statements) { + parachain_state->get().statement_store->insert( + groups, statement, StatementOrigin::Remote); + } + + auto opt_post_confirmation = + candidates_.confirm_candidate(candidate_hash, + response.candidate_receipt, + response.persisted_validation_data, + group_index, + hasher_); + if (!opt_post_confirmation) { + SL_WARN(logger_, + "Candidate re-confirmed by request/response: logic error. (relay " + "parent={}, candidate={})", + relay_parent, + candidate_hash); + return; + } + + auto &post_confirmation = *opt_post_confirmation; + apply_post_confirmation(post_confirmation); + + auto opt_confirmed = candidates_.get_confirmed(candidate_hash); + BOOST_ASSERT(opt_confirmed); + + if (!opt_confirmed->get().is_importable(std::nullopt)) { + return; + } + + auto it = groups.groups.find(group_index); + if (it == groups.groups.end()) { + SL_WARN(logger_, + "Group was not found. (relay parent={}, candidate={}, group " + "index={})", + relay_parent, + candidate_hash, + group_index); + return; + } + + send_backing_fresh_statements(opt_confirmed->get(), + relay_parent, + parachain_state->get(), + it->second, + candidate_hash); + } + + void ParachainProcessorImpl::new_confirmed_candidate_fragment_tree_updates( + const HypotheticalCandidate &candidate) { + fragment_tree_update_inner(std::nullopt, std::nullopt, {candidate}); + } + + void ParachainProcessorImpl::new_leaf_fragment_tree_updates( + const Hash &leaf_hash) { + fragment_tree_update_inner({leaf_hash}, std::nullopt, std::nullopt); + } + + void + ParachainProcessorImpl::prospective_backed_notification_fragment_tree_updates( + ParachainId para_id, const Hash ¶_head) { + std::pair, ParachainId> p{{para_head}, + para_id}; + fragment_tree_update_inner(std::nullopt, p, std::nullopt); + } + + void ParachainProcessorImpl::fragment_tree_update_inner( + std::optional> active_leaf_hash, + std::optional, ParachainId>> + required_parent_info, + std::optional> + known_hypotheticals) { + std::vector hypotheticals; + if (!known_hypotheticals) { + hypotheticals = candidates_.frontier_hypotheticals(required_parent_info); + } else { + hypotheticals.emplace_back(known_hypotheticals->get()); + } + + auto frontier = prospective_parachains_->answerHypotheticalFrontierRequest( + hypotheticals, active_leaf_hash, false); + for (const auto &[hypo, membership] : frontier) { + if (membership.empty()) { + continue; + } + + for (const auto &[leaf_hash, _] : membership) { + candidates_.note_importable_under(hypo, leaf_hash); + } + + if (auto c = if_type(hypo)) { + auto confirmed_candidate = + candidates_.get_confirmed(c->get().candidate_hash); + auto prs = + tryGetStateByRelayParent(c->get().receipt.descriptor.relay_parent); + + if (prs && confirmed_candidate) { + const auto group_index = + group_for_para(prs->get().availability_cores, + prs->get().group_rotation_info, + c->get().receipt.descriptor.para_id); + auto opt_session_info = + retrieveSessionInfo(c->get().receipt.descriptor.relay_parent); + if (!opt_session_info || !group_index + || *group_index >= opt_session_info->validator_groups.size()) { + return; + } + + const auto &group = opt_session_info->validator_groups[*group_index]; + send_backing_fresh_statements( + *confirmed_candidate, + c->get().receipt.descriptor.relay_parent, + prs->get(), + group, + c->get().candidate_hash); + } + } + } + } + + std::optional ParachainProcessorImpl::group_for_para( + const std::vector &availability_cores, + const runtime::GroupDescriptor &group_rotation_info, + ParachainId para_id) const { + std::optional core_index; + for (CoreIndex i = 0; i < availability_cores.size(); ++i) { + const auto c = visit_in_place( + availability_cores[i], + [](const runtime::OccupiedCore &core) -> std::optional { + return core.candidate_descriptor.para_id; + }, + [](const runtime::ScheduledCore &core) -> std::optional { + return core.para_id; + }, + [](const auto &) -> std::optional { + return std::nullopt; + }); + + if (c && *c == para_id) { + core_index = i; + break; + } + } + + if (!core_index) { + return std::nullopt; + } + return group_rotation_info.groupForCore(*core_index, + availability_cores.size()); + } + + void ParachainProcessorImpl::apply_post_confirmation( + const PostConfirmation &post_confirmation) { + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// `send_cluster_candidate_statements` + + new_confirmed_candidate_fragment_tree_updates( + post_confirmation.hypothetical); + } + + void ParachainProcessorImpl::send_backing_fresh_statements( + const ConfirmedCandidate &confirmed, + const RelayHash &relay_parent, + ParachainProcessorImpl::RelayParentState &per_relay_parent, + const std::vector &group, + const CandidateHash &candidate_hash) { + if (!per_relay_parent.statement_store) { + return; + } + + std::vector> + imported; + per_relay_parent.statement_store->fresh_statements_for_backing( + group, + candidate_hash, + [&](const IndexedAndSigned + &statement) { + const auto &v = statement.payload.ix; + const auto &compact = getPayload(statement); + imported.emplace_back(v, compact); + + handleStatement( + relay_parent, + SignedFullStatementWithPVD{ + .payload = + { + .payload = visit_in_place( + compact.inner_value, + [&](const network::vstaging::SecondedCandidateHash + &) -> StatementWithPVD { + return StatementWithPVDSeconded{ + .committed_receipt = confirmed.receipt, + .pvd = confirmed.persisted_validation_data, + }; + }, + [](const network::vstaging::ValidCandidateHash + &val) -> StatementWithPVD { + return StatementWithPVDValid{ + .candidate_hash = val.hash, + }; + }, + [](const auto &) -> StatementWithPVD { + UNREACHABLE; + }), + .ix = statement.payload.ix, + }, + .signature = statement.signature, + }); + }); + + for (const auto &[v, s] : imported) { + per_relay_parent.statement_store->note_known_by_backing(v, s); + } + } + + void ParachainProcessorImpl::process_legacy_statement( + const libp2p::peer::PeerId &peer_id, + const network::StatementDistributionMessage &msg) { + BOOST_ASSERT(runningInThisThread(main_thread_context_)); + if (auto statement_msg{boost::get(&msg)}) { + if (auto r = canProcessParachains(); r.has_error()) { + return; + } + if (auto r = isParachainValidator(statement_msg->relay_parent); + r.has_error() || !r.value()) { + return; + } + + SL_TRACE( + logger_, "Imported statement on {}", statement_msg->relay_parent); + + std::optional stm; + if (auto ccr = if_type( + getPayload(statement_msg->statement).candidate_state)) { + std::optional pvd = + fetchPersistedValidationData(statement_msg->relay_parent, + ccr->get().descriptor.para_id); + if (!pvd) { + SL_TRACE(logger_, "No pvd fetched."); + return; + } + stm = StatementWithPVDSeconded{ + .committed_receipt = ccr->get(), + .pvd = std::move(*pvd), + }; + } else if (auto h = if_type( + getPayload(statement_msg->statement).candidate_state)) { + stm = StatementWithPVDValid{ + .candidate_hash = h->get(), + }; + } + + handleStatement(statement_msg->relay_parent, + SignedFullStatementWithPVD{ + .payload = + { + .payload = std::move(*stm), + .ix = statement_msg->statement.payload.ix, + }, + .signature = statement_msg->statement.signature, + }); + } else { + const auto large = boost::get(&msg); + SL_ERROR(logger_, + "Ignoring LargeStatement about {} from {}", + large->payload.payload.candidate_hash, + peer_id); + } + } + + void ParachainProcessorImpl::onValidationProtocolMsg( + const libp2p::peer::PeerId &peer_id, + const network::VersionedValidatorProtocolMessage &message) { + REINVOKE(main_thread_context_, onValidationProtocolMsg, peer_id, message); + + SL_TRACE( + logger_, "Incoming validator protocol message. (peer={})", peer_id); + visit_in_place( + message, + [&](const network::ValidatorProtocolMessage &m) { + SL_TRACE(logger_, "V1"); + visit_in_place( + m, + [&](const network::BitfieldDistributionMessage &val) { + process_bitfield_distribution(val); + }, + [&](const network::StatementDistributionMessage &val) { + process_legacy_statement(peer_id, val); + }, + [&](const auto &) {}); + }, + [&](const network::vstaging::ValidatorProtocolMessage &m) { + SL_TRACE(logger_, "V2"); + visit_in_place( + m, + [&](const network::vstaging::BitfieldDistributionMessage &val) { + process_bitfield_distribution(val); + }, + [&](const network::vstaging::StatementDistributionMessage &val) { + process_vstaging_statement(peer_id, val); + }, + [&](const auto &) {}); + }); + + // if (auto + // msg{boost::get(&message)}) { + // return; + // } + } + + template + void ParachainProcessorImpl::requestPoV( + const libp2p::peer::PeerInfo &peer_info, + const CandidateHash &candidate_hash, + F &&callback) { + /// TODO(iceseer): request PoV from validator, who seconded candidate + /// But now we can assume, that if we received either `seconded` or `valid` + /// from some peer, than we expect this peer has valid PoV, which we can + /// request. + + logger_->info("Requesting PoV.(candidate hash={}, peer={})", + candidate_hash, + peer_info.id); + + auto protocol = router_->getReqPovProtocol(); + BOOST_ASSERT(protocol); + + protocol->request(peer_info, candidate_hash, std::forward(callback)); + } + + std::optional + ParachainProcessorImpl::retrieveSessionInfo(const RelayHash &relay_parent) { + if (auto session_index = + parachain_host_->session_index_for_child(relay_parent); + session_index.has_value()) { + if (auto session_info = parachain_host_->session_info( + relay_parent, session_index.value()); + session_info.has_value()) { + return session_info.value(); + } + } + return std::nullopt; + } + + void ParachainProcessorImpl::kickOffValidationWork( + const RelayHash &relay_parent, + AttestingData &attesting_data, + const runtime::PersistedValidationData &persisted_validation_data, + RelayParentState ¶chain_state) { + BOOST_ASSERT(runningInThisThread(main_thread_context_)); + + const auto candidate_hash{attesting_data.candidate.hash(*hasher_)}; + if (!parachain_state.awaiting_validation.insert(candidate_hash).second) { + return; + } + + const auto &collator_id = + collatorIdFromDescriptor(attesting_data.candidate.descriptor); + if (parachain_state.required_collator + && collator_id != *parachain_state.required_collator) { + parachain_state.issued_statements.insert(candidate_hash); + return; + } + + auto session_info = retrieveSessionInfo(relay_parent); + if (!session_info) { + SL_WARN(logger_, "No session info.(relay_parent={})", relay_parent); + return; + } + + if (session_info->discovery_keys.size() <= attesting_data.from_validator) { + SL_ERROR(logger_, + "Invalid validator index.(relay_parent={}, validator_index={})", + relay_parent, + attesting_data.from_validator); + return; + } + + const auto &authority_id = + session_info->discovery_keys[attesting_data.from_validator]; + if (auto peer = query_audi_->get(authority_id)) { + auto pvd{persisted_validation_data}; + requestPoV( + *peer, + candidate_hash, + [candidate{attesting_data.candidate}, + pvd{std::move(pvd)}, + candidate_hash, + wself{weak_from_this()}, relay_parent, peer_id{peer->id}](auto &&pov_response_result) mutable { if (auto self = wself.lock()) { auto parachain_state = self->tryGetStateByRelayParent(relay_parent); if (!parachain_state) { - self->logger_->warn( + SL_TRACE( + self->logger_, "After request pov no parachain state on relay_parent {}", relay_parent); return; @@ -634,19 +1944,76 @@ namespace kagome::parachain { relay_parent, candidate_hash, peer_id); - self->appendAsyncValidationTask( + self->validateAsync( std::move(candidate), std::move(*p), - relay_parent, + std::move(pvd), peer_id, - parachain_state->get(), - candidate_hash, + relay_parent, parachain_state->get().table_context.validators.size()); } }); } } + outcome::result + ParachainProcessorImpl::OnFetchAttestedCandidateRequest( + const network::vstaging::AttestedCandidateRequest &request) { + auto confirmed = candidates_.get_confirmed(request.candidate_hash); + if (!confirmed) { + return Error::NOT_CONFIRMED; + } + + auto relay_parent_state = + tryGetStateByRelayParent(confirmed->get().relay_parent()); + if (!relay_parent_state) { + return Error::NO_STATE; + } + BOOST_ASSERT(relay_parent_state->get().statement_store); + BOOST_ASSERT(relay_parent_state->get().our_index); + + std::optional opt_session_info = + retrieveSessionInfo(confirmed->get().relay_parent()); + if (!opt_session_info) { + return Error::NO_SESSION_INFO; + } + if (confirmed->get().group_index() + >= opt_session_info->validator_groups.size()) { + SL_ERROR(logger_, + "Unexpected array bound for groups. (relay parent={})", + confirmed->get().relay_parent()); + return Error::OUT_OF_BOUND; + } + const auto &group = + opt_session_info->validator_groups[confirmed->get().group_index()]; + + auto init_with_not = [](scale::BitVec &dst, const scale::BitVec &src) { + dst.bits.reserve(src.bits.size()); + for (const auto i : src.bits) { + dst.bits.emplace_back(!i); + } + }; + + network::vstaging::StatementFilter and_mask; + init_with_not(and_mask.seconded_in_group, request.mask.seconded_in_group); + init_with_not(and_mask.validated_in_group, request.mask.validated_in_group); + + std::vector> + statements; + relay_parent_state->get().statement_store->groupStatements( + group, + request.candidate_hash, + and_mask, + [&](const IndexedAndSigned + &statement) { statements.emplace_back(statement); }); + + return network::vstaging::AttestedCandidateResponse{ + .candidate_receipt = confirmed->get().receipt, + .persisted_validation_data = confirmed->get().persisted_validation_data, + .statements = std::move(statements), + }; + } + outcome::result ParachainProcessorImpl::OnFetchChunkRequest( const network::FetchChunkRequest &request) { @@ -660,85 +2027,6 @@ namespace kagome::parachain { return network::FetchChunkResponse{}; } - template - void ParachainProcessorImpl::appendAsyncValidationTask( - network::CandidateReceipt &&candidate, - network::ParachainBlock &&pov, - const primitives::BlockHash &relay_parent, - const libp2p::peer::PeerId &peer_id, - RelayParentState ¶chain_state, - const primitives::BlockHash &candidate_hash, - size_t n_validators) { - BOOST_ASSERT(runningInThisThread(main_thread_context_)); - parachain_state.awaiting_validation.insert(candidate_hash); - - logger_->info( - "Starting validation task.(para id={}, " - "relay parent={}, peer={})", - candidate.descriptor.para_id, - relay_parent, - peer_id); - - sequenceIgnore( - wrap(worker_thread_context_, - asAsync([wself{weak_from_this()}, - candidate{std::move(candidate)}, - pov{std::move(pov)}, - peer_id, - relay_parent, - n_validators]() mutable - -> outcome::result< - ParachainProcessorImpl::ValidateAndSecondResult> { - if (auto self = wself.lock()) { - if (auto result = - self->validateAndMakeAvailable(std::move(candidate), - std::move(pov), - peer_id, - relay_parent, - n_validators); - result.has_error()) { - self->logger_->warn("Validation task failed.(error={})", - result.error().message()); - return result.as_failure(); - } else { - return result; - } - } - return Error::NO_INSTANCE; - })), - wrap(main_thread_context_, - asAsync([wself{weak_from_this()}, peer_id, candidate_hash]( - auto &&validate_and_second_result) mutable - -> outcome::result { - if (auto self = wself.lock()) { - auto parachain_state = self->tryGetStateByRelayParent( - validate_and_second_result.relay_parent); - if (!parachain_state) { - self->logger_->warn( - "After validation no parachain state on relay_parent {}", - validate_and_second_result.relay_parent); - return Error::OUT_OF_VIEW; - } - - self->logger_->info( - "Async validation complete.(relay parent={}, para_id={})", - validate_and_second_result.relay_parent, - validate_and_second_result.candidate.descriptor.para_id); - - parachain_state->get().awaiting_validation.erase( - candidate_hash); - auto q{std::move(validate_and_second_result)}; - if constexpr (kMode == ValidationTaskType::kSecond) { - self->onValidationComplete(peer_id, std::move(q)); - } else { - self->onAttestComplete(peer_id, std::move(q)); - } - return outcome::success(); - } - return Error::NO_INSTANCE; - }))); - } - std::optional< std::reference_wrapper> ParachainProcessorImpl::tryGetStateByRelayParent( @@ -763,14 +2051,13 @@ namespace kagome::parachain { } void ParachainProcessorImpl::handleStatement( - const libp2p::peer::PeerId &peer_id, const primitives::BlockHash &relay_parent, - const network::SignedStatement &statement) { + const SignedFullStatementWithPVD &statement) { BOOST_ASSERT(runningInThisThread(main_thread_context_)); + auto opt_parachain_state = tryGetStateByRelayParent(relay_parent); if (!opt_parachain_state) { - logger_->trace( - "Handled statement from {}:{} out of view", peer_id, relay_parent); + logger_->trace("Handled statement from {} out of view", relay_parent); return; } @@ -779,38 +2066,46 @@ namespace kagome::parachain { auto &fallbacks = parachain_state.fallbacks; auto &awaiting_validation = parachain_state.awaiting_validation; - if (auto result = - importStatement(relay_parent, statement, parachain_state)) { - if (result->imported.group_id != assignment) { + auto res = importStatement(relay_parent, statement, parachain_state); + if (res.has_error()) { + SL_TRACE(logger_, + "Statement rejected. (relay_parent={}, error={}).", + relay_parent, + res.error()); + return; + } + + post_import_statement_actions(relay_parent, parachain_state, res.value()); + if (auto result = res.value()) { + if (result->group_id != assignment) { SL_TRACE( logger_, "Registered statement from not our group(our: {}, registered: {}).", assignment, - result->imported.group_id); + result->group_id); return; } + const auto &candidate_hash = result->candidate; SL_TRACE(logger_, - "Registered incoming statement.(relay_parent={}, peer={}).", - relay_parent, - peer_id); + "Registered incoming statement.(relay_parent={}).", + relay_parent); std::optional> attesting_ref = visit_in_place( - parachain::getPayload(statement).candidate_state, - [&](const network::CommittedCandidateReceipt &seconded) + parachain::getPayload(statement), + [&](const StatementWithPVDSeconded &val) -> std::optional> { - auto const &candidate_hash = result->imported.candidate; - auto opt_candidate = - backing_store_->get_candidate(candidate_hash); + auto opt_candidate = backing_store_->getCadidateInfo( + relay_parent, candidate_hash); if (!opt_candidate) { logger_->error("No candidate {}", candidate_hash); return std::nullopt; } AttestingData attesting{ - .candidate = - candidateFromCommittedCandidateReceipt(*opt_candidate), - .pov_hash = seconded.descriptor.pov_hash, + .candidate = candidateFromCommittedCandidateReceipt( + opt_candidate->get().candidate), + .pov_hash = val.committed_receipt.descriptor.pov_hash, .from_validator = statement.payload.ix, .backing = {}}; @@ -818,9 +2113,9 @@ namespace kagome::parachain { std::make_pair(candidate_hash, std::move(attesting))); return it->second; }, - [&](const primitives::BlockHash &candidate_hash) + [&](const StatementWithPVDValid &val) -> std::optional> { - auto it = fallbacks.find(candidate_hash); + auto it = fallbacks.find(val.candidate_hash); if (it == fallbacks.end()) { return std::nullopt; } @@ -828,7 +2123,8 @@ namespace kagome::parachain { || *parachain_state.our_index == statement.payload.ix) { return std::nullopt; } - if (awaiting_validation.count(candidate_hash) > 0) { + if (awaiting_validation.find(val.candidate_hash) + != awaiting_validation.end()) { it->second.backing.push(statement.payload.ix); return std::nullopt; } @@ -842,43 +2138,184 @@ namespace kagome::parachain { }); if (attesting_ref) { - kickOffValidationWork( - relay_parent, attesting_ref->get(), parachain_state); + auto it = our_current_state_.per_candidate.find(candidate_hash); + if (it != our_current_state_.per_candidate.end()) { + kickOffValidationWork(relay_parent, + attesting_ref->get(), + it->second.persisted_validation_data, + parachain_state); + } } } } - std::optional + std::optional ParachainProcessorImpl::importStatementToTable( + const RelayHash &relay_parent, ParachainProcessorImpl::RelayParentState &relayParentState, const primitives::BlockHash &candidate_hash, const network::SignedStatement &statement) { SL_TRACE( logger_, "Import statement into table.(candidate={})", candidate_hash); + return backing_store_->put( + relay_parent, + relayParentState.table_context.groups, + statement, + relayParentState.prospective_parachains_mode.has_value()); + } - if (auto r = backing_store_->put(relayParentState.table_context.groups, - statement)) { - return ImportStatementSummary{ - .imported = *r, - .attested = false, - }; + void ParachainProcessorImpl::statementDistributionBackedCandidate( + const CandidateHash &candidate_hash) { + auto confirmed_opt = candidates_.get_confirmed(candidate_hash); + if (!confirmed_opt) { + SL_TRACE(logger_, + "Received backed candidate notification for unknown or " + "unconfirmed. (candidate_hash={})", + candidate_hash); + return; } - return std::nullopt; + const auto &confirmed = confirmed_opt->get(); + + const auto relay_parent = confirmed.relay_parent(); + auto relay_parent_state_opt = tryGetStateByRelayParent(relay_parent); + if (!relay_parent_state_opt) { + return; + } + BOOST_ASSERT(relay_parent_state_opt->get().statement_store); + + std::optional opt_session_info = + retrieveSessionInfo(relay_parent); + if (!opt_session_info) { + return; + } + + const auto group_index = confirmed.group_index(); + if (group_index >= opt_session_info->validator_groups.size()) { + return; + } + const auto group_size = + opt_session_info->validator_groups[group_index].size(); + + /// `provide_candidate_to_grid` + network::vstaging::StatementFilter filter = + local_knowledge_filter(group_size, + group_index, + candidate_hash, + *relay_parent_state_opt->get().statement_store); + + std::deque messages = { + network::VersionedValidatorProtocolMessage{ + kagome::network::vstaging::ValidatorProtocolMessage{ + kagome::network::vstaging::StatementDistributionMessage{ + kagome::network::vstaging::BackedCandidateManifest{ + .relay_parent = relay_parent, + .candidate_hash = candidate_hash, + .group_index = group_index, + .para_id = confirmed.para_id(), + .parent_head_data_hash = + confirmed.parent_head_data_hash(), + .statement_knowledge = filter}}}}, + network::VersionedValidatorProtocolMessage{ + kagome::network::vstaging::ValidatorProtocolMessage{ + kagome::network::vstaging::StatementDistributionMessage{ + kagome::network::vstaging::BackedCandidateAcknowledgement{ + .candidate_hash = candidate_hash, + .statement_knowledge = filter}}}}}; + + auto ex = post_acknowledgement_statement_messages( + relay_parent, + *relay_parent_state_opt->get().statement_store, + opt_session_info->validator_groups[group_index], + candidate_hash); + messages.insert(messages.end(), + std::make_move_iterator(ex.begin()), + std::make_move_iterator(ex.end())); + send_to_validators_group(relay_parent, messages); + + prospective_backed_notification_fragment_tree_updates( + confirmed.para_id(), confirmed.para_head()); } - void ParachainProcessorImpl::notifyBackedCandidate( - const network::SignedStatement &statement) { - logger_->error( - "Not implemented. Should notify somebody that backed candidate " - "appeared."); + std::vector + ParachainProcessorImpl::getBackedCandidates(const RelayHash &relay_parent) { + BOOST_ASSERT(runningInThisThread(main_thread_context_)); + + auto relay_parent_state_opt = tryGetStateByRelayParent(relay_parent); + if (!relay_parent_state_opt) { + return {}; + } + + if (relay_parent_state_opt->get().prospective_parachains_mode) { + std::vector backed; + for (size_t core_idx = 0; + core_idx < relay_parent_state_opt->get().availability_cores.size(); + ++core_idx) { + const runtime::CoreState &core = + relay_parent_state_opt->get().availability_cores[core_idx]; + std::optional> response = visit_in_place( + core, + [&](const network::ScheduledCore &scheduled_core) + -> std::optional> { + return prospective_parachains_->answerGetBackableCandidate( + relay_parent, scheduled_core.para_id, {}); + }, + [&](const runtime::OccupiedCore &occupied_core) + -> std::optional> { + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// `bitfields_indicate_availability` check + if (occupied_core.next_up_on_available) { + return prospective_parachains_->answerGetBackableCandidate( + relay_parent, + occupied_core.next_up_on_available->para_id, + {occupied_core.candidate_hash}); + } + return std::nullopt; + }, + [&](const runtime::FreeCore &) + -> std::optional> { + return std::nullopt; + }); + + if (!response) { + SL_TRACE(logger_, + "No backable candidate returned by prospective parachains. " + "(relay_parent={}, core_idx={})", + relay_parent, + core_idx); + continue; + } + + const CandidateHash &c_hash = response->first; + const RelayHash &r_hash = response->second; + + auto per_relay_state = tryGetStateByRelayParent(r_hash); + if (!per_relay_state) { + continue; + } + + if (auto attested = attested_candidate( + r_hash, + c_hash, + per_relay_state->get().table_context, + per_relay_state->get().minimum_backing_votes)) { + if (auto b = table_attested_to_backed( + std::move(*attested), per_relay_state->get().table_context)) { + backed.emplace_back(std::move(*b)); + } + } + } + return backed; + } else { + return backing_store_->get(relay_parent); + } } std::optional ParachainProcessorImpl::attested( - network::CommittedCandidateReceipt &&candidate, + const network::CommittedCandidateReceipt &candidate, const BackingStore::StatementInfo &data, size_t validity_threshold) { - const auto &validity_votes = data.second; + const auto &validity_votes = data.validity_votes; const auto valid_votes = validity_votes.size(); if (valid_votes < validity_threshold) { return std::nullopt; @@ -894,13 +2331,13 @@ namespace kagome::parachain { [](const BackingStore::ValidityVoteIssued &val) { return network::ValidityAttestation{ network::ValidityAttestation::Implicit{}, - ((BackingStore::Statement &)val).signature, + ((ValidatorSignature &)val), }; }, [](const BackingStore::ValidityVoteValid &val) { return network::ValidityAttestation{ network::ValidityAttestation::Explicit{}, - ((BackingStore::Statement &)val).signature, + ((ValidatorSignature &)val), }; }); @@ -909,25 +2346,30 @@ namespace kagome::parachain { } return AttestedCandidate{ - .group_id = data.first, - .candidate = std::move(candidate), + .group_id = data.group_id, + .candidate = candidate, .validity_votes = std::move(validity_votes_out), }; } std::optional ParachainProcessorImpl::attested_candidate( + const RelayHash &relay_parent, const CandidateHash &digest, - const ParachainProcessorImpl::TableContext &context) { - if (auto opt_validity_votes = backing_store_->get_validity_votes(digest)) { + const ParachainProcessorImpl::TableContext &context, + uint32_t minimum_backing_votes) { + if (auto opt_validity_votes = + backing_store_->getCadidateInfo(relay_parent, digest)) { auto &data = opt_validity_votes->get(); - const GroupIndex group = data.first; - auto candidate{backing_store_->get_candidate(digest)}; - BOOST_ASSERT(candidate); + size_t len = std::numeric_limits::max(); + if (auto it = context.groups.find(data.group_id); + it != context.groups.end()) { + len = it->second.size(); + } - const auto v_threshold = context.requisite_votes(group); - return attested(std::move(*candidate), data, v_threshold); + const auto v_threshold = std::min(len, size_t(minimum_backing_votes)); + return attested(data.candidate, data, v_threshold); } return std::nullopt; } @@ -989,52 +2431,241 @@ namespace kagome::parachain { return std::nullopt; } - std::optional + outcome::result> ParachainProcessorImpl::importStatement( const network::RelayHash &relay_parent, - const network::SignedStatement &statement, - ParachainProcessorImpl::RelayParentState &relayParentState) { - auto import_result = importStatementToTable( - relayParentState, - candidateHashFrom(parachain::getPayload(statement)), - statement); - - if (import_result) { - SL_TRACE(logger_, - "Import result.(candidate={}, group id={}, validity votes={})", - import_result->imported.candidate, - import_result->imported.group_id, - import_result->imported.validity_votes); - - if (auto attested = attested_candidate(import_result->imported.candidate, - relayParentState.table_context)) { - if (relayParentState.backed_hashes - .insert(candidateHash(*hasher_, attested->candidate)) - .second) { - if (auto backed = table_attested_to_backed( - std::move(*attested), relayParentState.table_context)) { - SL_INFO( - logger_, - "Candidate backed.(candidate={}, para id={}, relay_parent={})", - import_result->imported.candidate, - import_result->imported.group_id, - relay_parent); - backing_store_->add(relay_parent, std::move(*backed)); - } + const SignedFullStatementWithPVD &statement, + ParachainProcessorImpl::RelayParentState &rp_state) { + const CandidateHash candidate_hash = + candidateHashFrom(parachain::getPayload(statement)); + + SL_TRACE(logger_, + "Importing statement.(relay_parent={}, validator_index={}, " + "candidate_hash={})", + relay_parent, + statement.payload.ix, + candidate_hash); + + if (auto seconded = if_type( + parachain::getPayload(statement)); + seconded + && our_current_state_.per_candidate.find(candidate_hash) + == our_current_state_.per_candidate.end()) { + auto &candidate = seconded->get().committed_receipt; + if (rp_state.prospective_parachains_mode) { + fragment::FragmentTreeMembership membership = + prospective_parachains_->introduceCandidate( + candidate.descriptor.para_id, + candidate, + crypto::Hashed>{ + seconded->get().pvd}, + candidate_hash); + if (membership.empty()) { + SL_TRACE(logger_, "`membership` is empty."); + return Error::REJECTED_BY_PROSPECTIVE_PARACHAINS; } + + prospective_parachains_->candidateSeconded(candidate.descriptor.para_id, + candidate_hash); } + our_current_state_.per_candidate.insert( + {candidate_hash, + PerCandidateState{ + .persisted_validation_data = seconded->get().pvd, + .seconded_locally = false, + .para_id = seconded->get().committed_receipt.descriptor.para_id, + .relay_parent = + seconded->get().committed_receipt.descriptor.relay_parent, + }}); + } + + network::SignedStatement stmnt{ + .payload = + { + .payload = visit_in_place( + parachain::getPayload(statement), + [&](const StatementWithPVDSeconded &val) { + return network::CandidateState{val.committed_receipt}; + }, + [&](const StatementWithPVDValid &val) { + return network::CandidateState{val.candidate_hash}; + }), + .ix = statement.payload.ix, + }, + .signature = statement.signature, + }; + return importStatementToTable( + relay_parent, rp_state, candidate_hash, stmnt); + } + + void ParachainProcessorImpl::unblockAdvertisements( + ParachainProcessorImpl::RelayParentState &rp_state, + ParachainId para_id, + const Hash ¶_head) { + std::optional> unblocked{}; + auto it = our_current_state_.blocked_advertisements.find(para_id); + if (it != our_current_state_.blocked_advertisements.end()) { + auto i = it->second.find(para_head); + if (i != it->second.end()) { + unblocked = std::move(i->second); + it->second.erase(i); + } + } + + if (unblocked) { + requestUnblockedCollations( + rp_state, para_id, para_head, std::move(*unblocked)); + } + } + + void ParachainProcessorImpl::requestUnblockedCollations( + ParachainProcessorImpl::RelayParentState &rp_state, + ParachainId para_id, + const Hash ¶_head, + std::vector &&blocked_vec) { + for (auto blocked = blocked_vec.begin(); blocked != blocked_vec.end();) { + const auto is_seconding_allowed = + canSecond(rp_state, + para_id, + blocked->candidate_relay_parent, + blocked->candidate_hash, + para_head); + if (is_seconding_allowed) { + auto result = + enqueueCollation(rp_state, + blocked->candidate_relay_parent, + para_id, + blocked->peer_id, + blocked->collator_id, + std::make_optional(std::make_pair( + blocked->candidate_hash, para_head))); + if (result.has_error()) { + SL_DEBUG(logger_, + "Enqueue collation failed.(candidate={}, para id={}, " + "relay_parent={}, para_head={}, peer_id={})", + blocked->candidate_hash, + para_id, + blocked->candidate_relay_parent, + para_head, + blocked->peer_id); + } + blocked = blocked_vec.erase(blocked); + } else { + ++blocked; + } + } + if (!blocked_vec.empty()) { + our_current_state_.blocked_advertisements[para_id][para_head] = + std::move(blocked_vec); + } + } + + template + outcome::result< + std::optional> + ParachainProcessorImpl::sign_import_and_distribute_statement( + ParachainProcessorImpl::RelayParentState &rp_state, + const ValidateAndSecondResult &validation_result) { + if (auto statement = + createAndSignStatement(validation_result)) { + const SignedFullStatementWithPVD stm = visit_in_place( + getPayload(*statement).candidate_state, + [&](const network::CommittedCandidateReceipt &receipt) + -> SignedFullStatementWithPVD { + return SignedFullStatementWithPVD{ + .payload = + { + .payload = + StatementWithPVDSeconded{ + .committed_receipt = receipt, + .pvd = validation_result.pvd, + }, + .ix = statement->payload.ix, + }, + .signature = statement->signature, + }; + }, + [&](const network::CandidateHash &candidateHash) + -> SignedFullStatementWithPVD { + return SignedFullStatementWithPVD{ + .payload = + { + .payload = + StatementWithPVDValid{ + .candidate_hash = candidateHash, + }, + .ix = statement->payload.ix, + }, + .signature = statement->signature, + }; + }, + [&](const auto &) -> SignedFullStatementWithPVD { + return SignedFullStatementWithPVD{}; + }); + + OUTCOME_TRY( + summary, + importStatement(validation_result.relay_parent, stm, rp_state)); + share_local_statement_vstaging( + rp_state, validation_result.relay_parent, stm); + + post_import_statement_actions( + validation_result.relay_parent, rp_state, summary); + return stm; } + return std::nullopt; + } - if (import_result && import_result->attested) { - notifyBackedCandidate(statement); + void ParachainProcessorImpl::post_import_statement_actions( + const RelayHash &relay_parent, + ParachainProcessorImpl::RelayParentState &rp_state, + std::optional &summary) { + if (!summary) { + return; + } + + SL_TRACE(logger_, + "Import result.(candidate={}, group id={}, validity votes={})", + summary->candidate, + summary->group_id, + summary->validity_votes); + + if (auto attested = attested_candidate(relay_parent, + summary->candidate, + rp_state.table_context, + rp_state.minimum_backing_votes)) { + if (rp_state.backed_hashes + .insert(candidateHash(*hasher_, attested->candidate)) + .second) { + if (auto backed = table_attested_to_backed(std::move(*attested), + rp_state.table_context)) { + const auto para_id = backed->candidate.descriptor.para_id; + SL_INFO( + logger_, + "Candidate backed.(candidate={}, para id={}, relay_parent={})", + summary->candidate, + summary->group_id, + relay_parent); + if (rp_state.prospective_parachains_mode) { + prospective_parachains_->candidateBacked(para_id, + summary->candidate); + unblockAdvertisements( + rp_state, para_id, backed->candidate.descriptor.para_head_hash); + statementDistributionBackedCandidate(summary->candidate); + } else { + backing_store_->add(relay_parent, std::move(*backed)); + } + } + } } - return import_result; } template std::optional ParachainProcessorImpl::createAndSignStatement( - ValidateAndSecondResult &validation_result) { + const ValidateAndSecondResult &validation_result) { static_assert(kStatementType == StatementType::kSeconded || kStatementType == StatementType::kValid); @@ -1055,17 +2686,15 @@ namespace kagome::parachain { if constexpr (kStatementType == StatementType::kSeconded) { return createAndSignStatementFromPayload( network::Statement{ - .candidate_state = - network::CommittedCandidateReceipt{ - .descriptor = validation_result.candidate.descriptor, - .commitments = - std::move(*validation_result.commitments)}}, + network::CandidateState{network::CommittedCandidateReceipt{ + .descriptor = validation_result.candidate.descriptor, + .commitments = *validation_result.commitments}}}, *parachain_state->get().our_index, parachain_state->get()); } else if constexpr (kStatementType == StatementType::kValid) { return createAndSignStatementFromPayload( - network::Statement{.candidate_state = candidateHashFrom( - validation_result.candidate)}, + network::Statement{network::CandidateState{ + validation_result.candidate.hash(*hasher_)}}, *parachain_state->get().our_index, parachain_state->get()); } @@ -1116,9 +2745,9 @@ namespace kagome::parachain { if (!stream_result.has_value()) { self->logger_->verbose("Unable to create stream {} with {}: {}", - protocol->protocolName(), - peer_id, - stream_result.error()); + protocol->protocolName(), + peer_id, + stream_result.error()); return; } @@ -1136,7 +2765,7 @@ namespace kagome::parachain { template bool ParachainProcessorImpl::tryOpenOutgoingCollatingStream( const libp2p::peer::PeerId &peer_id, F &&callback) { - auto protocol = router_->getCollationProtocol(); + auto protocol = router_->getCollationProtocolVStaging(); BOOST_ASSERT(protocol); return tryOpenOutgoingStream( @@ -1145,8 +2774,19 @@ namespace kagome::parachain { template bool ParachainProcessorImpl::tryOpenOutgoingValidationStream( - const libp2p::peer::PeerId &peer_id, F &&callback) { - auto protocol = router_->getValidationProtocol(); + const libp2p::peer::PeerId &peer_id, + network::CollationVersion version, + F &&callback) { + std::shared_ptr protocol; + switch (version) { + case network::CollationVersion::V1: + case network::CollationVersion::VStaging: { + protocol = router_->getValidationProtocolVStaging(); + } break; + default: { + UNREACHABLE; + } break; + } BOOST_ASSERT(protocol); return tryOpenOutgoingStream( @@ -1176,12 +2816,34 @@ namespace kagome::parachain { } void ParachainProcessorImpl::onIncomingCollationStream( - const libp2p::peer::PeerId &peer_id) { + const libp2p::peer::PeerId &peer_id, network::CollationVersion version) { + REINVOKE(main_thread_context_, onIncomingCollationStream, peer_id, version); + + auto peer_state = [&]() { + auto res = pm_->getPeerState(peer_id); + if (!res) { + SL_TRACE(logger_, "From unknown peer {}", peer_id); + res = pm_->createDefaultPeerState(peer_id); + } + return res; + }(); + + peer_state->get().version = version; if (tryOpenOutgoingCollatingStream( - peer_id, [wptr{weak_from_this()}, peer_id](auto &&stream) { + peer_id, [wptr{weak_from_this()}, peer_id, version](auto &&stream) { if (auto self = wptr.lock()) { - self->sendMyView( - peer_id, stream, self->router_->getCollationProtocol()); + switch (version) { + case network::CollationVersion::V1: + case network::CollationVersion::VStaging: { + self->sendMyView( + peer_id, + stream, + self->router_->getCollationProtocolVStaging()); + } break; + default: { + UNREACHABLE; + } break; + } } })) { SL_DEBUG(logger_, "Initiated collation protocol with {}", peer_id); @@ -1189,12 +2851,38 @@ namespace kagome::parachain { } void ParachainProcessorImpl::onIncomingValidationStream( - const libp2p::peer::PeerId &peer_id) { + const libp2p::peer::PeerId &peer_id, network::CollationVersion version) { + REINVOKE( + main_thread_context_, onIncomingValidationStream, peer_id, version); + + SL_TRACE(logger_, "Received incoming validation stream {}", peer_id); + auto peer_state = [&]() { + auto res = pm_->getPeerState(peer_id); + if (!res) { + SL_TRACE(logger_, "From unknown peer {}", peer_id); + res = pm_->createDefaultPeerState(peer_id); + } + return res; + }(); + + peer_state->get().version = version; if (tryOpenOutgoingValidationStream( - peer_id, [wptr{weak_from_this()}, peer_id](auto &&stream) { + peer_id, + version, + [wptr{weak_from_this()}, peer_id, version](auto &&stream) { if (auto self = wptr.lock()) { - self->sendMyView( - peer_id, stream, self->router_->getValidationProtocol()); + switch (version) { + case network::CollationVersion::V1: + case network::CollationVersion::VStaging: { + self->sendMyView( + peer_id, + stream, + self->router_->getValidationProtocolVStaging()); + } break; + default: { + UNREACHABLE; + } break; + } } })) { logger_->info("Initiated validation protocol with {}", peer_id); @@ -1239,34 +2927,57 @@ namespace kagome::parachain { auto stream_engine = pm_->getStreamEngine(); BOOST_ASSERT(stream_engine); - auto collation_protocol = router_->getCollationProtocol(); + auto collation_protocol = router_->getCollationProtocolVStaging(); BOOST_ASSERT(collation_protocol); auto &statements_queue = our_current_state_.seconded_statements[peer_id]; while (!statements_queue.empty()) { auto p{std::move(statements_queue.front())}; statements_queue.pop_front(); - network::SignedStatement &statement = p.second; RelayHash &rp = p.first; - pending_candidates.exclusiveAccess( - [&](auto &container) { container.erase(rp); }); + network::SignedStatement statement = visit_in_place( + getPayload(p.second), + [&](const StatementWithPVDSeconded &s) -> network::SignedStatement { + return { + .payload = + { + .payload = network::CandidateState{s.committed_receipt}, + .ix = p.second.payload.ix, + }, + .signature = p.second.signature, + }; + }, + [&](const StatementWithPVDValid &s) -> network::SignedStatement { + return { + .payload = + { + .payload = network::CandidateState{s.candidate_hash}, + .ix = p.second.payload.ix, + }, + .signature = p.second.signature, + }; + }); + pending_candidates.erase(rp); stream_engine->send( peer_id, collation_protocol, std::make_shared< - network::WireMessage>( - network::CollationProtocolMessage( - network::CollationMessage(network::Seconded{ - .relay_parent = rp, .statement = statement})))); + network::WireMessage>( + network::vstaging::CollatorProtocolMessage( + network::vstaging::CollationMessage( + network::vstaging:: + CollatorProtocolMessageCollationSeconded{ + .relay_parent = rp, + .statement = std::move(statement)})))); } } void ParachainProcessorImpl::notify( const libp2p::peer::PeerId &peer_id, const primitives::BlockHash &relay_parent, - const network::SignedStatement &statement) { + const SignedFullStatementWithPVD &statement) { our_current_state_.seconded_statements[peer_id].emplace_back( std::make_pair(relay_parent, statement)); handleNotify(peer_id, relay_parent); @@ -1297,7 +3008,7 @@ namespace kagome::parachain { void ParachainProcessorImpl::onValidationComplete( const libp2p::peer::PeerId &peer_id, - ValidateAndSecondResult &&validation_result) { + const ValidateAndSecondResult &validation_result) { logger_->trace("On validation complete. (peer={}, relay parent={})", peer_id, validation_result.relay_parent); @@ -1313,89 +3024,251 @@ namespace kagome::parachain { auto ¶chain_state = opt_parachain_state->get(); auto &seconded = parachain_state.seconded; - const auto candidate_hash = candidateHashFrom(validation_result.candidate); + const auto candidate_hash = validation_result.candidate.hash(*hasher_); if (!validation_result.result) { - logger_->warn("Candidate {} validation failed with: {}", - candidate_hash, - validation_result.result.error()); - // send invalid - } else if (!seconded - && parachain_state.issued_statements.count(candidate_hash) - == 0) { + SL_WARN(logger_, + "Candidate {} validation failed with: {}", + candidate_hash, + validation_result.result.error()); + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// send invalid + return; + } + + if (!seconded + && parachain_state.issued_statements.count(candidate_hash) == 0) { logger_->trace( "Second candidate complete. (candidate={}, peer={}, relay parent={})", candidate_hash, peer_id, validation_result.relay_parent); + const auto parent_head_data_hash = + hasher_->blake2b_256(validation_result.pvd.parent_head); + const auto ph = + hasher_->blake2b_256(validation_result.commitments->para_head); + if (parent_head_data_hash == ph) { + return; + } + + HypotheticalCandidateComplete hypothetical_candidate{ + .candidate_hash = candidate_hash, + .receipt = + network::CommittedCandidateReceipt{ + .descriptor = validation_result.candidate.descriptor, + .commitments = *validation_result.commitments, + }, + .persisted_validation_data = validation_result.pvd, + }; + + fragment::FragmentTreeMembership fragment_tree_membership; + if (auto seconding_allowed = + secondingSanityCheck(hypothetical_candidate, false)) { + fragment_tree_membership = std::move(*seconding_allowed); + } else { + return; + } + seconded = candidate_hash; - parachain_state.issued_statements.insert(candidate_hash); - if (auto statement = createAndSignStatement( - validation_result)) { - importStatement( - validation_result.relay_parent, *statement, parachain_state); - notifyStatementDistributionSystem(validation_result.relay_parent, - *statement); - notify(peer_id, validation_result.relay_parent, *statement); + + auto res = sign_import_and_distribute_statement( + parachain_state, validation_result); + if (res.has_error()) { + SL_WARN(logger_, + "Attempted to second candidate but was rejected by prospective " + "parachains. (candidate_hash={}, relay_parent={}, error={})", + candidate_hash, + validation_result.relay_parent, + res.error()); + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// send invalid + return; + } + + if (!res.value()) { + return; + } + + auto &stmt = *res.value(); + if (auto it = our_current_state_.per_candidate.find(candidate_hash); + it != our_current_state_.per_candidate.end()) { + it->second.seconded_locally = true; + } else { + SL_WARN(logger_, + "Missing `per_candidate` for seconded candidate. (candidate " + "hash={})", + candidate_hash); + } + + for (const auto &[leaf, depths] : fragment_tree_membership) { + auto it = our_current_state_.per_leaf.find(leaf); + if (it == our_current_state_.per_leaf.end()) { + SL_WARN(logger_, + "Missing `per_leaf` for known active leaf. (leaf={})", + leaf); + continue; + } + + 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); + } } + + parachain_state.issued_statements.insert(candidate_hash); + notify(peer_id, validation_result.relay_parent, stmt); } } - void ParachainProcessorImpl::notifyStatementDistributionSystem( + void ParachainProcessorImpl::share_local_statement_v1( + RelayParentState &per_relay_parent, const primitives::BlockHash &relay_parent, - const network::SignedStatement &statement) { - auto se = pm_->getStreamEngine(); - BOOST_ASSERT(se); - - // TODO(turuslan): #1757, LargeStatement - auto message = std::make_shared< - network::WireMessage>( - network::ValidatorProtocolMessage{ + const SignedFullStatementWithPVD &statement) { + send_to_validators_group( + relay_parent, + {network::ValidatorProtocolMessage{ network::StatementDistributionMessage{network::Seconded{ - .relay_parent = relay_parent, .statement = statement}}}); + .relay_parent = relay_parent, + .statement = network::SignedStatement{ + .payload = + { + .payload = visit_in_place( + parachain::getPayload(statement), + [&](const StatementWithPVDSeconded &val) { + return network::CandidateState{ + val.committed_receipt}; + }, + [&](const StatementWithPVDValid &val) { + return network::CandidateState{ + val.candidate_hash}; + }), + .ix = statement.payload.ix, + }, + .signature = statement.signature, + }}}}}); + } - // Send to all peers in our group. - // If our group is smaller than `kMinGossipPeers` - // select other peers randomly. - std::unordered_set group_set; - if (auto r = runtime_info_->get_session_info(relay_parent)) { - auto &[session, info] = r.value(); - if (info.our_group) { - for (auto &i : session.validator_groups[*info.our_group]) { - if (auto peer = query_audi_->get(session.discovery_keys[i])) { - group_set.emplace(peer->id); + void ParachainProcessorImpl::share_local_statement_vstaging( + RelayParentState &per_relay_parent, + const primitives::BlockHash &relay_parent, + const SignedFullStatementWithPVD &statement) { + const CandidateHash candidate_hash = + candidateHashFrom(getPayload(statement)); + SL_TRACE(logger_, + "Sharing statement. (relay parent={}, candidate hash={})", + relay_parent, + candidate_hash); + + BOOST_ASSERT(per_relay_parent.our_index); + std::optional opt_session_info = + retrieveSessionInfo(relay_parent); + if (!opt_session_info) { + SL_ERROR(logger_, + "Retrieve session info failed. (relay parent={})", + relay_parent); + return; + } + + Groups groups{opt_session_info->validator_groups}; + + const std::optional &local_assignment = + per_relay_parent.assignment; + const network::ValidatorIndex local_index = *per_relay_parent.our_index; + const auto local_group_opt = groups.byValidatorIndex(local_index); + if (!opt_session_info) { + SL_ERROR(logger_, + "Local validator info is not present. (relay parent={})", + relay_parent); + return; + } + const GroupIndex local_group = *local_group_opt; + + std::optional> expected = visit_in_place( + getPayload(statement), + [&](const StatementWithPVDSeconded &v) + -> std::optional> { + return std::make_pair(v.committed_receipt.descriptor.para_id, + v.committed_receipt.descriptor.relay_parent); + }, + [&](const StatementWithPVDValid &v) + -> std::optional> { + if (auto p = candidates_.get_confirmed(v.candidate_hash)) { + return std::make_pair(p->get().para_id(), p->get().relay_parent()); } - } - } + return std::nullopt; + }); + const bool is_seconded = + is_type(getPayload(statement)); + + if (!expected) { + SL_ERROR( + logger_, "Invalid share statement. (relay parent={})", relay_parent); + return; } - std::deque group, any; - auto protocol = router_->getValidationProtocol(); - se->forEachPeer(protocol, [&](const network::PeerId &peer) { - (group_set.count(peer) != 0 ? group : any).emplace_back(peer); - }); - auto lucky = kMinGossipPeers - std::min(group.size(), kMinGossipPeers); - if (lucky != 0) { - std::shuffle(any.begin(), any.end(), random_); - any.erase(any.begin() + std::min(any.size(), lucky), any.end()); - } else { - any.clear(); + const auto &[expected_para, expected_relay_parent] = *expected; + + if (local_index != statement.payload.ix) { + SL_ERROR(logger_, + "Invalid share statement because of validator index. (relay " + "parent={})", + relay_parent); + return; } - logger_->trace( - "Broadcasting StatementDistributionMessage.(relay_parent={}, validator " - "index={}, sig={})", - relay_parent, - statement.payload.ix, - statement.signature); + BOOST_ASSERT(per_relay_parent.statement_store); + BOOST_ASSERT(per_relay_parent.prospective_parachains_mode); + + const auto seconding_limit = + per_relay_parent.prospective_parachains_mode->max_candidate_depth + 1; + if (is_seconded + && per_relay_parent.statement_store->seconded_count(local_index) + == seconding_limit) { + SL_WARN( + logger_, + "Local node has issued too many `Seconded` statements. (limit={})", + seconding_limit); + return; + } - auto send = [&](const network::PeerId &peer) { - se->send(peer, protocol, message); - }; - for (auto &peer : group) { - send(peer); + if (!local_assignment || *local_assignment != expected_para + || relay_parent != expected_relay_parent) { + SL_ERROR( + logger_, + "Invalid share statement because local assignment. (relay parent={})", + relay_parent); + return; + } + + IndexedAndSigned compact_statement = + signed_to_compact(statement); + std::optional post_confirmation; + if (auto s = + if_type(getPayload(statement))) { + post_confirmation = + candidates_.confirm_candidate(candidate_hash, + s->get().committed_receipt, + s->get().pvd, + local_group, + hasher_); + } + + if (auto r = per_relay_parent.statement_store->insert( + groups, compact_statement, StatementOrigin::Local); + !r || !*r) { + SL_ERROR(logger_, + "Invalid share statement because statement store insertion " + "failed. (relay parent={})", + relay_parent); + return; } - for (auto &peer : any) { - send(peer); + + circulate_statement(relay_parent, compact_statement); + if (post_confirmation) { + apply_post_confirmation(*post_confirmation); } } @@ -1403,8 +3276,8 @@ namespace kagome::parachain { ParachainProcessorImpl::validateCandidate( const network::CandidateReceipt &candidate, const network::ParachainBlock &pov, - const primitives::BlockHash &relay_parent) { - return pvf_->pvfSync(candidate, pov); + runtime::PersistedValidationData &&pvd) { + return pvf_->pvfSync(candidate, pov, pvd); } outcome::result> @@ -1426,21 +3299,71 @@ namespace kagome::parachain { logger_->trace("Put chunks set.(candidate={})", candidate_hash); } - outcome::result - ParachainProcessorImpl::validateAndMakeAvailable( + template + void ParachainProcessorImpl::makeAvailable( + const libp2p::peer::PeerId &peer_id, + const primitives::BlockHash &candidate_hash, + ValidateAndSecondResult &&validate_and_second_result) { + REINVOKE(main_thread_context_, + makeAvailable, + peer_id, + candidate_hash, + std::move(validate_and_second_result)); + + auto parachain_state = + tryGetStateByRelayParent(validate_and_second_result.relay_parent); + if (!parachain_state) { + SL_TRACE(logger_, + "After validation no parachain state on relay_parent {}", + validate_and_second_result.relay_parent); + return; + } + + SL_INFO(logger_, + "Async validation complete.(relay parent={}, para_id={})", + validate_and_second_result.relay_parent, + validate_and_second_result.candidate.descriptor.para_id); + + parachain_state->get().awaiting_validation.erase(candidate_hash); + auto q{std::move(validate_and_second_result)}; + if constexpr (kMode == ValidationTaskType::kSecond) { + onValidationComplete(peer_id, std::move(q)); + } else { + onAttestComplete(peer_id, std::move(q)); + } + } + + template + void ParachainProcessorImpl::validateAsync( network::CandidateReceipt &&candidate, network::ParachainBlock &&pov, + runtime::PersistedValidationData &&pvd, const libp2p::peer::PeerId &peer_id, const primitives::BlockHash &relay_parent, size_t n_validators) { + REINVOKE(main_thread_context_, + validateAsync, + std::move(candidate), + std::move(pov), + std::move(pvd), + peer_id, + relay_parent, + n_validators); + + SL_INFO(logger_, + "Starting validation task.(para id={}, " + "relay parent={}, peer={})", + candidate.descriptor.para_id, + relay_parent, + peer_id); + TicToc _measure{"Parachain validation", logger_}; - const auto candidate_hash{candidateHashFrom(candidate)}; + const auto candidate_hash{candidate.hash(*hasher_)}; + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 /// checks if we still need to execute parachain task - auto need_to_process = our_current_state_.active_leaves.sharedAccess( - [&](const auto &active_leaves) { - return active_leaves.count(relay_parent) != 0ull; - }); + auto need_to_process = + our_current_state_.active_leaves.count(relay_parent) != 0ull; if (!need_to_process) { SL_TRACE(logger_, @@ -1449,10 +3372,11 @@ namespace kagome::parachain { relay_parent, candidate.descriptor.para_id, candidate_hash); - return Error::VALIDATION_FAILED; + return; } - auto validation_result = validateCandidate(candidate, pov, relay_parent); + auto pvd_copy{pvd}; + auto validation_result = validateCandidate(candidate, pov, std::move(pvd)); if (!validation_result) { logger_->warn( "Candidate {} on relay_parent {}, para_id {} validation failed with " @@ -1461,13 +3385,13 @@ namespace kagome::parachain { candidate.descriptor.relay_parent, candidate.descriptor.para_id, validation_result.error().message()); - return Error::VALIDATION_FAILED; + return; } - need_to_process = our_current_state_.active_leaves.sharedAccess( - [&](const auto &active_leaves) { - return active_leaves.count(relay_parent) != 0ull; - }); + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// checks if we still need to execute parachain task + need_to_process = + our_current_state_.active_leaves.count(relay_parent) != 0ull; if (!need_to_process) { SL_TRACE(logger_, @@ -1477,7 +3401,7 @@ namespace kagome::parachain { relay_parent, candidate.descriptor.para_id, candidate_hash); - return Error::VALIDATION_FAILED; + return; } auto &[comms, data] = validation_result.value(); @@ -1485,7 +3409,17 @@ namespace kagome::parachain { .pov = std::move(pov), .validation_data = std::move(data), }; - OUTCOME_TRY(chunks, validateErasureCoding(available_data, n_validators)); + + std::vector chunks; + if (auto res = validateErasureCoding(available_data, n_validators); + res.has_error()) { + SL_WARN(logger_, + "Erasure coding validation failed. (error={})", + res.error().message()); + return; + } else { + chunks = std::move(res.value()); + } notifyAvailableData(std::move(chunks), relay_parent, @@ -1493,18 +3427,22 @@ namespace kagome::parachain { available_data.pov, available_data.validation_data); - return ValidateAndSecondResult{ - .result = outcome::success(), - .relay_parent = relay_parent, - .commitments = - std::make_shared(std::move(comms)), - .candidate = std::move(candidate), - .pov = std::move(available_data.pov), - }; + makeAvailable( + peer_id, + candidate_hash, + ValidateAndSecondResult{ + .result = outcome::success(), + .relay_parent = relay_parent, + .commitments = std::make_shared( + std::move(comms)), + .candidate = std::move(candidate), + .pov = std::move(available_data.pov), + .pvd = std::move(pvd_copy), + }); } void ParachainProcessorImpl::onAttestComplete( - const libp2p::peer::PeerId &, ValidateAndSecondResult &&result) { + const libp2p::peer::PeerId &, const ValidateAndSecondResult &result) { auto parachain_state = tryGetStateByRelayParent(result.relay_parent); if (!parachain_state) { logger_->warn( @@ -1517,16 +3455,23 @@ namespace kagome::parachain { result.relay_parent, result.candidate.descriptor.para_id); - const auto candidate_hash = candidateHashFrom(result.candidate); + const auto candidate_hash = result.candidate.hash(*hasher_); parachain_state->get().fallbacks.erase(candidate_hash); if (parachain_state->get().issued_statements.count(candidate_hash) == 0) { if (result.result) { - if (auto statement = - createAndSignStatement(result)) { - importStatement( - result.relay_parent, *statement, parachain_state->get()); - notifyStatementDistributionSystem(result.relay_parent, *statement); + if (const auto r = + sign_import_and_distribute_statement( + parachain_state->get(), result); + r.has_error()) { + SL_WARN(logger_, + "Sign import and distribute failed. (relay_parent={}, " + "candidate_hash={}, para_id={}, error={})", + result.relay_parent, + candidate_hash, + result.candidate.descriptor.para_id, + r.error()); + return; } } parachain_state->get().issued_statements.insert(candidate_hash); @@ -1559,18 +3504,421 @@ namespace kagome::parachain { if (!attesting.backing.empty()) { attesting.from_validator = attesting.backing.front(); attesting.backing.pop(); - kickOffValidationWork(relay_parent, attesting, *parachain_state); + auto it = our_current_state_.per_candidate.find(candidate_hash); + if (it != our_current_state_.per_candidate.end()) { + kickOffValidationWork(relay_parent, + attesting, + it->second.persisted_validation_data, + *parachain_state); + } + } + } + + bool ParachainProcessorImpl::isRelayParentInImplicitView( + const RelayHash &relay_parent, + const ProspectiveParachainsModeOpt &relay_parent_mode, + const ImplicitView &implicit_view, + const std::unordered_map + &active_leaves, + ParachainId para_id) { + if (!relay_parent_mode) { + return active_leaves.count(relay_parent) != 0ull; + } + + 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; + } + } + } + } + return false; + } + + outcome::result> + ParachainProcessorImpl::insertAdvertisement( + network::PeerState &peer_data, + const RelayHash &on_relay_parent, + const ProspectiveParachainsModeOpt &relay_parent_mode, + const std::optional> + &candidate_hash) { + if (!peer_data.collator_state) { + SL_WARN(logger_, "Undeclared collator."); + return Error::UNDECLARED_COLLATOR; + } + + if (!isRelayParentInImplicitView(on_relay_parent, + relay_parent_mode, + *our_current_state_.implicit_view, + our_current_state_.active_leaves, + peer_data.collator_state->para_id)) { + return Error::OUT_OF_VIEW; + } + + if (!relay_parent_mode) { + if (peer_data.collator_state->advertisements.count(on_relay_parent) + != 0ull) { + return Error::DUPLICATE; + } + + if (candidate_hash) { + peer_data.collator_state->advertisements[on_relay_parent] = { + *candidate_hash}; + } + } else if (candidate_hash) { + auto &candidates = + peer_data.collator_state->advertisements[on_relay_parent]; + if (candidates.size() > relay_parent_mode->max_candidate_depth) { + return Error::PEER_LIMIT_REACHED; + } + auto [_, inserted] = candidates.insert(*candidate_hash); + if (!inserted) { + return Error::DUPLICATE; + } + } else { + return Error::PROTOCOL_MISMATCH; + } + + peer_data.collator_state->last_active = std::chrono::system_clock::now(); + return std::make_pair(peer_data.collator_state->collator_id, + peer_data.collator_state->para_id); + } + + ParachainProcessorImpl::SecondingAllowed + ParachainProcessorImpl::secondingSanityCheck( + const HypotheticalCandidate &hypothetical_candidate, + bool backed_in_path_only) { + const auto &active_leaves = our_current_state_.per_leaf; + const auto &implicit_view = *our_current_state_.implicit_view; + + fragment::FragmentTreeMembership membership; + const auto candidate_para = candidatePara(hypothetical_candidate); + const auto candidate_relay_parent = relayParent(hypothetical_candidate); + 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.count(depth) != 0ull) { + return false; + } + } + membership.emplace_back(head, std::move(depths)); + return true; + }; + + for (const auto &[head, leaf_state] : active_leaves) { + if (leaf_state.prospective_parachains_mode) { + const auto allowed_parents_for_para = + implicit_view.knownAllowedRelayParentsUnder(head, + {candidate_para.get()}); + if (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( + 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()); + } + } + + if (!proc_response(std::move(r), head, leaf_state)) { + return std::nullopt; + } + } 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.count(0) != 0ull) { + return std::nullopt; + } + if (!proc_response(std::vector{0ull}, head, leaf_state)) { + return std::nullopt; + } + } + } + } + + if (membership.empty()) { + return std::nullopt; + } + + return membership; + } + + bool ParachainProcessorImpl::canSecond( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + ParachainId candidate_para_id, + const Hash &relay_parent, + const CandidateHash &candidate_hash, + const Hash &parent_head_data_hash) { + if (per_relay_parent.prospective_parachains_mode) { + if (auto seconding_allowed = secondingSanityCheck( + 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; + } + } + } + } + return false; + } + + void ParachainProcessorImpl::handleAdvertisement( + network::CollationEvent &&pending_collation, + std::optional> &&prospective_candidate) { + REINVOKE(main_thread_context_, + handleAdvertisement, + std::move(pending_collation), + std::move(prospective_candidate)); + + const RelayHash &relay_parent = + pending_collation.pending_collation.relay_parent; + const libp2p::peer::PeerId &peer_id = + pending_collation.pending_collation.peer_id; + const ParachainId ¶_id = pending_collation.pending_collation.para_id; + + auto opt_per_relay_parent = tryGetStateByRelayParent(relay_parent); + if (!opt_per_relay_parent) { + SL_TRACE( + logger_, "Relay parent unknown. (relay_parent={})", relay_parent); + return; + } + + auto &per_relay_parent = opt_per_relay_parent->get(); + const ProspectiveParachainsModeOpt &relay_parent_mode = + per_relay_parent.prospective_parachains_mode; + const std::optional &assignment = + per_relay_parent.assignment; + + auto peer_state = pm_->getPeerState(peer_id); + if (!peer_state) { + SL_TRACE(logger_, "Unknown peer. (peerd_id={})", peer_id); + return; + } + + const auto collator_state = peer_state->get().collator_state; + if (!collator_state) { + SL_TRACE(logger_, "Undeclared collator. (peerd_id={})", peer_id); + return; + } + + const ParachainId collator_para_id = collator_state->para_id; + if (!assignment) { + SL_TRACE(logger_, + "Invalid assignment. (peerd_id={}, collator={})", + peer_id, + collator_para_id); + return; + } + + if (relay_parent_mode && !prospective_candidate) { + SL_WARN(logger_, "Protocol mismatch. (peer_id={})", peer_id); + return; + } + + const auto candidate_hash = + utils::map(prospective_candidate, + [](const auto &pair) { return std::cref(pair.first); }); + + auto insert_res = insertAdvertisement( + peer_state->get(), relay_parent, relay_parent_mode, candidate_hash); + if (insert_res.has_error()) { + SL_TRACE(logger_, + "Insert advertisement error. (error={})", + insert_res.error().message()); + return; + } + + const auto &[collator_id, parachain_id] = insert_res.value(); + if (!per_relay_parent.collations.hasSecondedSpace(relay_parent_mode)) { + SL_TRACE(logger_, "Seconded limit reached."); + return; + } + + if (prospective_candidate) { + auto &&[ch, parent_head_data_hash] = *prospective_candidate; + const bool is_seconding_allowed = !relay_parent_mode + || canSecond(per_relay_parent, + collator_para_id, + relay_parent, + ch, + parent_head_data_hash); + + if (!is_seconding_allowed) { + SL_TRACE(logger_, + "Seconding is not allowed by backing, queueing advertisement. " + "(candidate hash={}, relay_parent = {}, para id={})", + ch, + relay_parent, + para_id); + + our_current_state_ + .blocked_advertisements[collator_para_id][parent_head_data_hash] + .emplace_back( + BlockedAdvertisement{.peer_id = peer_id, + .collator_id = collator_id, + .candidate_relay_parent = relay_parent, + .candidate_hash = ch}); + + return; + } + } + + if (auto result = enqueueCollation(per_relay_parent, + relay_parent, + para_id, + peer_id, + collator_id, + std::move(prospective_candidate)); + result.has_error()) { + SL_TRACE(logger_, + "Failed to request advertised collation. (relay parent={}, para " + "id={}, peer_id={}, error={})", + relay_parent, + para_id, + peer_id, + result.error().message()); + } + } + + outcome::result ParachainProcessorImpl::enqueueCollation( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + const RelayHash &relay_parent, + ParachainId para_id, + const libp2p::peer::PeerId &peer_id, + const CollatorId &collator_id, + std::optional> &&prospective_candidate) { + BOOST_ASSERT(runningInThisThread(main_thread_context_)); + SL_TRACE(logger_, + "Received advertise collation. (peer id={}, para id={}, relay " + "parent={})", + peer_id, + para_id, + relay_parent); + + const auto &relay_parent_mode = + per_relay_parent.prospective_parachains_mode; + auto &collations = per_relay_parent.collations; + + if (!collations.hasSecondedSpace(relay_parent_mode)) { + SL_TRACE(logger_, + "Limit of seconded collations reached for valid advertisement. " + "(peer={}, para id={}, relay parent={})", + peer_id, + para_id, + relay_parent); + return outcome::success(); + } + + PendingCollation pending_collation{ + .relay_parent = relay_parent, + .para_id = para_id, + .peer_id = peer_id, + .commitments_hash = {}, + .prospective_candidate = std::move(prospective_candidate), + }; + + switch (collations.status) { + case CollationStatus::Fetching: + case CollationStatus::WaitingOnValidation: { + SL_TRACE(logger_, + "Added collation to the pending list. (peer_id={}, para " + "id={}, relay parent={})", + peer_id, + para_id, + relay_parent); + + collations.waiting_queue.emplace_back(std::move(pending_collation), + collator_id); + } break; + case CollationStatus::Waiting: { + fetchCollation( + per_relay_parent, std::move(pending_collation), collator_id); + } break; + case CollationStatus::Seconded: { + if (relay_parent_mode) { + // Limit is not reached, it's allowed to second another + // collation. + fetchCollation( + per_relay_parent, std::move(pending_collation), collator_id); + } else { + SL_TRACE(logger_, + "A collation has already been seconded. (peer_id={}, para " + "id={}, relay parent={})", + peer_id, + para_id, + relay_parent); + } + } break; + } + + return outcome::success(); + } + + void ParachainProcessorImpl::fetchCollation( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + PendingCollation &&pc, + const CollatorId &id) { + auto peer_state = pm_->getPeerState(pc.peer_id); + if (!peer_state) { + SL_TRACE( + logger_, "No peer state. Unknown peer. (peer id={})", pc.peer_id); + return; } + + const auto candidate_hash = + utils::map(pc.prospective_candidate, + [](const auto &pair) { return std::cref(pair.first); }); + + if (peer_state->get().hasAdvertised(pc.relay_parent, candidate_hash)) { + fetchCollation( + per_relay_parent, std::move(pc), id, peer_state->get().version); + return; + } + SL_WARN(logger_, "Not advertised. (peer id={})", pc.peer_id); } - void ParachainProcessorImpl::requestCollations( - const network::CollationEvent &pending_collation) { - router_->getReqCollationProtocol()->request( - pending_collation.pending_collation.peer_id, - network::CollationFetchingRequest{ - .relay_parent = pending_collation.pending_collation.relay_parent, - .para_id = pending_collation.pending_collation.para_id}, - [pending_collation{pending_collation}, wptr{weak_from_this()}]( + void ParachainProcessorImpl::fetchCollation( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + PendingCollation &&pc, + const CollatorId &id, + network::CollationVersion version) { + if (our_current_state_.collation_requests_cancel_handles.count(pc) + != 0ull) { + SL_WARN(logger_, + "Already requested. (relay parent={}, para id={})", + pc.relay_parent, + pc.para_id); + return; + } + + const auto peer_id = pc.peer_id; + auto response_callback = + [pending_collation{pc}, wptr{weak_from_this()}]( outcome::result &&result) mutable { auto self = wptr.lock(); @@ -1578,22 +3926,64 @@ namespace kagome::parachain { return; } - self->logger_->info( - "Fetching collation from(peer={}, relay parent={})", - pending_collation.pending_collation.peer_id, - pending_collation.pending_collation.relay_parent); + const RelayHash &relay_parent = pending_collation.relay_parent; + const libp2p::peer::PeerId &peer_id = pending_collation.peer_id; + + SL_TRACE(self->logger_, + "Fetching collation from(peer={}, relay parent={})", + peer_id, + relay_parent); if (!result) { - self->logger_->warn( - "Fetch collation from {}:{} failed with: {}", - pending_collation.pending_collation.peer_id, - pending_collation.pending_collation.relay_parent, - result.error()); + SL_WARN(self->logger_, + "Fetch collation from {}:{} failed with: {}", + peer_id, + relay_parent, + result.error()); + /// TODO(iceseer): do https://github.com/qdrvm/kagome/issues/1888 + /// dequeue_next_collation_and_fetch return; } self->handleFetchedCollation(std::move(pending_collation), std::move(result).value()); - }); + }; + + SL_TRACE(logger_, + "Requesting collation. (peer id={}, para id={}, relay parent={})", + pc.peer_id, + pc.para_id, + pc.relay_parent); + + our_current_state_.collation_requests_cancel_handles.insert(std::move(pc)); + const auto maybe_candidate_hash = + utils::map(pc.prospective_candidate, + [](const auto &pair) { return std::cref(pair.first); }); + per_relay_parent.collations.status = CollationStatus::Fetching; + per_relay_parent.collations.fetching_from.emplace(id, maybe_candidate_hash); + + if (network::CollationVersion::V1 == version) { + network::CollationFetchingRequest fetch_collation_request{ + .relay_parent = pc.relay_parent, + .para_id = pc.para_id, + }; + router_->getReqCollationProtocol()->request( + peer_id, + std::move(fetch_collation_request), + std::move(response_callback)); + } else if (network::CollationVersion::VStaging == version + && maybe_candidate_hash) { + network::vstaging::CollationFetchingRequest fetch_collation_request{ + .relay_parent = pc.relay_parent, + .para_id = pc.para_id, + .candidate_hash = maybe_candidate_hash->get(), + }; + router_->getReqCollationProtocol()->request( + peer_id, + std::move(fetch_collation_request), + std::move(response_callback)); + } else { + UNREACHABLE; + } } } // namespace kagome::parachain diff --git a/core/parachain/validator/impl/statements_store.hpp b/core/parachain/validator/impl/statements_store.hpp new file mode 100644 index 0000000000..e1e09ff4cb --- /dev/null +++ b/core/parachain/validator/impl/statements_store.hpp @@ -0,0 +1,298 @@ +/** + * Copyright Quadrivium Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include "network/types/collator_messages_vstaging.hpp" +#include "parachain/types.hpp" +#include "parachain/validator/collations.hpp" +#include "primitives/common.hpp" + +namespace kagome::parachain { + + struct Groups { + std::unordered_map> groups; + std::unordered_map by_validator_index; + + Groups(std::unordered_map> &&g) + : groups{std::move(g)} { + for (const auto &[g, vxs] : groups) { + for (const auto &v : vxs) { + by_validator_index[v] = g; + } + } + } + + Groups(const std::vector> &grs) { + for (GroupIndex g = 0; g < grs.size(); ++g) { + const auto &group = grs[g]; + groups[g] = group; + for (const auto &v : group) { + by_validator_index[v] = g; + } + } + } + + std::optional byValidatorIndex( + ValidatorIndex validator_index) const { + auto it = by_validator_index.find(validator_index); + if (it != by_validator_index.end()) { + return it->second; + } + return std::nullopt; + } + }; + + struct ValidatorMeta { + GroupIndex group; + size_t within_group_index; + size_t seconded_count; + }; + + struct GroupStatements { + scale::BitVec seconded; + scale::BitVec valid; + + GroupStatements(size_t len) { + seconded.bits.assign(len, false); + valid.bits.assign(len, false); + } + + void note_seconded(size_t within_group_index) { + BOOST_ASSERT(within_group_index < seconded.bits.size()); + seconded.bits[within_group_index] = true; + } + + void note_validated(size_t within_group_index) { + BOOST_ASSERT(within_group_index < valid.bits.size()); + valid.bits[within_group_index] = true; + } + }; + + struct StoredStatement { + IndexedAndSigned statement; + bool known_by_backing; + }; + + struct Fingerprint { + ValidatorIndex index; + network::vstaging::CompactStatement statement; + + size_t inner_hash() const { + size_t r{0ull}; + boost::hash_combine(r, std::hash()(index)); + visit_in_place( + statement.inner_value, + [&](const network::Empty &) { UNREACHABLE; }, + [&](const auto &v) { + boost::hash_combine(r, std::hash()(v.hash)); + }); + return r; + } + + bool operator==(const Fingerprint &rhs) const { + return index == rhs.index + && statement.inner_value == rhs.statement.inner_value; + } + }; + + enum StatementOrigin { Local, Remote }; + + struct StatementStore { + std::unordered_map validator_meta; + std::unordered_map> + group_statements; + std::unordered_map> + known_statements; + log::Logger logger = log::createLogger("StatementStore", "parachain"); + + StatementStore(const Groups &groups) { + for (const auto &[g, vxs] : groups.groups) { + for (size_t i = 0; i < vxs.size(); ++i) { + const auto &v = vxs[i]; + validator_meta.emplace(v, + ValidatorMeta{ + .group = g, + .within_group_index = i, + .seconded_count = 0, + }); + } + } + } + + void fill_statement_filter( + GroupIndex group_index, + const CandidateHash &candidate_hash, + network::vstaging::StatementFilter &statement_filter) const { + if (auto it_gs = group_statements.find(group_index); + it_gs != group_statements.end()) { + if (auto it_ch = it_gs->second.find(candidate_hash); + it_ch != it_gs->second.end()) { + const GroupStatements &statements = it_ch->second; + statement_filter.seconded_in_group = statements.seconded; + statement_filter.validated_in_group = statements.valid; + } + } + } + + size_t seconded_count(const ValidatorIndex &validator_index) const { + auto it = validator_meta.find(validator_index); + if (it != validator_meta.end()) { + return it->second.seconded_count; + } + return 0; + } + + void note_known_by_backing( + ValidatorIndex validator_index, + const network::vstaging::CompactStatement &statement) { + auto it = known_statements.find(Fingerprint{ + .index = validator_index, + .statement = statement, + }); + if (it != known_statements.end()) { + auto &stored = it->second; + stored.known_by_backing = true; + } + } + + template + void fresh_statements_for_backing( + const std::vector &validators, + const CandidateHash &candidate_hash, + F &&cb) const { + auto call = [&](const Fingerprint &fingerprint) { + auto it = known_statements.find(fingerprint); + if (it != known_statements.end()) { + const StoredStatement &s = it->second; + if (!s.known_by_backing) { + std::forward(cb)(s.statement); + } + } + }; + + for (const auto &vi : validators) { + call(Fingerprint{ + .index = vi, + .statement = + network::vstaging::SecondedCandidateHash{ + .hash = candidate_hash, + }, + }); + call(Fingerprint{ + .index = vi, + .statement = + network::vstaging::ValidCandidateHash{ + .hash = candidate_hash, + }, + }); + } + } + + template + void groupStatements(const std::vector &group_validators, + const CandidateHash &candidate_hash, + const network::vstaging::StatementFilter &filter, + F &&cb) const { + auto call = [&](const scale::BitVec &target, + network::vstaging::CompactStatement &&stm) { + Fingerprint fingerprint{ + .index = 0, + .statement = std::move(stm), + }; + for (size_t i = 0; i < target.bits.size(); ++i) { + if (!target.bits[i]) { + continue; + } + if (i >= group_validators.size()) { + continue; + } + + const ValidatorIndex v = group_validators[i]; + fingerprint.index = v; + auto it = known_statements.find(fingerprint); + if (it != known_statements.end()) { + cb(it->second.statement); + } + } + }; + + call(filter.seconded_in_group, + network::vstaging::SecondedCandidateHash{ + .hash = candidate_hash, + }); + call(filter.validated_in_group, + network::vstaging::ValidCandidateHash{ + .hash = candidate_hash, + }); + } + + std::optional insert( + const Groups &groups, + const IndexedAndSigned &statement, + StatementOrigin origin) { + const auto validator_index = statement.payload.ix; + auto it = validator_meta.find(validator_index); + if (it == validator_meta.end()) { + return std::nullopt; + } + + auto &vm = it->second; + const network::vstaging::CompactStatement compact = getPayload(statement); + Fingerprint fingerprint{ + .index = validator_index, + .statement = compact, + }; + + auto it_st = known_statements.find(fingerprint); + if (it_st != known_statements.end()) { + if (StatementOrigin::Local == origin) { + it_st->second.known_by_backing = true; + } + return false; + } + known_statements.emplace( + fingerprint, + StoredStatement{ + .statement = statement, + .known_by_backing = (origin == StatementOrigin::Local), + }); + + const auto &candidate_hash = candidateHash(compact); + const bool seconded = is_type( + compact.inner_value); + const auto group_index = vm.group; + + auto it_gr = groups.groups.find(group_index); + if (it_gr == groups.groups.end()) { + SL_ERROR(logger, + "Groups passed into `insert` differ from those used at store " + "creation. (group index={})", + group_index); + return std::nullopt; + } + + const std::vector &group = it_gr->second; + auto [it_s, _] = group_statements[group_index].try_emplace(candidate_hash, + group.size()); + if (seconded) { + vm.seconded_count += 1; + it_s->second.note_seconded(vm.within_group_index); + } else { + it_s->second.note_validated(vm.within_group_index); + } + return true; + } + }; + +} // namespace kagome::parachain diff --git a/core/parachain/validator/parachain_processor.hpp b/core/parachain/validator/parachain_processor.hpp index 9ed777bed2..2ef9a4a7b0 100644 --- a/core/parachain/validator/parachain_processor.hpp +++ b/core/parachain/validator/parachain_processor.hpp @@ -22,13 +22,18 @@ #include "network/peer_manager.hpp" #include "network/peer_view.hpp" #include "network/protocols/req_collation_protocol.hpp" -#include "network/types/collator_messages.hpp" +#include "network/types/collator_messages_vstaging.hpp" #include "outcome/outcome.hpp" #include "parachain/availability/bitfield/signer.hpp" #include "parachain/availability/store/store.hpp" #include "parachain/backing/store.hpp" #include "parachain/pvf/precheck.hpp" #include "parachain/pvf/pvf.hpp" +#include "parachain/validator/backing_implicit_view.hpp" +#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/signer.hpp" #include "primitives/common.hpp" #include "primitives/event_types.hpp" @@ -46,7 +51,6 @@ namespace kagome::common { namespace kagome::network { class Router; - struct PendingCollation; } // namespace kagome::network namespace kagome::crypto { @@ -61,8 +65,15 @@ namespace kagome::dispute { namespace kagome::parachain { + struct BackedCandidatesSource { + virtual ~BackedCandidatesSource() {} + virtual std::vector getBackedCandidates( + const RelayHash &relay_parent) = 0; + }; + struct ParachainProcessorImpl - : std::enable_shared_from_this { + : BackedCandidatesSource, + std::enable_shared_from_this { enum class Error { RESPONSE_ALREADY_RECEIVED = 1, COLLATION_NOT_FOUND, @@ -73,7 +84,15 @@ namespace kagome::parachain { DUPLICATE, NO_INSTANCE, NOT_A_VALIDATOR, - NOT_SYNCHRONIZED + NOT_SYNCHRONIZED, + UNDECLARED_COLLATOR, + PEER_LIMIT_REACHED, + PROTOCOL_MISMATCH, + NOT_CONFIRMED, + NO_STATE, + NO_SESSION_INFO, + OUT_OF_BOUND, + REJECTED_BY_PROSPECTIVE_PARACHAINS }; static constexpr uint64_t kBackgroundWorkers = 5; @@ -106,32 +125,36 @@ namespace kagome::parachain { std::shared_ptr app_state_manager, primitives::events::BabeStateSubscriptionEnginePtr babe_status_observable, - std::shared_ptr query_audi); + std::shared_ptr query_audi, + std::shared_ptr prospective_parachains); ~ParachainProcessorImpl() = default; bool prepare(); bool start(); - void requestCollations(const network::CollationEvent &pending_collation); + void handleAdvertisement( + network::CollationEvent &&pending_collation, + std::optional> &&prospective_candidate); outcome::result canProcessParachains() const; - outcome::result advCanBeProcessed( - const primitives::BlockHash &relay_parent, - const libp2p::peer::PeerId &peer_id); - void handleStatement(const libp2p::peer::PeerId &peer_id, - const primitives::BlockHash &relay_parent, - const network::SignedStatement &statement); void onIncomingCollator(const libp2p::peer::PeerId &peer_id, network::CollatorPublicKey pubkey, network::ParachainId para_id); - void onIncomingCollationStream(const libp2p::peer::PeerId &peer_id); - void onIncomingValidationStream(const libp2p::peer::PeerId &peer_id); + void onIncomingCollationStream(const libp2p::peer::PeerId &peer_id, + network::CollationVersion version); + void onIncomingValidationStream(const libp2p::peer::PeerId &peer_id, + network::CollationVersion version); void onValidationProtocolMsg( const libp2p::peer::PeerId &peer_id, - const network::ValidatorProtocolMessage &message); + const network::VersionedValidatorProtocolMessage &message); outcome::result OnFetchChunkRequest( const network::FetchChunkRequest &request); + outcome::result + OnFetchAttestedCandidateRequest( + const network::vstaging::AttestedCandidateRequest &request); + std::vector getBackedCandidates( + const RelayHash &relay_parent) override; network::ResponsePov getPov(CandidateHash &&candidate_hash); auto getAvStore() { return av_store_; @@ -143,6 +166,10 @@ namespace kagome::parachain { private: enum struct StatementType { kSeconded = 0, kValid }; using Commitments = std::shared_ptr; + using WorkersContext = boost::asio::io_context; + using WorkGuard = boost::asio::executor_work_guard< + boost::asio::io_context::executor_type>; + using SecondingAllowed = std::optional; struct ValidateAndSecondResult { outcome::result result; @@ -150,6 +177,7 @@ namespace kagome::parachain { Commitments commitments; network::CandidateReceipt candidate; network::ParachainBlock pov; + runtime::PersistedValidationData pvd; }; struct AttestingData { @@ -167,13 +195,6 @@ namespace kagome::parachain { size_t minimum_votes(size_t n_validators) const { return std::min(size_t(2ull), n_validators); } - - size_t requisite_votes(ParachainId group) const { - if (auto it = groups.find(group); it != groups.end()) { - return minimum_votes(it->second.size()); - } - return std::numeric_limits::max(); - } }; struct AttestedCandidate { @@ -186,13 +207,58 @@ namespace kagome::parachain { validity_votes; }; + struct StatementWithPVDSeconded { + network::CommittedCandidateReceipt committed_receipt; + runtime::PersistedValidationData pvd; + }; + + struct StatementWithPVDValid { + CandidateHash candidate_hash; + }; + + using StatementWithPVD = + boost::variant; + + using SignedFullStatementWithPVD = IndexedAndSigned; + IndexedAndSigned signed_to_compact( + const SignedFullStatementWithPVD &s) const { + const Hash h = candidateHashFrom(getPayload(s)); + return { + .payload = + { + .payload = visit_in_place( + getPayload(s), + [&](const StatementWithPVDSeconded &) + -> network::vstaging::CompactStatement { + return network::vstaging::SecondedCandidateHash{ + .hash = h, + }; + }, + [&](const StatementWithPVDValid &) + -> network::vstaging::CompactStatement { + return network::vstaging::ValidCandidateHash{ + .hash = h, + }; + }), + .ix = s.payload.ix, + }, + .signature = s.signature, + }; + } + struct RelayParentState { + ProspectiveParachainsModeOpt prospective_parachains_mode; std::optional assignment; std::optional seconded; std::optional our_index; std::optional required_collator; + Collations collations; TableContext table_context; + std::optional statement_store; + std::vector availability_cores; + runtime::GroupDescriptor group_rotation_info; + uint32_t minimum_backing_votes; std::unordered_set awaiting_validation; std::unordered_set issued_statements; @@ -201,23 +267,134 @@ namespace kagome::parachain { std::unordered_set backed_hashes{}; }; + struct PerCandidateState { + runtime::PersistedValidationData persisted_validation_data; + bool seconded_locally; + ParachainId para_id; + Hash relay_parent; + }; + + struct ManifestSummary { + Hash claimed_parent_hash; + GroupIndex claimed_group_index; + network::vstaging::StatementFilter statement_knowledge; + }; + + struct ManifestImportSuccess { + bool acknowledge; + ValidatorIndex sender_index; + }; + using ManifestImportSuccessOpt = std::optional; + /* * Validation. */ + outcome::result advCanBeProcessed( + const primitives::BlockHash &relay_parent, + const libp2p::peer::PeerId &peer_id); outcome::result validateCandidate( const network::CandidateReceipt &candidate, const network::ParachainBlock &pov, - const primitives::BlockHash &relay_parent); + runtime::PersistedValidationData &&pvd); outcome::result> validateErasureCoding( const runtime::AvailableData &validating_data, size_t n_validators); - outcome::result validateAndMakeAvailable( - network::CandidateReceipt &&candidate, - network::ParachainBlock &&pov, + template + void validateAsync(network::CandidateReceipt &&candidate, + network::ParachainBlock &&pov, + runtime::PersistedValidationData &&pvd, + const libp2p::peer::PeerId &peer_id, + const primitives::BlockHash &relay_parent, + size_t n_validators); + + template + void makeAvailable(const libp2p::peer::PeerId &peer_id, + const primitives::BlockHash &candidate_hash, + ValidateAndSecondResult &&result); + void handleStatement(const primitives::BlockHash &relay_parent, + const SignedFullStatementWithPVD &statement); + void process_bitfield_distribution( + const network::BitfieldDistributionMessage &val); + void process_legacy_statement( const libp2p::peer::PeerId &peer_id, - const primitives::BlockHash &relay_parent, - size_t n_validators); + const network::StatementDistributionMessage &msg); + void process_vstaging_statement( + const libp2p::peer::PeerId &peer_id, + const network::vstaging::StatementDistributionMessage &msg); + void send_backing_fresh_statements( + const ConfirmedCandidate &confirmed, + const RelayHash &relay_parent, + ParachainProcessorImpl::RelayParentState &per_relay_parent, + const std::vector &group, + const CandidateHash &candidate_hash); + void apply_post_confirmation(const PostConfirmation &post_confirmation); + std::optional group_for_para( + const std::vector &availability_cores, + const runtime::GroupDescriptor &group_rotation_info, + ParachainId para_id) const; + void new_confirmed_candidate_fragment_tree_updates( + const HypotheticalCandidate &candidate); + void new_leaf_fragment_tree_updates(const Hash &leaf_hash); + void prospective_backed_notification_fragment_tree_updates( + ParachainId para_id, const Hash ¶_head); + void fragment_tree_update_inner( + std::optional> active_leaf_hash, + std::optional, + ParachainId>> required_parent_info, + std::optional> + known_hypotheticals); + void handleFetchedStatementResponse( + outcome::result &&r, + const RelayHash &relay_parent, + const CandidateHash &candidate_hash, + Groups &&groups, + GroupIndex group_index); + ManifestImportSuccessOpt handle_incoming_manifest_common( + const libp2p::peer::PeerId &peer_id, + const CandidateHash &candidate_hash, + const RelayHash &relay_parent, + const ManifestSummary &manifest_summary, + ParachainId para_id); + network::vstaging::StatementFilter local_knowledge_filter( + size_t group_size, + GroupIndex group_index, + const CandidateHash &candidate_hash, + const StatementStore &statement_store); + std::deque + acknowledgement_and_statement_messages( + StatementStore &statement_store, + const std::vector &group, + const network::vstaging::StatementFilter &local_knowledge, + const CandidateHash &candidate_hash, + const RelayHash &relay_parent); + std::deque + post_acknowledgement_statement_messages( + const RelayHash &relay_parent, + const StatementStore &statement_store, + const std::vector &group, + const CandidateHash &candidate_hash); + void send_to_validators_group( + const RelayHash &relay_parent, + const std::deque &messages); + void circulate_statement( + const RelayHash &relay_parent, + const IndexedAndSigned &statement); + + outcome::result> insertAdvertisement( + network::PeerState &peer_data, + const RelayHash &relay_parent, + const ProspectiveParachainsModeOpt &relay_parent_mode, + const std::optional> + &candidate_hash); + + bool isRelayParentInImplicitView( + const RelayHash &relay_parent, + const ProspectiveParachainsModeOpt &relay_parent_mode, + const ImplicitView &implicit_view, + const std::unordered_map + &active_leaves, + ParachainId para_id); template void requestPoV(const libp2p::peer::PeerInfo &peer_info, @@ -225,10 +402,13 @@ namespace kagome::parachain { F &&callback); std::optional attested_candidate( - const CandidateHash &digest, const TableContext &context); + const RelayHash &relay_parent, + const CandidateHash &digest, + const TableContext &context, + uint32_t minimum_backing_votes); std::optional attested( - network::CommittedCandidateReceipt &&candidate, + const network::CommittedCandidateReceipt &candidate, const BackingStore::StatementInfo &data, size_t validity_threshold); std::optional table_attested_to_backed( @@ -239,58 +419,56 @@ namespace kagome::parachain { /* * Logic. */ + std::optional + requestProspectiveValidationData(const RelayHash &relay_parent, + const Hash &parent_head_data_hash, + ParachainId para_id); + std::optional + requestPersistedValidationData(const RelayHash &relay_parent, + ParachainId para_id); + + std::optional + fetchPersistedValidationData(const RelayHash &relay_parent, + ParachainId para_id); void onValidationComplete(const libp2p::peer::PeerId &peer_id, - ValidateAndSecondResult &&result); + const ValidateAndSecondResult &result); void onAttestComplete(const libp2p::peer::PeerId &peer_id, - ValidateAndSecondResult &&result); + const ValidateAndSecondResult &result); void onAttestNoPoVComplete(const network::RelayHash &relay_parent, const CandidateHash &candidate_hash); - template - void appendAsyncValidationTask(network::CandidateReceipt &&candidate, - network::ParachainBlock &&pov, - const primitives::BlockHash &relay_parent, - const libp2p::peer::PeerId &peer_id, - RelayParentState ¶chain_state, - const primitives::BlockHash &candidate_hash, - size_t n_validators); - void kickOffValidationWork(const RelayHash &relay_parent, - AttestingData &attesting_data, - RelayParentState ¶chain_state); + void kickOffValidationWork( + const RelayHash &relay_parent, + AttestingData &attesting_data, + const runtime::PersistedValidationData &persisted_validation_data, + RelayParentState ¶chain_state); std::optional retrieveSessionInfo( const RelayHash &relay_parent); - void handleFetchedCollation(network::CollationEvent &&pending_collation, + void handleFetchedCollation(PendingCollation &&pending_collation, network::CollationFetchingResponse &&response); template std::optional createAndSignStatement( - ValidateAndSecondResult &validation_result); + const ValidateAndSecondResult &validation_result); + template + outcome::result< + std::optional> + sign_import_and_distribute_statement( + ParachainProcessorImpl::RelayParentState &rp_state, + const ValidateAndSecondResult &validation_result); + void post_import_statement_actions( + const RelayHash &relay_parent, + ParachainProcessorImpl::RelayParentState &rp_state, + std::optional &summary); template std::optional createAndSignStatementFromPayload( T &&payload, ValidatorIndex validator_ix, RelayParentState ¶chain_state); - std::optional importStatement( + outcome::result> importStatement( const network::RelayHash &relay_parent, - const network::SignedStatement &statement, + const SignedFullStatementWithPVD &statement, ParachainProcessorImpl::RelayParentState &relayParentState); - /* - * Helpers. - */ - primitives::BlockHash candidateHashFrom( - const network::CandidateReceipt &candidate) { - return hasher_->blake2b_256(scale::encode(candidate).value()); - } - primitives::BlockHash candidateHashFrom( - const network::CollationFetchingResponse &collation) { - return visit_in_place( - collation.response_data, - [&](const network::CollationResponse &collation_response) - -> primitives::BlockHash { - return candidateHashFrom(collation_response.receipt); - }); - } - const network::CandidateDescriptor &candidateDescriptorFrom( const network::CollationFetchingResponse &collation) { return visit_in_place( @@ -324,7 +502,7 @@ namespace kagome::parachain { } network::CandidateReceipt candidateFromCommittedCandidateReceipt( - const network::CommittedCandidateReceipt &data) { + const network::CommittedCandidateReceipt &data) const { network::CandidateReceipt receipt; receipt.descriptor = data.descriptor, receipt.commitments_hash = @@ -333,48 +511,74 @@ namespace kagome::parachain { } primitives::BlockHash candidateHashFrom( - const network::Statement &statement) { + const StatementWithPVD &statement) const { return visit_in_place( - statement.candidate_state, - [&](const network::CommittedCandidateReceipt &data) { + statement, + [&](const StatementWithPVDSeconded &val) { return hasher_->blake2b_256( - scale::encode(candidateFromCommittedCandidateReceipt(data)) + ::scale::encode(candidateFromCommittedCandidateReceipt( + val.committed_receipt)) .value()); }, - [&](const primitives::BlockHash &candidate_hash) { - return candidate_hash; - }, - [](const auto &) { - BOOST_ASSERT(!"Not used!"); - return primitives::BlockHash{}; - }); + [&](const StatementWithPVDValid &val) { return val.candidate_hash; }); } /* * Notification */ void broadcastView(const network::View &view) const; + void broadcastViewToGroup(const primitives::BlockHash &relay_parent, + const network::View &view); void broadcastViewExcept(const libp2p::peer::PeerId &peer_id, const network::View &view) const; - void notifyBackedCandidate(const network::SignedStatement &statement); + template + void notify_internal(std::shared_ptr &context, F &&func) { + BOOST_ASSERT(context); + boost::asio::post(*context, std::forward(func)); + } + void statementDistributionBackedCandidate( + const CandidateHash &candidate_hash); void notifyAvailableData(std::vector &&chunk_list, const primitives::BlockHash &relay_parent, const network::CandidateHash &candidate_hash, const network::ParachainBlock &pov, const runtime::PersistedValidationData &data); - void notifyStatementDistributionSystem( + void share_local_statement_vstaging( + RelayParentState &per_relay_parent, const primitives::BlockHash &relay_parent, - const network::SignedStatement &statement); + const SignedFullStatementWithPVD &statement); + void share_local_statement_v1(RelayParentState &per_relay_parent, + const primitives::BlockHash &relay_parent, + const SignedFullStatementWithPVD &statement); void notify(const libp2p::peer::PeerId &peer_id, const primitives::BlockHash &relay_parent, - const network::SignedStatement &statement); + const SignedFullStatementWithPVD &statement); void handleNotify(const libp2p::peer::PeerId &peer_id, const primitives::BlockHash &relay_parent); + void onDeactivateBlocks(const primitives::events::ChainEventParams &event); + void onViewUpdated(const network::ExView &event); + void OnBroadcastBitfields(const primitives::BlockHash &relay_parent, + const network::SignedBitfield &bitfield); + void onUpdatePeerView(const libp2p::peer::PeerId &peer_id, + const network::View &view); + void fetchCollation( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + PendingCollation &&pc, + const CollatorId &id); + void fetchCollation( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + PendingCollation &&pc, + const CollatorId &id, + network::CollationVersion version); std::optional> tryGetStateByRelayParent(const primitives::BlockHash &relay_parent); RelayParentState &storeStateByRelayParent( const primitives::BlockHash &relay_parent, RelayParentState &&val); + void send_peer_messages_for_relay_parent( + std::optional> + peer_id, + const RelayHash &relay_parent); void createBackingTask(const primitives::BlockHash &relay_parent); outcome::result initNewBackingTask( @@ -385,19 +589,47 @@ namespace kagome::parachain { F &&callback); template bool tryOpenOutgoingValidationStream(const libp2p::peer::PeerId &peer_id, + network::CollationVersion version, F &&callback); template bool tryOpenOutgoingStream(const libp2p::peer::PeerId &peer_id, std::shared_ptr protocol, F &&callback); + outcome::result enqueueCollation( + ParachainProcessorImpl::RelayParentState &per_relay_parent, + const RelayHash &relay_parent, + ParachainId para_id, + const libp2p::peer::PeerId &peer_id, + const CollatorId &collator_id, + std::optional> &&prospective_candidate); void sendMyView(const libp2p::peer::PeerId &peer_id, const std::shared_ptr &stream, const std::shared_ptr &protocol); bool isValidatingNode() const; - - std::optional importStatementToTable( + void unblockAdvertisements( + ParachainProcessorImpl::RelayParentState &rp_state, + ParachainId para_id, + const Hash ¶_head); + void requestUnblockedCollations( + ParachainProcessorImpl::RelayParentState &rp_state, + ParachainId para_id, + const Hash ¶_head, + std::vector &&unblocked); + + bool canSecond(ParachainProcessorImpl::RelayParentState &per_relay_parent, + ParachainId candidate_para_id, + const Hash &candidate_relay_parent, + const CandidateHash &candidate_hash, + const Hash &parent_head_data_hash); + + ParachainProcessorImpl::SecondingAllowed secondingSanityCheck( + const HypotheticalCandidate &hypothetical_candidate, + bool backed_in_path_only); + + std::optional importStatementToTable( + const RelayHash &relay_parent, ParachainProcessorImpl::RelayParentState &relayParentState, const primitives::BlockHash &candidate_hash, const network::SignedStatement &statement); @@ -414,18 +646,35 @@ namespace kagome::parachain { state_by_relay_parent; std::unordered_map< libp2p::peer::PeerId, - std::deque>> + std::deque>> seconded_statements; + std::optional implicit_view; + std::unordered_map per_leaf; + std::unordered_map per_candidate; /// Added as independent member to prevent extra locks for /// `state_by_relay_parent` which is used in internal thread only - SafeObject> active_leaves; + std::unordered_map active_leaves; + std::unordered_map< + ParachainId, + std::unordered_map>> + blocked_advertisements; + std::unordered_set + collation_requests_cancel_handles; + + struct { + std::unordered_set implicit_view; + network::View view; + } statementDistributionV2; } our_current_state_; - SafeObject> - pending_candidates; + + std::unordered_map pending_candidates; WeakIoContext main_thread_context_; std::shared_ptr hasher_; std::shared_ptr peer_view_; network::PeerView::MyViewSubscriberPtr my_view_sub_; + network::PeerView::PeerViewSubscriberPtr remote_view_sub_; std::shared_ptr pvf_; std::shared_ptr signer_factory_; @@ -443,6 +692,8 @@ namespace kagome::parachain { std::shared_ptr chain_sub_; WeakIoContext worker_thread_context_; std::default_random_engine random_; + std::shared_ptr prospective_parachains_; + Candidates candidates_; metrics::RegistryPtr metrics_registry_ = metrics::createRegistry(); metrics::Gauge *metric_is_parachain_validator_; diff --git a/core/parachain/validator/prospective_parachains.hpp b/core/parachain/validator/prospective_parachains.hpp new file mode 100644 index 0000000000..5fb5d77796 --- /dev/null +++ b/core/parachain/validator/prospective_parachains.hpp @@ -0,0 +1,718 @@ +/** + * 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 { + + 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_); + } + + std::shared_ptr getBlockTree() { + BOOST_ASSERT(block_tree_); + return block_tree_; + } + + std::vector> + answerMinimumRelayParentsRequest(const RelayHash &relay_parent) const { + std::vector> v; + auto it = view.active_leaves.find(relay_parent); + if (it != view.active_leaves.end()) { + const RelayBlockViewData &leaf_data = it->second; + for (const auto &[para_id, fragment_tree] : leaf_data.fragment_trees) { + v.emplace_back(para_id, + fragment_tree.scope.earliestRelayParent().number); + } + } + return v; + } + + std::optional> answerGetBackableCandidate( + const RelayHash &relay_parent, + ParachainId para, + 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 std::nullopt; + } + 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 std::nullopt; + } + 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 std::nullopt; + } + const fragment::CandidateStorage &storage = storage_it->second; + + auto child_hash = tree.selectChild( + required_path, [&](const CandidateHash &candidate) -> bool { + return storage.isBacked(candidate); + }); + if (!child_hash) { + SL_TRACE(logger, + "Child hash is null. (para_id={}, " + "relay_parent={})", + para, + relay_parent); + return std::nullopt; + } + + auto candidate_relay_parent = + storage.relayParentByCandidateHash(*child_hash); + if (!candidate_relay_parent) { + SL_ERROR(logger, + "Candidate is present in fragment tree but not in candidate's " + "storage! (relay_parent={}, para_id={}, child_hash={})", + relay_parent, + para, + *child_hash); + return std::nullopt; + } + + return std::make_pair(*child_hash, *candidate_relay_parent); + } + + fragment::FragmentTreeMembership answerTreeMembershipRequest( + ParachainId para, const CandidateHash &candidate) { + return fragmentTreeMembership(view.active_leaves, para, candidate); + } + + std::optional + answerProspectiveValidationDataRequest( + const RelayHash &candidate_relay_parent, + const Hash &parent_head_data_hash, + ParachainId para_id) { + if (auto it = view.candidate_storage.find(para_id); + it != view.candidate_storage.end()) { + fragment::CandidateStorage &storage = it->second; + std::optional head_data = + utils::fromRefToOwn(storage.headDataByHash(parent_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) { + 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().message()); + 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().message()); + 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)); + OUTCOME_TRY(required_session, + parachain_host_->session_index_for_child(relay_hash)); + + block_info.reserve(hashes.size()); + for (const auto &hash : hashes) { + 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) { + block_info.emplace_back(*info); + } else { + 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) { + 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().message()); + } + } + + OUTCOME_TRY(scope, + fragment::Scope::withAncestors(para, + block_info, + constraints, + compact_pending, + mode->max_candidate_depth, + ancestry)); + fragment_trees.emplace(para, + fragment::FragmentTree::populate( + hasher_, scope, candidate_storage)); + } + + 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::transform(candidates.begin(), + candidates.end(), + 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/signer.hpp b/core/parachain/validator/signer.hpp index f1513f9e31..5c431ac669 100644 --- a/core/parachain/validator/signer.hpp +++ b/core/parachain/validator/signer.hpp @@ -43,7 +43,7 @@ namespace kagome::parachain { template auto signable(const crypto::Hasher &hasher, const T &payload) const { auto &&signable = toSignable(hasher, payload); - return scale::encode(std::tie(signable, *this)).value(); + return ::scale::encode(std::tie(signable, *this)).value(); } /// Current session index. diff --git a/core/primitives/block_header.cpp b/core/primitives/block_header.cpp index 4119bf8ba8..a1939106d6 100644 --- a/core/primitives/block_header.cpp +++ b/core/primitives/block_header.cpp @@ -8,10 +8,9 @@ namespace kagome::primitives { - void calculateBlockHash(BlockHeader &header, const crypto::Hasher &hasher) { - auto enc_res = scale::encode(header); - BOOST_ASSERT_MSG(enc_res.has_value(), "Header should be encoded errorless"); - header.hash_opt.emplace(hasher.blake2b_256(enc_res.value())); + void calculateBlockHash(const BlockHeader &header, + const crypto::Hasher &hasher) { + header.updateHash(hasher); } } // namespace kagome::primitives diff --git a/core/primitives/block_header.hpp b/core/primitives/block_header.hpp index 7e05543681..01fab04d78 100644 --- a/core/primitives/block_header.hpp +++ b/core/primitives/block_header.hpp @@ -28,7 +28,7 @@ namespace kagome::primitives { storage::trie::RootHash state_root{}; ///< Merkle tree root of state common::Hash256 extrinsics_root{}; ///< Hash of included extrinsics Digest digest{}; ///< Chain-specific auxiliary data - std::optional hash_opt{}; ///< Block hash if calculated + mutable std::optional hash_opt{}; ///< Block hash if calculated bool operator==(const BlockHeader &rhs) const { return std::tie(parent_hash, number, state_root, extrinsics_root, digest) @@ -56,6 +56,13 @@ namespace kagome::primitives { return hash_opt.value(); } + void updateHash(const crypto::Hasher &hasher) const { + auto enc_res = scale::encode(*this); + BOOST_ASSERT_MSG(enc_res.has_value(), + "Header should be encoded errorless"); + hash_opt.emplace(hasher.blake2b_256(enc_res.value())); + } + BlockInfo blockInfo() const { return {number, hash()}; } @@ -131,6 +138,7 @@ namespace kagome::primitives { return s; } - void calculateBlockHash(BlockHeader &header, const crypto::Hasher &hasher); + void calculateBlockHash(const BlockHeader &header, + const crypto::Hasher &hasher); } // namespace kagome::primitives diff --git a/core/primitives/common.hpp b/core/primitives/common.hpp index 26b99638e8..5f2eca698e 100644 --- a/core/primitives/common.hpp +++ b/core/primitives/common.hpp @@ -60,6 +60,13 @@ struct std::hash> { } }; +template +struct InnerHash { + size_t operator()(const T &p) const noexcept { + return p.inner_hash(); + } +}; + template struct fmt::formatter> { // Presentation format: 's' - short, 'l' - long. diff --git a/core/primitives/inherent_data.hpp b/core/primitives/inherent_data.hpp index 43acd1fa9f..6e63f895c7 100644 --- a/core/primitives/inherent_data.hpp +++ b/core/primitives/inherent_data.hpp @@ -51,7 +51,7 @@ namespace kagome::primitives { auto [it, inserted] = data.try_emplace(std::move(identifier), common::Buffer()); if (inserted) { - it->second = common::Buffer(scale::encode(inherent).value()); + it->second = common::Buffer(::scale::encode(inherent).value()); return outcome::success(); } return InherentDataError::IDENTIFIER_ALREADY_EXISTS; @@ -63,7 +63,7 @@ namespace kagome::primitives { */ template void replaceData(InherentIdentifier identifier, const T &inherent) { - data[identifier] = common::Buffer(scale::encode(inherent).value()); + data[identifier] = common::Buffer(::scale::encode(inherent).value()); } /** diff --git a/core/primitives/math.hpp b/core/primitives/math.hpp index eed2dfb68a..5dc394d52b 100644 --- a/core/primitives/math.hpp +++ b/core/primitives/math.hpp @@ -9,6 +9,9 @@ #include #include #include +#include + +#include "macro/endianness_utils.hpp" namespace kagome::math { @@ -36,17 +39,29 @@ namespace kagome::math { return res; } + template + inline outcome::result checked_sub(T &x, T y, E e) { + static_assert(std::numeric_limits::is_integer + && !std::numeric_limits::is_signed, + "Value must be integer and unsigned!"); + if (x >= y) { + x -= y; + return outcome::success(); + } + return e; + } + template >, bool> = true> constexpr auto toLE(const T &value) { #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ constexpr size_t size = sizeof(std::decay_t); if constexpr (size == 8) { - return __builtin_bswap64(value); + return LE_BE_SWAP64(value); } else if constexpr (size == 4) { - return __builtin_bswap32(value); + return LE_BE_SWAP32(value); } else if constexpr (size == 2) { - return __builtin_bswap16(value); + return LE_BE_SWAP16(value); } #endif return value; diff --git a/core/runtime/module_instance.hpp b/core/runtime/module_instance.hpp index 2442514c15..e049b3bc7f 100644 --- a/core/runtime/module_instance.hpp +++ b/core/runtime/module_instance.hpp @@ -43,7 +43,7 @@ namespace kagome::runtime { template static outcome::result encodeArgs(const Args &...args) { if constexpr (sizeof...(args) > 0) { - return common::map_result(scale::encode(args...), [](auto &&vec) { + return common::map_result(::scale::encode(args...), [](auto &&vec) { return common::Buffer{vec}; }); } diff --git a/core/runtime/runtime_api/impl/parachain_host.cpp b/core/runtime/runtime_api/impl/parachain_host.cpp index 39a2edaf13..3588d677b1 100644 --- a/core/runtime/runtime_api/impl/parachain_host.cpp +++ b/core/runtime/runtime_api/impl/parachain_host.cpp @@ -268,4 +268,27 @@ namespace kagome::runtime { ctx, "ParachainHost_submit_pvf_check_statement", statement, signature); } + outcome::result> + ParachainHostImpl::staging_para_backing_state( + const primitives::BlockHash &block, ParachainId id) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block)); + return executor_->call>( + ctx, "ParachainHost_para_backing_state", id); + } + + outcome::result + ParachainHostImpl::staging_async_backing_params( + const primitives::BlockHash &block) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block)); + return executor_->call( + ctx, "ParachainHost_async_backing_params"); + } + + outcome::result ParachainHostImpl::minimum_backing_votes( + const primitives::BlockHash &block, SessionIndex index) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block)); + return executor_->call(ctx, + "ParachainHost_minimum_backing_votes"); + } + } // namespace kagome::runtime diff --git a/core/runtime/runtime_api/impl/parachain_host.hpp b/core/runtime/runtime_api/impl/parachain_host.hpp index ece7fffbcd..ce6777260d 100644 --- a/core/runtime/runtime_api/impl/parachain_host.hpp +++ b/core/runtime/runtime_api/impl/parachain_host.hpp @@ -102,6 +102,16 @@ namespace kagome::runtime { const parachain::PvfCheckStatement &statement, const parachain::Signature &signature) override; + outcome::result> + staging_para_backing_state(const primitives::BlockHash &block, + ParachainId id) override; + + outcome::result + staging_async_backing_params(const primitives::BlockHash &block) override; + + outcome::result minimum_backing_votes( + const primitives::BlockHash &block, SessionIndex index) override; + private: bool prepare(); void clearCaches(const std::vector &blocks); diff --git a/core/runtime/runtime_api/impl/transaction_payment_api.cpp b/core/runtime/runtime_api/impl/transaction_payment_api.cpp index 7dd1111f79..9b4628b8d3 100644 --- a/core/runtime/runtime_api/impl/transaction_payment_api.cpp +++ b/core/runtime/runtime_api/impl/transaction_payment_api.cpp @@ -37,6 +37,7 @@ namespace kagome::runtime { const primitives::Extrinsic &ext, uint32_t len) { OUTCOME_TRY(runtime_version, core_api_->version(block)); + static const common::Hash64 transaction_payment_api_hash = hasher_->blake2b_64( common::Buffer::fromString("TransactionPaymentApi")); diff --git a/core/runtime/runtime_api/parachain_host.hpp b/core/runtime/runtime_api/parachain_host.hpp index d146a77be1..ac9423b191 100644 --- a/core/runtime/runtime_api/parachain_host.hpp +++ b/core/runtime/runtime_api/parachain_host.hpp @@ -10,6 +10,7 @@ #include "common/unused.hpp" #include "dispute_coordinator/provisioner/impl/prioritized_selection.hpp" #include "dispute_coordinator/types.hpp" +#include "parachain/types.hpp" #include "primitives/block_id.hpp" #include "primitives/common.hpp" #include "primitives/parachain_host.hpp" @@ -203,6 +204,23 @@ namespace kagome::runtime { const primitives::BlockHash &block, const parachain::PvfCheckStatement &statement, const parachain::Signature &signature) = 0; + + /** + * @return the state of parachain backing for a given para. + */ + virtual outcome::result> + staging_para_backing_state(const primitives::BlockHash &block, + ParachainId id) = 0; + + /** + * @return candidate's acceptance limitations for asynchronous backing for a + * relay parent. + */ + virtual outcome::result + staging_async_backing_params(const primitives::BlockHash &block) = 0; + + virtual outcome::result minimum_backing_votes( + const primitives::BlockHash &block, SessionIndex index) = 0; }; } // namespace kagome::runtime diff --git a/core/runtime/runtime_api/parachain_host_types.hpp b/core/runtime/runtime_api/parachain_host_types.hpp index a70695a9f9..73156a486c 100644 --- a/core/runtime/runtime_api/parachain_host_types.hpp +++ b/core/runtime/runtime_api/parachain_host_types.hpp @@ -91,7 +91,7 @@ namespace kagome::runtime { /// Returns the index of the group needed to validate the core at the given /// index, assuming the given number of cores. - GroupIndex groupForCore(CoreIndex core_index, size_t cores) { + GroupIndex groupForCore(CoreIndex core_index, size_t cores) const { if (group_rotation_frequency == 0) { return core_index; } @@ -131,7 +131,7 @@ namespace kagome::runtime { enum class OccupiedCoreAssumption : uint8_t { Included, // 0 TimedOut, // 1 - Unused // 2 + Free // 2 }; struct PersistedValidationData { SCALE_TIE(4); diff --git a/core/scale/encoder/primitives.hpp b/core/scale/encoder/primitives.hpp index f3d03ed96c..eb48622c88 100644 --- a/core/scale/encoder/primitives.hpp +++ b/core/scale/encoder/primitives.hpp @@ -127,15 +127,7 @@ namespace kagome::scale { template outcome::result> encode(const Args &...args) { - std::vector res; - encode( - [&](const uint8_t *const val, size_t count) { - if (count != 0ull) { - res.insert(res.end(), &val[0], &val[count]); - } - }, - args...); - return res; + return ::scale::encode(args...); } inline size_t bitUpperBorder(const ::scale::CompactInteger &x) { diff --git a/core/scale/kagome_scale.hpp b/core/scale/kagome_scale.hpp index 945a0f97f3..ea0221b0a0 100644 --- a/core/scale/kagome_scale.hpp +++ b/core/scale/kagome_scale.hpp @@ -17,17 +17,26 @@ #include "primitives/block_header.hpp" #include "primitives/block_id.hpp" #include "primitives/justification.hpp" +#include "runtime/runtime_api/parachain_host_types.hpp" #include "scale/big_fixed_integers.hpp" #include "scale/encode_append.hpp" #include "scale/libp2p_types.hpp" namespace kagome::scale { + namespace __outcome_detail { + template + using Category = ::scale::__outcome_detail::Category; + } using CompactInteger = ::scale::CompactInteger; using BitVec = ::scale::BitVec; using ScaleDecoderStream = ::scale::ScaleDecoderStream; using ScaleEncoderStream = ::scale::ScaleEncoderStream; using PeerInfoSerializable = ::scale::PeerInfoSerializable; using DecodeError = ::scale::DecodeError; + template + using Fixed = ::scale::Fixed; + template + using Compact = ::scale::Compact; using uint128_t = ::scale::uint128_t; using ::scale::decode; @@ -57,6 +66,10 @@ namespace kagome::scale { template constexpr void encode(const F &func, const primitives::Consensus &c); + template + constexpr void encode(const F &func, + const runtime::PersistedValidationData &c); + template constexpr void encode(const F &func, const primitives::Seal &c); @@ -139,6 +152,15 @@ namespace kagome::scale { encode(func, static_cast(c)); } + template + constexpr void encode(const F &func, + const kagome::runtime::PersistedValidationData &c) { + encode(func, c.parent_head); + encode(func, c.relay_parent_number); + encode(func, c.relay_parent_storage_root); + encode(func, c.max_pov_size); + } + template constexpr void encode(const F &func, const primitives::Seal &c) { encode(func, static_cast(c)); diff --git a/core/utils/map.hpp b/core/utils/map.hpp new file mode 100644 index 0000000000..c4c02b7f17 --- /dev/null +++ b/core/utils/map.hpp @@ -0,0 +1,38 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef KAGOME_UTILS_MAP_HPP +#define KAGOME_UTILS_MAP_HPP + +#include +#include +#include + +namespace kagome::utils { + + template + inline std::optional::value_type &&)>::type> + map(T &&source, F &&func) { + if (source) { + return {std::forward(func)(*std::forward(source))}; + } + return std::nullopt; + } + + template + inline auto fromRefToOwn( + const std::optional> &opt_ref) { + std::optional> val{}; + if (opt_ref) { + val = opt_ref->get(); + } + return val; + }; + +} // namespace kagome::utils + +#endif // KAGOME_UTILS_MAP_HPP \ No newline at end of file diff --git a/test/core/api/service/system/system_api_test.cpp b/test/core/api/service/system/system_api_test.cpp index 45b3d03024..999978f6af 100644 --- a/test/core/api/service/system/system_api_test.cpp +++ b/test/core/api/service/system/system_api_test.cpp @@ -15,9 +15,11 @@ #include "mock/core/network/peer_manager_mock.hpp" #include "mock/core/runtime/account_nonce_api_mock.hpp" #include "mock/core/transaction_pool/transaction_pool_mock.hpp" +#include "scale/kagome_scale.hpp" #include "scale/scale.hpp" #include "testutil/literals.hpp" #include "testutil/outcome.hpp" +#include "testutil/scale_test_comparator.hpp" using kagome::api::SystemApi; using kagome::api::SystemApiImpl; @@ -123,7 +125,9 @@ TEST_F(SystemApiTest, GetNonceWithPendingTxs) { std::vector>> ready_txs; for (size_t i = 0; i < kReadyTxNum; i++) { - EXPECT_OUTCOME_TRUE(enc_nonce, scale::encode(kAccountId, kInitialNonce + i)) + EXPECT_OUTCOME_TRUE( + enc_nonce, + testutil::scaleEncodeAndCompareWithRef(kAccountId, kInitialNonce + i)) encoded_nonces[i] = std::move(enc_nonce); ready_txs.emplace_back( std::make_pair(Hash256{{static_cast(i)}}, diff --git a/test/core/blockchain/block_storage_test.cpp b/test/core/blockchain/block_storage_test.cpp index 6106374810..f9ad1daedf 100644 --- a/test/core/blockchain/block_storage_test.cpp +++ b/test/core/blockchain/block_storage_test.cpp @@ -12,6 +12,7 @@ #include "mock/core/crypto/hasher_mock.hpp" #include "mock/core/storage/generic_storage_mock.hpp" #include "mock/core/storage/spaced_storage_mock.hpp" +#include "scale/kagome_scale.hpp" #include "scale/scale.hpp" #include "storage/database_error.hpp" #include "testutil/literals.hpp" diff --git a/test/core/consensus/babe/babe_test.cpp b/test/core/consensus/babe/babe_test.cpp index f9660c65d7..6ee60ffe0b 100644 --- a/test/core/consensus/babe/babe_test.cpp +++ b/test/core/consensus/babe/babe_test.cpp @@ -29,6 +29,7 @@ #include "mock/core/crypto/vrf_provider_mock.hpp" #include "mock/core/dispute_coordinator/dispute_coordinator_mock.hpp" #include "mock/core/network/block_announce_transmitter_mock.hpp" +#include "mock/core/parachain/backed_candidates_source.hpp" #include "mock/core/parachain/backing_store_mock.hpp" #include "mock/core/parachain/bitfield_store_mock.hpp" #include "mock/core/runtime/offchain_worker_api_mock.hpp" @@ -201,6 +202,8 @@ class BabeTest : public testing::Test { storage_sub_engine = std::make_shared(); chain_sub_engine = std::make_shared(); announce_transmitter = std::make_shared(); + backed_candidates_source_ = + std::make_shared(); offchain_worker_api = std::make_shared(); ON_CALL(*offchain_worker_api, offchain_worker(_, _)) @@ -218,7 +221,7 @@ class BabeTest : public testing::Test { sr25519_provider, block_validator, bitfield_store, - backing_store, + backed_candidates_source_, dispute_coordinator, proposer, storage_sub_engine, @@ -246,6 +249,8 @@ class BabeTest : public testing::Test { std::shared_ptr proposer; std::shared_ptr storage_sub_engine; std::shared_ptr chain_sub_engine; + std::shared_ptr + backed_candidates_source_; std::shared_ptr announce_transmitter; std::shared_ptr offchain_worker_api; std::shared_ptr io_ = diff --git a/test/core/host_api/child_storage_extension_test.cpp b/test/core/host_api/child_storage_extension_test.cpp index 722e1cbdd9..3048373d41 100644 --- a/test/core/host_api/child_storage_extension_test.cpp +++ b/test/core/host_api/child_storage_extension_test.cpp @@ -17,6 +17,7 @@ #include "mock/core/storage/trie/trie_batches_mock.hpp" #include "runtime/ptr_size.hpp" #include "scale/encode_append.hpp" +#include "scale/kagome_scale.hpp" #include "storage/predefined_keys.hpp" #include "storage/trie/polkadot_trie/trie_error.hpp" #include "storage/trie/types.hpp" @@ -25,6 +26,7 @@ #include "testutil/outcome/dummy_error.hpp" #include "testutil/prepare_loggers.hpp" #include "testutil/runtime/memory.hpp" +#include "testutil/scale_test_comparator.hpp" using kagome::common::Buffer; using kagome::common::BufferView; @@ -108,7 +110,8 @@ TEST_P(ReadOutcomeParameterizedTest, GetTest) { std::vector encoded_opt_value; if (GetParam()) { - encoded_opt_value = scale::encode(GetParam().value()).value(); + encoded_opt_value = + testutil::scaleEncodeAndCompareWithRef(GetParam().value()).value(); } // 'func' (lambda) diff --git a/test/core/host_api/storage_extension_test.cpp b/test/core/host_api/storage_extension_test.cpp index 2cad02523e..43f93cb74f 100644 --- a/test/core/host_api/storage_extension_test.cpp +++ b/test/core/host_api/storage_extension_test.cpp @@ -19,12 +19,14 @@ #include "mock/core/storage/trie/trie_batches_mock.hpp" #include "runtime/ptr_size.hpp" #include "scale/encode_append.hpp" +#include "scale/kagome_scale.hpp" #include "storage/predefined_keys.hpp" #include "testutil/literals.hpp" #include "testutil/outcome.hpp" #include "testutil/outcome/dummy_error.hpp" #include "testutil/prepare_loggers.hpp" #include "testutil/runtime/memory.hpp" +#include "testutil/scale_test_comparator.hpp" using kagome::common::Buffer; using kagome::common::BufferView; diff --git a/test/core/network/rpc_libp2p_test.cpp b/test/core/network/rpc_libp2p_test.cpp index b8ef129ccb..c38e13909d 100644 --- a/test/core/network/rpc_libp2p_test.cpp +++ b/test/core/network/rpc_libp2p_test.cpp @@ -13,10 +13,12 @@ #include "mock/libp2p/host/host_mock.hpp" #include "network/helpers/scale_message_read_writer.hpp" #include "network/types/blocks_response.hpp" +#include "scale/kagome_scale.hpp" #include "scale/scale.hpp" #include "testutil/libp2p/message_read_writer_helper.hpp" #include "testutil/literals.hpp" #include "testutil/prepare_loggers.hpp" +#include "testutil/scale_test_comparator.hpp" using namespace kagome; using namespace network; @@ -64,8 +66,10 @@ class RpcLibp2pTest : public testing::Test { // request and response BlocksResponse request_{.blocks = {primitives::BlockData{}}}; BlocksResponse response_{.blocks = {primitives::BlockData{}}}; - kagome::common::Buffer encoded_request_{scale::encode(request_).value()}; - kagome::common::Buffer encoded_response_{scale::encode(response_).value()}; + kagome::common::Buffer encoded_request_{ + testutil::scaleEncodeAndCompareWithRef(request_).value()}; + kagome::common::Buffer encoded_response_{ + testutil::scaleEncodeAndCompareWithRef(response_).value()}; }; /** diff --git a/test/core/parachain/CMakeLists.txt b/test/core/parachain/CMakeLists.txt index d54581ccc9..341048212b 100644 --- a/test/core/parachain/CMakeLists.txt +++ b/test/core/parachain/CMakeLists.txt @@ -21,3 +21,12 @@ target_link_libraries(assignments_test base_fs_test validator_parachain ) + +addtest(prospective_parachains + prospective_parachains.cpp + ) + +target_link_libraries(prospective_parachains + validator_parachain + log_configurator + ) diff --git a/test/core/parachain/assignments.cpp b/test/core/parachain/assignments.cpp index 46c0edb0f4..fccc4181a0 100644 --- a/test/core/parachain/assignments.cpp +++ b/test/core/parachain/assignments.cpp @@ -47,17 +47,15 @@ struct AssignmentsTest : public test::BaseFS_Test { const char *const (&accounts)[N], size_t random) { for (const auto &acc : accounts) { - [[maybe_unused]] auto _ = - cs->generateSr25519Keypair(KeyTypes::ASSIGNMENT, - std::string_view{acc}) - .value(); + std::ignore = cs->generateSr25519Keypair(KeyTypes::ASSIGNMENT, + std::string_view{acc}) + .value(); } for (size_t ix = 0ull; ix < random; ++ix) { auto seed = std::to_string(ix); - [[maybe_unused]] auto _ = - cs->generateSr25519Keypair(KeyTypes::ASSIGNMENT, - std::string_view{seed}) - .value(); + std::ignore = cs->generateSr25519Keypair(KeyTypes::ASSIGNMENT, + std::string_view{seed}) + .value(); } return cs->getSr25519PublicKeys(KeyTypes::ASSIGNMENT).value(); } diff --git a/test/core/parachain/prospective_parachains.cpp b/test/core/parachain/prospective_parachains.cpp new file mode 100644 index 0000000000..861e8a7ea7 --- /dev/null +++ b/test/core/parachain/prospective_parachains.cpp @@ -0,0 +1,2770 @@ +/** + * 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/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 "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_); + } + + 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_; + + 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_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 = [&]() -> 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; ++x) { + assert(number - x - 1 != 0); + if (x == 0) { + d = get_parent_hash(d); + continue; + } + 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)) + .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)); + } + } + + 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_candidate( + const TestLeaf &leaf, + ParachainId para_id, + std::vector required_path, + const std::optional> &expected_result) { + auto resp = prospective_parachain_->answerGetBackableCandidate( + leaf.hash, para_id, 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}, + }; + 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_EQ(resp, 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))); + + 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_checkBackableQuery) { + 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_candidate(leaf_a, 1, {candidate_hash_a}, std::nullopt); + + second_candidate(candidate_a); + second_candidate(candidate_b); + + get_backable_candidate(leaf_a, 1, {candidate_hash_a}, std::nullopt); + + back_candidate(candidate_a, candidate_hash_a); + back_candidate(candidate_b, candidate_hash_b); + + get_backable_candidate( + leaf_a, 1, {}, std::make_pair(candidate_hash_a, leaf_a.hash)); + get_backable_candidate(leaf_a, + 1, + {candidate_hash_a}, + std::make_pair(candidate_hash_b, leaf_a.hash)); + + get_backable_candidate(leaf_a, 1, {candidate_hash_b}, std::nullopt); + + 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_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_candidate(leaf_b, + para_id, + {candidate_hash_a}, + std::make_pair(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_candidate( + leaf_a, + para_id, + {}, + std::make_pair(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_candidate(leaf_b, para_id, {}, std::nullopt); +} + +TEST_F(ProspectiveParachainsTest, FragmentTree_usesAncestryOnlyWithinSession) { + std::vector ancestry_hashes{ + fromNumber(4), fromNumber(3), fromNumber(2)}; + 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)) + .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)); + } + } + + 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); + + 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); + 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}, + }; + 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()); + } +} diff --git a/test/core/parachain/pvf_test.cpp b/test/core/parachain/pvf_test.cpp index 1c4208f63e..1d6966a721 100644 --- a/test/core/parachain/pvf_test.cpp +++ b/test/core/parachain/pvf_test.cpp @@ -27,6 +27,7 @@ #include "testutil/literals.hpp" #include "testutil/outcome.hpp" #include "testutil/prepare_loggers.hpp" +#include "utils/struct_to_tuple.hpp" using kagome::application::AppConfigurationMock; using kagome::common::Buffer; diff --git a/test/core/storage/trie_pruner/trie_pruner_test.cpp b/test/core/storage/trie_pruner/trie_pruner_test.cpp index 8cf192f5b3..a088978005 100644 --- a/test/core/storage/trie_pruner/trie_pruner_test.cpp +++ b/test/core/storage/trie_pruner/trie_pruner_test.cpp @@ -643,8 +643,8 @@ TEST_F(TriePrunerTest, RestoreStateFromGenesis) { ON_CALL(*block_tree, bestBlock()) .WillByDefault(Return(BlockInfo{6, hash_from_header(headers.at(6))})); - //ON_CALL(*block_tree, bestLeaf()) - // .WillByDefault(Return(BlockInfo{6, hash_from_header(headers.at(6))})); + // ON_CALL(*block_tree, bestLeaf()) + // .WillByDefault(Return(BlockInfo{6, hash_from_header(headers.at(6))})); auto mock_block = [&](unsigned int number) { auto str_number = std::to_string(number); diff --git a/test/mock/core/network/peer_manager_mock.hpp b/test/mock/core/network/peer_manager_mock.hpp index 19a5ca5871..86e2273a2d 100644 --- a/test/mock/core/network/peer_manager_mock.hpp +++ b/test/mock/core/network/peer_manager_mock.hpp @@ -72,6 +72,11 @@ namespace kagome::network { forOnePeer, (const PeerId &, std::function), (const, override)); + + MOCK_METHOD(std::optional>, + createDefaultPeerState, + (const PeerId &), + (override)); }; } // namespace kagome::network diff --git a/test/mock/core/network/router_mock.hpp b/test/mock/core/network/router_mock.hpp index e10758f31e..41d505cccf 100644 --- a/test/mock/core/network/router_mock.hpp +++ b/test/mock/core/network/router_mock.hpp @@ -34,11 +34,21 @@ namespace kagome::network { (), (const, override)); + MOCK_METHOD(std::shared_ptr, + getValidationProtocolVStaging, + (), + (const, override)); + MOCK_METHOD(std::shared_ptr, getBlockAnnounceProtocol, (), (const, override)); + MOCK_METHOD(std::shared_ptr, + getCollationProtocolVStaging, + (), + (const, override)); + MOCK_METHOD(std::shared_ptr, getCollationProtocol, (), @@ -58,6 +68,11 @@ namespace kagome::network { (), (const, override)); + MOCK_METHOD(std::shared_ptr, + getFetchAttestedCandidateProtocol, + (), + (const, override)); + MOCK_METHOD(std::shared_ptr, getPropagateTransactionsProtocol, (), diff --git a/test/mock/core/parachain/backed_candidates_source.hpp b/test/mock/core/parachain/backed_candidates_source.hpp new file mode 100644 index 0000000000..ebe6978f50 --- /dev/null +++ b/test/mock/core/parachain/backed_candidates_source.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "parachain/validator/parachain_processor.hpp" + +namespace kagome::parachain { + + struct BackedCandidatesSourceMock : public BackedCandidatesSource { + MOCK_METHOD(std::vector, + getBackedCandidates, + (const RelayHash &), + (override)); + }; + +} // namespace kagome::parachain diff --git a/test/mock/core/parachain/backing_store_mock.hpp b/test/mock/core/parachain/backing_store_mock.hpp index e38d991330..dde9d84c2f 100644 --- a/test/mock/core/parachain/backing_store_mock.hpp +++ b/test/mock/core/parachain/backing_store_mock.hpp @@ -17,8 +17,10 @@ namespace kagome::parachain { MOCK_METHOD( std::optional, put, - ((const std::unordered_map> &), - Statement), + (const RelayHash &, + (const std::unordered_map> &), + Statement, + bool), (override)); MOCK_METHOD(std::vector, @@ -26,22 +28,19 @@ namespace kagome::parachain { (const primitives::BlockHash &), (const, override)); - MOCK_METHOD(void, remove, (const primitives::BlockHash &), (override)); - MOCK_METHOD(void, add, (const primitives::BlockHash &, BackedCandidate &&), (override)); - MOCK_METHOD(std::optional, - get_candidate, - (const network::CandidateHash &), - (const, override)); - MOCK_METHOD(std::optional>, - get_validity_votes, - (const network::CandidateHash &), + getCadidateInfo, + (const primitives::BlockHash &, const network::CandidateHash &), (const, override)); + + MOCK_METHOD(void, onActivateLeaf, (const RelayHash &), (override)); + + MOCK_METHOD(void, onDeactivateLeaf, (const RelayHash &), (override)); }; } // namespace kagome::parachain diff --git a/test/mock/core/runtime/parachain_host_mock.hpp b/test/mock/core/runtime/parachain_host_mock.hpp index 03ad29d0fb..2b10cba388 100644 --- a/test/mock/core/runtime/parachain_host_mock.hpp +++ b/test/mock/core/runtime/parachain_host_mock.hpp @@ -132,6 +132,22 @@ namespace kagome::runtime { const parachain::PvfCheckStatement &, const parachain::Signature &), (override)); + + MOCK_METHOD( + outcome::result>, + staging_para_backing_state, + (const primitives::BlockHash &, ParachainId), + (override)); + + MOCK_METHOD(outcome::result, + staging_async_backing_params, + (const primitives::BlockHash &), + (override)); + + MOCK_METHOD(outcome::result, + minimum_backing_votes, + (const primitives::BlockHash &, SessionIndex), + (override)); }; } // namespace kagome::runtime diff --git a/test/testutil/scale_test_comparator.hpp b/test/testutil/scale_test_comparator.hpp new file mode 100644 index 0000000000..ad2f4cf578 --- /dev/null +++ b/test/testutil/scale_test_comparator.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "scale/kagome_scale.hpp" +#include "scale/scale.hpp" + +namespace testutil { + + template + inline outcome::result> scaleEncodeAndCompareWithRef( + T &&...t) { + return ::scale::encode(std::forward(t)...); + } + +} // namespace testutil