diff --git a/core/consensus/babe/impl/babe.cpp b/core/consensus/babe/impl/babe.cpp index 01b180569d..25e59e4617 100644 --- a/core/consensus/babe/impl/babe.cpp +++ b/core/consensus/babe/impl/babe.cpp @@ -9,6 +9,7 @@ #include #include +#include #include "application/app_configuration.hpp" #include "application/app_state_manager.hpp" @@ -30,11 +31,14 @@ #include "dispute_coordinator/dispute_coordinator.hpp" #include "metrics/histogram_timer.hpp" #include "network/block_announce_transmitter.hpp" +#include "offchain/offchain_worker_factory.hpp" +#include "offchain/offchain_worker_pool.hpp" #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/babe_api.hpp" #include "runtime/runtime_api/offchain_worker_api.hpp" #include "storage/changes_trie/impl/storage_changes_tracker_impl.hpp" #include "storage/trie/serialization/ordered_trie_hash.hpp" @@ -83,7 +87,10 @@ namespace kagome::consensus::babe { primitives::events::StorageSubscriptionEnginePtr storage_sub_engine, primitives::events::ChainSubscriptionEnginePtr chain_sub_engine, std::shared_ptr announce_transmitter, + std::shared_ptr babe_api, std::shared_ptr offchain_worker_api, + std::shared_ptr offchain_worker_factory, + std::shared_ptr offchain_worker_pool, std::shared_ptr main_pool_handler, std::shared_ptr worker_pool_handler) : log_(log::createLogger("Babe", "babe")), @@ -104,7 +111,10 @@ namespace kagome::consensus::babe { storage_sub_engine_(std::move(storage_sub_engine)), chain_sub_engine_(std::move(chain_sub_engine)), announce_transmitter_(std::move(announce_transmitter)), + babe_api_(std::move(babe_api)), offchain_worker_api_(std::move(offchain_worker_api)), + offchain_worker_factory_(std::move(offchain_worker_factory)), + offchain_worker_pool_(std::move(offchain_worker_pool)), main_pool_handler_(std::move(main_pool_handler)), worker_pool_handler_(std::move(worker_pool_handler)), is_validator_by_config_(app_config.roles().flags.authority != 0), @@ -123,7 +133,10 @@ namespace kagome::consensus::babe { BOOST_ASSERT(chain_sub_engine_); BOOST_ASSERT(chain_sub_engine_); BOOST_ASSERT(announce_transmitter_); + BOOST_ASSERT(babe_api_); BOOST_ASSERT(offchain_worker_api_); + BOOST_ASSERT(offchain_worker_factory_); + BOOST_ASSERT(offchain_worker_pool_); BOOST_ASSERT(main_pool_handler_); BOOST_ASSERT(worker_pool_handler_); @@ -171,6 +184,11 @@ namespace kagome::consensus::babe { return babe::getSlot(header); } + outcome::result Babe::getAuthority( + const primitives::BlockHeader &header) const { + return babe::getAuthority(header); + } + outcome::result Babe::processSlot( SlotNumber slot, const primitives::BlockInfo &best_block) { auto slot_timestamp = clock_.now(); @@ -228,6 +246,102 @@ namespace kagome::consensus::babe { return validating_->validateHeader(block_header); } + outcome::result Babe::reportEquivocation( + const primitives::BlockHash &first_hash, + const primitives::BlockHash &second_hash) const { + BOOST_ASSERT(first_hash != second_hash); + + auto first_header_res = block_tree_->getBlockHeader(first_hash); + if (first_header_res.has_error()) { + SL_WARN(log_, + "Can't obtain equivocating header of block {}: {}", + first_hash, + first_header_res.error()); + return first_header_res.as_failure(); + } + auto &first_header = first_header_res.value(); + + auto second_header_res = block_tree_->getBlockHeader(second_hash); + if (second_header_res.has_error()) { + SL_WARN(log_, + "Can't obtain equivocating header of block {}: {}", + second_hash, + second_header_res.error()); + return second_header_res.as_failure(); + } + auto &second_header = second_header_res.value(); + + auto slot_res = getSlot(first_header); + BOOST_ASSERT(slot_res.has_value()); + auto slot = slot_res.value(); + BOOST_ASSERT_MSG( + [&] { + slot_res = getSlot(second_header); + return slot_res.has_value() and slot_res.value() == slot; + }(), + "Equivocating blocks must be block of one slot"); + + auto authority_index_res = getAuthority(first_header); + BOOST_ASSERT(authority_index_res.has_value()); + auto authority_index = authority_index_res.value(); + BOOST_ASSERT_MSG( + [&] { + authority_index_res = getAuthority(second_header); + return authority_index_res.has_value() + and authority_index_res.value() == authority_index; + }(), + "Equivocating blocks must be block of one authority"); + + auto parent = second_header.parentInfo().value(); + auto epoch_res = slots_util_.get()->slotToEpoch(parent, slot); + if (epoch_res.has_error()) { + SL_WARN(log_, "Can't compute epoch by slot: {}", epoch_res.error()); + return epoch_res.as_failure(); + } + auto epoch = epoch_res.value(); + + auto config_res = config_repo_->config(parent, epoch); + if (config_res.has_error()) { + SL_WARN(log_, "Can't obtain config: {}", config_res.error()); + return config_res.as_failure(); + } + auto &config = config_res.value(); + + const auto &authorities = config->authorities; + const auto &authority = authorities[authority_index].id; + + EquivocationProof equivocation_proof{ + .offender = authority, + .slot = slot, + .first_header = std::move(first_header), + .second_header = std::move(second_header), + }; + + auto ownership_proof_res = babe_api_->generate_key_ownership_proof( + block_tree_->bestBlock().hash, slot, equivocation_proof.offender); + if (ownership_proof_res.has_error()) { + SL_WARN( + log_, "Can't get ownership proof: {}", ownership_proof_res.error()); + return ownership_proof_res.as_failure(); + } + auto &ownership_proof_opt = ownership_proof_res.value(); + if (not ownership_proof_opt.has_value()) { + SL_WARN(log_, + "Can't get ownership proof: runtime call returns none", + ownership_proof_res.error()); + return ownership_proof_res.as_failure(); // FIXME; + } + auto &ownership_proof = ownership_proof_opt.value(); + + offchain_worker_pool_->addWorker(offchain_worker_factory_->make()); + ::libp2p::common::FinalAction remove( + [&] { offchain_worker_pool_->removeWorker(); }); + return babe_api_->submit_report_equivocation_unsigned_extrinsic( + second_header.parent_hash, + std::move(equivocation_proof), + ownership_proof); + } + bool Babe::changeEpoch(EpochNumber epoch, const primitives::BlockInfo &block) const { return lottery_->changeEpoch(epoch, block); diff --git a/core/consensus/babe/impl/babe.hpp b/core/consensus/babe/impl/babe.hpp index b3d7f0d4ca..9c6f849150 100644 --- a/core/consensus/babe/impl/babe.hpp +++ b/core/consensus/babe/impl/babe.hpp @@ -55,19 +55,25 @@ namespace kagome::dispute { class DisputeCoordinator; } +namespace kagome::network { + class BlockAnnounceTransmitter; +} + +namespace kagome::offchain { + class OffchainWorkerFactory; + class OffchainWorkerPool; +} // namespace kagome::offchain + namespace kagome::parachain { class BitfieldStore; struct ParachainProcessorImpl; struct BackedCandidatesSource; } // namespace kagome::parachain -namespace kagome::network { - class BlockAnnounceTransmitter; -} - namespace kagome::runtime { + class BabeApi; class OffchainWorkerApi; -} +} // namespace kagome::runtime namespace kagome::storage::changes_trie { class StorageChangesTrackerImpl; @@ -106,7 +112,11 @@ namespace kagome::consensus::babe { primitives::events::StorageSubscriptionEnginePtr storage_sub_engine, primitives::events::ChainSubscriptionEnginePtr chain_sub_engine, std::shared_ptr announce_transmitter, + std::shared_ptr babe_api, std::shared_ptr offchain_worker_api, + std::shared_ptr + offchain_worker_factory, + std::shared_ptr offchain_worker_pool, std::shared_ptr main_pool_handler, std::shared_ptr worker_pool_handler); @@ -118,12 +128,19 @@ namespace kagome::consensus::babe { outcome::result getSlot( const primitives::BlockHeader &header) const override; + outcome::result getAuthority( + const primitives::BlockHeader &header) const override; + outcome::result processSlot( SlotNumber slot, const primitives::BlockInfo &best_block) override; outcome::result validateHeader( const primitives::BlockHeader &block_header) const override; + outcome::result reportEquivocation( + const primitives::BlockHash &first, + const primitives::BlockHash &second) const override; + private: bool changeEpoch(EpochNumber epoch, const primitives::BlockInfo &block) const override; @@ -166,7 +183,10 @@ namespace kagome::consensus::babe { primitives::events::StorageSubscriptionEnginePtr storage_sub_engine_; primitives::events::ChainSubscriptionEnginePtr chain_sub_engine_; std::shared_ptr announce_transmitter_; + std::shared_ptr babe_api_; std::shared_ptr offchain_worker_api_; + std::shared_ptr offchain_worker_factory_; + std::shared_ptr offchain_worker_pool_; std::shared_ptr main_pool_handler_; std::shared_ptr worker_pool_handler_; diff --git a/core/consensus/babe/impl/babe_digests_util.cpp b/core/consensus/babe/impl/babe_digests_util.cpp index 9446ac61c7..0cca47294a 100644 --- a/core/consensus/babe/impl/babe_digests_util.cpp +++ b/core/consensus/babe/impl/babe_digests_util.cpp @@ -42,6 +42,12 @@ namespace kagome::consensus::babe { return babe_block_header.slot_number; } + outcome::result getAuthority( + const primitives::BlockHeader &header) { + OUTCOME_TRY(babe_block_header, getBabeBlockHeader(header)); + return babe_block_header.authority_index; + } + outcome::result getBabeBlockHeader( const primitives::BlockHeader &block_header) { [[unlikely]] if (block_header.number == 0) { diff --git a/core/consensus/babe/impl/babe_digests_util.hpp b/core/consensus/babe/impl/babe_digests_util.hpp index 1b8084f498..075128d883 100644 --- a/core/consensus/babe/impl/babe_digests_util.hpp +++ b/core/consensus/babe/impl/babe_digests_util.hpp @@ -26,6 +26,9 @@ namespace kagome::consensus::babe { outcome::result getSlot(const primitives::BlockHeader &header); + outcome::result getAuthority( + const primitives::BlockHeader &header); + outcome::result getBabeBlockHeader( const primitives::BlockHeader &block_header); diff --git a/core/consensus/babe/types/equivocation_proof.hpp b/core/consensus/babe/types/equivocation_proof.hpp index c5779cbe15..e0608944cf 100644 --- a/core/consensus/babe/types/equivocation_proof.hpp +++ b/core/consensus/babe/types/equivocation_proof.hpp @@ -12,6 +12,15 @@ namespace kagome::consensus::babe { + /// An opaque type used to represent the key ownership proof at the runtime + /// API boundary. The inner value is an encoded representation of the actual + /// key ownership proof which will be parameterized when defining the runtime. + /// At the runtime API boundary this type is unknown and as such we keep this + /// opaque representation, implementors of the runtime API will have to make + /// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type. + using OpaqueKeyOwnershipProof = + Tagged; + /// Represents an equivocation proof. An equivocation happens when a validator /// produces more than one block on the same slot. The proof of equivocation /// are the given distinct headers that were signed by the validator and which diff --git a/core/consensus/grandpa/environment.hpp b/core/consensus/grandpa/environment.hpp index 5cb951e1b5..a366238000 100644 --- a/core/consensus/grandpa/environment.hpp +++ b/core/consensus/grandpa/environment.hpp @@ -8,6 +8,7 @@ #include "consensus/grandpa/chain.hpp" #include "consensus/grandpa/justification_observer.hpp" +#include "consensus/grandpa/types/equivocation_proof.hpp" namespace kagome::primitives { struct Justification; @@ -18,7 +19,7 @@ namespace libp2p::peer { } namespace kagome::consensus::grandpa { - class Grandpa; + class VotingRound; struct MovableRoundState; } // namespace kagome::consensus::grandpa @@ -116,6 +117,15 @@ namespace kagome::consensus::grandpa { */ virtual outcome::result getJustification( const BlockHash &block_hash) = 0; + + /// Report the given equivocation to the GRANDPA runtime module. This method + /// generates a session membership proof of the offender and then submits an + /// extrinsic to report the equivocation. In particular, the session + /// membership proof must be generated at the block at which the given set + /// was active which isn't necessarily the best block if there are pending + /// authority set changes. + virtual outcome::result reportEquivocation( + const VotingRound &round, const Equivocation &equivocation) const = 0; }; } // namespace kagome::consensus::grandpa diff --git a/core/consensus/grandpa/impl/environment_impl.cpp b/core/consensus/grandpa/impl/environment_impl.cpp index 1c6f7587b2..6379e8a289 100644 --- a/core/consensus/grandpa/impl/environment_impl.cpp +++ b/core/consensus/grandpa/impl/environment_impl.cpp @@ -6,10 +6,12 @@ #include "consensus/grandpa/impl/environment_impl.hpp" -#include #include #include +#include +#include + #include "application/app_state_manager.hpp" #include "blockchain/block_header_repository.hpp" #include "blockchain/block_tree.hpp" @@ -19,13 +21,17 @@ #include "consensus/grandpa/i_verified_justification_queue.hpp" #include "consensus/grandpa/justification_observer.hpp" #include "consensus/grandpa/movable_round_state.hpp" +#include "consensus/grandpa/voting_round.hpp" #include "consensus/grandpa/voting_round_error.hpp" #include "crypto/hasher.hpp" #include "dispute_coordinator/dispute_coordinator.hpp" #include "dispute_coordinator/types.hpp" #include "network/grandpa_transmitter.hpp" +#include "offchain/offchain_worker_factory.hpp" +#include "offchain/offchain_worker_pool.hpp" #include "parachain/backing/store.hpp" #include "primitives/common.hpp" +#include "runtime/runtime_api/grandpa_api.hpp" #include "runtime/runtime_api/parachain_host.hpp" #include "scale/scale.hpp" #include "utils/pool_handler.hpp" @@ -44,10 +50,13 @@ namespace kagome::consensus::grandpa { std::shared_ptr approved_ancestor, LazySPtr justification_observer, std::shared_ptr verified_justification_queue, + std::shared_ptr grandpa_api, std::shared_ptr dispute_coordinator, std::shared_ptr parachain_api, std::shared_ptr backing_store, std::shared_ptr hasher, + std::shared_ptr offchain_worker_factory, + std::shared_ptr offchain_worker_pool, std::shared_ptr main_pool_handler) : block_tree_{std::move(block_tree)}, header_repository_{std::move(header_repository)}, @@ -56,20 +65,26 @@ namespace kagome::consensus::grandpa { approved_ancestor_(std::move(approved_ancestor)), justification_observer_(std::move(justification_observer)), verified_justification_queue_(std::move(verified_justification_queue)), + grandpa_api_(std::move(grandpa_api)), dispute_coordinator_(std::move(dispute_coordinator)), parachain_api_(std::move(parachain_api)), backing_store_(std::move(backing_store)), hasher_(std::move(hasher)), + offchain_worker_factory_(std::move(offchain_worker_factory)), + offchain_worker_pool_(std::move(offchain_worker_pool)), main_pool_handler_(std::move(main_pool_handler)), logger_{log::createLogger("GrandpaEnvironment", "grandpa")} { BOOST_ASSERT(block_tree_ != nullptr); BOOST_ASSERT(header_repository_ != nullptr); BOOST_ASSERT(authority_manager_ != nullptr); BOOST_ASSERT(transmitter_ != nullptr); + BOOST_ASSERT(grandpa_api_ != nullptr); BOOST_ASSERT(dispute_coordinator_ != nullptr); BOOST_ASSERT(parachain_api_ != nullptr); BOOST_ASSERT(backing_store_ != nullptr); BOOST_ASSERT(hasher_ != nullptr); + BOOST_ASSERT(offchain_worker_factory_ != nullptr); + BOOST_ASSERT(offchain_worker_pool_ != nullptr); BOOST_ASSERT(main_pool_handler_ != nullptr); auto kApprovalLag = "kagome_parachain_approval_checking_finality_lag"; @@ -438,4 +453,55 @@ namespace kagome::consensus::grandpa { return outcome::success(std::move(grandpa_justification)); } + outcome::result EnvironmentImpl::reportEquivocation( + const VotingRound &round, const Equivocation &equivocation) const { + auto last_finalized = round.lastFinalizedBlock(); + auto authority_set_id = round.voterSetId(); + + // generate key ownership proof at that block + auto key_owner_proof_res = grandpa_api_->generate_key_ownership_proof( + last_finalized.hash, authority_set_id, equivocation.offender()); + if (key_owner_proof_res.has_error()) { + SL_WARN( + logger_, + "Round #{}: can't generate key ownership proof for equivocation report: {}", + equivocation.round(), + key_owner_proof_res.error()); + return key_owner_proof_res.as_failure(); + } + const auto &key_owner_proof_opt = key_owner_proof_res.value(); + + if (not key_owner_proof_opt.has_value()) { + SL_DEBUG( + logger_, + "Round #{}: can't generate key ownership proof for equivocation report: " + "Equivocation offender is not part of the authority set.", + equivocation.round()); + return outcome::success(); // ensure if an error type is right + } + const auto &key_owner_proof = key_owner_proof_opt.value(); + + // submit an equivocation report at **best** block + EquivocationProof equivocation_proof{ + .set_id = authority_set_id, + .equivocation = std::move(equivocation), + }; + + offchain_worker_pool_->addWorker(offchain_worker_factory_->make()); + ::libp2p::common::FinalAction remove( + [&] { offchain_worker_pool_->removeWorker(); }); + auto submit_res = + grandpa_api_->submit_report_equivocation_unsigned_extrinsic( + last_finalized.hash, equivocation_proof, key_owner_proof); + if (submit_res.has_error()) { + SL_WARN(logger_, + "Round #{}: can't submit equivocation report: {}", + equivocation.round(), + key_owner_proof_res.error()); + return submit_res.as_failure(); + } + + return outcome::success(); + } + } // namespace kagome::consensus::grandpa diff --git a/core/consensus/grandpa/impl/environment_impl.hpp b/core/consensus/grandpa/impl/environment_impl.hpp index 5bd75cbc29..4a4bc161b2 100644 --- a/core/consensus/grandpa/impl/environment_impl.hpp +++ b/core/consensus/grandpa/impl/environment_impl.hpp @@ -24,7 +24,7 @@ namespace kagome::common { namespace kagome::consensus::grandpa { class AuthorityManager; -} +} // namespace kagome::consensus::grandpa namespace kagome::dispute { class DisputeCoordinator; @@ -34,9 +34,15 @@ namespace kagome::network { class GrandpaTransmitter; } +namespace kagome::offchain { + class OffchainWorkerFactory; + class OffchainWorkerPool; +} // namespace kagome::offchain + namespace kagome::runtime { class ParachainHost; -} + class GrandpaApi; +} // namespace kagome::runtime namespace kagome::parachain { class BackingStore; @@ -57,10 +63,14 @@ namespace kagome::consensus::grandpa { LazySPtr justification_observer, std::shared_ptr verified_justification_queue, + std::shared_ptr grandpa_api, std::shared_ptr dispute_coordinator, std::shared_ptr parachain_api, std::shared_ptr backing_store, std::shared_ptr hasher, + std::shared_ptr + offchain_worker_factory, + std::shared_ptr offchain_worker_pool, std::shared_ptr main_pool_handler); ~EnvironmentImpl() override = default; @@ -122,6 +132,10 @@ namespace kagome::consensus::grandpa { outcome::result getJustification( const BlockHash &block_hash) override; + outcome::result reportEquivocation( + const VotingRound &round, + const Equivocation &equivocation) const override; + private: std::shared_ptr block_tree_; std::shared_ptr header_repository_; @@ -130,10 +144,13 @@ namespace kagome::consensus::grandpa { std::shared_ptr approved_ancestor_; LazySPtr justification_observer_; std::shared_ptr verified_justification_queue_; + std::shared_ptr grandpa_api_; std::shared_ptr dispute_coordinator_; std::shared_ptr parachain_api_; std::shared_ptr backing_store_; std::shared_ptr hasher_; + std::shared_ptr offchain_worker_factory_; + std::shared_ptr offchain_worker_pool_; std::shared_ptr main_pool_handler_; metrics::RegistryPtr metrics_registry_ = metrics::createRegistry(); diff --git a/core/consensus/grandpa/impl/vote_tracker_impl.cpp b/core/consensus/grandpa/impl/vote_tracker_impl.cpp index 41a6eb86c0..6d9778827f 100644 --- a/core/consensus/grandpa/impl/vote_tracker_impl.cpp +++ b/core/consensus/grandpa/impl/vote_tracker_impl.cpp @@ -25,7 +25,7 @@ namespace kagome::consensus::grandpa { return visit_in_place( known_vote_variant, [&](const SignedMessage &known_vote) { - // If it is known vote, it means duplicate + // If it is known a vote, it means duplicate if (vote == known_vote) { return PushResult::DUPLICATED; } @@ -35,7 +35,7 @@ namespace kagome::consensus::grandpa { return PushResult::EQUIVOCATED; }, [](const EquivocatorySignedMessage &) { - // This is vote of known equivocator + // This is a vote of known equivocator return PushResult::EQUIVOCATED; }); } @@ -67,6 +67,13 @@ namespace kagome::consensus::grandpa { return votes; } + std::optional VoteTrackerImpl::getMessage(Id id) const { + if (auto it = messages_.find(id); it != messages_.end()) { + return it->second; + } + return std::nullopt; + } + size_t VoteTrackerImpl::getTotalWeight() const { return total_weight_; } diff --git a/core/consensus/grandpa/impl/vote_tracker_impl.hpp b/core/consensus/grandpa/impl/vote_tracker_impl.hpp index aba5565912..67bea05b14 100644 --- a/core/consensus/grandpa/impl/vote_tracker_impl.hpp +++ b/core/consensus/grandpa/impl/vote_tracker_impl.hpp @@ -18,6 +18,8 @@ namespace kagome::consensus::grandpa { std::vector getMessages() const override; + std::optional getMessage(Id id) const override; + size_t getTotalWeight() const override; private: diff --git a/core/consensus/grandpa/impl/voting_round_impl.cpp b/core/consensus/grandpa/impl/voting_round_impl.cpp index f3c290a764..a349eee459 100644 --- a/core/consensus/grandpa/impl/voting_round_impl.cpp +++ b/core/consensus/grandpa/impl/voting_round_impl.cpp @@ -1118,7 +1118,7 @@ namespace kagome::consensus::grandpa { OptRef grandpa_context, const SignedMessage &vote) { BOOST_ASSERT(vote.is()); - // Check if voter is contained in current voter set + // Check if a voter is contained in a current voter set auto index_and_weight_opt = voter_set_->indexAndWeight(vote.id); if (!index_and_weight_opt) { SL_DEBUG( @@ -1189,6 +1189,18 @@ namespace kagome::consensus::grandpa { case VoteTracker::PushResult::EQUIVOCATED: { equivocators[index] = true; graph_->remove(type, vote.id); + + auto maybe_votes_opt = tracker.getMessage(vote.id); + BOOST_ASSERT(maybe_votes_opt.has_value()); + const auto &votes_opt = + if_type(maybe_votes_opt.value()); + BOOST_ASSERT(votes_opt.has_value()); + const auto &votes = votes_opt.value().get(); + + Equivocation equivocation{round_number_, votes.first, votes.second}; + + std::ignore = env_->reportEquivocation(*this, equivocation); + return VotingRoundError::EQUIVOCATED_VOTE; } default: diff --git a/core/consensus/grandpa/structs.hpp b/core/consensus/grandpa/structs.hpp index 241b22e99e..4cb4ea58ec 100644 --- a/core/consensus/grandpa/structs.hpp +++ b/core/consensus/grandpa/structs.hpp @@ -71,67 +71,17 @@ namespace kagome::consensus::grandpa { return s >> signed_msg.message >> signed_msg.signature >> signed_msg.id; } - template - struct Equivocated { - Message first; - Message second; - }; - using EquivocatorySignedMessage = std::pair; using VoteVariant = boost::variant; - namespace detail { - /// Proof of an equivocation (double-vote) in a given round. - template - struct Equivocation { // NOLINT - /// The round number equivocated in. - RoundNumber round; - /// The identity of the equivocator. - Id id; - Equivocated proof; - }; - } // namespace detail - class SignedPrevote : public SignedMessage { using SignedMessage::SignedMessage; }; - template > - Stream &operator<<(Stream &s, const SignedPrevote &signed_msg) { - assert(signed_msg.template is()); - return s << boost::strict_get(signed_msg.message) - << signed_msg.signature << signed_msg.id; - } - - template > - Stream &operator>>(Stream &s, SignedPrevote &signed_msg) { - signed_msg.message = Prevote{}; - return s >> boost::strict_get(signed_msg.message) - >> signed_msg.signature >> signed_msg.id; - } - class SignedPrecommit : public SignedMessage { using SignedMessage::SignedMessage; }; - template > - Stream &operator<<(Stream &s, const SignedPrecommit &signed_msg) { - assert(signed_msg.template is()); - return s << boost::strict_get(signed_msg.message) - << signed_msg.signature << signed_msg.id; - } - - template > - Stream &operator>>(Stream &s, SignedPrecommit &signed_msg) { - signed_msg.message = Precommit{}; - return s >> boost::strict_get(signed_msg.message) - >> signed_msg.signature >> signed_msg.id; - } - // justification that contains a list of signed precommits justifying the // validity of the block struct GrandpaJustification { @@ -156,9 +106,6 @@ namespace kagome::consensus::grandpa { } }; - using PrevoteEquivocation = detail::Equivocation; - using PrecommitEquivocation = detail::Equivocation; - struct TotalWeight { uint64_t prevote = 0; uint64_t precommit = 0; diff --git a/core/consensus/grandpa/types/equivocation_proof.hpp b/core/consensus/grandpa/types/equivocation_proof.hpp new file mode 100644 index 0000000000..35ccd8a667 --- /dev/null +++ b/core/consensus/grandpa/types/equivocation_proof.hpp @@ -0,0 +1,80 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "consensus/grandpa/structs.hpp" +#include "consensus/grandpa/types/authority.hpp" +#include "consensus/grandpa/vote_types.hpp" +#include "primitives/block_header.hpp" +#include "scale/tie.hpp" + +namespace kagome::consensus::grandpa { + + /// An opaque type used to represent the key ownership proof at the runtime + /// API boundary. The inner value is an encoded representation of the actual + /// key ownership proof which will be parameterized when defining the runtime. + /// At the runtime API boundary this type is unknown and as such we keep this + /// opaque representation, implementors of the runtime API will have to make + /// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type. + using OpaqueKeyOwnershipProof = + Tagged; + + /// Wrapper object for GRANDPA equivocation proofs, useful for unifying + /// prevote and precommit equivocations under a common type. + // https://github.com/paritytech/polkadot-sdk/blob/0e49ed72aa365475e30069a5c30e251a009fdacf/substrate/primitives/consensus/grandpa/src/lib.rs#L272 + struct Equivocation { + /// Round stage: prevote or precommit + const VoteType stage; + /// The round number equivocated in. + const RoundNumber round_number; + /// The first vote in the equivocation. + const SignedMessage first; + /// The second vote in the equivocation. + const SignedMessage second; + + Equivocation(RoundNumber round_number, + const SignedMessage &first, + const SignedMessage &second) + : stage{first.is() ? VoteType::Prevote : VoteType::Precommit}, + round_number(round_number), + first(first), + second(second) { + BOOST_ASSERT((first.is() and second.is()) + or (first.is() and second.is())); + BOOST_ASSERT(first.id == second.id); + }; + + AuthorityId offender() const { + return first.id; + } + + RoundNumber round() const { + return round_number; + } + + friend scale::ScaleEncoderStream &operator<<( + scale::ScaleEncoderStream &s, const Equivocation &equivocation) { + return s << equivocation.stage << equivocation.round_number + << equivocation.first.id << equivocation.first + << equivocation.second; + } + }; + + /// Proof of voter misbehavior on a given set id. Misbehavior/equivocation + /// in GRANDPA happens when a voter votes on the same round (either at + /// prevote or precommit stage) for different blocks. Proving is achieved + /// by collecting the signed messages of conflicting votes. + struct EquivocationProof { + SCALE_TIE(2); + + AuthoritySetId set_id; + Equivocation equivocation; + }; + +} // namespace kagome::consensus::grandpa diff --git a/core/consensus/grandpa/vote_tracker.hpp b/core/consensus/grandpa/vote_tracker.hpp index 6db59a30cc..72e9881025 100644 --- a/core/consensus/grandpa/vote_tracker.hpp +++ b/core/consensus/grandpa/vote_tracker.hpp @@ -45,6 +45,8 @@ namespace kagome::consensus::grandpa { */ virtual std::vector getMessages() const = 0; + virtual std::optional getMessage(Id id) const = 0; + /** * @returns total weight of all accepted (non-duplicate) messages */ diff --git a/core/consensus/production_consensus.hpp b/core/consensus/production_consensus.hpp index 6ec7ae549b..763ee74900 100644 --- a/core/consensus/production_consensus.hpp +++ b/core/consensus/production_consensus.hpp @@ -19,6 +19,8 @@ namespace kagome::consensus { SingleValidator, }; + using AuthorityIndex = uint32_t; + /// Consensus responsible for choice slot leaders, block production, etc. class ProductionConsensus { public: @@ -40,12 +42,24 @@ namespace kagome::consensus { virtual outcome::result getSlot( const primitives::BlockHeader &header) const = 0; + virtual outcome::result getAuthority( + const primitives::BlockHeader &header) const = 0; + virtual outcome::result processSlot( SlotNumber slot, const primitives::BlockInfo &parent) = 0; virtual outcome::result validateHeader( const primitives::BlockHeader &block_header) const = 0; + /// Submit the equivocation report based on two blocks of one validator + /// produced during a single slot + /// @arg first - hash of first equivocating block + /// @arg second - hash of second equivocating block + /// @return success or error + virtual outcome::result reportEquivocation( + const primitives::BlockHash &first, + const primitives::BlockHash &second) const = 0; + protected: /// Changes epoch /// @param epoch epoch that switch to diff --git a/core/consensus/timeline/impl/block_header_appender_impl.cpp b/core/consensus/timeline/impl/block_header_appender_impl.cpp index 921e45ea53..7e7418ab9b 100644 --- a/core/consensus/timeline/impl/block_header_appender_impl.cpp +++ b/core/consensus/timeline/impl/block_header_appender_impl.cpp @@ -11,16 +11,19 @@ #include "consensus/babe/babe_config_repository.hpp" #include "consensus/timeline/impl/block_addition_error.hpp" #include "consensus/timeline/impl/block_appender_base.hpp" +#include "consensus/timeline/timeline.hpp" namespace kagome::consensus { BlockHeaderAppenderImpl::BlockHeaderAppenderImpl( std::shared_ptr block_tree, std::shared_ptr hasher, - std::unique_ptr appender) + std::unique_ptr appender, + LazySPtr timeline) : block_tree_{std::move(block_tree)}, hasher_{std::move(hasher)}, appender_{std::move(appender)}, + timeline_{std::move(timeline)}, logger_{log::createLogger("BlockHeaderAppender", "block_appender")} { BOOST_ASSERT(block_tree_ != nullptr); BOOST_ASSERT(hasher_ != nullptr); @@ -60,6 +63,8 @@ namespace kagome::consensus { return; } + timeline_.get()->checkAndReportEquivocation(block.header); + appender_->applyJustifications( block_info, justification, diff --git a/core/consensus/timeline/impl/block_header_appender_impl.hpp b/core/consensus/timeline/impl/block_header_appender_impl.hpp index 092c317154..a5517c4c25 100644 --- a/core/consensus/timeline/impl/block_header_appender_impl.hpp +++ b/core/consensus/timeline/impl/block_header_appender_impl.hpp @@ -11,6 +11,7 @@ #include #include "consensus/babe/types/babe_configuration.hpp" +#include "injector/lazy.hpp" #include "log/logger.hpp" #include "primitives/block_header.hpp" @@ -22,6 +23,10 @@ namespace kagome::crypto { class Hasher; } +namespace kagome::consensus { + class Timeline; +} + namespace kagome::consensus { class BlockAppenderBase; @@ -32,7 +37,8 @@ namespace kagome::consensus { public: BlockHeaderAppenderImpl(std::shared_ptr block_tree, std::shared_ptr hasher, - std::unique_ptr appender); + std::unique_ptr appender, + LazySPtr timeline); void appendHeader( primitives::BlockHeader &&block_header, @@ -42,8 +48,8 @@ namespace kagome::consensus { private: std::shared_ptr block_tree_; std::shared_ptr hasher_; - std::unique_ptr appender_; + LazySPtr timeline_; struct { std::chrono::high_resolution_clock::time_point time; diff --git a/core/consensus/timeline/impl/timeline_impl.cpp b/core/consensus/timeline/impl/timeline_impl.cpp index ea06793838..df99c17f5f 100644 --- a/core/consensus/timeline/impl/timeline_impl.cpp +++ b/core/consensus/timeline/impl/timeline_impl.cpp @@ -766,4 +766,54 @@ namespace kagome::consensus { remains_ms); } + void TimelineImpl::checkAndReportEquivocation( + const primitives::BlockHeader &header) { + auto consensus = consensus_selector_.get()->getProductionConsensus(header); + BOOST_ASSERT_MSG(consensus, "Must be returned at least fallback consensus"); + + auto slot_res = consensus->getSlot(header); + if (slot_res.has_error()) { + return; + } + auto slot = slot_res.value(); + + if (slot + kMaxSlotObserveForEquivocation < current_slot_) { + return; + } + + auto authority_index_res = consensus->getAuthority(header); + if (authority_index_res.has_error()) { + return; + } + AuthorityIndex authority_index = authority_index_res.value(); + + auto &hash = header.hash(); + + auto [it, just_added] = data_for_equvocation_checks_.emplace( + std::tuple(slot, authority_index), std::tuple(hash, false)); + + if (just_added) { // Newly registered block + return; + } + + auto &is_reported = std::get<1>(it->second); + if (is_reported) { // Known equivocation + return; + } + + const auto &prev_block_hash = std::get<0>(it->second); + if (prev_block_hash == hash) { // Duplicate + return; + } + + auto report_res = consensus->reportEquivocation(prev_block_hash, hash); + + if (report_res.has_error()) { + SL_WARN(log_, "Can't report equivocation: {}", report_res.error()); + return; + } + + is_reported = true; + } + } // namespace kagome::consensus diff --git a/core/consensus/timeline/impl/timeline_impl.hpp b/core/consensus/timeline/impl/timeline_impl.hpp index 55273d2e01..524514eaa9 100644 --- a/core/consensus/timeline/impl/timeline_impl.hpp +++ b/core/consensus/timeline/impl/timeline_impl.hpp @@ -14,6 +14,7 @@ #include "log/logger.hpp" #include "metrics/metrics.hpp" #include "network/block_announce_observer.hpp" +#include "primitives/block_header.hpp" #include "primitives/event_types.hpp" #include "telemetry/service.hpp" @@ -63,6 +64,8 @@ namespace kagome::consensus { class TimelineImpl final : public Timeline, public network::BlockAnnounceObserver, public std::enable_shared_from_this { + static const size_t kMaxSlotObserveForEquivocation = 1000; + public: TimelineImpl( const application::AppConfiguration &app_config, @@ -107,6 +110,9 @@ namespace kagome::consensus { void onBlockAnnounce(const libp2p::peer::PeerId &peer_id, const network::BlockAnnounce &announce) override; + void checkAndReportEquivocation( + const primitives::BlockHeader &header) override; + private: bool updateSlot(TimePoint now); @@ -170,6 +176,12 @@ namespace kagome::consensus { metrics::RegistryPtr metrics_registry_ = metrics::createRegistry(); metrics::Gauge *metric_is_major_syncing_; + using AuthorityIndex = uint32_t; + + std::map, + std::tuple> + data_for_equvocation_checks_; + telemetry::Telemetry telemetry_; }; diff --git a/core/consensus/timeline/timeline.hpp b/core/consensus/timeline/timeline.hpp index d5ed0a9bdc..95c008ad47 100644 --- a/core/consensus/timeline/timeline.hpp +++ b/core/consensus/timeline/timeline.hpp @@ -8,6 +8,10 @@ #include "consensus/timeline/sync_state.hpp" +namespace kagome::primitives { + struct BlockHeader; +} + namespace kagome::consensus { class Timeline { @@ -32,6 +36,10 @@ namespace kagome::consensus { * current run, otherwise - false. */ virtual bool wasSynchronized() const = 0; + + /// Check block for possible equivocation and report if any + virtual void checkAndReportEquivocation( + const primitives::BlockHeader &header) = 0; }; } // namespace kagome::consensus diff --git a/core/runtime/runtime_api/babe_api.hpp b/core/runtime/runtime_api/babe_api.hpp index 63b8f2e704..a99e8ea392 100644 --- a/core/runtime/runtime_api/babe_api.hpp +++ b/core/runtime/runtime_api/babe_api.hpp @@ -8,7 +8,9 @@ #include +#include "common/tagged.hpp" #include "consensus/babe/types/babe_configuration.hpp" +#include "consensus/babe/types/equivocation_proof.hpp" #include "primitives/common.hpp" namespace kagome::runtime { @@ -32,6 +34,36 @@ namespace kagome::runtime { */ virtual outcome::result next_epoch( const primitives::BlockHash &block) = 0; + + /// Generates a proof of key ownership for the given authority in the + /// current epoch. An example usage of this module is coupled with the + /// session historical module to prove that a given authority key is + /// tied to a given staking identity during a specific session. Proofs + /// of key ownership are necessary for submitting equivocation reports. + /// NOTE: even though the API takes a `slot` as parameter the current + /// implementations ignores this parameter and instead relies on this + /// method being called at the correct block height, i.e. any point at + /// which the epoch for the given slot is live on-chain. Future + /// implementations will instead use indexed data through an offchain + /// worker, not requiring older states to be available. + virtual outcome::result< + std::optional> + generate_key_ownership_proof(const primitives::BlockHash &block_hash, + consensus::SlotNumber slot, + consensus::babe::AuthorityId authority_id) = 0; + + /// Submits an unsigned extrinsic to report an equivocation. The caller + /// must provide the equivocation proof and a key ownership proof + /// (should be obtained using `generate_key_ownership_proof`). The + /// extrinsic will be unsigned and should only be accepted for local + /// authorship (not to be broadcast to the network). This method returns + /// `None` when creation of the extrinsic fails, e.g. if equivocation + /// reporting is disabled for the given runtime (i.e. this method is + /// hardcoded to return `None`). Only useful in an offchain context. + virtual outcome::result submit_report_equivocation_unsigned_extrinsic( + const primitives::BlockHash &block_hash, + consensus::babe::EquivocationProof equivocation_proof, + consensus::babe::OpaqueKeyOwnershipProof key_owner_proof) = 0; }; } // namespace kagome::runtime diff --git a/core/runtime/runtime_api/grandpa_api.hpp b/core/runtime/runtime_api/grandpa_api.hpp index 93e0720f4a..f8808e7868 100644 --- a/core/runtime/runtime_api/grandpa_api.hpp +++ b/core/runtime/runtime_api/grandpa_api.hpp @@ -7,6 +7,7 @@ #pragma once #include "consensus/grandpa/types/authority.hpp" +#include "consensus/grandpa/types/equivocation_proof.hpp" #include "primitives/common.hpp" namespace kagome::runtime { @@ -35,6 +36,18 @@ namespace kagome::runtime { */ virtual outcome::result current_set_id( const primitives::BlockHash &block) = 0; + + virtual outcome::result< + std::optional> + generate_key_ownership_proof( + const primitives::BlockHash &block_hash, + consensus::SlotNumber slot, + consensus::grandpa::AuthorityId authority_id) = 0; + + virtual outcome::result submit_report_equivocation_unsigned_extrinsic( + const primitives::BlockHash &block_hash, + consensus::grandpa::EquivocationProof equivocation_proof, + consensus::grandpa::OpaqueKeyOwnershipProof key_owner_proof) = 0; }; } // namespace kagome::runtime diff --git a/core/runtime/runtime_api/impl/babe_api.cpp b/core/runtime/runtime_api/impl/babe_api.cpp index ad47ad0c77..6bd7e2828e 100644 --- a/core/runtime/runtime_api/impl/babe_api.cpp +++ b/core/runtime/runtime_api/impl/babe_api.cpp @@ -15,8 +15,8 @@ namespace kagome::runtime { BOOST_ASSERT(executor_); } - outcome::result BabeApiImpl::configuration( - const primitives::BlockHash &block) { + outcome::result + BabeApiImpl::configuration(const primitives::BlockHash &block) { OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block)); return executor_->call( ctx, "BabeApi_configuration"); @@ -27,4 +27,29 @@ namespace kagome::runtime { OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block)); return executor_->call(ctx, "BabeApi_next_epoch"); } + + outcome::result> + BabeApiImpl::generate_key_ownership_proof( + const primitives::BlockHash &block_hash, + consensus::SlotNumber slot, + consensus::babe::AuthorityId authority_id) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block_hash)); + return executor_ + ->call>( + ctx, "BabeApi_generate_key_ownership_proof", slot, authority_id); + } + + outcome::result + BabeApiImpl::submit_report_equivocation_unsigned_extrinsic( + const primitives::BlockHash &block_hash, + consensus::babe::EquivocationProof equivocation_proof, + consensus::babe::OpaqueKeyOwnershipProof key_owner_proof) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block_hash)); + return executor_->call( + ctx, + "BabeApi_submit_report_equivocation_unsigned_extrinsic", + equivocation_proof, + key_owner_proof); + } + } // namespace kagome::runtime diff --git a/core/runtime/runtime_api/impl/babe_api.hpp b/core/runtime/runtime_api/impl/babe_api.hpp index 0a0a754de7..36d39e7805 100644 --- a/core/runtime/runtime_api/impl/babe_api.hpp +++ b/core/runtime/runtime_api/impl/babe_api.hpp @@ -22,6 +22,17 @@ namespace kagome::runtime { outcome::result next_epoch( const primitives::BlockHash &block) override; + outcome::result> + generate_key_ownership_proof( + const primitives::BlockHash &block_hash, + consensus::SlotNumber slot, + consensus::babe::AuthorityId authority_id) override; + + outcome::result submit_report_equivocation_unsigned_extrinsic( + const primitives::BlockHash &block_hash, + consensus::babe::EquivocationProof equivocation_proof, + consensus::babe::OpaqueKeyOwnershipProof key_owner_proof) override; + private: std::shared_ptr executor_; }; diff --git a/core/runtime/runtime_api/impl/grandpa_api.cpp b/core/runtime/runtime_api/impl/grandpa_api.cpp index 35e2d7fbed..dfce5ebf0c 100644 --- a/core/runtime/runtime_api/impl/grandpa_api.cpp +++ b/core/runtime/runtime_api/impl/grandpa_api.cpp @@ -18,15 +18,37 @@ namespace kagome::runtime { outcome::result GrandpaApiImpl::authorities( const primitives::BlockHash &block_hash) { OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block_hash)); - return executor_->call(ctx, - "GrandpaApi_grandpa_authorities"); + return executor_->call(ctx, "GrandpaApi_grandpa_authorities"); } outcome::result GrandpaApiImpl::current_set_id( const primitives::BlockHash &block_hash) { OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block_hash)); - return executor_->call( - ctx, "GrandpaApi_current_set_id"); + return executor_->call(ctx, "GrandpaApi_current_set_id"); + } + + outcome::result> + GrandpaApiImpl::generate_key_ownership_proof( + const primitives::BlockHash &block_hash, + consensus::SlotNumber slot, + consensus::grandpa::AuthorityId authority_id) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block_hash)); + return executor_ + ->call>( + ctx, "GrandpaApi_generate_key_ownership_proof", slot, authority_id); + } + + outcome::result + GrandpaApiImpl::submit_report_equivocation_unsigned_extrinsic( + const primitives::BlockHash &block_hash, + consensus::grandpa::EquivocationProof equivocation_proof, + consensus::grandpa::OpaqueKeyOwnershipProof key_owner_proof) { + OUTCOME_TRY(ctx, executor_->ctx().ephemeralAt(block_hash)); + return executor_->call( + ctx, + "GrandpaApi_submit_report_equivocation_unsigned_extrinsic", + equivocation_proof, + key_owner_proof); } } // namespace kagome::runtime diff --git a/core/runtime/runtime_api/impl/grandpa_api.hpp b/core/runtime/runtime_api/impl/grandpa_api.hpp index ccd1c97cb3..f59879ba53 100644 --- a/core/runtime/runtime_api/impl/grandpa_api.hpp +++ b/core/runtime/runtime_api/impl/grandpa_api.hpp @@ -22,6 +22,17 @@ namespace kagome::runtime { outcome::result current_set_id( const primitives::BlockHash &block) override; + outcome::result> + generate_key_ownership_proof( + const primitives::BlockHash &block_hash, + consensus::SlotNumber slot, + consensus::grandpa::AuthorityId authority_id) override; + + outcome::result submit_report_equivocation_unsigned_extrinsic( + const primitives::BlockHash &block_hash, + consensus::grandpa::EquivocationProof equivocation_proof, + consensus::grandpa::OpaqueKeyOwnershipProof key_owner_proof) override; + private: std::shared_ptr executor_; }; diff --git a/test/core/consensus/babe/babe_test.cpp b/test/core/consensus/babe/babe_test.cpp index 128bcae884..b7726d0365 100644 --- a/test/core/consensus/babe/babe_test.cpp +++ b/test/core/consensus/babe/babe_test.cpp @@ -33,9 +33,12 @@ #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/offchain/offchain_worker_factory_mock.hpp" +#include "mock/core/offchain/offchain_worker_pool_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/babe_api_mock.hpp" #include "mock/core/runtime/offchain_worker_api_mock.hpp" #include "primitives/event_types.hpp" #include "storage/trie/serialization/ordered_trie_hash.hpp" @@ -83,6 +86,8 @@ using kagome::consensus::babe::BabeConfigRepositoryMock; using kagome::consensus::babe::BabeConfiguration; using kagome::consensus::babe::BabeLotteryMock; using kagome::consensus::babe::DigestError; +using kagome::consensus::babe::EquivocationProof; +using kagome::consensus::babe::OpaqueKeyOwnershipProof; using kagome::consensus::babe::Randomness; using kagome::consensus::babe::SlotLeadership; using kagome::consensus::babe::SlotType; @@ -100,6 +105,8 @@ using kagome::crypto::VRFVerifyOutput; using kagome::dispute::DisputeCoordinatorMock; using kagome::dispute::MultiDisputeStatementSet; using kagome::network::BlockAnnounceTransmitterMock; +using kagome::offchain::OffchainWorkerFactoryMock; +using kagome::offchain::OffchainWorkerPoolMock; using kagome::parachain::BackingStoreMock; using kagome::parachain::BitfieldStoreMock; using kagome::primitives::Block; @@ -113,6 +120,7 @@ using kagome::primitives::Extrinsic; using kagome::primitives::PreRuntime; using kagome::primitives::events::ChainSubscriptionEngine; using kagome::primitives::events::StorageSubscriptionEngine; +using kagome::runtime::BabeApiMock; using kagome::runtime::OffchainWorkerApiMock; using kagome::storage::trie::calculateOrderedTrieHash; using kagome::storage::trie::StateVersion; @@ -127,18 +135,17 @@ using namespace std::chrono_literals; // TODO (kamilsa): workaround unless we bump gtest version to 1.8.1+ namespace kagome::primitives { - std::ostream &operator<<(std::ostream &s, - const detail::DigestItemCommon &dic) { + std::ostream &operator<<(std::ostream &s, const detail::DigestItemCommon &) { return s; } } // namespace kagome::primitives -static Digest make_digest(SlotNumber slot) { +static Digest make_digest(SlotNumber slot, AuthorityIndex authority_index = 0) { Digest digest; BabeBlockHeader babe_header{ .slot_assignment_type = SlotType::SecondaryPlain, - .authority_index = 0, + .authority_index = authority_index, .slot_number = slot, }; Buffer encoded_header{scale::encode(babe_header).value()}; @@ -236,6 +243,8 @@ class BabeTest : public testing::Test { backed_candidates_source_ = std::make_shared(); + babe_api = std::make_shared(); + offchain_worker_api = std::make_shared(); ON_CALL(*offchain_worker_api, offchain_worker(_, _)) .WillByDefault(Return(outcome::success())); @@ -256,6 +265,9 @@ class BabeTest : public testing::Test { app_state_manager, worker_thread_pool); worker_pool_handler->start(); + offchain_worker_factory = std::make_shared(); + offchain_worker_pool = std::make_shared(); + babe = std::make_shared( app_config, clock, @@ -275,7 +287,10 @@ class BabeTest : public testing::Test { storage_sub_engine, chain_sub_engine, announce_transmitter, + babe_api, offchain_worker_api, + offchain_worker_factory, + offchain_worker_pool, main_pool_handler, worker_pool_handler); } @@ -303,8 +318,11 @@ class BabeTest : public testing::Test { std::shared_ptr backed_candidates_source_; std::shared_ptr announce_transmitter; + std::shared_ptr babe_api; std::shared_ptr offchain_worker_api; std::shared_ptr app_state_manager; + std::shared_ptr offchain_worker_factory; + std::shared_ptr offchain_worker_pool; std::shared_ptr watchdog; std::shared_ptr main_thread_pool; std::shared_ptr main_pool_handler; @@ -442,7 +460,7 @@ TEST_F(BabeTest, SlotLeader) { EXPECT_CALL(*lottery, changeEpoch(epoch, best_block_info)) .WillOnce(Return(true)); EXPECT_CALL(*lottery, getSlotLeadership(best_block_info.hash, slot)) - .WillOnce(Return(SlotLeadership{.keypair = our_keypair})); + .WillOnce(Return(SlotLeadership{.keypair = our_keypair})); // NOLINT EXPECT_CALL(*block_tree, getBlockHeader(best_block_info.hash)) .WillOnce(Return(best_block_header)); @@ -465,3 +483,53 @@ TEST_F(BabeTest, SlotLeader) { latch.wait(); } + +TEST_F(BabeTest, EquivocationReport) { + SlotNumber slot = 1; + AuthorityIndex authority_index = 1; + const AuthorityId &authority_id = + babe_config->authorities[authority_index].id; + + BlockHeader first{ + 1, // number + "parent"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(slot, authority_index), // digest + "block_#1_first"_hash256 // hash + }; + BlockHeader second{ + 1, // number + "parent"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(slot, authority_index), // digest + "block_#1_second"_hash256 // hash + }; + + OpaqueKeyOwnershipProof ownership_proof{"ownership_proof"_bytes}; + + EquivocationProof equivocation_proof{ + .offender = authority_id, + .slot = slot, + .first_header = first, + .second_header = second, + }; + + ON_CALL(*block_tree, getBlockHeader(first.hash())) + .WillByDefault(Return(first)); + ON_CALL(*block_tree, getBlockHeader(second.hash())) + .WillByDefault(Return(second)); + ON_CALL(*slots_util, slotToEpoch(_, _)) + .WillByDefault(Return(outcome::success(0))); + EXPECT_CALL(*babe_api, generate_key_ownership_proof(_, _, _)) + .WillOnce(Return(outcome::success(ownership_proof))); + + EXPECT_CALL(*babe_api, + submit_report_equivocation_unsigned_extrinsic( + "parent"_hash256, equivocation_proof, ownership_proof)) + .WillOnce(Return(outcome::success())); + + ASSERT_OUTCOME_SUCCESS_TRY( + babe->reportEquivocation(first.hash(), second.hash())); +} diff --git a/test/core/consensus/grandpa/chain_test.cpp b/test/core/consensus/grandpa/chain_test.cpp index 438aa6424a..673bae2fda 100644 --- a/test/core/consensus/grandpa/chain_test.cpp +++ b/test/core/consensus/grandpa/chain_test.cpp @@ -19,8 +19,11 @@ #include "mock/core/crypto/hasher_mock.hpp" #include "mock/core/dispute_coordinator/dispute_coordinator_mock.hpp" #include "mock/core/network/grandpa_transmitter_mock.hpp" +#include "mock/core/offchain/offchain_worker_factory_mock.hpp" +#include "mock/core/offchain/offchain_worker_pool_mock.hpp" #include "mock/core/parachain/approved_ancestor.hpp" #include "mock/core/parachain/backing_store_mock.hpp" +#include "mock/core/runtime/grandpa_api_mock.hpp" #include "mock/core/runtime/parachain_host_mock.hpp" #include "testutil/lazy.hpp" #include "testutil/literals.hpp" @@ -45,13 +48,17 @@ using kagome::consensus::grandpa::JustificationObserver; using kagome::crypto::HasherMock; using kagome::dispute::DisputeCoordinatorMock; using kagome::network::GrandpaTransmitterMock; +using kagome::offchain::OffchainWorkerFactoryMock; +using kagome::offchain::OffchainWorkerPoolMock; using kagome::parachain::ApprovedAncestorMock; using kagome::parachain::BackingStoreMock; using kagome::primitives::BlockHash; using kagome::primitives::BlockHeader; using kagome::primitives::BlockInfo; using kagome::primitives::BlockNumber; +using kagome::runtime::GrandpaApiMock; using kagome::runtime::ParachainHostMock; + using testing::_; using testing::Invoke; using testing::Return; @@ -71,6 +78,9 @@ class ChainTest : public testing::Test { std::make_shared(app_state_manager, main_thread_pool); main_pool_handler->start(); + offchain_worker_factory = std::make_shared(); + offchain_worker_pool = std::make_shared(); + chain = std::make_shared( tree, header_repo, @@ -79,10 +89,13 @@ class ChainTest : public testing::Test { approved_ancestor, testutil::sptr_to_lazy(grandpa_), nullptr, + grandpa_api, dispute_coordinator, parachain_api, backing_store, hasher, + offchain_worker_factory, + offchain_worker_pool, main_pool_handler); } @@ -133,7 +146,8 @@ class ChainTest : public testing::Test { std::shared_ptr grandpa_transmitter = std::make_shared(); std::shared_ptr grandpa_ = std::make_shared(); - + std::shared_ptr grandpa_api = + std::make_shared(); std::shared_ptr dispute_coordinator = std::make_shared(); std::shared_ptr parachain_api = @@ -149,6 +163,9 @@ class ChainTest : public testing::Test { std::shared_ptr main_thread_pool; std::shared_ptr main_pool_handler; + std::shared_ptr offchain_worker_factory; + std::shared_ptr offchain_worker_pool; + std::shared_ptr chain; }; diff --git a/test/core/consensus/grandpa/voting_round/voting_round_test.cpp b/test/core/consensus/grandpa/voting_round/voting_round_test.cpp index fcbfd39400..c5f923166e 100644 --- a/test/core/consensus/grandpa/voting_round/voting_round_test.cpp +++ b/test/core/consensus/grandpa/voting_round/voting_round_test.cpp @@ -20,14 +20,17 @@ #include "mock/core/consensus/grandpa/vote_crypto_provider_mock.hpp" #include "mock/core/consensus/grandpa/voting_round_mock.hpp" #include "mock/core/crypto/hasher_mock.hpp" +#include "mock/core/runtime/grandpa_api_mock.hpp" #include "testutil/prepare_loggers.hpp" using namespace kagome::consensus::grandpa; using kagome::consensus::grandpa::Authority; using kagome::consensus::grandpa::AuthoritySet; +using kagome::consensus::grandpa::Equivocation; using kagome::crypto::Ed25519Keypair; using kagome::crypto::Ed25519Signature; using kagome::crypto::HasherMock; +using kagome::runtime::GrandpaApiMock; using Propagation = kagome::consensus::grandpa::VotingRound::Propagation; using namespace std::chrono_literals; @@ -35,6 +38,7 @@ using namespace std::chrono_literals; using testing::_; using testing::AnyNumber; using testing::Invoke; +using testing::Ref; using testing::Return; using testing::ReturnRef; using testing::Truly; @@ -309,12 +313,48 @@ TEST_F(VotingRoundTest, EstimateIsValid) { TEST_F(VotingRoundTest, EquivocateDoesNotDoubleCount) { auto alice1 = preparePrevote(kAlice, kAliceSignature, Prevote{9, "FC"_H}); - round_->onPrevote({}, alice1, Propagation::NEEDLESS); auto alice2 = preparePrevote(kAlice, kAliceSignature, Prevote{9, "ED"_H}); - round_->onPrevote({}, alice2, Propagation::NEEDLESS); auto alice3 = preparePrevote(kAlice, kAliceSignature, Prevote{6, "F"_H}); + + Equivocation equivocation{round_->roundNumber(), alice1, alice2}; + + { + auto matcher = [&](const Equivocation &equivocation) { + auto &first = equivocation.first; + auto &second = equivocation.second; + + if (equivocation.offender() != kAlice) { + return false; + } + if (first.id != equivocation.offender() + or second.id != equivocation.offender()) { + return false; + } + if (not first.is() or not second.is()) { + return false; + } + std::cout << "Equivocation: " // + << "first vote for " << first.getBlockHash().data() << ", " + << "second vote for " << second.getBlockHash().data() + << std::endl; + return true; + }; + + EXPECT_CALL(*env_, reportEquivocation(_, Truly(matcher))) + .WillOnce(Return(outcome::success())); + } + + // Regular vote + round_->onPrevote({}, alice1, Propagation::NEEDLESS); + + // Different vote in the same round; equivocation must be reported + round_->onPrevote({}, alice2, Propagation::NEEDLESS); + + // Another vote in the same round; should be ignored, cause already reported round_->onPrevote({}, alice3, Propagation::NEEDLESS); + round_->update(false, true, false); + ASSERT_EQ(round_->prevoteGhost(), std::nullopt); auto bob = preparePrevote(kBob, kBobSignature, Prevote{7, "FA"_H}); round_->onPrevote({}, bob, Propagation::NEEDLESS); @@ -600,3 +640,131 @@ TEST_F(VotingRoundTest, SunnyDayScenario) { ASSERT_TRUE(state.finalized.has_value()); EXPECT_EQ(state.finalized.value(), best_block); } + +/** + * Executes one round of grandpa round with mocked environment which mimics the + * network of 3 nodes: Alice (current peer), Bob and Eve. Round is executed from + * the Alice's perspective (so Bob's and Eve's behaviour is mocked) + * + * Scenario is the following: + * @given + * 1. Base block (last finalized one) in graph is BlockInfo{4, "C"_H} + * 2. Best block (the one that Alice is trying to finalize) is BlockInfo{10, + * "FC"_H} + * 3. Last round state with: + * prevote_ghost = Prevote{3, "B"_H} + * estimate = BlockInfo{4, "C"_H} + * finalized = BlockInfo{3, "B"_H} + * 4. Peers: + * Alice with weight 4 (primary), + * Bob with weight 7 and + * Eve with weight 3 + * + * @when + * The following test scenario is executed: + * 1. Alice proposes BlockInfo{4, "C"_H} (last round's estimate) + * 2. Everyone receive primary propose + * 3. Alice prevotes Prevote{10, "FC"_H} which is the best chain containing + * primary vote + * 4. Everyone receive Prevote{10, "FC"_H} and send their prevotes for the same + * block + * 5. Alice precommits Precommit{10, "FC"_H} which is prevote_ghost for the + * current round + * 6. Everyone receive Precommit{10, "FC"_H} and send their precommit for the + * same round + * 7. Alice receives enough precommits to commit Precommit{10, "FC"_H} + * 8. Round completes with round state containing `prevote_ghost`, `estimate` + * and `finalized` equal to the best block that Alice voted for + */ +TEST_F(VotingRoundTest, Equivocation) { + EXPECT_CALL(*env_, finalize(_, _)) + .Times(AnyNumber()) + .WillRepeatedly(Return(outcome::success())); + auto base_block = previous_round_->bestFinalCandidate(); + + ASSERT_EQ(base_block, (BlockInfo{3, "C"_H})); + + BlockInfo best_block{9, "FC"_H}; + + // Voting round is executed by Alice. + // Alice is also a Primary (alice's voter index % round number is zero) + { + auto matcher = [&](const SignedMessage &primary_propose) { + if (primary_propose.is() and primary_propose.id == kAlice + and primary_propose.getBlockHash() == base_block.hash) { + std::cout << "Proposed: " << primary_propose.getBlockHash().data() + << std::endl; + return true; + } + return false; + }; + EXPECT_CALL(*env_, onVoted(_, _, Truly(matcher))) + .WillOnce(onProposed(this)); // propose; + } + + // After prevote stage timer is out, Alice is doing prevote + { + auto matcher = [&](const SignedMessage &prevote) { + if (prevote.is() and prevote.id == kAlice + and prevote.getBlockHash() == best_block.hash) { + std::cout << "Prevoted: " << prevote.getBlockHash().data() << std::endl; + return true; + } + return false; + }; + // Is doing prevote + EXPECT_CALL(*env_, onVoted(_, _, Truly(matcher))) + .WillOnce(onPrevoted(this)); // prevote; + } + + // After precommit stage timer is out, Alice is doing precommit + { + auto matcher = [&](const SignedMessage &precommit) { + if (precommit.is() and precommit.id == kAlice + and precommit.getBlockHash() == best_block.hash) { + std::cout << "Precommitted: " << precommit.getBlockHash().data() + << std::endl; + return true; + } + return false; + }; + // Is doing precommit + EXPECT_CALL(*env_, onVoted(_, _, Truly(matcher))) + .WillOnce(onPrecommitted(this)); // precommit; + } + + round_->play(); + round_->endPrevoteStage(); + round_->endPrecommitStage(); + + auto state = round_->state(); + + Precommit precommit{best_block.number, best_block.hash}; + + auto alice_precommit = preparePrecommit(kAlice, kAliceSignature, precommit); + auto bob_precommit = preparePrecommit(kBob, kBobSignature, precommit); + + bool has_alice_precommit = false; + bool has_bob_precommit = false; + + auto lookup = [&](const auto &vote) { + has_alice_precommit = vote == alice_precommit or has_alice_precommit; + has_bob_precommit = vote == bob_precommit or has_bob_precommit; + }; + + for (auto &vote_variant : state.votes) { + kagome::visit_in_place( + vote_variant, + [&](const SignedMessage &vote) { lookup(vote); }, + [&](const EquivocatorySignedMessage &pair) { + lookup(pair.first); + lookup(pair.second); + }); + } + + EXPECT_TRUE(has_alice_precommit); + EXPECT_TRUE(has_bob_precommit); + + ASSERT_TRUE(state.finalized.has_value()); + EXPECT_EQ(state.finalized.value(), best_block); +} diff --git a/test/core/consensus/timeline/timeline_test.cpp b/test/core/consensus/timeline/timeline_test.cpp index c782e7b38b..7a17778813 100644 --- a/test/core/consensus/timeline/timeline_test.cpp +++ b/test/core/consensus/timeline/timeline_test.cpp @@ -143,6 +143,8 @@ class TimelineTest : public testing::Test { production_consensus = std::make_shared(); ON_CALL(*consensus_selector, getProductionConsensusByInfo(_)) .WillByDefault(Return(production_consensus)); + ON_CALL(*consensus_selector, getProductionConsensusByHeader(_)) + .WillByDefault(Return(production_consensus)); ON_CALL(*production_consensus, getSlot(best_block_header)) .WillByDefault(Return(1)); @@ -419,3 +421,91 @@ TEST_F(TimelineTest, Validator) { Mock::VerifyAndClearExpectations(scheduler.get()); } } + +/** + * @given start timeline + * @when consensus returns we are not validator + * @then we has not synchronized and waiting announce or incoming stream + */ +TEST_F(TimelineTest, Equivocation) { + BlockHeader new_block{ + 10, // number + "block_#9"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(10), // digest + "block_#10_s10_a0"_hash256 // hash + }; + + EXPECT_CALL(*production_consensus, getSlot(new_block)).WillOnce(Return(10)); + EXPECT_CALL(*production_consensus, getAuthority(new_block)) + .WillOnce(Return(0)); + EXPECT_CALL(*production_consensus, reportEquivocation(_, _)).Times(0); + timeline->checkAndReportEquivocation(new_block); + + BlockHeader another_slot_block{ + 10, // number + "block_#9_fork"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(11), // digest + "block_#10_s11_a0"_hash256 // hash + }; + + EXPECT_CALL(*production_consensus, getSlot(another_slot_block)) + .WillOnce(Return(11)); + EXPECT_CALL(*production_consensus, getAuthority(another_slot_block)) + .WillOnce(Return(0)); + EXPECT_CALL(*production_consensus, reportEquivocation(_, _)).Times(0); + timeline->checkAndReportEquivocation(another_slot_block); + + BlockHeader another_validator_block{ + 10, // number + "block_#9"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(10), // digest + "block_#10_s10_a1"_hash256 // hash + }; + + EXPECT_CALL(*production_consensus, getSlot(another_validator_block)) + .WillOnce(Return(10)); + EXPECT_CALL(*production_consensus, getAuthority(another_validator_block)) + .WillOnce(Return(1)); + EXPECT_CALL(*production_consensus, reportEquivocation(_, _)).Times(0); + timeline->checkAndReportEquivocation(another_validator_block); + + BlockHeader equivocating_block{ + 10, // number + "block_#9"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(10), // digest + "block_#10_s10_a0_e1"_hash256 // hash + }; + + EXPECT_CALL(*production_consensus, getSlot(equivocating_block)) + .WillOnce(Return(10)); + EXPECT_CALL(*production_consensus, getAuthority(equivocating_block)) + .WillOnce(Return(0)); + EXPECT_CALL(*production_consensus, + reportEquivocation(new_block.hash(), equivocating_block.hash())) + .WillOnce(Return(outcome::success())); + timeline->checkAndReportEquivocation(equivocating_block); + + BlockHeader one_more_equivocating_block{ + 10, // number + "block_#9"_hash256, // parent + {}, // state_root + {}, // extrinsic_root + make_digest(10), // digest + "block_#10_s10_a0_e2"_hash256 // hash + }; + + EXPECT_CALL(*production_consensus, getSlot(one_more_equivocating_block)) + .WillOnce(Return(10)); + EXPECT_CALL(*production_consensus, getAuthority(one_more_equivocating_block)) + .WillOnce(Return(0)); + EXPECT_CALL(*production_consensus, reportEquivocation(_, _)).Times(0); + timeline->checkAndReportEquivocation(one_more_equivocating_block); +} diff --git a/test/mock/core/consensus/grandpa/environment_mock.hpp b/test/mock/core/consensus/grandpa/environment_mock.hpp index 2f98e45f3b..98a33eac02 100644 --- a/test/mock/core/consensus/grandpa/environment_mock.hpp +++ b/test/mock/core/consensus/grandpa/environment_mock.hpp @@ -60,6 +60,11 @@ namespace kagome::consensus::grandpa { (const BlockHash &block_hash), (override)); + MOCK_METHOD(outcome::result, + reportEquivocation, + (const VotingRound &, const Equivocation &), + (const, override)); + MOCK_METHOD(void, onCatchUpRequested, (const libp2p::peer::PeerId &peer_id, diff --git a/test/mock/core/consensus/production_consensus_mock.hpp b/test/mock/core/consensus/production_consensus_mock.hpp index 73a3467126..3f420cbafc 100644 --- a/test/mock/core/consensus/production_consensus_mock.hpp +++ b/test/mock/core/consensus/production_consensus_mock.hpp @@ -24,6 +24,11 @@ namespace kagome::consensus { (const primitives::BlockHeader &), (const, override)); + MOCK_METHOD(outcome::result, + getAuthority, + (const primitives::BlockHeader &), + (const, override)); + MOCK_METHOD(outcome::result, processSlot, (SlotNumber, const primitives::BlockInfo &), @@ -53,5 +58,10 @@ namespace kagome::consensus { validateHeader, (const primitives::BlockHeader &), (const, override)); + + MOCK_METHOD(outcome::result, + reportEquivocation, + (const primitives::BlockHash &, const primitives::BlockHash &), + (const, override)); }; } // namespace kagome::consensus diff --git a/test/mock/core/consensus/timeline/timeline_mock.hpp b/test/mock/core/consensus/timeline/timeline_mock.hpp index 1d9462b061..cb1205ea75 100644 --- a/test/mock/core/consensus/timeline/timeline_mock.hpp +++ b/test/mock/core/consensus/timeline/timeline_mock.hpp @@ -17,6 +17,11 @@ namespace kagome::consensus { MOCK_METHOD(SyncState, getCurrentState, (), (const, override)); MOCK_METHOD(bool, wasSynchronized, (), (const, override)); + + MOCK_METHOD(void, + checkAndReportEquivocation, + (const primitives::BlockHeader &), + (override)); }; } // namespace kagome::consensus diff --git a/test/mock/core/offchain/offchain_worker_factory_mock.hpp b/test/mock/core/offchain/offchain_worker_factory_mock.hpp index 9a45276842..09d107bee3 100644 --- a/test/mock/core/offchain/offchain_worker_factory_mock.hpp +++ b/test/mock/core/offchain/offchain_worker_factory_mock.hpp @@ -14,10 +14,7 @@ namespace kagome::offchain { class OffchainWorkerFactoryMock : public OffchainWorkerFactory { public: - MOCK_METHOD2( - make, - std::shared_ptr(std::shared_ptr, - const primitives::BlockHeader &)); + MOCK_METHOD(std::shared_ptr, make, (), (override)); }; } // namespace kagome::offchain diff --git a/test/mock/core/runtime/babe_api_mock.hpp b/test/mock/core/runtime/babe_api_mock.hpp index 363dc2064c..f3ed1801a5 100644 --- a/test/mock/core/runtime/babe_api_mock.hpp +++ b/test/mock/core/runtime/babe_api_mock.hpp @@ -23,6 +23,21 @@ namespace kagome::runtime { next_epoch, (const primitives::BlockHash &), (override)); + + MOCK_METHOD(outcome::result< + std::optional>, + generate_key_ownership_proof, + (const primitives::BlockHash &, + consensus::SlotNumber, + consensus::babe::AuthorityId), + (override)); + + MOCK_METHOD(outcome::result, + submit_report_equivocation_unsigned_extrinsic, + (const primitives::BlockHash &, + consensus::babe::EquivocationProof, + consensus::babe::OpaqueKeyOwnershipProof), + (override)); }; } // namespace kagome::runtime diff --git a/test/mock/core/runtime/grandpa_api_mock.hpp b/test/mock/core/runtime/grandpa_api_mock.hpp index f04abecdbe..f7643288b0 100644 --- a/test/mock/core/runtime/grandpa_api_mock.hpp +++ b/test/mock/core/runtime/grandpa_api_mock.hpp @@ -23,6 +23,21 @@ namespace kagome::runtime { current_set_id, (const primitives::BlockHash &block), (override)); + + MOCK_METHOD(outcome::result< + std::optional>, + generate_key_ownership_proof, + (const primitives::BlockHash &, + consensus::SlotNumber, + consensus::grandpa::AuthorityId), + (override)); + + MOCK_METHOD(outcome::result, + submit_report_equivocation_unsigned_extrinsic, + (const primitives::BlockHash &, + consensus::grandpa::EquivocationProof, + consensus::grandpa::OpaqueKeyOwnershipProof), + (override)); }; } // namespace kagome::runtime