From 934d0290a931c7e1d1f25a57bb011f7da73cf3d5 Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 10 Aug 2023 11:24:53 +0200 Subject: [PATCH] Bridges subtree sync (#2991) * Squashed 'bridges/' changes from 0417308a48..278119fec2 278119fec2 Grandpa: Store the authority set changes (#2336) (#2337) 19f9c8ffdb remove message sender origin (#2322) 3c7c271d2e GRANDPA module: store accepted justifications (#2298) (#2301) fb7d12e793 GRANDPA justifications: equivocation detection primitives (#2295) (#2297) d03a2ed450 More backports from Cumulus subtree to polkadot-staging (#2283) 3c4ada921b Update dependecies (#2277) (#2281) 3e195c9e76 GRANDPA: optimize votes_ancestries when needed (#2262) (#2264) 7065bbabc6 Implement RuntimeDebug for GrandpaJustification (#2254) 8c9e59bcbc Define generate_grandpa_key_ownership_proof() (#2247) (#2248) 0b46956df7 Deduplicate Grandpa consensus log reading logic (#2245) (#2246) 96c9701710 Fix deps from Cumulus (#2244) git-subtree-dir: bridges git-subtree-split: 278119fec2b45990cf1271999b0c21befe7003d9 * Subtree update * Squashed 'bridges/' changes from 278119fec2..edf33a2c85 edf33a2c85 Backport fix (for wasm `std` env) (#2339) git-subtree-dir: bridges git-subtree-split: edf33a2c85399d366e008228f2d9e63e8a492d95 --- Cargo.lock | 3 + bridges/bin/runtime-common/src/messages.rs | 10 +- .../src/messages_xcm_extension.rs | 13 +- bridges/modules/grandpa/src/lib.rs | 87 +++- bridges/modules/grandpa/src/storage_types.rs | 4 +- bridges/modules/messages/src/lib.rs | 87 +--- bridges/modules/messages/src/mock.rs | 3 +- bridges/modules/parachains/src/lib.rs | 36 +- .../chain-bridge-hub-kusama/src/lib.rs | 2 +- .../chain-bridge-hub-polkadot/src/lib.rs | 2 +- .../chain-bridge-hub-rococo/src/lib.rs | 2 +- .../chain-bridge-hub-wococo/src/lib.rs | 2 +- bridges/primitives/chain-kusama/Cargo.toml | 2 + bridges/primitives/chain-kusama/src/lib.rs | 3 +- bridges/primitives/chain-polkadot/Cargo.toml | 2 + bridges/primitives/chain-polkadot/src/lib.rs | 3 +- bridges/primitives/chain-rococo/Cargo.toml | 2 + bridges/primitives/chain-rococo/src/lib.rs | 3 +- bridges/primitives/chain-wococo/Cargo.toml | 2 + bridges/primitives/chain-wococo/src/lib.rs | 3 +- .../header-chain/src/justification.rs | 444 ------------------ .../header-chain/src/justification/mod.rs | 127 +++++ .../verification/equivocation.rs | 179 +++++++ .../src/justification/verification/mod.rs | 279 +++++++++++ .../justification/verification/optimizer.rs | 132 ++++++ .../src/justification/verification/strict.rs | 106 +++++ bridges/primitives/header-chain/src/lib.rs | 25 +- .../tests/implementation_match.rs | 27 +- .../tests/justification/equivocation.rs | 124 +++++ .../optimizer.rs} | 178 +------ .../tests/justification/strict.rs | 196 ++++++++ .../primitives/header-chain/tests/tests.rs | 7 + .../primitives/messages/src/source_chain.rs | 22 +- bridges/primitives/runtime/src/chain.rs | 23 +- bridges/primitives/runtime/src/lib.rs | 2 +- .../bridge-hubs/bridge-hub-rococo/Cargo.toml | 2 + .../src/bridge_hub_rococo_config.rs | 7 - .../src/bridge_hub_wococo_config.rs | 7 - .../bridge-hubs/bridge-hub-rococo/src/lib.rs | 8 + 39 files changed, 1375 insertions(+), 791 deletions(-) delete mode 100644 bridges/primitives/header-chain/src/justification.rs create mode 100644 bridges/primitives/header-chain/src/justification/mod.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/equivocation.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/mod.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/optimizer.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/strict.rs create mode 100644 bridges/primitives/header-chain/tests/justification/equivocation.rs rename bridges/primitives/header-chain/tests/{justification.rs => justification/optimizer.rs} (55%) create mode 100644 bridges/primitives/header-chain/tests/justification/strict.rs create mode 100644 bridges/primitives/header-chain/tests/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 8c00f1fb556..7164f6b25b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,6 +1257,7 @@ dependencies = [ "bp-runtime", "frame-support", "sp-api", + "sp-std", ] [[package]] @@ -1310,6 +1311,7 @@ dependencies = [ "bp-runtime", "frame-support", "sp-api", + "sp-std", ] [[package]] @@ -1472,6 +1474,7 @@ version = "0.1.0" dependencies = [ "bp-bridge-hub-rococo", "bp-bridge-hub-wococo", + "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot-core", diff --git a/bridges/bin/runtime-common/src/messages.rs b/bridges/bin/runtime-common/src/messages.rs index e6c7deb98a2..316c4d3cad9 100644 --- a/bridges/bin/runtime-common/src/messages.rs +++ b/bridges/bin/runtime-common/src/messages.rs @@ -72,8 +72,6 @@ pub type HasherOf = bp_runtime::HasherOf>; pub type AccountIdOf = bp_runtime::AccountIdOf>; /// Type of balances that is used on the chain. pub type BalanceOf = bp_runtime::BalanceOf>; -/// Type of origin that is used on the chain. -pub type OriginOf = ::RuntimeOrigin; /// Sub-module that is declaring types required for processing This -> Bridged chain messages. pub mod source { @@ -138,17 +136,11 @@ pub mod source { #[derive(RuntimeDebug)] pub struct FromThisChainMessageVerifier(PhantomData); - impl LaneMessageVerifier>, FromThisChainMessagePayload> - for FromThisChainMessageVerifier + impl LaneMessageVerifier for FromThisChainMessageVerifier where B: MessageBridge, - // matches requirements from the `frame_system::Config::Origin` - OriginOf>: Clone - + Into>>, OriginOf>>>, - AccountIdOf>: PartialEq + Clone, { fn verify_message( - _submitter: &OriginOf>, _lane: &LaneId, _lane_outbound_data: &OutboundLaneData, _payload: &FromThisChainMessagePayload, diff --git a/bridges/bin/runtime-common/src/messages_xcm_extension.rs b/bridges/bin/runtime-common/src/messages_xcm_extension.rs index 96fdf1d5018..0317d745c15 100644 --- a/bridges/bin/runtime-common/src/messages_xcm_extension.rs +++ b/bridges/bin/runtime-common/src/messages_xcm_extension.rs @@ -110,12 +110,7 @@ impl MessageDispat /// one side, where on the other it can be dispatched by [`XcmBlobMessageDispatch`]. pub trait XcmBlobHauler { /// Runtime message sender adapter. - type MessageSender: MessagesBridge; - - /// Runtime message sender origin, which is used by [`Self::MessageSender`]. - type MessageSenderOrigin; - /// Our location within the Consensus Universe. - fn message_sender_origin() -> Self::MessageSenderOrigin; + type MessageSender: MessagesBridge; /// Return message lane (as "point-to-point link") used to deliver XCM messages. fn xcm_lane() -> LaneId; @@ -124,12 +119,10 @@ pub trait XcmBlobHauler { /// XCM bridge adapter which connects [`XcmBlobHauler`] with [`XcmBlobHauler::MessageSender`] and /// makes sure that XCM blob is sent to the [`pallet_bridge_messages`] queue to be relayed. pub struct XcmBlobHaulerAdapter(sp_std::marker::PhantomData); -impl> HaulBlob - for XcmBlobHaulerAdapter -{ +impl HaulBlob for XcmBlobHaulerAdapter { fn haul_blob(blob: sp_std::prelude::Vec) -> Result<(), HaulBlobError> { let lane = H::xcm_lane(); - H::MessageSender::send_message(H::message_sender_origin(), lane, blob) + H::MessageSender::send_message(lane, blob) .map(|artifacts| (lane, artifacts.nonce).using_encoded(sp_io::hashing::blake2_256)) .map(|result| { log::info!( diff --git a/bridges/modules/grandpa/src/lib.rs b/bridges/modules/grandpa/src/lib.rs index eb49849ac88..259a6c0f36b 100644 --- a/bridges/modules/grandpa/src/lib.rs +++ b/bridges/modules/grandpa/src/lib.rs @@ -39,8 +39,8 @@ pub use storage_types::StoredAuthoritySet; use bp_header_chain::{ - justification::GrandpaJustification, ChainWithGrandpa, GrandpaConsensusLogReader, HeaderChain, - InitializationData, StoredHeaderData, StoredHeaderDataBuilder, + justification::GrandpaJustification, AuthoritySet, ChainWithGrandpa, GrandpaConsensusLogReader, + HeaderChain, HeaderGrandpaInfo, InitializationData, StoredHeaderData, StoredHeaderDataBuilder, }; use bp_runtime::{BlockNumberOf, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule}; use finality_grandpa::voter_set::VoterSet; @@ -49,7 +49,7 @@ use sp_runtime::{ traits::{Header as HeaderT, Zero}, SaturatedConversion, }; -use sp_std::{boxed::Box, convert::TryInto}; +use sp_std::{boxed::Box, convert::TryInto, prelude::*}; mod call_ext; #[cfg(test)] @@ -194,11 +194,12 @@ pub mod pallet { let authority_set = >::get(); let unused_proof_size = authority_set.unused_proof_size(); let set_id = authority_set.set_id; - verify_justification::(&justification, hash, number, authority_set.into())?; + let authority_set: AuthoritySet = authority_set.into(); + verify_justification::(&justification, hash, number, authority_set)?; - let is_authorities_change_enacted = + let maybe_new_authority_set = try_enact_authority_change::(&finality_target, set_id)?; - let may_refund_call_fee = is_authorities_change_enacted && + let may_refund_call_fee = maybe_new_authority_set.is_some() && // if we have seen too many mandatory headers in this block, we don't want to refund Self::free_mandatory_headers_remaining() > 0 && // if arguments out of expected bounds, we don't want to refund @@ -237,7 +238,14 @@ pub mod pallet { let actual_weight = pre_dispatch_weight .set_proof_size(pre_dispatch_weight.proof_size().saturating_sub(unused_proof_size)); - Self::deposit_event(Event::UpdatedBestFinalizedHeader { number, hash }); + Self::deposit_event(Event::UpdatedBestFinalizedHeader { + number, + hash, + grandpa_info: HeaderGrandpaInfo { + justification, + authority_set: maybe_new_authority_set, + }, + }); Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee }) } @@ -402,6 +410,8 @@ pub mod pallet { UpdatedBestFinalizedHeader { number: BridgedBlockNumber, hash: BridgedBlockHash, + /// The Grandpa info associated to the new best finalized header. + grandpa_info: HeaderGrandpaInfo>, }, } @@ -437,9 +447,7 @@ pub mod pallet { pub(crate) fn try_enact_authority_change, I: 'static>( header: &BridgedHeader, current_set_id: sp_consensus_grandpa::SetId, - ) -> Result { - let mut change_enacted = false; - + ) -> Result, DispatchError> { // We don't support forced changes - at that point governance intervention is required. ensure!( GrandpaConsensusLogReader::>::find_forced_change( @@ -468,7 +476,6 @@ pub mod pallet { // Since our header schedules a change and we know the delay is 0, it must also enact // the change. >::put(&next_authorities); - change_enacted = true; log::info!( target: LOG_TARGET, @@ -477,9 +484,11 @@ pub mod pallet { current_set_id + 1, next_authorities, ); + + return Ok(Some(next_authorities.into())) }; - Ok(change_enacted) + Ok(None) } /// Verify a GRANDPA justification (finality proof) for a given header. @@ -603,10 +612,22 @@ pub mod pallet { } } -impl, I: 'static> Pallet { - /// Get the best finalized block number. - pub fn best_finalized_number() -> Option> { - BestFinalized::::get().map(|id| id.number()) +impl, I: 'static> Pallet +where + ::RuntimeEvent: TryInto>, +{ + /// Get the GRANDPA justifications accepted in the current block. + pub fn synced_headers_grandpa_info() -> Vec>> { + frame_system::Pallet::::read_events_no_consensus() + .filter_map(|event| { + if let Event::::UpdatedBestFinalizedHeader { grandpa_info, .. } = + event.event.try_into().ok()? + { + return Some(grandpa_info) + } + None + }) + .collect() } } @@ -913,10 +934,18 @@ mod tests { event: TestEvent::Grandpa(Event::UpdatedBestFinalizedHeader { number: *header.number(), hash: header.hash(), + grandpa_info: HeaderGrandpaInfo { + justification: justification.clone(), + authority_set: None, + }, }), topics: vec![], }], ); + assert_eq!( + Pallet::::synced_headers_grandpa_info(), + vec![HeaderGrandpaInfo { justification, authority_set: None }] + ); }) } @@ -1022,7 +1051,7 @@ mod tests { let result = Pallet::::submit_finality_proof( RuntimeOrigin::signed(1), Box::new(header.clone()), - justification, + justification.clone(), ); assert_ok!(result); assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::No); @@ -1037,6 +1066,30 @@ mod tests { StoredAuthoritySet::::try_new(next_authorities, next_set_id) .unwrap(), ); + + // Here + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: TestEvent::Grandpa(Event::UpdatedBestFinalizedHeader { + number: *header.number(), + hash: header.hash(), + grandpa_info: HeaderGrandpaInfo { + justification: justification.clone(), + authority_set: Some(>::get().into()), + }, + }), + topics: vec![], + }], + ); + assert_eq!( + Pallet::::synced_headers_grandpa_info(), + vec![HeaderGrandpaInfo { + justification, + authority_set: Some(>::get().into()), + }] + ); }) } diff --git a/bridges/modules/grandpa/src/storage_types.rs b/bridges/modules/grandpa/src/storage_types.rs index 70448286335..59fcb5d3f07 100644 --- a/bridges/modules/grandpa/src/storage_types.rs +++ b/bridges/modules/grandpa/src/storage_types.rs @@ -20,7 +20,7 @@ use crate::{Config, Error}; use bp_header_chain::{AuthoritySet, ChainWithGrandpa}; use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::{traits::Get, BoundedVec, RuntimeDebugNoBound}; +use frame_support::{traits::Get, BoundedVec, CloneNoBound, RuntimeDebugNoBound}; use scale_info::TypeInfo; use sp_consensus_grandpa::{AuthorityId, AuthorityList, AuthorityWeight, SetId}; use sp_std::marker::PhantomData; @@ -39,7 +39,7 @@ impl, I: 'static> Get for StoredAuthorityListLimit { } /// A bounded GRANDPA Authority List and ID. -#[derive(Clone, Decode, Encode, Eq, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound)] +#[derive(CloneNoBound, Decode, Encode, Eq, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound)] #[scale_info(skip_type_params(T, I))] pub struct StoredAuthoritySet, I: 'static> { /// List of GRANDPA authorities for the current round. diff --git a/bridges/modules/messages/src/lib.rs b/bridges/modules/messages/src/lib.rs index 3bfd5bc7e98..91ab3c965b9 100644 --- a/bridges/modules/messages/src/lib.rs +++ b/bridges/modules/messages/src/lib.rs @@ -153,7 +153,7 @@ pub mod pallet { /// Target header chain. type TargetHeaderChain: TargetHeaderChain; /// Message payload verifier. - type LaneMessageVerifier: LaneMessageVerifier; + type LaneMessageVerifier: LaneMessageVerifier; /// Delivery confirmation payments. type DeliveryConfirmationPayments: DeliveryConfirmationPayments; @@ -643,8 +643,7 @@ pub mod pallet { } } -impl bp_messages::source_chain::MessagesBridge - for Pallet +impl bp_messages::source_chain::MessagesBridge for Pallet where T: Config, I: 'static, @@ -652,17 +651,15 @@ where type Error = sp_runtime::DispatchErrorWithPostInfo; fn send_message( - sender: T::RuntimeOrigin, lane: LaneId, message: T::OutboundPayload, ) -> Result { - crate::send_message::(sender, lane, message) + crate::send_message::(lane, message) } } /// Function that actually sends message. fn send_message, I: 'static>( - submitter: T::RuntimeOrigin, lane_id: LaneId, payload: T::OutboundPayload, ) -> sp_std::result::Result< @@ -688,18 +685,16 @@ fn send_message, I: 'static>( // now let's enforce any additional lane rules let mut lane = outbound_lane::(lane_id); - T::LaneMessageVerifier::verify_message(&submitter, &lane_id, &lane.data(), &payload).map_err( - |err| { - log::trace!( - target: LOG_TARGET, - "Message to lane {:?} is rejected by lane verifier: {:?}", - lane_id, - err, - ); + T::LaneMessageVerifier::verify_message(&lane_id, &lane.data(), &payload).map_err(|err| { + log::trace!( + target: LOG_TARGET, + "Message to lane {:?} is rejected by lane verifier: {:?}", + lane_id, + err, + ); - Error::::MessageRejectedByLaneVerifier(err) - }, - )?; + Error::::MessageRejectedByLaneVerifier(err) + })?; // finally, save message in outbound storage and emit event let encoded_payload = payload.encode(); @@ -915,7 +910,7 @@ mod tests { let message_nonce = outbound_lane::(TEST_LANE_ID).data().latest_generated_nonce + 1; - send_message::(RuntimeOrigin::signed(1), TEST_LANE_ID, REGULAR_PAYLOAD) + send_message::(TEST_LANE_ID, REGULAR_PAYLOAD) .expect("send_message has failed"); // check event with assigned nonce @@ -982,11 +977,7 @@ mod tests { )); assert_noop!( - send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - REGULAR_PAYLOAD, - ), + send_message::(TEST_LANE_ID, REGULAR_PAYLOAD,), Error::::NotOperatingNormally, ); @@ -1036,11 +1027,7 @@ mod tests { ); assert_noop!( - send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - REGULAR_PAYLOAD, - ), + send_message::(TEST_LANE_ID, REGULAR_PAYLOAD,), Error::::NotOperatingNormally, ); @@ -1090,11 +1077,7 @@ mod tests { .extra .extend_from_slice(&[0u8; MAX_OUTBOUND_PAYLOAD_SIZE as usize]); assert_noop!( - send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - message_payload.clone(), - ), + send_message::(TEST_LANE_ID, message_payload.clone(),), Error::::MessageRejectedByPallet( VerificationError::MessageTooLarge ), @@ -1105,11 +1088,7 @@ mod tests { message_payload.extra.pop(); } assert_eq!(message_payload.encoded_size() as u32, MAX_OUTBOUND_PAYLOAD_SIZE); - assert_ok!(send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - message_payload, - ),); + assert_ok!(send_message::(TEST_LANE_ID, message_payload,),); }) } @@ -1118,11 +1097,7 @@ mod tests { run_test(|| { // messages with this payload are rejected by target chain verifier assert_noop!( - send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - PAYLOAD_REJECTED_BY_TARGET_CHAIN, - ), + send_message::(TEST_LANE_ID, PAYLOAD_REJECTED_BY_TARGET_CHAIN,), Error::::MessageRejectedByChainVerifier(VerificationError::Other( mock::TEST_ERROR )), @@ -1137,7 +1112,7 @@ mod tests { let mut message = REGULAR_PAYLOAD; message.reject_by_lane_verifier = true; assert_noop!( - send_message::(RuntimeOrigin::signed(1), TEST_LANE_ID, message,), + send_message::(TEST_LANE_ID, message,), Error::::MessageRejectedByLaneVerifier(VerificationError::Other( mock::TEST_ERROR )), @@ -1293,16 +1268,8 @@ mod tests { #[test] fn receive_messages_delivery_proof_rewards_relayers() { run_test(|| { - assert_ok!(send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - REGULAR_PAYLOAD, - )); - assert_ok!(send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID, - REGULAR_PAYLOAD, - )); + assert_ok!(send_message::(TEST_LANE_ID, REGULAR_PAYLOAD,)); + assert_ok!(send_message::(TEST_LANE_ID, REGULAR_PAYLOAD,)); // this reports delivery of message 1 => reward is paid to TEST_RELAYER_A let single_message_delivery_proof = TestMessagesDeliveryProof(Ok(( @@ -1899,11 +1866,7 @@ mod tests { send_regular_message(); receive_messages_delivery_proof(); // send + receive confirmation for lane 2 - assert_ok!(send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID_2, - REGULAR_PAYLOAD, - )); + assert_ok!(send_message::(TEST_LANE_ID_2, REGULAR_PAYLOAD,)); assert_ok!(Pallet::::receive_messages_delivery_proof( RuntimeOrigin::signed(1), TestMessagesDeliveryProof(Ok(( @@ -1979,11 +1942,7 @@ mod tests { fn outbound_message_from_unconfigured_lane_is_rejected() { run_test(|| { assert_noop!( - send_message::( - RuntimeOrigin::signed(1), - TEST_LANE_ID_3, - REGULAR_PAYLOAD, - ), + send_message::(TEST_LANE_ID_3, REGULAR_PAYLOAD,), Error::::InactiveOutboundLane, ); }); diff --git a/bridges/modules/messages/src/mock.rs b/bridges/modules/messages/src/mock.rs index 62bc76c5e01..83752523efb 100644 --- a/bridges/modules/messages/src/mock.rs +++ b/bridges/modules/messages/src/mock.rs @@ -310,9 +310,8 @@ impl TargetHeaderChain for TestTargetHeaderChain { #[derive(Debug, Default)] pub struct TestLaneMessageVerifier; -impl LaneMessageVerifier for TestLaneMessageVerifier { +impl LaneMessageVerifier for TestLaneMessageVerifier { fn verify_message( - _submitter: &RuntimeOrigin, _lane: &LaneId, _lane_outbound_data: &OutboundLaneData, payload: &TestPayload, diff --git a/bridges/modules/parachains/src/lib.rs b/bridges/modules/parachains/src/lib.rs index 4f78a45d4b7..24bc3535e61 100644 --- a/bridges/modules/parachains/src/lib.rs +++ b/bridges/modules/parachains/src/lib.rs @@ -691,12 +691,13 @@ pub(crate) mod tests { use super::*; use crate::mock::{ run_test, test_relay_header, BigParachainHeader, RegularParachainHasher, - RegularParachainHeader, RuntimeEvent as TestEvent, RuntimeOrigin, TestRuntime, - UNTRACKED_PARACHAIN_ID, + RegularParachainHeader, RelayBlockHeader, RuntimeEvent as TestEvent, RuntimeOrigin, + TestRuntime, UNTRACKED_PARACHAIN_ID, }; use bp_test_utils::prepare_parachain_heads_proof; use codec::Encode; + use bp_header_chain::{justification::GrandpaJustification, HeaderGrandpaInfo}; use bp_parachains::{ BestParaHeadHash, BridgeParachainCall, ImportedParaHeadsKeyProvider, ParasInfoKeyProvider, }; @@ -740,7 +741,10 @@ pub(crate) mod tests { test_relay_header(0, state_root).hash() } - fn proceed(num: RelayBlockNumber, state_root: RelayBlockHash) -> ParaHash { + fn proceed( + num: RelayBlockNumber, + state_root: RelayBlockHash, + ) -> (ParaHash, GrandpaJustification) { pallet_bridge_grandpa::Pallet::::on_initialize( 0, ); @@ -752,11 +756,11 @@ pub(crate) mod tests { pallet_bridge_grandpa::Pallet::::submit_finality_proof( RuntimeOrigin::signed(1), Box::new(header), - justification, + justification.clone(), ) ); - hash + (hash, justification) } fn initial_best_head(parachain: u32) -> ParaInfo { @@ -993,7 +997,7 @@ pub(crate) mod tests { ); // import head#10 of parachain#1 at relay block #1 - let relay_1_hash = proceed(1, state_root_10); + let (relay_1_hash, justification) = proceed(1, state_root_10); assert_ok!(import_parachain_1_head(1, state_root_10, parachains_10, proof_10)); assert_eq!( ParasInfo::::get(ParaId(1)), @@ -1032,6 +1036,10 @@ pub(crate) mod tests { pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader { number: 1, hash: relay_1_hash, + grandpa_info: HeaderGrandpaInfo { + justification, + authority_set: None, + }, } ), topics: vec![], @@ -1149,7 +1157,7 @@ pub(crate) mod tests { // try to import head#0 of parachain#1 at relay block#1 // => call succeeds, but nothing is changed - let relay_1_hash = proceed(1, state_root); + let (relay_1_hash, justification) = proceed(1, state_root); assert_ok!(import_parachain_1_head(1, state_root, parachains, proof)); assert_eq!(ParasInfo::::get(ParaId(1)), Some(initial_best_head(1))); assert_eq!( @@ -1169,6 +1177,10 @@ pub(crate) mod tests { pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader { number: 1, hash: relay_1_hash, + grandpa_info: HeaderGrandpaInfo { + justification, + authority_set: None, + } } ), topics: vec![], @@ -1197,7 +1209,7 @@ pub(crate) mod tests { initialize(state_root_5); // head#10 of parachain#1 at relay block#1 - let relay_1_hash = proceed(1, state_root_10); + let (relay_1_hash, justification) = proceed(1, state_root_10); assert_ok!(import_parachain_1_head(1, state_root_10, parachains_10, proof_10)); assert_eq!( ParasInfo::::get(ParaId(1)), @@ -1218,6 +1230,10 @@ pub(crate) mod tests { pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader { number: 1, hash: relay_1_hash, + grandpa_info: HeaderGrandpaInfo { + justification: justification.clone(), + authority_set: None, + } } ), topics: vec![], @@ -1255,6 +1271,10 @@ pub(crate) mod tests { pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader { number: 1, hash: relay_1_hash, + grandpa_info: HeaderGrandpaInfo { + justification, + authority_set: None, + } } ), topics: vec![], diff --git a/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs b/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs index 7405f561fb2..03f295e07f1 100644 --- a/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs @@ -29,7 +29,7 @@ use frame_support::{ sp_runtime::{MultiAddress, MultiSigner}, RuntimeDebug, }; -use sp_std::prelude::*; +use sp_std::prelude::Vec; /// BridgeHubKusama parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs b/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs index e1fc0d7bc47..ceacfe67ff2 100644 --- a/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs @@ -25,7 +25,7 @@ use bp_runtime::{ decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, Chain, Parachain, }; use frame_support::{dispatch::DispatchClass, RuntimeDebug}; -use sp_std::prelude::*; +use sp_std::prelude::Vec; /// BridgeHubPolkadot parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs b/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs index 50206c8e6b3..f8d46ff32bd 100644 --- a/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs @@ -29,7 +29,7 @@ use frame_support::{ sp_runtime::{MultiAddress, MultiSigner}, RuntimeDebug, }; -use sp_std::prelude::*; +use sp_std::prelude::Vec; /// BridgeHubRococo parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs b/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs index 7d14460c737..abbb0cc20ea 100644 --- a/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs @@ -25,7 +25,7 @@ use bp_runtime::{ decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, Chain, Parachain, }; use frame_support::{dispatch::DispatchClass, RuntimeDebug}; -use sp_std::prelude::*; +use sp_std::prelude::Vec; /// BridgeHubWococo parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-kusama/Cargo.toml b/bridges/primitives/chain-kusama/Cargo.toml index 7f48ded1a37..65b0729aa17 100644 --- a/bridges/primitives/chain-kusama/Cargo.toml +++ b/bridges/primitives/chain-kusama/Cargo.toml @@ -18,6 +18,7 @@ bp-runtime = { path = "../runtime", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } [features] default = ["std"] @@ -27,4 +28,5 @@ std = [ "bp-runtime/std", "frame-support/std", "sp-api/std", + "sp-std/std", ] diff --git a/bridges/primitives/chain-kusama/src/lib.rs b/bridges/primitives/chain-kusama/src/lib.rs index 229905a3d4a..b758484aefe 100644 --- a/bridges/primitives/chain-kusama/src/lib.rs +++ b/bridges/primitives/chain-kusama/src/lib.rs @@ -23,6 +23,7 @@ pub use bp_polkadot_core::*; use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::weights::Weight; +use sp_std::prelude::Vec; /// Kusama Chain pub struct Kusama; @@ -69,4 +70,4 @@ pub const WITH_KUSAMA_GRANDPA_PALLET_NAME: &str = "BridgeKusamaGrandpa"; /// reserve. pub const MAX_NESTED_PARACHAIN_HEAD_DATA_SIZE: u32 = 128; -decl_bridge_finality_runtime_apis!(kusama); +decl_bridge_finality_runtime_apis!(kusama, grandpa); diff --git a/bridges/primitives/chain-polkadot/Cargo.toml b/bridges/primitives/chain-polkadot/Cargo.toml index def26bdda1c..1522f1dadb7 100644 --- a/bridges/primitives/chain-polkadot/Cargo.toml +++ b/bridges/primitives/chain-polkadot/Cargo.toml @@ -18,6 +18,7 @@ bp-runtime = { path = "../runtime", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } [features] default = ["std"] @@ -27,4 +28,5 @@ std = [ "bp-runtime/std", "frame-support/std", "sp-api/std", + "sp-std/std", ] diff --git a/bridges/primitives/chain-polkadot/src/lib.rs b/bridges/primitives/chain-polkadot/src/lib.rs index 628634bb46f..eb62c1729ad 100644 --- a/bridges/primitives/chain-polkadot/src/lib.rs +++ b/bridges/primitives/chain-polkadot/src/lib.rs @@ -23,6 +23,7 @@ pub use bp_polkadot_core::*; use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::weights::Weight; +use sp_std::prelude::Vec; /// Polkadot Chain pub struct Polkadot; @@ -69,4 +70,4 @@ pub const WITH_POLKADOT_GRANDPA_PALLET_NAME: &str = "BridgePolkadotGrandpa"; /// reserve. pub const MAX_NESTED_PARACHAIN_HEAD_DATA_SIZE: u32 = 128; -decl_bridge_finality_runtime_apis!(polkadot); +decl_bridge_finality_runtime_apis!(polkadot, grandpa); diff --git a/bridges/primitives/chain-rococo/Cargo.toml b/bridges/primitives/chain-rococo/Cargo.toml index 4e21bd38b7a..26e035b439a 100644 --- a/bridges/primitives/chain-rococo/Cargo.toml +++ b/bridges/primitives/chain-rococo/Cargo.toml @@ -17,6 +17,7 @@ bp-runtime = { path = "../runtime", default-features = false } # Substrate Based Dependencies sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } [features] @@ -27,4 +28,5 @@ std = [ "bp-runtime/std", "frame-support/std", "sp-api/std", + "sp-std/std", ] diff --git a/bridges/primitives/chain-rococo/src/lib.rs b/bridges/primitives/chain-rococo/src/lib.rs index a825c8b3978..140069d7569 100644 --- a/bridges/primitives/chain-rococo/src/lib.rs +++ b/bridges/primitives/chain-rococo/src/lib.rs @@ -23,6 +23,7 @@ pub use bp_polkadot_core::*; use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::{parameter_types, weights::Weight}; +use sp_std::prelude::Vec; /// Rococo Chain pub struct Rococo; @@ -73,4 +74,4 @@ pub const WITH_ROCOCO_GRANDPA_PALLET_NAME: &str = "BridgeRococoGrandpa"; /// reserve. pub const MAX_NESTED_PARACHAIN_HEAD_DATA_SIZE: u32 = 128; -decl_bridge_finality_runtime_apis!(rococo); +decl_bridge_finality_runtime_apis!(rococo, grandpa); diff --git a/bridges/primitives/chain-wococo/Cargo.toml b/bridges/primitives/chain-wococo/Cargo.toml index 25fd7b9fd94..3bde102d8f2 100644 --- a/bridges/primitives/chain-wococo/Cargo.toml +++ b/bridges/primitives/chain-wococo/Cargo.toml @@ -19,6 +19,7 @@ bp-rococo = { path = "../chain-rococo", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } [features] default = ["std"] @@ -29,4 +30,5 @@ std = [ "bp-rococo/std", "frame-support/std", "sp-api/std", + "sp-std/std", ] diff --git a/bridges/primitives/chain-wococo/src/lib.rs b/bridges/primitives/chain-wococo/src/lib.rs index fb63613427d..8247b63238c 100644 --- a/bridges/primitives/chain-wococo/src/lib.rs +++ b/bridges/primitives/chain-wococo/src/lib.rs @@ -26,6 +26,7 @@ pub use bp_rococo::{ use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::weights::Weight; +use sp_std::prelude::Vec; /// Wococo Chain pub struct Wococo; @@ -62,4 +63,4 @@ impl ChainWithGrandpa for Wococo { /// Name of the With-Wococo GRANDPA pallet instance that is deployed at bridged chains. pub const WITH_WOCOCO_GRANDPA_PALLET_NAME: &str = "BridgeWococoGrandpa"; -decl_bridge_finality_runtime_apis!(wococo); +decl_bridge_finality_runtime_apis!(wococo, grandpa); diff --git a/bridges/primitives/header-chain/src/justification.rs b/bridges/primitives/header-chain/src/justification.rs deleted file mode 100644 index 714546a42ef..00000000000 --- a/bridges/primitives/header-chain/src/justification.rs +++ /dev/null @@ -1,444 +0,0 @@ -// Copyright 2019-2021 Parity Technologies (UK) Ltd. -// This file is part of Parity Bridges Common. - -// Parity Bridges Common is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity Bridges Common is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity Bridges Common. If not, see . - -//! Pallet for checking GRANDPA Finality Proofs. -//! -//! Adapted copy of substrate/client/finality-grandpa/src/justification.rs. If origin -//! will ever be moved to the sp_consensus_grandpa, we should reuse that implementation. - -use crate::ChainWithGrandpa; - -use bp_runtime::{BlockNumberOf, Chain, HashOf, HeaderId}; -use codec::{Decode, Encode, MaxEncodedLen}; -use finality_grandpa::voter_set::VoterSet; -use frame_support::{RuntimeDebug, RuntimeDebugNoBound}; -use scale_info::TypeInfo; -use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, SetId}; -use sp_runtime::{traits::Header as HeaderT, SaturatedConversion}; -use sp_std::{ - collections::{btree_map::BTreeMap, btree_set::BTreeSet}, - prelude::*, -}; - -/// A GRANDPA Justification is a proof that a given header was finalized -/// at a certain height and with a certain set of authorities. -/// -/// This particular proof is used to prove that headers on a bridged chain -/// (so not our chain) have been finalized correctly. -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound)] -pub struct GrandpaJustification { - /// The round (voting period) this justification is valid for. - pub round: u64, - /// The set of votes for the chain which is to be finalized. - pub commit: - finality_grandpa::Commit, - /// A proof that the chain of blocks in the commit are related to each other. - pub votes_ancestries: Vec
, -} - -impl GrandpaJustification { - /// Returns reasonable size of justification using constants from the provided chain. - /// - /// An imprecise analogue of `MaxEncodedLen` implementation. We don't use it for - /// any precise calculations - that's just an estimation. - pub fn max_reasonable_size(required_precommits: u32) -> u32 - where - C: Chain + ChainWithGrandpa, - { - // we don't need precise results here - just estimations, so some details - // are removed from computations (e.g. bytes required to encode vector length) - - // structures in `finality_grandpa` crate are not implementing `MaxEncodedLength`, so - // here's our estimation for the `finality_grandpa::Commit` struct size - // - // precommit is: hash + number - // signed precommit is: precommit + signature (64b) + authority id - // commit is: hash + number + vec of signed precommits - let signed_precommit_size: u32 = BlockNumberOf::::max_encoded_len() - .saturating_add(HashOf::::max_encoded_len().saturated_into()) - .saturating_add(64) - .saturating_add(AuthorityId::max_encoded_len().saturated_into()) - .saturated_into(); - let max_expected_signed_commit_size = signed_precommit_size - .saturating_mul(required_precommits) - .saturating_add(BlockNumberOf::::max_encoded_len().saturated_into()) - .saturating_add(HashOf::::max_encoded_len().saturated_into()); - - // justification is a signed GRANDPA commit, `votes_ancestries` vector and round number - let max_expected_votes_ancestries_size = C::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY - .saturating_mul(C::AVERAGE_HEADER_SIZE_IN_JUSTIFICATION); - - 8u32.saturating_add(max_expected_signed_commit_size) - .saturating_add(max_expected_votes_ancestries_size) - } - - pub fn commit_target_id(&self) -> HeaderId { - HeaderId(self.commit.target_number, self.commit.target_hash) - } -} - -impl crate::FinalityProof for GrandpaJustification { - fn target_header_number(&self) -> H::Number { - self.commit.target_number - } -} - -/// Justification verification error. -#[derive(Eq, RuntimeDebug, PartialEq)] -pub enum Error { - /// Failed to decode justification. - JustificationDecode, - /// Justification is finalizing unexpected header. - InvalidJustificationTarget, - /// Justification contains redundant votes. - RedundantVotesInJustification, - /// Justification contains unknown authority precommit. - UnknownAuthorityVote, - /// Justification contains duplicate authority precommit. - DuplicateAuthorityVote, - /// The authority has provided an invalid signature. - InvalidAuthoritySignature, - /// The justification contains precommit for header that is not a descendant of the commit - /// header. - UnrelatedAncestryVote, - /// The cumulative weight of all votes in the justification is not enough to justify commit - /// header finalization. - TooLowCumulativeWeight, - /// The justification contains extra (unused) headers in its `votes_ancestries` field. - RedundantVotesAncestries, -} - -/// Given GRANDPA authorities set size, return number of valid authorities votes that the -/// justification must have to be valid. -/// -/// This function assumes that all authorities have the same vote weight. -pub fn required_justification_precommits(authorities_set_length: u32) -> u32 { - authorities_set_length - authorities_set_length.saturating_sub(1) / 3 -} - -/// Decode justification target. -pub fn decode_justification_target( - raw_justification: &[u8], -) -> Result<(Header::Hash, Header::Number), Error> { - GrandpaJustification::
::decode(&mut &*raw_justification) - .map(|justification| (justification.commit.target_hash, justification.commit.target_number)) - .map_err(|_| Error::JustificationDecode) -} - -/// Verify and optimize given justification by removing unknown and duplicate votes. -pub fn verify_and_optimize_justification( - finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, - justification: &mut GrandpaJustification
, -) -> Result<(), Error> { - let mut optimizer = OptimizationCallbacks { - extra_precommits: vec![], - redundant_votes_ancestries: Default::default(), - }; - verify_justification_with_callbacks( - finalized_target, - authorities_set_id, - authorities_set, - justification, - &mut optimizer, - )?; - optimizer.optimize(justification); - - Ok(()) -} - -/// Verify that justification, that is generated by given authority set, finalizes given header. -pub fn verify_justification( - finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, - justification: &GrandpaJustification
, -) -> Result<(), Error> { - verify_justification_with_callbacks( - finalized_target, - authorities_set_id, - authorities_set, - justification, - &mut StrictVerificationCallbacks, - ) -} - -/// Verification callbacks. -trait VerificationCallbacks { - /// Called when we see a precommit from unknown authority. - fn on_unkown_authority(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit with duplicate vote from known authority. - fn on_duplicate_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit with an invalid signature. - fn on_invalid_authority_signature(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit after we've collected enough votes from authorities. - fn on_redundant_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit that is not a descendant of the commit target. - fn on_unrelated_ancestry_vote(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when there are redundant headers in the votes ancestries. - fn on_redundant_votes_ancestries( - &mut self, - redundant_votes_ancestries: BTreeSet, - ) -> Result<(), Error>; -} - -/// Verification callbacks that reject all unknown, duplicate or redundant votes. -struct StrictVerificationCallbacks; - -impl VerificationCallbacks
for StrictVerificationCallbacks { - fn on_unkown_authority(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::UnknownAuthorityVote) - } - - fn on_duplicate_authority_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::DuplicateAuthorityVote) - } - - fn on_invalid_authority_signature(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::InvalidAuthoritySignature) - } - - fn on_redundant_authority_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::RedundantVotesInJustification) - } - - fn on_unrelated_ancestry_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::UnrelatedAncestryVote) - } - - fn on_redundant_votes_ancestries( - &mut self, - _redundant_votes_ancestries: BTreeSet, - ) -> Result<(), Error> { - Err(Error::RedundantVotesAncestries) - } -} - -/// Verification callbacks for justification optimization. -struct OptimizationCallbacks { - extra_precommits: Vec, - redundant_votes_ancestries: BTreeSet, -} - -impl OptimizationCallbacks
{ - fn optimize(self, justification: &mut GrandpaJustification
) { - for invalid_precommit_idx in self.extra_precommits.into_iter().rev() { - justification.commit.precommits.remove(invalid_precommit_idx); - } - if !self.redundant_votes_ancestries.is_empty() { - justification - .votes_ancestries - .retain(|header| !self.redundant_votes_ancestries.contains(&header.hash())) - } - } -} - -impl VerificationCallbacks
for OptimizationCallbacks
{ - fn on_unkown_authority(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_duplicate_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_invalid_authority_signature(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_redundant_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_unrelated_ancestry_vote(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_redundant_votes_ancestries( - &mut self, - redundant_votes_ancestries: BTreeSet, - ) -> Result<(), Error> { - self.redundant_votes_ancestries = redundant_votes_ancestries; - Ok(()) - } -} - -/// Verify that justification, that is generated by given authority set, finalizes given header. -fn verify_justification_with_callbacks>( - finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, - justification: &GrandpaJustification
, - callbacks: &mut C, -) -> Result<(), Error> { - // ensure that it is justification for the expected header - if (justification.commit.target_hash, justification.commit.target_number) != finalized_target { - return Err(Error::InvalidJustificationTarget) - } - - let threshold = authorities_set.threshold().get(); - let mut chain = AncestryChain::new(justification); - let mut signature_buffer = Vec::new(); - let mut votes = BTreeSet::new(); - let mut cumulative_weight = 0u64; - - for (precommit_idx, signed) in justification.commit.precommits.iter().enumerate() { - // if we have collected enough precommits, we probabably want to fail/remove extra - // precommits - if cumulative_weight >= threshold { - callbacks.on_redundant_authority_vote(precommit_idx)?; - continue - } - - // authority must be in the set - let authority_info = match authorities_set.get(&signed.id) { - Some(authority_info) => authority_info, - None => { - callbacks.on_unkown_authority(precommit_idx)?; - continue - }, - }; - - // check if authority has already voted in the same round. - // - // there's a lot of code in `validate_commit` and `import_precommit` functions inside - // `finality-grandpa` crate (mostly related to reporting equivocations). But the only thing - // that we care about is that only first vote from the authority is accepted - if votes.contains(&signed.id) { - callbacks.on_duplicate_authority_vote(precommit_idx)?; - continue - } - - // all precommits must be descendants of the target block - let route = - match chain.ancestry(&signed.precommit.target_hash, &signed.precommit.target_number) { - Some(route) => route, - None => { - callbacks.on_unrelated_ancestry_vote(precommit_idx)?; - continue - }, - }; - - // verify authority signature - if !sp_consensus_grandpa::check_message_signature_with_buffer( - &finality_grandpa::Message::Precommit(signed.precommit.clone()), - &signed.id, - &signed.signature, - justification.round, - authorities_set_id, - &mut signature_buffer, - ) { - callbacks.on_invalid_authority_signature(precommit_idx)?; - continue - } - - // now we can count the vote since we know that it is valid - votes.insert(signed.id.clone()); - chain.mark_route_as_visited(route); - cumulative_weight = cumulative_weight.saturating_add(authority_info.weight().get()); - } - - // check that the cumulative weight of validators that voted for the justification target (or - // one of its descendents) is larger than the required threshold. - if cumulative_weight < threshold { - return Err(Error::TooLowCumulativeWeight) - } - - // check that there are no extra headers in the justification - if !chain.is_fully_visited() { - callbacks.on_redundant_votes_ancestries(chain.unvisited)?; - } - - Ok(()) -} - -/// Votes ancestries with useful methods. -#[derive(RuntimeDebug)] -pub struct AncestryChain { - /// We expect all forks in the ancestry chain to be descendants of base. - base: HeaderId, - /// Header hash => parent header hash mapping. - pub parents: BTreeMap, - /// Hashes of headers that were not visited by `ancestry()`. - pub unvisited: BTreeSet, -} - -impl AncestryChain
{ - /// Create new ancestry chain. - pub fn new(justification: &GrandpaJustification
) -> AncestryChain
{ - let mut parents = BTreeMap::new(); - let mut unvisited = BTreeSet::new(); - for ancestor in &justification.votes_ancestries { - let hash = ancestor.hash(); - let parent_hash = *ancestor.parent_hash(); - parents.insert(hash, parent_hash); - unvisited.insert(hash); - } - AncestryChain { base: justification.commit_target_id(), parents, unvisited } - } - - /// Returns a route if the precommit target block is a descendant of the `base` block. - pub fn ancestry( - &self, - precommit_target_hash: &Header::Hash, - precommit_target_number: &Header::Number, - ) -> Option> { - if precommit_target_number < &self.base.number() { - return None - } - - let mut route = vec![]; - let mut current_hash = *precommit_target_hash; - loop { - if current_hash == self.base.hash() { - break - } - - current_hash = match self.parents.get(¤t_hash) { - Some(parent_hash) => { - let is_visited_before = self.unvisited.get(¤t_hash).is_none(); - if is_visited_before { - // If the current header has been visited in a previous call, it is a - // descendent of `base` (we assume that the previous call was successful). - return Some(route) - } - route.push(current_hash); - - *parent_hash - }, - None => return None, - }; - } - - Some(route) - } - - fn mark_route_as_visited(&mut self, route: Vec) { - for hash in route { - self.unvisited.remove(&hash); - } - } - - fn is_fully_visited(&self) -> bool { - self.unvisited.is_empty() - } -} diff --git a/bridges/primitives/header-chain/src/justification/mod.rs b/bridges/primitives/header-chain/src/justification/mod.rs new file mode 100644 index 00000000000..17be6cfd796 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/mod.rs @@ -0,0 +1,127 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for checking GRANDPA Finality Proofs. +//! +//! Adapted copy of substrate/client/finality-grandpa/src/justification.rs. If origin +//! will ever be moved to the sp_consensus_grandpa, we should reuse that implementation. + +mod verification; + +use crate::ChainWithGrandpa; +pub use verification::{ + equivocation::{EquivocationsCollector, Error as EquivocationsCollectorError}, + optimizer::verify_and_optimize_justification, + strict::verify_justification, + AncestryChain, Error as JustificationVerificationError, PrecommitError, +}; + +use bp_runtime::{BlockNumberOf, Chain, HashOf, HeaderId}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{RuntimeDebug, RuntimeDebugNoBound}; +use scale_info::TypeInfo; +use sp_consensus_grandpa::{AuthorityId, AuthoritySignature}; +use sp_runtime::{traits::Header as HeaderT, SaturatedConversion}; +use sp_std::prelude::*; + +/// A GRANDPA Justification is a proof that a given header was finalized +/// at a certain height and with a certain set of authorities. +/// +/// This particular proof is used to prove that headers on a bridged chain +/// (so not our chain) have been finalized correctly. +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound)] +pub struct GrandpaJustification { + /// The round (voting period) this justification is valid for. + pub round: u64, + /// The set of votes for the chain which is to be finalized. + pub commit: + finality_grandpa::Commit, + /// A proof that the chain of blocks in the commit are related to each other. + pub votes_ancestries: Vec
, +} + +impl GrandpaJustification { + /// Returns reasonable size of justification using constants from the provided chain. + /// + /// An imprecise analogue of `MaxEncodedLen` implementation. We don't use it for + /// any precise calculations - that's just an estimation. + pub fn max_reasonable_size(required_precommits: u32) -> u32 + where + C: Chain + ChainWithGrandpa, + { + // we don't need precise results here - just estimations, so some details + // are removed from computations (e.g. bytes required to encode vector length) + + // structures in `finality_grandpa` crate are not implementing `MaxEncodedLength`, so + // here's our estimation for the `finality_grandpa::Commit` struct size + // + // precommit is: hash + number + // signed precommit is: precommit + signature (64b) + authority id + // commit is: hash + number + vec of signed precommits + let signed_precommit_size: u32 = BlockNumberOf::::max_encoded_len() + .saturating_add(HashOf::::max_encoded_len().saturated_into()) + .saturating_add(64) + .saturating_add(AuthorityId::max_encoded_len().saturated_into()) + .saturated_into(); + let max_expected_signed_commit_size = signed_precommit_size + .saturating_mul(required_precommits) + .saturating_add(BlockNumberOf::::max_encoded_len().saturated_into()) + .saturating_add(HashOf::::max_encoded_len().saturated_into()); + + let max_expected_votes_ancestries_size = C::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY + .saturating_mul(C::AVERAGE_HEADER_SIZE_IN_JUSTIFICATION); + + // justification is round number (u64=8b), a signed GRANDPA commit and the + // `votes_ancestries` vector + 8u32.saturating_add(max_expected_signed_commit_size) + .saturating_add(max_expected_votes_ancestries_size) + } + + /// Return identifier of header that this justification claims to finalize. + pub fn commit_target_id(&self) -> HeaderId { + HeaderId(self.commit.target_number, self.commit.target_hash) + } +} + +impl crate::FinalityProof for GrandpaJustification { + fn target_header_number(&self) -> H::Number { + self.commit.target_number + } +} + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum Error { + /// Failed to decode justification. + JustificationDecode, +} + +/// Given GRANDPA authorities set size, return number of valid authorities votes that the +/// justification must have to be valid. +/// +/// This function assumes that all authorities have the same vote weight. +pub fn required_justification_precommits(authorities_set_length: u32) -> u32 { + authorities_set_length - authorities_set_length.saturating_sub(1) / 3 +} + +/// Decode justification target. +pub fn decode_justification_target( + raw_justification: &[u8], +) -> Result<(Header::Hash, Header::Number), Error> { + GrandpaJustification::
::decode(&mut &*raw_justification) + .map(|justification| (justification.commit.target_hash, justification.commit.target_number)) + .map_err(|_| Error::JustificationDecode) +} diff --git a/bridges/primitives/header-chain/src/justification/verification/equivocation.rs b/bridges/primitives/header-chain/src/justification/verification/equivocation.rs new file mode 100644 index 00000000000..0ade3736c22 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/equivocation.rs @@ -0,0 +1,179 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for extracting equivocations from multiple GRANDPA Finality Proofs. + +use crate::justification::{ + verification::{ + Error as JustificationVerificationError, JustificationVerifier, PrecommitError, + SignedPrecommit, + }, + GrandpaJustification, +}; + +use crate::justification::verification::IterationFlow; +use finality_grandpa::voter_set::VoterSet; +use frame_support::RuntimeDebug; +use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, EquivocationProof, Precommit, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + prelude::*, +}; + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum Error { + /// Justification is targeting unexpected round. + InvalidRound, + /// Justification verification error. + JustificationVerification(JustificationVerificationError), +} + +enum AuthorityVotes { + SingleVote(SignedPrecommit
), + Equivocation( + finality_grandpa::Equivocation, AuthoritySignature>, + ), +} + +/// Structure that can extract equivocations from multiple GRANDPA justifications. +pub struct EquivocationsCollector<'a, Header: HeaderT> { + round: u64, + authorities_set_id: SetId, + authorities_set: &'a VoterSet, + + votes: BTreeMap>, +} + +impl<'a, Header: HeaderT> EquivocationsCollector<'a, Header> { + /// Create a new instance of `EquivocationsCollector`. + pub fn new( + authorities_set_id: SetId, + authorities_set: &'a VoterSet, + base_justification: &GrandpaJustification
, + ) -> Result { + let mut checker = Self { + round: base_justification.round, + authorities_set_id, + authorities_set, + votes: BTreeMap::new(), + }; + + checker.parse_justification(base_justification)?; + Ok(checker) + } + + /// Parse an additional justification for equivocations. + pub fn parse_justification( + &mut self, + justification: &GrandpaJustification
, + ) -> Result<(), Error> { + // The justification should target the same round as the base justification. + if self.round != justification.round { + return Err(Error::InvalidRound) + } + + self.verify_justification( + (justification.commit.target_hash, justification.commit.target_number), + self.authorities_set_id, + self.authorities_set, + justification, + ) + .map_err(Error::JustificationVerification) + } + + /// Extract the equivocation proofs that have been collected. + pub fn into_equivocation_proofs(self) -> Vec> { + let mut equivocations = vec![]; + for (_authority, vote) in self.votes { + if let AuthorityVotes::Equivocation(equivocation) = vote { + equivocations.push(EquivocationProof::new( + self.authorities_set_id, + sp_consensus_grandpa::Equivocation::Precommit(equivocation), + )); + } + } + + equivocations + } +} + +impl<'a, Header: HeaderT> JustificationVerifier
for EquivocationsCollector<'a, Header> { + fn process_redundant_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Ok(IterationFlow::Run) + } + + fn process_known_authority_vote( + &mut self, + _precommit_idx: usize, + _signed: &SignedPrecommit
, + ) -> Result { + Ok(IterationFlow::Run) + } + + fn process_unknown_authority_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Ok(()) + } + + fn process_unrelated_ancestry_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Ok(IterationFlow::Run) + } + + fn process_invalid_signature_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Ok(()) + } + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
) { + match self.votes.get_mut(&signed.id) { + Some(vote) => match vote { + AuthorityVotes::SingleVote(first_vote) => { + if first_vote.precommit != signed.precommit { + *vote = AuthorityVotes::Equivocation(finality_grandpa::Equivocation { + round_number: self.round, + identity: signed.id.clone(), + first: (first_vote.precommit.clone(), first_vote.signature.clone()), + second: (signed.precommit.clone(), signed.signature.clone()), + }); + } + }, + AuthorityVotes::Equivocation(_) => {}, + }, + None => { + self.votes.insert(signed.id.clone(), AuthorityVotes::SingleVote(signed.clone())); + }, + } + } + + fn process_redundant_votes_ancestries( + &mut self, + _redundant_votes_ancestries: BTreeSet, + ) -> Result<(), JustificationVerificationError> { + Ok(()) + } +} diff --git a/bridges/primitives/header-chain/src/justification/verification/mod.rs b/bridges/primitives/header-chain/src/justification/verification/mod.rs new file mode 100644 index 00000000000..7cec1f14e96 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/mod.rs @@ -0,0 +1,279 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for checking GRANDPA Finality Proofs. + +pub mod equivocation; +pub mod optimizer; +pub mod strict; + +use crate::justification::GrandpaJustification; + +use bp_runtime::HeaderId; +use finality_grandpa::voter_set::VoterSet; +use frame_support::RuntimeDebug; +use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + prelude::*, +}; + +type SignedPrecommit
= finality_grandpa::SignedPrecommit< +
::Hash, +
::Number, + AuthoritySignature, + AuthorityId, +>; + +/// Votes ancestries with useful methods. +#[derive(RuntimeDebug)] +pub struct AncestryChain { + /// We expect all forks in the ancestry chain to be descendants of base. + base: HeaderId, + /// Header hash => parent header hash mapping. + pub parents: BTreeMap, + /// Hashes of headers that were not visited by `ancestry()`. + pub unvisited: BTreeSet, +} + +impl AncestryChain
{ + /// Create new ancestry chain. + pub fn new(justification: &GrandpaJustification
) -> AncestryChain
{ + let mut parents = BTreeMap::new(); + let mut unvisited = BTreeSet::new(); + for ancestor in &justification.votes_ancestries { + let hash = ancestor.hash(); + let parent_hash = *ancestor.parent_hash(); + parents.insert(hash, parent_hash); + unvisited.insert(hash); + } + AncestryChain { base: justification.commit_target_id(), parents, unvisited } + } + + /// Returns a route if the precommit target block is a descendant of the `base` block. + pub fn ancestry( + &self, + precommit_target_hash: &Header::Hash, + precommit_target_number: &Header::Number, + ) -> Option> { + if precommit_target_number < &self.base.number() { + return None + } + + let mut route = vec![]; + let mut current_hash = *precommit_target_hash; + loop { + if current_hash == self.base.hash() { + break + } + + current_hash = match self.parents.get(¤t_hash) { + Some(parent_hash) => { + let is_visited_before = self.unvisited.get(¤t_hash).is_none(); + if is_visited_before { + // If the current header has been visited in a previous call, it is a + // descendent of `base` (we assume that the previous call was successful). + return Some(route) + } + route.push(current_hash); + + *parent_hash + }, + None => return None, + }; + } + + Some(route) + } + + fn mark_route_as_visited(&mut self, route: Vec) { + for hash in route { + self.unvisited.remove(&hash); + } + } + + fn is_fully_visited(&self) -> bool { + self.unvisited.is_empty() + } +} + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum Error { + /// Justification is finalizing unexpected header. + InvalidJustificationTarget, + /// Error validating a precommit + Precommit(PrecommitError), + /// The cumulative weight of all votes in the justification is not enough to justify commit + /// header finalization. + TooLowCumulativeWeight, + /// The justification contains extra (unused) headers in its `votes_ancestries` field. + RedundantVotesAncestries, +} + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum PrecommitError { + /// Justification contains redundant votes. + RedundantAuthorityVote, + /// Justification contains unknown authority precommit. + UnknownAuthorityVote, + /// Justification contains duplicate authority precommit. + DuplicateAuthorityVote, + /// The authority has provided an invalid signature. + InvalidAuthoritySignature, + /// The justification contains precommit for header that is not a descendant of the commit + /// header. + UnrelatedAncestryVote, +} + +enum IterationFlow { + Run, + Skip, +} + +/// Verification callbacks. +trait JustificationVerifier { + fn process_redundant_vote( + &mut self, + precommit_idx: usize, + ) -> Result; + + fn process_known_authority_vote( + &mut self, + precommit_idx: usize, + signed: &SignedPrecommit
, + ) -> Result; + + fn process_unknown_authority_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError>; + + fn process_unrelated_ancestry_vote( + &mut self, + precommit_idx: usize, + ) -> Result; + + fn process_invalid_signature_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError>; + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
); + + /// Called when there are redundant headers in the votes ancestries. + fn process_redundant_votes_ancestries( + &mut self, + redundant_votes_ancestries: BTreeSet, + ) -> Result<(), Error>; + + fn verify_justification( + &mut self, + finalized_target: (Header::Hash, Header::Number), + authorities_set_id: SetId, + authorities_set: &VoterSet, + justification: &GrandpaJustification
, + ) -> Result<(), Error> { + // ensure that it is justification for the expected header + if (justification.commit.target_hash, justification.commit.target_number) != + finalized_target + { + return Err(Error::InvalidJustificationTarget) + } + + let threshold = authorities_set.threshold().get(); + let mut chain = AncestryChain::new(justification); + let mut signature_buffer = Vec::new(); + let mut cumulative_weight = 0u64; + + for (precommit_idx, signed) in justification.commit.precommits.iter().enumerate() { + if cumulative_weight >= threshold { + let action = + self.process_redundant_vote(precommit_idx).map_err(Error::Precommit)?; + if matches!(action, IterationFlow::Skip) { + continue + } + } + + // authority must be in the set + let authority_info = match authorities_set.get(&signed.id) { + Some(authority_info) => { + // The implementer may want to do extra checks here. + // For example to see if the authority has already voted in the same round. + let action = self + .process_known_authority_vote(precommit_idx, signed) + .map_err(Error::Precommit)?; + if matches!(action, IterationFlow::Skip) { + continue + } + + authority_info + }, + None => { + self.process_unknown_authority_vote(precommit_idx).map_err(Error::Precommit)?; + continue + }, + }; + + // all precommits must be descendants of the target block + let maybe_route = + chain.ancestry(&signed.precommit.target_hash, &signed.precommit.target_number); + if maybe_route.is_none() { + let action = self + .process_unrelated_ancestry_vote(precommit_idx) + .map_err(Error::Precommit)?; + if matches!(action, IterationFlow::Skip) { + continue + } + } + + // verify authority signature + if !sp_consensus_grandpa::check_message_signature_with_buffer( + &finality_grandpa::Message::Precommit(signed.precommit.clone()), + &signed.id, + &signed.signature, + justification.round, + authorities_set_id, + &mut signature_buffer, + ) { + self.process_invalid_signature_vote(precommit_idx).map_err(Error::Precommit)?; + continue + } + + // now we can count the vote since we know that it is valid + self.process_valid_vote(signed); + if let Some(route) = maybe_route { + chain.mark_route_as_visited(route); + cumulative_weight = cumulative_weight.saturating_add(authority_info.weight().get()); + } + } + + // check that the cumulative weight of validators that voted for the justification target + // (or one of its descendents) is larger than the required threshold. + if cumulative_weight < threshold { + return Err(Error::TooLowCumulativeWeight) + } + + // check that there are no extra headers in the justification + if !chain.is_fully_visited() { + self.process_redundant_votes_ancestries(chain.unvisited)?; + } + + Ok(()) + } +} diff --git a/bridges/primitives/header-chain/src/justification/verification/optimizer.rs b/bridges/primitives/header-chain/src/justification/verification/optimizer.rs new file mode 100644 index 00000000000..4cc6778ff51 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/optimizer.rs @@ -0,0 +1,132 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for optimizing GRANDPA Finality Proofs. + +use crate::justification::{ + verification::{Error, JustificationVerifier, PrecommitError}, + GrandpaJustification, +}; + +use crate::justification::verification::{IterationFlow, SignedPrecommit}; +use finality_grandpa::voter_set::VoterSet; +use sp_consensus_grandpa::{AuthorityId, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::{collections::btree_set::BTreeSet, prelude::*}; + +// Verification callbacks for justification optimization. +struct JustificationOptimizer { + votes: BTreeSet, + + extra_precommits: Vec, + redundant_votes_ancestries: BTreeSet, +} + +impl JustificationOptimizer
{ + fn optimize(self, justification: &mut GrandpaJustification
) { + for invalid_precommit_idx in self.extra_precommits.into_iter().rev() { + justification.commit.precommits.remove(invalid_precommit_idx); + } + if !self.redundant_votes_ancestries.is_empty() { + justification + .votes_ancestries + .retain(|header| !self.redundant_votes_ancestries.contains(&header.hash())) + } + } +} + +impl JustificationVerifier
for JustificationOptimizer
{ + fn process_redundant_vote( + &mut self, + precommit_idx: usize, + ) -> Result { + self.extra_precommits.push(precommit_idx); + Ok(IterationFlow::Skip) + } + + fn process_known_authority_vote( + &mut self, + precommit_idx: usize, + signed: &SignedPrecommit
, + ) -> Result { + // Skip duplicate votes + if self.votes.contains(&signed.id) { + self.extra_precommits.push(precommit_idx); + return Ok(IterationFlow::Skip) + } + + Ok(IterationFlow::Run) + } + + fn process_unknown_authority_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError> { + self.extra_precommits.push(precommit_idx); + Ok(()) + } + + fn process_unrelated_ancestry_vote( + &mut self, + precommit_idx: usize, + ) -> Result { + self.extra_precommits.push(precommit_idx); + Ok(IterationFlow::Skip) + } + + fn process_invalid_signature_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError> { + self.extra_precommits.push(precommit_idx); + Ok(()) + } + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
) { + self.votes.insert(signed.id.clone()); + } + + fn process_redundant_votes_ancestries( + &mut self, + redundant_votes_ancestries: BTreeSet, + ) -> Result<(), Error> { + self.redundant_votes_ancestries = redundant_votes_ancestries; + Ok(()) + } +} + +/// Verify and optimize given justification by removing unknown and duplicate votes. +pub fn verify_and_optimize_justification( + finalized_target: (Header::Hash, Header::Number), + authorities_set_id: SetId, + authorities_set: &VoterSet, + justification: &mut GrandpaJustification
, +) -> Result<(), Error> { + let mut optimizer = JustificationOptimizer { + votes: BTreeSet::new(), + extra_precommits: vec![], + redundant_votes_ancestries: Default::default(), + }; + optimizer.verify_justification( + finalized_target, + authorities_set_id, + authorities_set, + justification, + )?; + optimizer.optimize(justification); + + Ok(()) +} diff --git a/bridges/primitives/header-chain/src/justification/verification/strict.rs b/bridges/primitives/header-chain/src/justification/verification/strict.rs new file mode 100644 index 00000000000..da936c23582 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/strict.rs @@ -0,0 +1,106 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for checking if GRANDPA Finality Proofs are valid and optimal. + +use crate::justification::{ + verification::{Error, JustificationVerifier, PrecommitError}, + GrandpaJustification, +}; + +use crate::justification::verification::{IterationFlow, SignedPrecommit}; +use finality_grandpa::voter_set::VoterSet; +use sp_consensus_grandpa::{AuthorityId, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::collections::btree_set::BTreeSet; + +/// Verification callbacks that reject all unknown, duplicate or redundant votes. +struct StrictJustificationVerifier { + votes: BTreeSet, +} + +impl JustificationVerifier
for StrictJustificationVerifier { + fn process_redundant_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Err(PrecommitError::RedundantAuthorityVote) + } + + fn process_known_authority_vote( + &mut self, + _precommit_idx: usize, + signed: &SignedPrecommit
, + ) -> Result { + if self.votes.contains(&signed.id) { + // There's a lot of code in `validate_commit` and `import_precommit` functions + // inside `finality-grandpa` crate (mostly related to reporting equivocations). + // But the only thing that we care about is that only first vote from the + // authority is accepted + return Err(PrecommitError::DuplicateAuthorityVote) + } + + Ok(IterationFlow::Run) + } + + fn process_unknown_authority_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Err(PrecommitError::UnknownAuthorityVote) + } + + fn process_unrelated_ancestry_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Err(PrecommitError::UnrelatedAncestryVote) + } + + fn process_invalid_signature_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Err(PrecommitError::InvalidAuthoritySignature) + } + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
) { + self.votes.insert(signed.id.clone()); + } + + fn process_redundant_votes_ancestries( + &mut self, + _redundant_votes_ancestries: BTreeSet, + ) -> Result<(), Error> { + Err(Error::RedundantVotesAncestries) + } +} + +/// Verify that justification, that is generated by given authority set, finalizes given header. +pub fn verify_justification( + finalized_target: (Header::Hash, Header::Number), + authorities_set_id: SetId, + authorities_set: &VoterSet, + justification: &GrandpaJustification
, +) -> Result<(), Error> { + let mut verifier = StrictJustificationVerifier { votes: BTreeSet::new() }; + verifier.verify_justification( + finalized_target, + authorities_set_id, + authorities_set, + justification, + ) +} diff --git a/bridges/primitives/header-chain/src/lib.rs b/bridges/primitives/header-chain/src/lib.rs index 5268e7d5c5f..720dafdd93b 100644 --- a/bridges/primitives/header-chain/src/lib.rs +++ b/bridges/primitives/header-chain/src/lib.rs @@ -21,7 +21,7 @@ use bp_runtime::{ BasicOperatingMode, Chain, HashOf, HasherOf, HeaderOf, RawStorageProof, StorageProofChecker, - StorageProofError, + StorageProofError, UnderlyingChainProvider, }; use codec::{Codec, Decode, Encode, EncodeLike, MaxEncodedLen}; use core::{clone::Clone, cmp::Eq, default::Default, fmt::Debug}; @@ -172,6 +172,15 @@ impl ConsensusLogReader for GrandpaConsensusLogReader { } } +/// The Grandpa-related info associated to a header. +#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)] +pub struct HeaderGrandpaInfo { + /// The header justification + pub justification: justification::GrandpaJustification
, + /// The authority set introduced by the header. + pub authority_set: Option, +} + /// A minimized version of `pallet-bridge-grandpa::Call` that can be used without a runtime. #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] #[allow(non_camel_case_types)] @@ -234,3 +243,17 @@ pub trait ChainWithGrandpa: Chain { /// refund amount and doing calls which exceed the limit, may be costly to submitter. const AVERAGE_HEADER_SIZE_IN_JUSTIFICATION: u32; } + +/// A trait that provides the type of the underlying `ChainWithGrandpa`. +pub trait UnderlyingChainWithGrandpaProvider: UnderlyingChainProvider { + /// Underlying `ChainWithGrandpa` type. + type ChainWithGrandpa: ChainWithGrandpa; +} + +impl UnderlyingChainWithGrandpaProvider for T +where + T: UnderlyingChainProvider, + T::Chain: ChainWithGrandpa, +{ + type ChainWithGrandpa = T::Chain; +} diff --git a/bridges/primitives/header-chain/tests/implementation_match.rs b/bridges/primitives/header-chain/tests/implementation_match.rs index d5e42e21497..c4cd7f5f5b2 100644 --- a/bridges/primitives/header-chain/tests/implementation_match.rs +++ b/bridges/primitives/header-chain/tests/implementation_match.rs @@ -21,7 +21,9 @@ //! Some of tests in this module may partially duplicate tests from `justification.rs`, //! but their purpose is different. -use bp_header_chain::justification::{verify_justification, Error, GrandpaJustification}; +use bp_header_chain::justification::{ + verify_justification, GrandpaJustification, JustificationVerificationError, PrecommitError, +}; use bp_test_utils::{ header_id, make_justification_for_header, signed_precommit, test_header, Account, JustificationGeneratorParams, ALICE, BOB, CHARLIE, DAVE, EVE, FERDIE, TEST_GRANDPA_SET_ID, @@ -85,13 +87,6 @@ fn minimal_accounts_set() -> Vec<(Account, AuthorityWeight)> { vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1), (DAVE, 1)] } -/// Get a minimal subset of GRANDPA authorities that have enough cumulative vote weight to justify a -/// header finality. -pub fn minimal_voter_set() -> VoterSet { - VoterSet::new(minimal_accounts_set().iter().map(|(id, w)| (AuthorityId::from(*id), *w))) - .unwrap() -} - /// Make a valid GRANDPA justification with sensible defaults. pub fn make_default_justification(header: &TestHeader) -> GrandpaJustification { make_justification_for_header(JustificationGeneratorParams { @@ -124,7 +119,7 @@ fn same_result_when_precommit_target_has_lower_number_than_commit_target() { &full_voter_set(), &justification, ), - Err(Error::UnrelatedAncestryVote), + Err(JustificationVerificationError::Precommit(PrecommitError::UnrelatedAncestryVote)), ); // original implementation returns `Ok(validation_result)` @@ -157,7 +152,7 @@ fn same_result_when_precommit_target_is_not_descendant_of_commit_target() { &full_voter_set(), &justification, ), - Err(Error::UnrelatedAncestryVote), + Err(JustificationVerificationError::Precommit(PrecommitError::UnrelatedAncestryVote)), ); // original implementation returns `Ok(validation_result)` @@ -191,7 +186,7 @@ fn same_result_when_there_are_not_enough_cumulative_weight_to_finalize_commit_ta &full_voter_set(), &justification, ), - Err(Error::TooLowCumulativeWeight), + Err(JustificationVerificationError::TooLowCumulativeWeight), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == false`. @@ -229,7 +224,7 @@ fn different_result_when_justification_contains_duplicate_vote() { &full_voter_set(), &justification, ), - Err(Error::DuplicateAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -270,7 +265,7 @@ fn different_results_when_authority_equivocates_once_in_a_round() { &full_voter_set(), &justification, ), - Err(Error::DuplicateAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -323,7 +318,7 @@ fn different_results_when_authority_equivocates_twice_in_a_round() { &full_voter_set(), &justification, ), - Err(Error::DuplicateAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -362,7 +357,7 @@ fn different_results_when_there_are_more_than_enough_votes() { &full_voter_set(), &justification, ), - Err(Error::RedundantVotesInJustification), + Err(JustificationVerificationError::Precommit(PrecommitError::RedundantAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -403,7 +398,7 @@ fn different_results_when_there_is_a_vote_of_unknown_authority() { &full_voter_set(), &justification, ), - Err(Error::UnknownAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::UnknownAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. diff --git a/bridges/primitives/header-chain/tests/justification/equivocation.rs b/bridges/primitives/header-chain/tests/justification/equivocation.rs new file mode 100644 index 00000000000..072d5668ede --- /dev/null +++ b/bridges/primitives/header-chain/tests/justification/equivocation.rs @@ -0,0 +1,124 @@ +// Copyright 2020-2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Tests for Grandpa equivocations collector code. + +use bp_header_chain::justification::EquivocationsCollector; +use bp_test_utils::*; +use finality_grandpa::Precommit; +use sp_consensus_grandpa::EquivocationProof; + +type TestHeader = sp_runtime::testing::Header; + +#[test] +fn duplicate_votes_are_not_considered_equivocations() { + let voter_set = voter_set(); + let base_justification = make_default_justification::(&test_header(1)); + + let mut collector = + EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap(); + collector.parse_justification(&base_justification.clone()).unwrap(); + + assert_eq!(collector.into_equivocation_proofs().len(), 0); +} + +#[test] +fn equivocations_are_detected_in_base_justification_redundant_votes() { + let voter_set = voter_set(); + let mut base_justification = make_default_justification::(&test_header(1)); + + let first_vote = base_justification.commit.precommits[0].clone(); + let equivocation = signed_precommit::( + &ALICE, + header_id::(1), + base_justification.round, + TEST_GRANDPA_SET_ID, + ); + base_justification.commit.precommits.push(equivocation.clone()); + + let collector = + EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap(); + + assert_eq!( + collector.into_equivocation_proofs(), + vec![EquivocationProof::new( + 1, + sp_consensus_grandpa::Equivocation::Precommit(finality_grandpa::Equivocation { + round_number: 1, + identity: ALICE.into(), + first: ( + Precommit { + target_hash: first_vote.precommit.target_hash, + target_number: first_vote.precommit.target_number + }, + first_vote.signature + ), + second: ( + Precommit { + target_hash: equivocation.precommit.target_hash, + target_number: equivocation.precommit.target_number + }, + equivocation.signature + ) + }) + )] + ); +} + +#[test] +fn equivocations_are_detected_in_extra_justification_redundant_votes() { + let voter_set = voter_set(); + let base_justification = make_default_justification::(&test_header(1)); + let first_vote = base_justification.commit.precommits[0].clone(); + + let mut extra_justification = base_justification.clone(); + let equivocation = signed_precommit::( + &ALICE, + header_id::(1), + base_justification.round, + TEST_GRANDPA_SET_ID, + ); + extra_justification.commit.precommits.push(equivocation.clone()); + + let mut collector = + EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap(); + collector.parse_justification(&extra_justification).unwrap(); + + assert_eq!( + collector.into_equivocation_proofs(), + vec![EquivocationProof::new( + 1, + sp_consensus_grandpa::Equivocation::Precommit(finality_grandpa::Equivocation { + round_number: 1, + identity: ALICE.into(), + first: ( + Precommit { + target_hash: first_vote.precommit.target_hash, + target_number: first_vote.precommit.target_number + }, + first_vote.signature + ), + second: ( + Precommit { + target_hash: equivocation.precommit.target_hash, + target_number: equivocation.precommit.target_number + }, + equivocation.signature + ) + }) + )] + ); +} diff --git a/bridges/primitives/header-chain/tests/justification.rs b/bridges/primitives/header-chain/tests/justification/optimizer.rs similarity index 55% rename from bridges/primitives/header-chain/tests/justification.rs rename to bridges/primitives/header-chain/tests/justification/optimizer.rs index 26ed67fa65f..8d1ba5ac6fa 100644 --- a/bridges/primitives/header-chain/tests/justification.rs +++ b/bridges/primitives/header-chain/tests/justification/optimizer.rs @@ -14,189 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . -//! Tests for Grandpa Justification code. +//! Tests for Grandpa Justification optimizer code. -use bp_header_chain::justification::{ - required_justification_precommits, verify_and_optimize_justification, verify_justification, - Error, -}; +use bp_header_chain::justification::verify_and_optimize_justification; use bp_test_utils::*; use finality_grandpa::SignedPrecommit; use sp_consensus_grandpa::AuthoritySignature; type TestHeader = sp_runtime::testing::Header; -#[test] -fn valid_justification_accepted() { - let authorities = vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)]; - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: authorities.clone(), - ancestors: 7, - forks: 3, - }; - - let justification = make_justification_for_header::(params.clone()); - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Ok(()), - ); - - assert_eq!(justification.commit.precommits.len(), authorities.len()); - assert_eq!(justification.votes_ancestries.len(), params.ancestors as usize); -} - -#[test] -fn valid_justification_accepted_with_single_fork() { - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)], - ancestors: 5, - forks: 1, - }; - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &make_justification_for_header::(params) - ), - Ok(()), - ); -} - -#[test] -fn valid_justification_accepted_with_arbitrary_number_of_authorities() { - use finality_grandpa::voter_set::VoterSet; - use sp_consensus_grandpa::AuthorityId; - - let n = 15; - let required_signatures = required_justification_precommits(n as _); - let authorities = accounts(n).iter().map(|k| (*k, 1)).collect::>(); - - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: authorities.clone().into_iter().take(required_signatures as _).collect(), - ancestors: n.into(), - forks: required_signatures, - }; - - let authorities = authorities - .iter() - .map(|(id, w)| (AuthorityId::from(*id), *w)) - .collect::>(); - let voter_set = VoterSet::new(authorities).unwrap(); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set, - &make_justification_for_header::(params) - ), - Ok(()), - ); -} - -#[test] -fn justification_with_invalid_target_rejected() { - assert_eq!( - verify_justification::( - header_id::(2), - TEST_GRANDPA_SET_ID, - &voter_set(), - &make_default_justification::(&test_header(1)), - ), - Err(Error::InvalidJustificationTarget), - ); -} - -#[test] -fn justification_with_invalid_commit_rejected() { - let mut justification = make_default_justification::(&test_header(1)); - justification.commit.precommits.clear(); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Err(Error::TooLowCumulativeWeight), - ); -} - -#[test] -fn justification_with_invalid_authority_signature_rejected() { - let mut justification = make_default_justification::(&test_header(1)); - justification.commit.precommits[0].signature = - sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Err(Error::InvalidAuthoritySignature), - ); -} - -#[test] -fn justification_with_invalid_precommit_ancestry() { - let mut justification = make_default_justification::(&test_header(1)); - justification.votes_ancestries.push(test_header(10)); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Err(Error::RedundantVotesAncestries), - ); -} - -#[test] -fn justification_is_invalid_if_we_dont_meet_threshold() { - // Need at least three authorities to sign off or else the voter set threshold can't be reached - let authorities = vec![(ALICE, 1), (BOB, 1)]; - - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: authorities.clone(), - ancestors: 2 * authorities.len() as u32, - forks: 2, - }; - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &make_justification_for_header::(params) - ), - Err(Error::TooLowCumulativeWeight), - ); -} - #[test] fn optimizer_does_noting_with_minimal_justification() { let mut justification = make_default_justification::(&test_header(1)); diff --git a/bridges/primitives/header-chain/tests/justification/strict.rs b/bridges/primitives/header-chain/tests/justification/strict.rs new file mode 100644 index 00000000000..bf8fa5c9f45 --- /dev/null +++ b/bridges/primitives/header-chain/tests/justification/strict.rs @@ -0,0 +1,196 @@ +// Copyright 2020-2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Tests for Grandpa strict justification verifier code. + +use bp_header_chain::justification::{ + required_justification_precommits, verify_justification, JustificationVerificationError, + PrecommitError, +}; +use bp_test_utils::*; + +type TestHeader = sp_runtime::testing::Header; + +#[test] +fn valid_justification_accepted() { + let authorities = vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)]; + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: authorities.clone(), + ancestors: 7, + forks: 3, + }; + + let justification = make_justification_for_header::(params.clone()); + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Ok(()), + ); + + assert_eq!(justification.commit.precommits.len(), authorities.len()); + assert_eq!(justification.votes_ancestries.len(), params.ancestors as usize); +} + +#[test] +fn valid_justification_accepted_with_single_fork() { + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)], + ancestors: 5, + forks: 1, + }; + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &make_justification_for_header::(params) + ), + Ok(()), + ); +} + +#[test] +fn valid_justification_accepted_with_arbitrary_number_of_authorities() { + use finality_grandpa::voter_set::VoterSet; + use sp_consensus_grandpa::AuthorityId; + + let n = 15; + let required_signatures = required_justification_precommits(n as _); + let authorities = accounts(n).iter().map(|k| (*k, 1)).collect::>(); + + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: authorities.clone().into_iter().take(required_signatures as _).collect(), + ancestors: n.into(), + forks: required_signatures, + }; + + let authorities = authorities + .iter() + .map(|(id, w)| (AuthorityId::from(*id), *w)) + .collect::>(); + let voter_set = VoterSet::new(authorities).unwrap(); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set, + &make_justification_for_header::(params) + ), + Ok(()), + ); +} + +#[test] +fn justification_with_invalid_target_rejected() { + assert_eq!( + verify_justification::( + header_id::(2), + TEST_GRANDPA_SET_ID, + &voter_set(), + &make_default_justification::(&test_header(1)), + ), + Err(JustificationVerificationError::InvalidJustificationTarget), + ); +} + +#[test] +fn justification_with_invalid_commit_rejected() { + let mut justification = make_default_justification::(&test_header(1)); + justification.commit.precommits.clear(); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Err(JustificationVerificationError::TooLowCumulativeWeight), + ); +} + +#[test] +fn justification_with_invalid_authority_signature_rejected() { + let mut justification = make_default_justification::(&test_header(1)); + justification.commit.precommits[0].signature = + sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Err(JustificationVerificationError::Precommit(PrecommitError::InvalidAuthoritySignature)), + ); +} + +#[test] +fn justification_with_invalid_precommit_ancestry() { + let mut justification = make_default_justification::(&test_header(1)); + justification.votes_ancestries.push(test_header(10)); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Err(JustificationVerificationError::RedundantVotesAncestries), + ); +} + +#[test] +fn justification_is_invalid_if_we_dont_meet_threshold() { + // Need at least three authorities to sign off or else the voter set threshold can't be reached + let authorities = vec![(ALICE, 1), (BOB, 1)]; + + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: authorities.clone(), + ancestors: 2 * authorities.len() as u32, + forks: 2, + }; + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &make_justification_for_header::(params) + ), + Err(JustificationVerificationError::TooLowCumulativeWeight), + ); +} diff --git a/bridges/primitives/header-chain/tests/tests.rs b/bridges/primitives/header-chain/tests/tests.rs new file mode 100644 index 00000000000..7c525a9303a --- /dev/null +++ b/bridges/primitives/header-chain/tests/tests.rs @@ -0,0 +1,7 @@ +mod justification { + mod equivocation; + mod optimizer; + mod strict; +} + +mod implementation_match; diff --git a/bridges/primitives/messages/src/source_chain.rs b/bridges/primitives/messages/src/source_chain.rs index f3c50b84a09..09f6396ad74 100644 --- a/bridges/primitives/messages/src/source_chain.rs +++ b/bridges/primitives/messages/src/source_chain.rs @@ -71,11 +71,10 @@ pub trait TargetHeaderChain { /// Lane3 until some block, ...), then it may be built using this verifier. /// /// Any fee requirements should also be enforced here. -pub trait LaneMessageVerifier { +pub trait LaneMessageVerifier { /// Verify message payload and return Ok(()) if message is valid and allowed to be sent over the /// lane. fn verify_message( - submitter: &SenderOrigin, lane: &LaneId, outbound_data: &OutboundLaneData, payload: &Payload, @@ -124,32 +123,24 @@ pub struct SendMessageArtifacts { } /// Messages bridge API to be used from other pallets. -pub trait MessagesBridge { +pub trait MessagesBridge { /// Error type. type Error: Debug; /// Send message over the bridge. /// /// Returns unique message nonce or error if send has failed. - fn send_message( - sender: SenderOrigin, - lane: LaneId, - message: Payload, - ) -> Result; + fn send_message(lane: LaneId, message: Payload) -> Result; } /// Bridge that does nothing when message is being sent. #[derive(Eq, RuntimeDebug, PartialEq)] pub struct NoopMessagesBridge; -impl MessagesBridge for NoopMessagesBridge { +impl MessagesBridge for NoopMessagesBridge { type Error = &'static str; - fn send_message( - _sender: SenderOrigin, - _lane: LaneId, - _message: Payload, - ) -> Result { + fn send_message(_lane: LaneId, _message: Payload) -> Result { Ok(SendMessageArtifacts { nonce: 0 }) } } @@ -176,9 +167,8 @@ impl TargetHeaderChain for ForbidOutboun } } -impl LaneMessageVerifier for ForbidOutboundMessages { +impl LaneMessageVerifier for ForbidOutboundMessages { fn verify_message( - _submitter: &SenderOrigin, _lane: &LaneId, _outbound_data: &OutboundLaneData, _payload: &Payload, diff --git a/bridges/primitives/runtime/src/chain.rs b/bridges/primitives/runtime/src/chain.rs index 8c47662a7c1..a1b7e8d54b4 100644 --- a/bridges/primitives/runtime/src/chain.rs +++ b/bridges/primitives/runtime/src/chain.rs @@ -279,10 +279,11 @@ pub type TransactionEraOf = crate::TransactionEra, HashOf /// - `FinalityApi` /// - constants that are stringified names of runtime API methods: /// - `BEST_FINALIZED__HEADER_METHOD` +/// - `_ACCEPTED__FINALITY_PROOFS_METHOD` /// The name of the chain has to be specified in snake case (e.g. `rialto_parachain`). #[macro_export] macro_rules! decl_bridge_finality_runtime_apis { - ($chain: ident) => { + ($chain: ident $(, $consensus: ident => $justification_type: ty)?) => { bp_runtime::paste::item! { mod [<$chain _finality_api>] { use super::*; @@ -291,6 +292,13 @@ macro_rules! decl_bridge_finality_runtime_apis { pub const []: &str = stringify!([<$chain:camel FinalityApi_best_finalized>]); + $( + /// Name of the `FinalityApi::accepted__finality_proofs` + /// runtime method. + pub const [<$chain:upper _SYNCED_HEADERS_ $consensus:upper _INFO_METHOD>]: &str = + stringify!([<$chain:camel FinalityApi_synced_headers_ $consensus:lower _info>]); + )? + sp_api::decl_runtime_apis! { /// API for querying information about the finalized chain headers. /// @@ -299,6 +307,12 @@ macro_rules! decl_bridge_finality_runtime_apis { pub trait [<$chain:camel FinalityApi>] { /// Returns number and hash of the best finalized header known to the bridge module. fn best_finalized() -> Option>; + + $( + /// Returns the justifications accepted in the current block. + fn []( + ) -> Vec<$justification_type>; + )? } } } @@ -306,6 +320,9 @@ macro_rules! decl_bridge_finality_runtime_apis { pub use [<$chain _finality_api>]::*; } }; + ($chain: ident, grandpa) => { + decl_bridge_finality_runtime_apis!($chain, grandpa => bp_header_chain::HeaderGrandpaInfo
); + }; } /// Convenience macro that declares bridge messages runtime apis and related constants for a chain. @@ -376,8 +393,8 @@ macro_rules! decl_bridge_messages_runtime_apis { /// The name of the chain has to be specified in snake case (e.g. `rialto_parachain`). #[macro_export] macro_rules! decl_bridge_runtime_apis { - ($chain: ident) => { - bp_runtime::decl_bridge_finality_runtime_apis!($chain); + ($chain: ident $(, $consensus: ident)?) => { + bp_runtime::decl_bridge_finality_runtime_apis!($chain $(, $consensus)?); bp_runtime::decl_bridge_messages_runtime_apis!($chain); }; } diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs index c394af37fa4..150e015cb4f 100644 --- a/bridges/primitives/runtime/src/lib.rs +++ b/bridges/primitives/runtime/src/lib.rs @@ -77,7 +77,7 @@ pub const KUSAMA_CHAIN_ID: ChainId = *b"ksma"; /// Westend chain id. pub const WESTEND_CHAIN_ID: ChainId = *b"wend"; -/// AssetHubWestend chain id. +/// `AssetHubWestmint` chain id. pub const ASSET_HUB_WESTEND_CHAIN_ID: ChainId = *b"ahwe"; /// Rococo chain id. diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml index 9bfde047d30..2f7c95092f0 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml +++ b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml @@ -74,6 +74,7 @@ parachains-common = { path = "../../../../parachains/common", default-features = # Bridges bp-bridge-hub-rococo = { path = "../../../../bridges/primitives/chain-bridge-hub-rococo", default-features = false } bp-bridge-hub-wococo = { path = "../../../../bridges/primitives/chain-bridge-hub-wococo", default-features = false } +bp-header-chain = { path = "../../../../bridges/primitives/header-chain", default-features = false } bp-messages = { path = "../../../../bridges/primitives/messages", default-features = false } bp-parachains = { path = "../../../../bridges/primitives/parachains", default-features = false } bp-polkadot-core = { path = "../../../../bridges/primitives/polkadot-core", default-features = false } @@ -100,6 +101,7 @@ default = [ std = [ "bp-bridge-hub-rococo/std", "bp-bridge-hub-wococo/std", + "bp-header-chain/std", "bp-messages/std", "bp-parachains/std", "bp-polkadot-core/std", diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs index fd155e35604..196e282ec2f 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs +++ b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs @@ -78,13 +78,6 @@ impl XcmBlobHauler for ToBridgeHubWococoXcmBlobHauler { type MessageSender = pallet_bridge_messages::Pallet; - type MessageSenderOrigin = super::RuntimeOrigin; - - fn message_sender_origin() -> Self::MessageSenderOrigin { - pallet_xcm::Origin::from(MultiLocation::new(1, crate::xcm_config::UniversalLocation::get())) - .into() - } - fn xcm_lane() -> LaneId { DEFAULT_XCM_LANE_TO_BRIDGE_HUB_WOCOCO } diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs index 563e8cd294e..178a022dd49 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs +++ b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs @@ -78,13 +78,6 @@ impl XcmBlobHauler for ToBridgeHubRococoXcmBlobHauler { type MessageSender = pallet_bridge_messages::Pallet; - type MessageSenderOrigin = super::RuntimeOrigin; - - fn message_sender_origin() -> super::RuntimeOrigin { - pallet_xcm::Origin::from(MultiLocation::new(1, crate::xcm_config::UniversalLocation::get())) - .into() - } - fn xcm_lane() -> LaneId { DEFAULT_XCM_LANE_TO_BRIDGE_HUB_ROCOCO } diff --git a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs index 6e1bb7aba2d..27f5e14174c 100644 --- a/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs +++ b/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs @@ -783,12 +783,20 @@ impl_runtime_apis! { fn best_finalized() -> Option> { BridgeRococoGrandpa::best_finalized() } + fn synced_headers_grandpa_info( + ) -> Vec> { + BridgeRococoGrandpa::synced_headers_grandpa_info() + } } impl bp_wococo::WococoFinalityApi for Runtime { fn best_finalized() -> Option> { BridgeWococoGrandpa::best_finalized() } + fn synced_headers_grandpa_info( + ) -> Vec> { + BridgeWococoGrandpa::synced_headers_grandpa_info() + } }