diff --git a/Cargo.lock b/Cargo.lock index bc1fb03865e0..64b080659f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2072,6 +2072,7 @@ dependencies = [ "pallet-xcm-bridge-hub", "parachains-common", "parity-scale-codec", + "paste", "polkadot-parachain-primitives", "polkadot-runtime-common", "rococo-runtime-constants", @@ -2243,6 +2244,7 @@ dependencies = [ "pallet-xcm-bridge-hub", "parachains-common", "parity-scale-codec", + "paste", "polkadot-parachain-primitives", "polkadot-runtime-common", "scale-info", @@ -2286,6 +2288,8 @@ dependencies = [ "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", + "const-fnv1a-hash", + "const_format", "frame-support", "frame-system", "hash-db", @@ -3008,6 +3012,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + [[package]] name = "const-hex" version = "1.10.0" @@ -3049,6 +3059,26 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2 1.0.75", + "quote 1.0.35", + "unicode-xid 0.2.4", +] + [[package]] name = "constant_time_eq" version = "0.1.5" diff --git a/bridges/bin/runtime-common/Cargo.toml b/bridges/bin/runtime-common/Cargo.toml index 74049031afe6..83a4b179bc57 100644 --- a/bridges/bin/runtime-common/Cargo.toml +++ b/bridges/bin/runtime-common/Cargo.toml @@ -14,6 +14,8 @@ workspace = true codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } hash-db = { version = "0.16.0", default-features = false } log = { workspace = true } +const-fnv1a-hash = "1.1" +const_format = "0.2" scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } static_assertions = { version = "1.1", optional = true } tuplex = { version = "0.1", default-features = false } @@ -26,6 +28,7 @@ bp-parachains = { path = "../../primitives/parachains", default-features = false bp-polkadot-core = { path = "../../primitives/polkadot-core", default-features = false } bp-relayers = { path = "../../primitives/relayers", default-features = false } bp-runtime = { path = "../../primitives/runtime", default-features = false } +bp-test-utils = { path = "../../primitives/test-utils", default-features = false, optional = true } bp-xcm-bridge-hub = { path = "../../primitives/xcm-bridge-hub", default-features = false } bp-xcm-bridge-hub-router = { path = "../../primitives/xcm-bridge-hub-router", default-features = false } pallet-bridge-grandpa = { path = "../../modules/grandpa", default-features = false } @@ -63,6 +66,7 @@ std = [ "bp-polkadot-core/std", "bp-relayers/std", "bp-runtime/std", + "bp-test-utils?/std", "bp-xcm-bridge-hub-router/std", "bp-xcm-bridge-hub/std", "codec/std", @@ -99,4 +103,4 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", ] -integrity-test = ["static_assertions"] +integrity-test = ["bp-test-utils", "static_assertions"] diff --git a/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs b/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs index 2c152aef6822..1c41356d621f 100644 --- a/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs +++ b/bridges/bin/runtime-common/src/extensions/check_obsolete_extension.rs @@ -262,7 +262,25 @@ macro_rules! generate_bridge_reject_obsolete_headers_and_messages { #[derive(Clone, codec::Decode, Default, codec::Encode, Eq, PartialEq, sp_runtime::RuntimeDebug, scale_info::TypeInfo)] pub struct BridgeRejectObsoleteHeadersAndMessages; impl sp_runtime::traits::SignedExtension for BridgeRejectObsoleteHeadersAndMessages { - const IDENTIFIER: &'static str = "BridgeRejectObsoleteHeadersAndMessages"; + // What we do here is: + // - computing hash of all stringified **types**, passed to the macro + // - prefixing it with `BridgeReject_` + // + // So whenever any type passed to the `generate_bridge_reject_obsolete_headers_and_messages` + // changes, the `IDENTIFIER` is also changed. Keep in mind that it may change if the type + // stays the same, but e.g. name of type alias, used in type name changes: + // ```rust + // struct F; + // type A = u32; + // type B = u32; + // ``` + // Then `IDENTIFIER` of `F` is not equal to `F`, even though the type is the same. + const IDENTIFIER: &'static str = $crate::extensions::prelude::const_format::concatcp!( + "BridgeReject_", + $crate::extensions::prelude::const_fnv1a_hash::fnv1a_hash_str_128( + concat!($(stringify!($filter_call), )*) + ) + ); type AccountId = $account_id; type Call = $call; type AdditionalSigned = (); @@ -458,6 +476,11 @@ mod tests { SecondFilterCall ); + assert_eq!( + BridgeRejectObsoleteHeadersAndMessages::IDENTIFIER, + "BridgeReject_163603100942600502516220926454699703369" + ); + run_test(|| { assert_err!( BridgeRejectObsoleteHeadersAndMessages.validate(&42, &MockCall { data: 1 }, &(), 0), diff --git a/bridges/bin/runtime-common/src/extensions/mod.rs b/bridges/bin/runtime-common/src/extensions/mod.rs index 3f1b506aaae3..ee287c9b70f7 100644 --- a/bridges/bin/runtime-common/src/extensions/mod.rs +++ b/bridges/bin/runtime-common/src/extensions/mod.rs @@ -16,6 +16,12 @@ //! Bridge-specific transaction extensions. +/// All crates that we use in extensions macro. +pub mod prelude { + pub use const_fnv1a_hash; + pub use const_format; +} + pub mod check_obsolete_extension; pub mod priority_calculator; pub mod refund_relayer_extension; diff --git a/bridges/bin/runtime-common/src/lib.rs b/bridges/bin/runtime-common/src/lib.rs index 5679acd6006c..b8663466a314 100644 --- a/bridges/bin/runtime-common/src/lib.rs +++ b/bridges/bin/runtime-common/src/lib.rs @@ -32,5 +32,7 @@ mod mock; #[cfg(feature = "integrity-test")] pub mod integrity; +#[cfg(feature = "integrity-test")] +pub mod relayer_compatibility; const LOG_TARGET_BRIDGE_DISPATCH: &str = "runtime::bridge-dispatch"; diff --git a/bridges/bin/runtime-common/src/mock.rs b/bridges/bin/runtime-common/src/mock.rs index e323f1edfc71..476d94f5487d 100644 --- a/bridges/bin/runtime-common/src/mock.rs +++ b/bridges/bin/runtime-common/src/mock.rs @@ -45,7 +45,7 @@ use frame_support::{ use pallet_transaction_payment::Multiplier; use sp_runtime::{ testing::H256, - traits::{BlakeTwo256, ConstU32, ConstU64, ConstU8}, + traits::{BlakeTwo256, ConstU32, ConstU64, ConstU8, GetDefault}, FixedPointNumber, Perquintill, }; @@ -182,6 +182,7 @@ impl pallet_transaction_payment::Config for TestRuntime { impl pallet_bridge_grandpa::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type BridgedChain = BridgedUnderlyingChain; type MaxFreeHeadersPerBlock = ConstU32<4>; type FreeHeadersInterval = ConstU32<1_024>; @@ -191,6 +192,7 @@ impl pallet_bridge_grandpa::Config for TestRuntime { impl pallet_bridge_parachains::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type BridgesGrandpaPalletInstance = (); type ParasPalletName = BridgedParasPalletName; type ParaStoredHeaderDataBuilder = @@ -202,6 +204,7 @@ impl pallet_bridge_parachains::Config for TestRuntime { impl pallet_bridge_messages::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type WeightInfo = pallet_bridge_messages::weights::BridgeWeight; type ActiveOutboundLanes = ActiveOutboundLanes; type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane; diff --git a/bridges/bin/runtime-common/src/relayer_compatibility.rs b/bridges/bin/runtime-common/src/relayer_compatibility.rs new file mode 100644 index 000000000000..bfced7c70fec --- /dev/null +++ b/bridges/bin/runtime-common/src/relayer_compatibility.rs @@ -0,0 +1,356 @@ +// Copyright (C) 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 . + +//! An attempt to implement bridge versioning and make relayer (an offchain actor) +//! compatible with different runtime versions, implemeting the same bridge version. +//! +//! Before this versioning, we had to prepare + deploy a new relay version for every +//! runtime upgrade. And since relay is connected to 4 chains, we need to have a new +//! version every time one of chains is upgraded. Recently we have "eased" our +//! requirements and now only watch for bridge hub versions (2 chains). +//! +//! What we are trying to solve with that: +//! +//! - transaction encoding compatibility - when `spec_version`, `transaction_version`, our pallet +//! calls encoding, pallet ordering or a set of signed extension changes, relay starts building +//! invalid transactions. For first two things we have a CLI options to read versions right from +//! runtime. The rest should happen rarely; +//! +//! - bridge configuration compatibility. E.g. relayer compensation/reward scheme may change over +//! time. And relayer may everntually start losing its tokens by submitting valid and previously +//! compensated transactions; +//! +//! - unexpected/unknown issues. E.g. : change of `paras` pallet at the relay chain, storage trie +//! version changes, ... That is something that we expect needs to be detected by +//! zombienet/chopsticks tests in the fellowhsip repo. But so far it isn't automated and normally +//! when we are building new relay version for upgraded chains, we need to run Add P<>K bridge +//! manual zombienet test for asset transfer polkadot-fellows/runtimes#198 and at least detect it +//! manually. +//! +//! TLDR: by releasing a new relayer version on every runtime upgrade we were trying to ensure +//! that everything works properly. If we ever have an automated testing for that, we will be +//! able to solve that easier. Yet we have an issue, to make relayer fully generic, but it is +//! unlikely to be finished soon. +//! +//! What we can do to make our lives easier now and not to rebuild relayer on every upgrade. +//! Inspired by how pallet storage versioning works: we can add some constant to every bridge +//! pallet (GRANPA, parachains, messages) configuration and also add the same constant to relayer. +//! Relayer should exit if it sees that the constant it has is different from the constant in the +//! runtime. This should solve issues (1) and (2) from above. (3) is still actual and can't be +//! detected without having automated integration tests. +//! +//! This constant should 'seal' everything related to bridge transaction encoding and bridge +//! configuration compatibility: +//! +//! - a set of bridge calls encoding; +//! +//! - a set of bridge-related signed extensions IDs that are related to the bridge; +//! +//! - a set of all pallet settings that may affect relayer. + +use crate::{ + messages::{ + source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof, + }, + messages_xcm_extension::XcmAsPlainPayload, +}; +use bp_messages::{ + source_chain::TargetHeaderChain, target_chain::SourceHeaderChain, LaneId, + UnrewardedRelayersState, +}; +use bp_polkadot_core::parachains::ParaHeadsProof; +use bp_runtime::RelayerVersion; +use bp_test_utils::make_default_justification; +use codec::{Decode, Encode}; +use frame_support::{ + traits::{GetStorageVersion, PalletInfoAccess}, + weights::Weight, +}; +use pallet_bridge_grandpa::{ + BridgedHeader as BridgedGrandpaHeader, Call as GrandpaCall, Config as GrandpaConfig, + Pallet as GrandpaPallet, +}; +use pallet_bridge_messages::{ + Call as MessagesCall, Config as MessagesConfig, Pallet as MessagesPallet, +}; +use pallet_bridge_parachains::{ + Call as ParachainsCall, Config as ParachainsConfig, Pallet as ParachainsPallet, +}; +use sp_core::{blake2_256, Get, H256}; +use sp_runtime::traits::{Header, SignedExtension, TrailingZeroInput}; + +const LOG_TARGET: &str = "bridge"; + +/// A set of signed extensions that are: +/// +/// - not related to bridge operations; +/// +/// - have the `AdditionalSigned` set to `()`, so it doesn't break the bridge relayer. +const IGNORED_EXTENSIONS: [&'static str; 0] = []; + +/// Ensure that the running relayer is compatible with the `pallet-bridge-grandpa`, deployed +/// at `Runtime`. +pub fn ensure_grandpa_relayer_compatibility() +where + Runtime: GrandpaConfig, + I: 'static, + GrandpaPallet: PalletInfoAccess, + SignedExtra: SignedExtension, + Runtime::RuntimeCall: From>, +{ + let expected_version = Runtime::CompatibleWithRelayer::get(); + let actual_version = RelayerVersion { + manual: expected_version.manual, + auto: blake2_256( + &[ + pallet_storage_digest::>(), + grandpa_calls_digest::(), + runtime_version_digest::(), + siged_extensions_digest::(), + ] + .encode(), + ) + .into(), + }; + assert_eq!( + expected_version, actual_version, + "Expected GRANDPA relayer version: {expected_version:?}. Actual: {actual_version:?}", + ); +} + +/// Ensure that the running relayer is compatible with the `pallet-bridge-parachains`, deployed +/// at `Runtime`. +pub fn ensure_parachains_relayer_compatibility() +where + Runtime: ParachainsConfig, + I: 'static, + ParachainsPallet: PalletInfoAccess, + SignedExtra: SignedExtension, + Runtime::RuntimeCall: From>, +{ + let expected_version = >::CompatibleWithRelayer::get(); + let actual_version = RelayerVersion { + manual: expected_version.manual, + auto: blake2_256( + &[ + pallet_storage_digest::>(), + parachains_calls_digest::(), + runtime_version_digest::(), + siged_extensions_digest::(), + ] + .encode(), + ) + .into(), + }; + assert_eq!( + expected_version, actual_version, + "Expected parachains relayer version: {expected_version:?}. Actual: {actual_version:?}", + ); +} + +/// Ensure that the running relayer is compatible with the `pallet-bridge-messages`, deployed +/// at `Runtime`. +pub fn ensure_messages_relayer_compatibility< + Runtime, + I, + SignedExtra, + BridgedHash, + BridgedAccountId, +>() +where + Runtime: + MessagesConfig, + I: 'static, + MessagesPallet: PalletInfoAccess, + SignedExtra: SignedExtension, + Runtime::RuntimeCall: From>, + Runtime::SourceHeaderChain: + SourceHeaderChain>, + Runtime::TargetHeaderChain: TargetHeaderChain< + XcmAsPlainPayload, + Runtime::AccountId, + MessagesDeliveryProof = FromBridgedChainMessagesDeliveryProof, + >, + BridgedHash: Default, + BridgedAccountId: Decode, +{ + let expected_version = >::CompatibleWithRelayer::get(); + let actual_version = RelayerVersion { + manual: expected_version.manual, + auto: blake2_256( + &[ + pallet_storage_digest::>(), + messages_calls_digest::(), + messages_config_digest::(), + runtime_version_digest::(), + siged_extensions_digest::(), + ] + .encode(), + ) + .into(), + }; + assert_eq!( + expected_version, actual_version, + "Expected messages relayer version: {expected_version:?}. Actual: {actual_version:?}", + ); +} + +/// Seal bridge pallet storage version. +fn pallet_storage_digest

() -> H256 +where + P: PalletInfoAccess + GetStorageVersion, +{ + // keys of storage entries, used by pallet are computed using: + // 1) name of the pallet, used in `construct_runtime!` macro call; + // 2) name of the storage value/map/doublemap itself. + // + // When the `1` from above is changed, `PalletInfoAccess::name()` shall change; + // When the `2` from above is changed, `GetStorageVersion::in_code_storage_version()` shall + // change. + let pallet_name = P::name(); + let storage_version = P::on_chain_storage_version(); + log::info!(target: LOG_TARGET, "Sealing pallet storage: {:?}", (pallet_name, storage_version)); + blake2_256(&(pallet_name, storage_version).encode()).into() +} + +/// Seal bridge GRANDPA call encoding. +fn grandpa_calls_digest() -> H256 +where + Runtime: GrandpaConfig, + I: 'static, + Runtime::RuntimeCall: From>, +{ + // the relayer normally only uses the `submit_finality_proof` call. Let's ensure that + // the encoding stays the same. Obviously, we can not detect all encoding changes here, + // but such breaking changes are not assumed to be detected using this test. + let bridged_header = BridgedGrandpaHeader::::new( + 42u32.into(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ); + let bridged_justification = make_default_justification(&bridged_header); + let call: Runtime::RuntimeCall = GrandpaCall::::submit_finality_proof { + finality_target: Box::new(bridged_header), + justification: bridged_justification, + } + .into(); + log::info!(target: LOG_TARGET, "Sealing GRANDPA call encoding: {:?}", call); + blake2_256(&call.encode()).into() +} + +/// Seal bridge parachains call encoding. +fn parachains_calls_digest() -> H256 +where + Runtime: ParachainsConfig, + I: 'static, + Runtime::RuntimeCall: From>, +{ + // the relayer normally only uses the `submit_parachain_heads` call. Let's ensure that + // the encoding stays the same. Obviously, we can not detect all encoding changes here, + // but such breaking changes are not assumed to be detected using this test. + let call: Runtime::RuntimeCall = ParachainsCall::::submit_parachain_heads { + at_relay_block: (84, Default::default()), + parachains: vec![(42.into(), Default::default())], + parachain_heads_proof: ParaHeadsProof { storage_proof: vec![vec![42u8; 42]] }, + } + .into(); + log::info!(target: LOG_TARGET, "Sealing parachains call encoding: {:?}", call); + blake2_256(&call.encode()).into() +} + +/// Seal bridge messages call encoding. +fn messages_calls_digest() -> H256 +where + Runtime: + MessagesConfig, + I: 'static, + Runtime::RuntimeCall: From>, + Runtime::SourceHeaderChain: + SourceHeaderChain>, + Runtime::TargetHeaderChain: TargetHeaderChain< + XcmAsPlainPayload, + Runtime::AccountId, + MessagesDeliveryProof = FromBridgedChainMessagesDeliveryProof, + >, + BridgedHash: Default, + BridgedAccountId: Decode, +{ + // the relayer normally only uses the `receive_messages_proof` and + // `receive_messages_delivery_proof` calls. Let's ensure that the encoding stays the same. + // Obviously, we can not detect all encoding changes here, but such breaking changes are + // not assumed to be detected using this test. + let call1: Runtime::RuntimeCall = MessagesCall::::receive_messages_proof { + relayer_id_at_bridged_chain: BridgedAccountId::decode(&mut TrailingZeroInput::zeroes()) + .expect("is decoded successfully in tests; qed"), + proof: FromBridgedChainMessagesProof { + bridged_header_hash: BridgedHash::default(), + storage_proof: vec![vec![42u8; 42]], + lane: LaneId([0, 0, 0, 0]), + nonces_start: 42, + nonces_end: 42, + }, + messages_count: 1, + dispatch_weight: Weight::zero(), + } + .into(); + let call2: Runtime::RuntimeCall = MessagesCall::::receive_messages_delivery_proof { + proof: FromBridgedChainMessagesDeliveryProof { + bridged_header_hash: BridgedHash::default(), + storage_proof: vec![vec![42u8; 42]], + lane: LaneId([0, 0, 0, 0]), + }, + relayers_state: UnrewardedRelayersState::default(), + } + .into(); + log::info!(target: LOG_TARGET, "Sealing message calls encoding: {:?} {:?}", call1, call2); + blake2_256(&(call1, call2).encode()).into() +} + +/// Seal bridge messages pallet configuration settings that may affect running relayer. +fn messages_config_digest() -> H256 +where + Runtime: MessagesConfig, + I: 'static, +{ + let settings = ( + Runtime::MaxUnrewardedRelayerEntriesAtInboundLane::get(), + Runtime::MaxUnconfirmedMessagesAtInboundLane::get(), + ); + log::info!(target: LOG_TARGET, "Sealing messages pallet configuration: {:?}", settings); + blake2_256(&settings.encode()).into() +} + +/// Seal runtime version. We want to make relayer tolerant towards `spec_version` and +/// `transcation_version` changes. But any changes to storage trie format are breaking, +/// not only for the relay, but for all involved pallets on other chains as well. +fn runtime_version_digest() -> H256 { + let state_version = Runtime::Version::get().state_version; + log::info!(target: LOG_TARGET, "Sealing runtime version: {:?}", state_version); + blake2_256(&state_version.encode()).into() +} + +/// Seal all signed extensions that may break bridge. +fn siged_extensions_digest() -> H256 { + let extensions: Vec<_> = SignedExtra::metadata() + .into_iter() + .map(|m| m.identifier) + .filter(|id| !IGNORED_EXTENSIONS.contains(id)) + .collect(); + log::info!(target: LOG_TARGET, "Sealing runtime extensions: {:?}", extensions); + blake2_256(&extensions.encode()).into() +} diff --git a/bridges/modules/grandpa/src/lib.rs b/bridges/modules/grandpa/src/lib.rs index efcbfb1654b3..fe9c21594e88 100644 --- a/bridges/modules/grandpa/src/lib.rs +++ b/bridges/modules/grandpa/src/lib.rs @@ -42,8 +42,10 @@ use bp_header_chain::{ HeaderChain, InitializationData, StoredHeaderData, StoredHeaderDataBuilder, StoredHeaderGrandpaInfo, }; -use bp_runtime::{BlockNumberOf, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule}; -use frame_support::{dispatch::PostDispatchInfo, ensure, DefaultNoBound}; +use bp_runtime::{ + BlockNumberOf, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule, RelayerVersion, +}; +use frame_support::{dispatch::PostDispatchInfo, ensure, traits::Get, DefaultNoBound}; use sp_consensus_grandpa::SetId; use sp_runtime::{ traits::{Header as HeaderT, Zero}, @@ -100,6 +102,9 @@ pub mod pallet { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Version of the relayer that is compatible with this pallet configuration. + #[pallet::constant] + type CompatibleWithRelayer: Get; /// The chain we are bridging to here. type BridgedChain: ChainWithGrandpa; @@ -736,6 +741,11 @@ impl, I: 'static> Pallet where ::RuntimeEvent: TryInto>, { + /// Get compatible relayer version. + pub fn compatible_relayer_version() -> RelayerVersion { + T::CompatibleWithRelayer::get() + } + /// Get the GRANDPA justifications accepted in the current block. pub fn synced_headers_grandpa_info() -> Vec>> { frame_system::Pallet::::read_events_no_consensus() diff --git a/bridges/modules/grandpa/src/mock.rs b/bridges/modules/grandpa/src/mock.rs index 27df9d9c78f5..581fda927f45 100644 --- a/bridges/modules/grandpa/src/mock.rs +++ b/bridges/modules/grandpa/src/mock.rs @@ -23,6 +23,7 @@ use frame_support::{ construct_runtime, derive_impl, parameter_types, traits::Hooks, weights::Weight, }; use sp_core::sr25519::Signature; +use sp_runtime::traits::GetDefault; pub type AccountId = u64; pub type TestHeader = sp_runtime::testing::Header; @@ -55,6 +56,7 @@ parameter_types! { impl grandpa::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type BridgedChain = TestBridgedChain; type MaxFreeHeadersPerBlock = MaxFreeHeadersPerBlock; type FreeHeadersInterval = FreeHeadersInterval; diff --git a/bridges/modules/messages/src/lib.rs b/bridges/modules/messages/src/lib.rs index bc00db9eba5b..5c22fc71fd48 100644 --- a/bridges/modules/messages/src/lib.rs +++ b/bridges/modules/messages/src/lib.rs @@ -63,7 +63,8 @@ use bp_messages::{ UnrewardedRelayersState, VerificationError, }; use bp_runtime::{ - BasicOperatingMode, ChainId, OwnedBridgeModule, PreComputedSize, RangeInclusiveExt, Size, + BasicOperatingMode, ChainId, OwnedBridgeModule, PreComputedSize, RangeInclusiveExt, + RelayerVersion, Size, }; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{dispatch::PostDispatchInfo, ensure, fail, traits::Get, DefaultNoBound}; @@ -102,6 +103,9 @@ pub mod pallet { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Version of the relayer that is compatible with this pallet configuration. + #[pallet::constant] + type CompatibleWithRelayer: Get; /// Benchmarks results from runtime we're plugged into. type WeightInfo: WeightInfoExt; @@ -648,6 +652,11 @@ pub mod pallet { } impl, I: 'static> Pallet { + /// Get compatible relayer version. + pub fn compatible_relayer_version() -> RelayerVersion { + T::CompatibleWithRelayer::get() + } + /// Get stored data of the outbound message with given nonce. pub fn outbound_message_data(lane: LaneId, nonce: MessageNonce) -> Option { OutboundMessages::::get(MessageKey { lane_id: lane, nonce }).map(Into::into) diff --git a/bridges/modules/messages/src/mock.rs b/bridges/modules/messages/src/mock.rs index ec63f15b94b5..99b0b94b1c0e 100644 --- a/bridges/modules/messages/src/mock.rs +++ b/bridges/modules/messages/src/mock.rs @@ -36,7 +36,7 @@ use frame_support::{ weights::{constants::RocksDbWeight, Weight}, }; use scale_info::TypeInfo; -use sp_runtime::BuildStorage; +use sp_runtime::{traits::GetDefault, BuildStorage}; use std::{ collections::{BTreeMap, VecDeque}, ops::RangeInclusive, @@ -103,6 +103,7 @@ pub type TestWeightInfo = (); impl Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type WeightInfo = TestWeightInfo; type ActiveOutboundLanes = ActiveOutboundLanes; type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane; diff --git a/bridges/modules/parachains/src/lib.rs b/bridges/modules/parachains/src/lib.rs index 61e04aed3770..eabbf422519a 100644 --- a/bridges/modules/parachains/src/lib.rs +++ b/bridges/modules/parachains/src/lib.rs @@ -30,8 +30,10 @@ pub use weights_ext::WeightInfoExt; use bp_header_chain::{HeaderChain, HeaderChainError}; use bp_parachains::{parachain_head_storage_key_at_source, ParaInfo, ParaStoredHeaderData}; use bp_polkadot_core::parachains::{ParaHash, ParaHead, ParaHeadsProof, ParaId}; -use bp_runtime::{Chain, HashOf, HeaderId, HeaderIdOf, Parachain, StorageProofError}; -use frame_support::{dispatch::PostDispatchInfo, DefaultNoBound}; +use bp_runtime::{ + Chain, HashOf, HeaderId, HeaderIdOf, Parachain, RelayerVersion, StorageProofError, +}; +use frame_support::{dispatch::PostDispatchInfo, traits::Get, DefaultNoBound}; use pallet_bridge_grandpa::SubmitFinalityProofHelper; use sp_std::{marker::PhantomData, vec::Vec}; @@ -187,6 +189,9 @@ pub mod pallet { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Version of the relayer that is compatible with this pallet configuration. + #[pallet::constant] + type CompatibleWithRelayer: Get; /// Benchmarks results from runtime we're plugged into. type WeightInfo: WeightInfoExt; @@ -595,6 +600,11 @@ pub mod pallet { } impl, I: 'static> Pallet { + /// Get compatible relayer version. + pub fn compatible_relayer_version() -> RelayerVersion { + >::CompatibleWithRelayer::get() + } + /// Get stored parachain info. pub fn best_parachain_info(parachain: ParaId) -> Option { ParasInfo::::get(parachain) diff --git a/bridges/modules/parachains/src/mock.rs b/bridges/modules/parachains/src/mock.rs index dbb62845392d..f4bdcabed046 100644 --- a/bridges/modules/parachains/src/mock.rs +++ b/bridges/modules/parachains/src/mock.rs @@ -22,7 +22,7 @@ use frame_support::{ }; use sp_runtime::{ testing::H256, - traits::{BlakeTwo256, Header as HeaderT}, + traits::{BlakeTwo256, GetDefault, Header as HeaderT}, MultiSignature, }; @@ -177,6 +177,7 @@ parameter_types! { impl pallet_bridge_grandpa::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type BridgedChain = TestBridgedChain; type MaxFreeHeadersPerBlock = ConstU32<2>; type FreeHeadersInterval = FreeHeadersInterval; @@ -186,6 +187,7 @@ impl pallet_bridge_grandpa::Config for TestRun impl pallet_bridge_grandpa::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type BridgedChain = TestBridgedChain; type MaxFreeHeadersPerBlock = ConstU32<2>; type FreeHeadersInterval = FreeHeadersInterval; @@ -201,6 +203,7 @@ parameter_types! { impl pallet_bridge_parachains::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type WeightInfo = (); type BridgesGrandpaPalletInstance = pallet_bridge_grandpa::Instance1; type ParasPalletName = ParasPalletName; diff --git a/bridges/modules/xcm-bridge-hub/src/mock.rs b/bridges/modules/xcm-bridge-hub/src/mock.rs index 4c09bce56d73..21a8660ee437 100644 --- a/bridges/modules/xcm-bridge-hub/src/mock.rs +++ b/bridges/modules/xcm-bridge-hub/src/mock.rs @@ -35,7 +35,7 @@ use frame_support::{derive_impl, parameter_types, traits::ConstU32, weights::Run use sp_core::H256; use sp_runtime::{ testing::Header as SubstrateHeader, - traits::{BlakeTwo256, IdentityLookup}, + traits::{BlakeTwo256, GetDefault, IdentityLookup}, AccountId32, BuildStorage, }; use xcm::prelude::*; @@ -83,6 +83,7 @@ parameter_types! { impl pallet_bridge_messages::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = GetDefault; type WeightInfo = TestMessagesWeights; type BridgedChainId = (); diff --git a/bridges/primitives/runtime/src/chain.rs b/bridges/primitives/runtime/src/chain.rs index 369386e41b0c..10913f47b4ac 100644 --- a/bridges/primitives/runtime/src/chain.rs +++ b/bridges/primitives/runtime/src/chain.rs @@ -313,6 +313,9 @@ macro_rules! decl_bridge_finality_runtime_apis { /// Name of the `FinalityApi::best_finalized` runtime method. pub const []: &str = stringify!([<$chain:camel FinalityApi_best_finalized>]); + /// Name of the `FinalityApi::compatible_relayer_version` runtime method. + pub const [<$chain:upper _FINALITY_COMPATIBLE_RELAYER_VERSION>]: &str = + stringify!([<$chain:camel FinalityApi_compatible_relayer_version>]); /// Name of the `FinalityApi::free_headers_interval` runtime method. pub const []: &str = @@ -334,6 +337,9 @@ 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>; + /// Return version of relayer that is compatible with finality pallet configuration + /// and may safely submit transaction to this chain. + fn compatible_relayer_version() -> bp_runtime::RelayerVersion; /// Returns free headers interval, if it is configured in the runtime. /// The caller expects that if his transaction improves best known header @@ -382,6 +388,10 @@ macro_rules! decl_bridge_messages_runtime_apis { pub const []: &str = stringify!([]); + /// Name of the `FromInboundLaneApi::compatible_relayer_version` runtime method. + pub const []: &str = + stringify!([]); + sp_api::decl_runtime_apis! { /// Outbound message lane API for messages that are sent to this chain. /// @@ -413,6 +423,10 @@ macro_rules! decl_bridge_messages_runtime_apis { lane: bp_messages::LaneId, messages: sp_std::vec::Vec<(bp_messages::MessagePayload, bp_messages::OutboundMessageDetails)>, ) -> sp_std::vec::Vec; + + /// Return version of relayer that is compatible with messages pallet configuration + /// and may safely submit transaction to this chain. + fn compatible_relayer_version() -> bp_runtime::RelayerVersion; } } } diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs index 5daba0351ad4..9bca830d1b46 100644 --- a/bridges/primitives/runtime/src/lib.rs +++ b/bridges/primitives/runtime/src/lib.rs @@ -26,7 +26,7 @@ use frame_support::{ use frame_system::RawOrigin; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; -use sp_core::storage::StorageKey; +use sp_core::{storage::StorageKey, H256}; use sp_runtime::{ traits::{BadOrigin, Header as HeaderT, UniqueSaturatedInto}, RuntimeDebug, @@ -520,6 +520,59 @@ where } } +/// The version of offchain relayer, that is compatible with the pallet and runtime +/// configurations. Running relayer that is not compatible with that version may lead +/// to relayer slashing, losing tokens, submitting invalid transactions, ... +/// Every bridge pallet provides this version as a constant. It changes on some runtime +/// upgrades, which change the bridge code/logic, allowing relayer to keep running even +/// if unrelated runtime upgrade happens. +#[derive( + Encode, + Decode, + Clone, + Copy, + Default, + PartialEq, + Eq, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, + Serialize, + Deserialize, +)] +pub struct RelayerVersion { + /// Version that is increased manually. Increase it every time when something that + /// may break already running relayers is changed. This is an always increasing + /// value and relayers that are using older version will immediately shutdown, while + /// relayers using newer value will keep running until runtime is upgraded to that + /// new version. + /// + /// **IMPORTANT**: it **CAN** be changed every time [`Self::auto`] value is changed. + /// However, if tests have detected some change and [`Self::auto`] is changed, BUT + /// you are 100% sure that it won't break existing relayer, you may keep the previous + /// [`Self::manual`] value. It also **MUST** be changed if [`Self::auto`] stays the + /// same BUT there's some breaking change that is not detected by tests. + /// + /// Every change to this field shall result in building and deploying a new relayer + /// version. This is the only field that is checked by the relayer. + pub manual: u32, + /// Version that is generated by runtime tests. Change it everytime when related + /// tests righteously fail, meaning that something has been changed in pallet/runtime + /// configuration that breaks the existing relayer. You may also need to bump the + /// [`Self::manual`] version if new relayer needs to be built and deployed. This field + /// is ignored by the relayer and it will keep running even if [`Self::auto`] is + /// different, but the [`Self::manual`] stays the same. + pub auto: H256, +} + +impl RelayerVersion { + /// Create relayer version from `manual` value only. It MSUT NOT be used inside + /// runtime code. + pub const fn from_manual(manual: u32) -> Self { + Self { manual, auto: H256([0u8; 32]) } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/bridges/relays/client-substrate/src/chain.rs b/bridges/relays/client-substrate/src/chain.rs index 40269fe64c87..458848f05725 100644 --- a/bridges/relays/client-substrate/src/chain.rs +++ b/bridges/relays/client-substrate/src/chain.rs @@ -46,6 +46,12 @@ pub trait Chain: ChainBase + Clone { /// Keep in mind that this method is normally provided by the other chain, which is /// bridged with this chain. const BEST_FINALIZED_HEADER_ID_METHOD: &'static str; + /// Name of the runtime API method that is returning version of the + /// compatible relayer (`bp_runtime::RelayerVersion`). + /// + /// Keep in mind that this method is normally provided by the other chain, which is + /// bridged with this chain. + const WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD: &'static str; /// Name of the runtime API method that is returning interval between source chain /// headers that may be submitted for free to the target chain. /// @@ -119,6 +125,12 @@ pub trait ChainWithMessages: Chain + ChainWithMessagesBase { /// We assume that all chains that are bridging with this `ChainWithMessages` are using /// the same name. const WITH_CHAIN_RELAYERS_PALLET_NAME: Option<&'static str>; + /// Name of the runtime API method that is returning version of the + /// compatible relayer (`bp_runtime::RelayerVersion`). + /// + /// Keep in mind that this method is normally provided by the other chain, which is + /// bridged with this chain. + const WITH_CHAIN_COMPATIBLE_MESSAGES_RELAYER_VERSION_METHOD: &'static str; /// Name of the `ToOutboundLaneApi::message_details` runtime API method. /// The method is provided by the runtime that is bridged with this `ChainWithMessages`. diff --git a/bridges/relays/client-substrate/src/client.rs b/bridges/relays/client-substrate/src/client.rs index afbda8599b2a..0c8c0565ed0f 100644 --- a/bridges/relays/client-substrate/src/client.rs +++ b/bridges/relays/client-substrate/src/client.rs @@ -29,7 +29,7 @@ use crate::{ use async_std::sync::{Arc, Mutex, RwLock}; use async_trait::async_trait; -use bp_runtime::{HeaderIdProvider, StorageDoubleMapKeyProvider, StorageMapKeyProvider}; +use bp_runtime::{StorageDoubleMapKeyProvider, StorageMapKeyProvider}; use codec::{Decode, Encode}; use frame_support::weights::Weight; use futures::{SinkExt, StreamExt}; @@ -297,10 +297,10 @@ impl Client { impl Client { /// Return simple runtime version, only include `spec_version` and `transaction_version`. - pub async fn simple_runtime_version(&self) -> Result { + pub async fn simple_runtime_version(&self, at: HashOf) -> Result { Ok(match &self.chain_runtime_version { ChainRuntimeVersion::Auto => { - let runtime_version = self.runtime_version().await?; + let runtime_version = self.runtime_version(at).await?; SimpleRuntimeVersion::from_runtime_version(&runtime_version) }, ChainRuntimeVersion::Custom(version) => *version, @@ -403,9 +403,9 @@ impl Client { } /// Return runtime version. - pub async fn runtime_version(&self) -> Result { + pub async fn runtime_version(&self, at: HashOf) -> Result { self.jsonrpsee_execute(move |client| async move { - Ok(SubstrateStateClient::::runtime_version(&*client).await?) + Ok(SubstrateStateClient::::runtime_version(&*client, Some(at)).await?) }) .await } @@ -488,38 +488,15 @@ impl Client { .await } - /// Submit unsigned extrinsic for inclusion in a block. - /// - /// Note: The given transaction needs to be SCALE encoded beforehand. - pub async fn submit_unsigned_extrinsic(&self, transaction: Bytes) -> Result { - // one last check that the transaction is valid. Most of checks happen in the relay loop and - // it is the "final" check before submission. - let best_header_hash = self.best_header().await?.hash(); - self.validate_transaction(best_header_hash, PreEncoded(transaction.0.clone())) - .await - .map_err(|e| { - log::error!(target: "bridge", "Pre-submit {} transaction validation failed: {:?}", C::NAME, e); - e - })??; - - self.jsonrpsee_execute(move |client| async move { - let tx_hash = SubstrateAuthorClient::::submit_extrinsic(&*client, transaction) - .await - .map_err(|e| { - log::error!(target: "bridge", "Failed to send transaction to {} node: {:?}", C::NAME, e); - e - })?; - log::trace!(target: "bridge", "Sent transaction to {} node: {:?}", C::NAME, tx_hash); - Ok(tx_hash) - }) - .await - } - - async fn build_sign_params(&self, signer: AccountKeyPairOf) -> Result> + async fn build_sign_params( + &self, + signer: AccountKeyPairOf, + at: HashOf, + ) -> Result> where C: ChainWithTransactions, { - let runtime_version = self.simple_runtime_version().await?; + let runtime_version = self.simple_runtime_version(at).await?; Ok(SignParam:: { spec_version: runtime_version.spec_version, transaction_version: runtime_version.transaction_version, @@ -528,83 +505,26 @@ impl Client { }) } - /// Submit an extrinsic signed by given account. - /// - /// All calls of this method are synchronized, so there can't be more than one active - /// `submit_signed_extrinsic()` call. This guarantees that no nonces collision may happen - /// if all client instances are clones of the same initial `Client`. - /// - /// Note: The given transaction needs to be SCALE encoded beforehand. - pub async fn submit_signed_extrinsic( - &self, - signer: &AccountKeyPairOf, - prepare_extrinsic: impl FnOnce(HeaderIdOf, C::Nonce) -> Result> - + Send - + 'static, - ) -> Result - where - C: ChainWithTransactions, - C::AccountId: From<::Public>, - { - let _guard = self.submit_signed_extrinsic_lock.lock().await; - let transaction_nonce = self.next_account_index(signer.public().into()).await?; - let best_header = self.best_header().await?; - let signing_data = self.build_sign_params(signer.clone()).await?; - - // By using parent of best block here, we are protecing again best-block reorganizations. - // E.g. transaction may have been submitted when the best block was `A[num=100]`. Then it - // has been changed to `B[num=100]`. Hash of `A` has been included into transaction - // signature payload. So when signature will be checked, the check will fail and transaction - // will be dropped from the pool. - let best_header_id = best_header.parent_id().unwrap_or_else(|| best_header.id()); - - let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce)?; - let signed_extrinsic = C::sign_transaction(signing_data, extrinsic)?.encode(); - - // one last check that the transaction is valid. Most of checks happen in the relay loop and - // it is the "final" check before submission. - self.validate_transaction(best_header_id.1, PreEncoded(signed_extrinsic.clone())) - .await - .map_err(|e| { - log::error!(target: "bridge", "Pre-submit {} transaction validation failed: {:?}", C::NAME, e); - e - })??; - - self.jsonrpsee_execute(move |client| async move { - let tx_hash = - SubstrateAuthorClient::::submit_extrinsic(&*client, Bytes(signed_extrinsic)) - .await - .map_err(|e| { - log::error!(target: "bridge", "Failed to send transaction to {} node: {:?}", C::NAME, e); - e - })?; - log::trace!(target: "bridge", "Sent transaction to {} node: {:?}", C::NAME, tx_hash); - Ok(tx_hash) - }) - .await - } - /// Does exactly the same as `submit_signed_extrinsic`, but keeps watching for extrinsic status /// after submission. + /// + /// The best block`` pub async fn submit_and_watch_signed_extrinsic( &self, + best_header_id: HeaderIdOf, signer: &AccountKeyPairOf, - prepare_extrinsic: impl FnOnce(HeaderIdOf, C::Nonce) -> Result> - + Send - + 'static, + prepare_extrinsic: impl FnOnce(C::Nonce) -> Result> + Send + 'static, ) -> Result> where C: ChainWithTransactions, C::AccountId: From<::Public>, { let self_clone = self.clone(); - let signing_data = self.build_sign_params(signer.clone()).await?; + let signing_data = self.build_sign_params(signer.clone(), best_header_id.hash()).await?; let _guard = self.submit_signed_extrinsic_lock.lock().await; let transaction_nonce = self.next_account_index(signer.public().into()).await?; - let best_header = self.best_header().await?; - let best_header_id = best_header.id(); - let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce)?; + let extrinsic = prepare_extrinsic(transaction_nonce)?; let stall_timeout = transaction_stall_timeout( extrinsic.era.mortality_period(), C::AVERAGE_BLOCK_INTERVAL, @@ -653,14 +573,6 @@ impl Client { Ok(tracker) } - /// Returns pending extrinsics from transaction pool. - pub async fn pending_extrinsics(&self) -> Result> { - self.jsonrpsee_execute(move |client| async move { - Ok(SubstrateAuthorClient::::pending_extrinsics(&*client).await?) - }) - .await - } - /// Validate transaction at given block state. pub async fn validate_transaction( &self, diff --git a/bridges/relays/client-substrate/src/error.rs b/bridges/relays/client-substrate/src/error.rs index 0b4466818188..0b6e9cdda5cb 100644 --- a/bridges/relays/client-substrate/src/error.rs +++ b/bridges/relays/client-substrate/src/error.rs @@ -18,6 +18,7 @@ use crate::SimpleRuntimeVersion; use bp_polkadot_core::parachains::ParaId; +use bp_runtime::RelayerVersion; use jsonrpsee::core::ClientError as RpcError; use relay_utils::MaybeConnectionError; use sc_rpc_api::system::Health; @@ -129,6 +130,23 @@ pub enum Error { /// Actual runtime version. actual: SimpleRuntimeVersion, }, + /// Incompatible relayer version. + #[error( + "{source_chain} to {target_chain} {relayer_type} relayer version: {offchain_relayer_version:?} \ + is different from version set at {target_chain}: {onchain_relayer_version:?}" + )] + IncompatibleRelayerVersion { + /// Source chain name. + source_chain: &'static str, + /// Target chain name. + target_chain: &'static str, + /// Relayer type. + relayer_type: &'static str, + /// Offchain (actual) relayer version. + offchain_relayer_version: RelayerVersion, + /// Onchain (expected) relayer version. + onchain_relayer_version: RelayerVersion, + }, /// Custom logic error. #[error("{0}")] Custom(String), diff --git a/bridges/relays/client-substrate/src/guard.rs b/bridges/relays/client-substrate/src/guard.rs index 47454892cd03..fd9a36bcd883 100644 --- a/bridges/relays/client-substrate/src/guard.rs +++ b/bridges/relays/client-substrate/src/guard.rs @@ -20,6 +20,7 @@ use crate::{error::Error, Chain, Client}; use async_trait::async_trait; +use bp_runtime::HeaderIdProvider; use sp_version::RuntimeVersion; use std::{ fmt::Display, @@ -102,7 +103,8 @@ impl Environment for Client { type Error = Error; async fn runtime_version(&mut self) -> Result { - Client::::runtime_version(self).await + let best_block_id = self.best_header().await?.id(); + Client::::runtime_version(self, best_block_id.hash()).await } } diff --git a/bridges/relays/client-substrate/src/rpc.rs b/bridges/relays/client-substrate/src/rpc.rs index 60c29cdeb5c7..0da151815a7d 100644 --- a/bridges/relays/client-substrate/src/rpc.rs +++ b/bridges/relays/client-substrate/src/rpc.rs @@ -81,7 +81,7 @@ pub(crate) trait SubstrateAuthor { pub(crate) trait SubstrateState { /// Get current runtime version. #[method(name = "getRuntimeVersion")] - async fn runtime_version(&self) -> RpcResult; + async fn runtime_version(&self, at_block: Option) -> RpcResult; /// Call given runtime method. #[method(name = "call")] async fn call( diff --git a/bridges/relays/client-substrate/src/test_chain.rs b/bridges/relays/client-substrate/src/test_chain.rs index cfd241c022a2..11d0dee581a5 100644 --- a/bridges/relays/client-substrate/src/test_chain.rs +++ b/bridges/relays/client-substrate/src/test_chain.rs @@ -56,6 +56,7 @@ impl bp_runtime::Chain for TestChain { impl Chain for TestChain { const NAME: &'static str = "Test"; const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestMethod"; + const WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD: &'static str = "TestMethod"; const FREE_HEADERS_INTERVAL_METHOD: &'static str = "TestMethod"; const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0); @@ -79,6 +80,8 @@ impl ChainWithMessagesBase for TestChain { impl ChainWithMessages for TestChain { const WITH_CHAIN_RELAYERS_PALLET_NAME: Option<&'static str> = None; + const WITH_CHAIN_COMPATIBLE_MESSAGES_RELAYER_VERSION_METHOD: &'static str = + "TestRelayerVersionMethod"; const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str = "TestMessagesDetailsMethod"; const FROM_CHAIN_MESSAGE_DETAILS_METHOD: &'static str = "TestFromMessagesDetailsMethod"; } @@ -125,6 +128,8 @@ impl bp_runtime::UnderlyingChainProvider for TestParachain { impl Chain for TestParachain { const NAME: &'static str = "TestParachain"; const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestParachainMethod"; + const WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD: &'static str = + "TestParachainMethod"; const FREE_HEADERS_INTERVAL_METHOD: &'static str = "TestParachainMethod"; const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0); diff --git a/bridges/relays/lib-substrate-relay/src/equivocation/mod.rs b/bridges/relays/lib-substrate-relay/src/equivocation/mod.rs index f6d58cbaa4ab..9c4668e9aef2 100644 --- a/bridges/relays/lib-substrate-relay/src/equivocation/mod.rs +++ b/bridges/relays/lib-substrate-relay/src/equivocation/mod.rs @@ -27,7 +27,7 @@ use crate::{ }; use async_trait::async_trait; -use bp_runtime::{AccountIdOf, BlockNumberOf, HashOf}; +use bp_runtime::{AccountIdOf, BlockNumberOf, HashOf, HeaderIdProvider}; use equivocation_detector::EquivocationDetectionPipeline; use finality_relay::FinalityPipeline; use pallet_grandpa::{Call as GrandpaCall, Config as GrandpaConfig}; @@ -73,9 +73,10 @@ pub trait SubstrateEquivocationDetectionPipeline: enable_version_guard: bool, ) -> relay_substrate_client::Result<()> { if enable_version_guard { + let best_block_id = source_client.best_header().await?.id(); relay_substrate_client::guard::abort_on_spec_version_change( source_client.clone(), - source_client.simple_runtime_version().await?.spec_version, + source_client.simple_runtime_version(best_block_id.hash()).await?.spec_version, ); } Ok(()) diff --git a/bridges/relays/lib-substrate-relay/src/equivocation/source.rs b/bridges/relays/lib-substrate-relay/src/equivocation/source.rs index a0c7dcf5cbc3..d2c46a9f4504 100644 --- a/bridges/relays/lib-substrate-relay/src/equivocation/source.rs +++ b/bridges/relays/lib-substrate-relay/src/equivocation/source.rs @@ -26,7 +26,7 @@ use crate::{ }; use async_trait::async_trait; -use bp_runtime::{HashOf, TransactionEra}; +use bp_runtime::{HashOf, HeaderIdProvider, TransactionEra}; use equivocation_detector::SourceClient; use finality_relay::SourceClientBase; use relay_substrate_client::{ @@ -91,6 +91,7 @@ impl P::FinalityEngine::generate_source_key_ownership_proof(&self.client, at, &equivocation) .await?; + let best_block_id = self.client.best_header().await?.id(); let mortality = self.transaction_params.mortality; let call = P::ReportEquivocationCallBuilder::build_report_equivocation_call( equivocation, @@ -98,8 +99,9 @@ impl ); self.client .submit_and_watch_signed_extrinsic( + best_block_id, &self.transaction_params.signer, - move |best_block_id, transaction_nonce| { + move |transaction_nonce| { Ok(UnsignedTransaction::new(call.into(), transaction_nonce) .era(TransactionEra::new(best_block_id, mortality))) }, diff --git a/bridges/relays/lib-substrate-relay/src/error.rs b/bridges/relays/lib-substrate-relay/src/error.rs index 2ebd9130f391..dcf3d84d7cc9 100644 --- a/bridges/relays/lib-substrate-relay/src/error.rs +++ b/bridges/relays/lib-substrate-relay/src/error.rs @@ -52,6 +52,9 @@ pub enum Error { /// Failed to decode GRANDPA authorities at the given header of the source chain. #[error("Failed to decode {0} GRANDPA authorities set at header {1}: {2:?}")] DecodeAuthorities(&'static str, Hash, codec::Error), + /// Failed to retrieve best header from the chain. + #[error("Failed to retrieve best {0} header: {0:?}")] + RetrieveBestHeader(&'static str, client::Error), /// Failed to retrieve header by the hash from the source chain. #[error("Failed to retrieve {0} header with hash {1}: {2:?}")] RetrieveHeader(&'static str, Hash, client::Error), diff --git a/bridges/relays/lib-substrate-relay/src/finality/initialize.rs b/bridges/relays/lib-substrate-relay/src/finality/initialize.rs index 5dde46c39dd6..9167a9afa69e 100644 --- a/bridges/relays/lib-substrate-relay/src/finality/initialize.rs +++ b/bridges/relays/lib-substrate-relay/src/finality/initialize.rs @@ -24,7 +24,7 @@ use crate::{error::Error, finality_base::engine::Engine}; use sp_core::Pair; -use bp_runtime::HeaderIdOf; +use bp_runtime::{HeaderIdOf, HeaderIdProvider}; use relay_substrate_client::{ AccountKeyPairOf, Chain, ChainWithTransactions, Client, Error as SubstrateError, UnsignedTransaction, @@ -143,17 +143,26 @@ where initialization_data, ); + let best_block_id = target_client + .best_header() + .await + .map_err(|e| Error::RetrieveBestHeader(TargetChain::NAME, e))? + .id(); let tx_status = target_client - .submit_and_watch_signed_extrinsic(&target_signer, move |_, transaction_nonce| { - let tx = prepare_initialize_transaction(transaction_nonce, initialization_data); - if dry_run { - Err(SubstrateError::Custom( - "Not submitting extrinsic in `dry-run` mode!".to_string(), - )) - } else { - tx - } - }) + .submit_and_watch_signed_extrinsic( + best_block_id, + &target_signer, + move |transaction_nonce| { + let tx = prepare_initialize_transaction(transaction_nonce, initialization_data); + if dry_run { + Err(SubstrateError::Custom( + "Not submitting extrinsic in `dry-run` mode!".to_string(), + )) + } else { + tx + } + }, + ) .await .map_err(|err| Error::SubmitTransaction(TargetChain::NAME, err))? .wait() diff --git a/bridges/relays/lib-substrate-relay/src/finality/mod.rs b/bridges/relays/lib-substrate-relay/src/finality/mod.rs index 0293e1da224a..5ee5719d7419 100644 --- a/bridges/relays/lib-substrate-relay/src/finality/mod.rs +++ b/bridges/relays/lib-substrate-relay/src/finality/mod.rs @@ -25,6 +25,7 @@ use crate::{ use async_trait::async_trait; use bp_header_chain::justification::{GrandpaJustification, JustificationVerificationContext}; +use bp_runtime::{HeaderIdProvider, RelayerVersion}; use finality_relay::{ FinalityPipeline, FinalitySyncPipeline, HeadersToRelay, SourceClient, TargetClient, }; @@ -72,6 +73,11 @@ where /// Substrate -> Substrate finality proofs synchronization pipeline. #[async_trait] pub trait SubstrateFinalitySyncPipeline: BaseSubstrateFinalitySyncPipeline { + /// Version of this relayer. It must match version that the + /// `Self::SourceChain::WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD` + /// returns when called at `Self::TargetChain`. + const RELAYER_VERSION: Option; + /// How submit finality proof call is built? type SubmitFinalityProofCallBuilder: SubmitFinalityProofCallBuilder; @@ -81,9 +87,10 @@ pub trait SubstrateFinalitySyncPipeline: BaseSubstrateFinalitySyncPipeline { enable_version_guard: bool, ) -> relay_substrate_client::Result<()> { if enable_version_guard { + let best_block_id = target_client.best_header().await?.id(); relay_substrate_client::guard::abort_on_spec_version_change( target_client.clone(), - target_client.simple_runtime_version().await?.spec_version, + target_client.simple_runtime_version(best_block_id.hash()).await?.spec_version, ); } Ok(()) diff --git a/bridges/relays/lib-substrate-relay/src/finality/target.rs b/bridges/relays/lib-substrate-relay/src/finality/target.rs index 0874fa53549c..2e8b51c70c92 100644 --- a/bridges/relays/lib-substrate-relay/src/finality/target.rs +++ b/bridges/relays/lib-substrate-relay/src/finality/target.rs @@ -17,6 +17,7 @@ //! Substrate client as Substrate finality proof target. use crate::{ + ensure_relayer_compatibility, finality::{ FinalitySyncPipelineAdapter, SubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline, }, @@ -25,7 +26,7 @@ use crate::{ }; use async_trait::async_trait; -use bp_runtime::BlockNumberOf; +use bp_runtime::{BlockNumberOf, HeaderIdProvider}; use finality_relay::TargetClient; use relay_substrate_client::{ AccountKeyPairOf, Chain, Client, Error, HeaderIdOf, HeaderOf, SyncHeader, TransactionEra, @@ -137,6 +138,17 @@ impl TargetClient( + "finality", + &self.client, + best_block_id, + P::SourceChain::WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD, + &P::RELAYER_VERSION, + ) + .await?; + // now we may submit optimized finality proof let mortality = self.transaction_params.mortality; let call = P::SubmitFinalityProofCallBuilder::build_submit_finality_proof_call( @@ -147,8 +159,9 @@ impl TargetClient BatchCallBuilder for () { unreachable!("never called, because ()::new_builder() returns None; qed") } } + +/// Ensure that the relayer is compatible with on-chain bridge version. +pub async fn ensure_relayer_compatibility( + relayer_type: &'static str, + target_client: &Client, + at_target_block: HeaderIdOf, + onchain_relayer_version_method: &str, + offchain_relayer_version: &Option, +) -> Result<(), relay_substrate_client::Error> { + let Some(offchain_relayer_version) = offchain_relayer_version.as_ref() else { return Ok(()) }; + + // read onchain version + let onchain_relayer_version: RelayerVersion = target_client + .typed_state_call(onchain_relayer_version_method.into(), (), Some(at_target_block.hash())) + .await?; + // if they are the same => just return, we are safe to submit transactions + if onchain_relayer_version.manual == offchain_relayer_version.manual { + return Ok(()) + } + + // else if offchain version is lower than onchain, we need to abort - we are running the old + // version. We also abort if the `manual` version is the same, but `auto` version is different. + // It means a programming error and we are incompatible + let error = relay_substrate_client::Error::IncompatibleRelayerVersion { + source_chain: SourceChain::NAME, + target_chain: TargetChain::NAME, + relayer_type, + offchain_relayer_version: *offchain_relayer_version, + onchain_relayer_version, + }; + if offchain_relayer_version.manual <= onchain_relayer_version.manual { + log::error!(target: "bridge-guard", "Aborting relay: {error}"); + std::process::abort(); + } + + // we are running a newer version, so let's just return an error and wait until runtime is + // upgraded + Err(error) +} diff --git a/bridges/relays/lib-substrate-relay/src/messages_lane.rs b/bridges/relays/lib-substrate-relay/src/messages_lane.rs index 58e9ded312df..a41ec1124f12 100644 --- a/bridges/relays/lib-substrate-relay/src/messages_lane.rs +++ b/bridges/relays/lib-substrate-relay/src/messages_lane.rs @@ -26,7 +26,8 @@ use crate::{ use async_std::sync::Arc; use bp_messages::{ChainWithMessages as _, LaneId, MessageNonce}; use bp_runtime::{ - AccountIdOf, Chain as _, EncodedOrDecodedCall, HeaderIdOf, TransactionEra, WeightExtraOps, + AccountIdOf, Chain as _, EncodedOrDecodedCall, HeaderIdOf, RelayerVersion, TransactionEra, + WeightExtraOps, }; use bridge_runtime_common::messages::{ source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof, @@ -50,6 +51,15 @@ use std::{fmt::Debug, marker::PhantomData}; /// Substrate -> Substrate messages synchronization pipeline. pub trait SubstrateMessageLane: 'static + Clone + Debug + Send + Sync { + /// Version of this relayer. It must match version that the + /// `Self::TargetChain::WITH_CHAIN_COMPATIBLE_MESSAGES_RELAYER_VERSION_METHOD` + /// returns when called at `Self::SourceChain`. + const AT_SOURCE_CHAIN_RELAYER_VERSION: Option; + /// Version of this relayer. It must match version that the + /// `Self::SourceChain::WITH_CHAIN_COMPATIBLE_MESSAGES_RELAYER_VERSION_METHOD` + /// returns when called at `Self::TargetChain`. + const AT_TARGET_CHAIN_RELAYER_VERSION: Option; + /// Messages of this chain are relayed to the `TargetChain`. type SourceChain: ChainWithMessages + ChainWithTransactions; /// Messages from the `SourceChain` are dispatched on this chain. diff --git a/bridges/relays/lib-substrate-relay/src/messages_source.rs b/bridges/relays/lib-substrate-relay/src/messages_source.rs index 49deff046f9c..6f97ef95fb6d 100644 --- a/bridges/relays/lib-substrate-relay/src/messages_source.rs +++ b/bridges/relays/lib-substrate-relay/src/messages_source.rs @@ -19,6 +19,7 @@ //! `` chain. use crate::{ + ensure_relayer_compatibility, finality_base::best_synced_header_id, messages_lane::{ BatchProofTransaction, MessageLaneAdapter, ReceiveMessagesDeliveryProofCallBuilder, @@ -356,11 +357,22 @@ where None => messages_proof_call, }; + let best_block_id = self.source_client.best_header().await?.id(); + ensure_relayer_compatibility::( + "finality", + &self.source_client, + best_block_id, + P::TargetChain::WITH_CHAIN_COMPATIBLE_MESSAGES_RELAYER_VERSION_METHOD, + &P::AT_SOURCE_CHAIN_RELAYER_VERSION, + ) + .await?; + let transaction_params = self.transaction_params.clone(); self.source_client .submit_and_watch_signed_extrinsic( + best_block_id, &self.transaction_params.signer, - move |best_block_id, transaction_nonce| { + move |transaction_nonce| { Ok(UnsignedTransaction::new(final_call.into(), transaction_nonce) .era(TransactionEra::new(best_block_id, transaction_params.mortality))) }, diff --git a/bridges/relays/lib-substrate-relay/src/messages_target.rs b/bridges/relays/lib-substrate-relay/src/messages_target.rs index 633b11f0b802..e2497f999cc0 100644 --- a/bridges/relays/lib-substrate-relay/src/messages_target.rs +++ b/bridges/relays/lib-substrate-relay/src/messages_target.rs @@ -19,6 +19,7 @@ //! `` chain. use crate::{ + ensure_relayer_compatibility, messages_lane::{ BatchProofTransaction, MessageLaneAdapter, ReceiveMessagesProofCallBuilder, SubstrateMessageLane, @@ -34,14 +35,15 @@ use bp_messages::{ storage_keys::inbound_lane_data_key, ChainWithMessages as _, InboundLaneData, LaneId, MessageNonce, UnrewardedRelayersState, }; +use bp_runtime::HeaderIdProvider; use bridge_runtime_common::messages::source::FromBridgedChainMessagesDeliveryProof; use messages_relay::{ message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf}, message_lane_loop::{NoncesSubmitArtifacts, TargetClient, TargetClientState}, }; use relay_substrate_client::{ - AccountIdOf, AccountKeyPairOf, BalanceOf, CallOf, Client, Error as SubstrateError, HashOf, - TransactionEra, TransactionTracker, UnsignedTransaction, + AccountIdOf, AccountKeyPairOf, BalanceOf, CallOf, ChainWithMessages, Client, + Error as SubstrateError, HashOf, TransactionEra, TransactionTracker, UnsignedTransaction, }; use relay_utils::relay_loop::Client as RelayClient; use sp_core::Pair; @@ -249,12 +251,23 @@ where None => messages_proof_call, }; + let best_block_id = self.target_client.best_header().await?.id(); + ensure_relayer_compatibility::( + "finality", + &self.target_client, + best_block_id, + P::SourceChain::WITH_CHAIN_COMPATIBLE_MESSAGES_RELAYER_VERSION_METHOD, + &P::AT_TARGET_CHAIN_RELAYER_VERSION, + ) + .await?; + let transaction_params = self.transaction_params.clone(); let tx_tracker = self .target_client .submit_and_watch_signed_extrinsic( + best_block_id, &self.transaction_params.signer, - move |best_block_id, transaction_nonce| { + move |transaction_nonce| { Ok(UnsignedTransaction::new(final_call.into(), transaction_nonce) .era(TransactionEra::new(best_block_id, transaction_params.mortality))) }, diff --git a/bridges/relays/lib-substrate-relay/src/parachains/mod.rs b/bridges/relays/lib-substrate-relay/src/parachains/mod.rs index 8b128bb770dd..6bb1b80ba7aa 100644 --- a/bridges/relays/lib-substrate-relay/src/parachains/mod.rs +++ b/bridges/relays/lib-substrate-relay/src/parachains/mod.rs @@ -19,6 +19,7 @@ use async_trait::async_trait; use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId}; +use bp_runtime::RelayerVersion; use pallet_bridge_parachains::{ Call as BridgeParachainsCall, Config as BridgeParachainsConfig, RelayBlockHash, RelayBlockHasher, RelayBlockNumber, @@ -38,6 +39,11 @@ pub mod target; /// will be used (at least) initially. #[async_trait] pub trait SubstrateParachainsPipeline: 'static + Clone + Debug + Send + Sync { + /// Version of this relayer. It must match version that the + /// `Self::SourceParachain::WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD` + /// returns when called at `Self::TargetChain`. + const RELAYER_VERSION: Option; + /// Headers of this parachain are submitted to the `Self::TargetChain`. type SourceParachain: Parachain; /// Relay chain that is storing headers of `Self::SourceParachain`. diff --git a/bridges/relays/lib-substrate-relay/src/parachains/target.rs b/bridges/relays/lib-substrate-relay/src/parachains/target.rs index 531d55b53223..994025a0160e 100644 --- a/bridges/relays/lib-substrate-relay/src/parachains/target.rs +++ b/bridges/relays/lib-substrate-relay/src/parachains/target.rs @@ -17,6 +17,7 @@ //! Parachain heads target. use crate::{ + ensure_relayer_compatibility, parachains::{ ParachainsPipelineAdapter, SubmitParachainHeadsCallBuilder, SubstrateParachainsPipeline, }, @@ -190,6 +191,16 @@ where proof: ParaHeadsProof, is_free_execution_expected: bool, ) -> Result { + let best_block_id = self.target_client.best_header().await?.id(); + ensure_relayer_compatibility::( + "parachains", + &self.target_client, + best_block_id, + P::SourceParachain::WITH_CHAIN_COMPATIBLE_FINALITY_RELAYER_VERSION_METHOD, + &P::RELAYER_VERSION, + ) + .await?; + let transaction_params = self.transaction_params.clone(); let call = P::SubmitParachainHeadsCallBuilder::build_submit_parachain_heads_call( at_relay_block, @@ -199,8 +210,9 @@ where ); self.target_client .submit_and_watch_signed_extrinsic( + best_block_id, &transaction_params.signer, - move |best_block_id, transaction_nonce| { + move |transaction_nonce| { Ok(UnsignedTransaction::new(call.into(), transaction_nonce) .era(TransactionEra::new(best_block_id, transaction_params.mortality))) }, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml index 574406ab305f..e340c41f1444 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml @@ -18,6 +18,7 @@ codec = { package = "parity-scale-codec", version = "3.0.0", default-features = ] } hex-literal = { version = "0.4.1" } log = { workspace = true } +paste = { version = "1.0", default-features = false } scale-info = { version = "2.11.1", default-features = false, features = [ "derive", ] } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs index 5551b05e2025..5cba2e73226a 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_common_config.rs @@ -25,9 +25,11 @@ use super::{ weights, AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeEvent, RuntimeOrigin, }; use bp_parachains::SingleParaStoredHeaderDataBuilder; -use bp_runtime::UnderlyingChainProvider; +use bp_runtime::{RelayerVersion, UnderlyingChainProvider}; use bridge_runtime_common::messages::ThisChainWithMessages; use frame_support::{parameter_types, traits::ConstU32}; +use hex_literal::hex; +use sp_core::H256; use sp_runtime::RuntimeDebug; parameter_types! { @@ -41,6 +43,19 @@ parameter_types! { pub const RelayerStakeLease: u32 = 8; pub const RelayerStakeReserveId: [u8; 8] = *b"brdgrlrs"; + pub const WithRococoBulletinCompatibleGrandpaRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("6138a8e2b887e0555545ae1a3335a31b1ee61c54b06fde481e50a67be5cdb1fb")), + }; + pub const WithWestendCompatibleGrandpaRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("e4898d272b30869b13f3a10fca4ed5bbbfee3745ad9b322bf0982624353b622a")), + }; + pub const WithWestendCompatibleParachainsRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("6a9443a8994da4b52f95f7d2e4e9905c0a6eb116042483a09b9c795c0e6f834a")), + }; + pub storage DeliveryRewardInBalance: u64 = 1_000_000; } @@ -48,6 +63,7 @@ parameter_types! { pub type BridgeGrandpaWestendInstance = pallet_bridge_grandpa::Instance3; impl pallet_bridge_grandpa::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithWestendCompatibleGrandpaRelayer; type BridgedChain = bp_westend::Westend; type MaxFreeHeadersPerBlock = ConstU32<4>; type FreeHeadersInterval = ConstU32<5>; @@ -59,6 +75,7 @@ impl pallet_bridge_grandpa::Config for Runtime { pub type BridgeParachainWestendInstance = pallet_bridge_parachains::Instance3; impl pallet_bridge_parachains::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithWestendCompatibleParachainsRelayer; type WeightInfo = weights::pallet_bridge_parachains::WeightInfo; type BridgesGrandpaPalletInstance = BridgeGrandpaWestendInstance; type ParasPalletName = WestendBridgeParachainPalletName; @@ -89,6 +106,7 @@ impl pallet_bridge_relayers::Config for Runtime { pub type BridgeGrandpaRococoBulletinInstance = pallet_bridge_grandpa::Instance4; impl pallet_bridge_grandpa::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithRococoBulletinCompatibleGrandpaRelayer; type BridgedChain = bp_polkadot_bulletin::PolkadotBulletin; type MaxFreeHeadersPerBlock = ConstU32<4>; type FreeHeadersInterval = ConstU32<5>; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_bulletin_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_bulletin_config.rs index 94b936889b77..8b6cf69543dd 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_bulletin_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_bulletin_config.rs @@ -25,7 +25,7 @@ use crate::{ XcmOverRococoBulletin, XcmRouter, }; use bp_messages::LaneId; -use bp_runtime::Chain; +use bp_runtime::{Chain, RelayerVersion}; use bridge_runtime_common::{ extensions::refund_relayer_extension::{ ActualFeeRefund, RefundBridgedMessages, RefundSignedExtensionAdapter, @@ -44,6 +44,8 @@ use bridge_runtime_common::{ }; use frame_support::{parameter_types, traits::PalletInfoAccess}; +use hex_literal::hex; +use sp_core::H256; use sp_runtime::RuntimeDebug; use xcm::{ latest::prelude::*, @@ -109,6 +111,11 @@ parameter_types! { /// XCM message that is never sent. pub NeverSentMessage: Option> = None; + + pub const WithRococoBulletinCompatibleMessagesRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("70eed2935a540a45fe0e94e5f24daf70e323061e60e3784c02b27151acf9a2b2")), + }; } pub const XCM_LANE_FOR_ROCOCO_PEOPLE_TO_ROCOCO_BULLETIN: LaneId = LaneId([0, 0, 0, 0]); @@ -178,15 +185,16 @@ pub type OnBridgeHubRococoRefundRococoBulletinMessages = RefundSignedExtensionAd >, ActualFeeRefund, PriorityBoostPerMessage, - StrOnBridgeHubRococoRefundRococoBulletinMessages, + StrRefundComplexRococoBulletinBridgeTransactions, >, >; -bp_runtime::generate_static_str_provider!(OnBridgeHubRococoRefundRococoBulletinMessages); +bp_runtime::generate_static_str_provider!(RefundComplexRococoBulletinBridgeTransactions); /// Add XCM messages support for BridgeHubRococo to support Rococo->Rococo Bulletin XCM messages. pub type WithRococoBulletinMessagesInstance = pallet_bridge_messages::Instance4; impl pallet_bridge_messages::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithRococoBulletinCompatibleMessagesRelayer; type WeightInfo = weights::pallet_bridge_messages_rococo_to_rococo_bulletin::WeightInfo; type BridgedChainId = RococoBulletinChainId; @@ -228,7 +236,11 @@ mod tests { use super::*; use crate::bridge_common_config::BridgeGrandpaRococoBulletinInstance; use bridge_runtime_common::{ - assert_complete_bridge_types, integrity::check_message_lane_weights, + assert_complete_bridge_types, + integrity::check_message_lane_weights, + relayer_compatibility::{ + ensure_grandpa_relayer_compatibility, ensure_messages_relayer_compatibility, + }, }; use parachains_common::Balance; use testnet_parachains_constants::rococo; @@ -294,5 +306,20 @@ mod tests { .into(); assert_eq!(BridgeRococoToRococoBulletinMessagesPalletInstance::get(), expected,); + + sp_io::TestExternalities::default().execute_with(|| { + ensure_grandpa_relayer_compatibility::< + Runtime, + BridgeGrandpaRococoBulletinInstance, + crate::SignedExtra, + >(); + ensure_messages_relayer_compatibility::< + Runtime, + BridgeGrandpaRococoBulletinInstance, + crate::SignedExtra, + _, + _, + >(); + }); } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_westend_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_westend_config.rs index 1681ac7f4687..57e0acdeef0d 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_westend_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_westend_config.rs @@ -26,7 +26,7 @@ use crate::{ XcmRouter, }; use bp_messages::LaneId; -use bp_runtime::Chain; +use bp_runtime::{Chain, RelayerVersion}; use bridge_runtime_common::{ extensions::refund_relayer_extension::{ ActualFeeRefund, RefundBridgedMessages, RefundSignedExtensionAdapter, @@ -46,6 +46,8 @@ use bridge_runtime_common::{ use codec::Encode; use frame_support::{parameter_types, traits::PalletInfoAccess}; +use hex_literal::hex; +use sp_core::H256; use sp_runtime::RuntimeDebug; use xcm::{ latest::prelude::*, @@ -99,6 +101,11 @@ parameter_types! { Parachain(::PARACHAIN_ID) ] ); + + pub const WithWestendCompatibleMessagesRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("73545f1e73536fb3ac1b0fae47726b3f77931b8e76de8703f510366a86af177a")), + }; } pub const XCM_LANE_FOR_ASSET_HUB_ROCOCO_TO_ASSET_HUB_WESTEND: LaneId = LaneId([0, 0, 0, 2]); @@ -186,15 +193,16 @@ pub type OnBridgeHubRococoRefundBridgeHubWestendMessages = RefundSignedExtension >, ActualFeeRefund, PriorityBoostPerMessage, - StrOnBridgeHubRococoRefundBridgeHubWestendMessages, + StrRefundComplexWestendBridgeTransactions, >, >; -bp_runtime::generate_static_str_provider!(OnBridgeHubRococoRefundBridgeHubWestendMessages); +bp_runtime::generate_static_str_provider!(RefundComplexWestendBridgeTransactions); /// Add XCM messages support for BridgeHubRococo to support Rococo->Westend XCM messages pub type WithBridgeHubWestendMessagesInstance = pallet_bridge_messages::Instance3; impl pallet_bridge_messages::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithWestendCompatibleMessagesRelayer; type WeightInfo = weights::pallet_bridge_messages_rococo_to_westend::WeightInfo; type BridgedChainId = BridgeHubWestendChainId; type ActiveOutboundLanes = ActiveOutboundLanesToBridgeHubWestend; @@ -252,6 +260,10 @@ mod tests { AssertBridgeMessagesPalletConstants, AssertBridgePalletNames, AssertChainConstants, AssertCompleteBridgeConstants, }, + relayer_compatibility::{ + ensure_grandpa_relayer_compatibility, ensure_messages_relayer_compatibility, + ensure_parachains_relayer_compatibility, + }, }; use parachains_common::Balance; use testnet_parachains_constants::rococo; @@ -348,5 +360,25 @@ mod tests { .into(); assert_eq!(BridgeRococoToWestendMessagesPalletInstance::get(), expected,); + + sp_io::TestExternalities::default().execute_with(|| { + ensure_grandpa_relayer_compatibility::< + Runtime, + BridgeGrandpaWestendInstance, + crate::SignedExtra, + >(); + ensure_parachains_relayer_compatibility::< + Runtime, + BridgeParachainWestendInstance, + crate::SignedExtra, + >(); + ensure_messages_relayer_compatibility::< + Runtime, + BridgeGrandpaWestendInstance, + crate::SignedExtra, + _, + _, + >(); + }); } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs index 2a7f46feee69..09ad04e81dbf 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs @@ -962,6 +962,9 @@ impl_runtime_apis! { fn best_finalized() -> Option> { BridgeWestendGrandpa::best_finalized() } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeWestendGrandpa::compatible_relayer_version() + } fn free_headers_interval() -> Option { ().unwrap_or(None) } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeWestendParachains::compatible_relayer_version() + } fn free_headers_interval() -> Option { // "free interval" is not currently used for parachains None @@ -996,6 +1002,9 @@ impl_runtime_apis! { bridge_to_westend_config::WithBridgeHubWestendMessagesInstance, >(lane, messages) } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeWestendMessages::compatible_relayer_version() + } } // This is exposed by BridgeHubRococo @@ -1016,13 +1025,14 @@ impl_runtime_apis! { fn best_finalized() -> Option> { BridgePolkadotBulletinGrandpa::best_finalized() } - + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgePolkadotBulletinGrandpa::compatible_relayer_version() + } fn free_headers_interval() -> Option { >::FreeHeadersInterval::get() } - fn synced_headers_grandpa_info( ) -> Vec> { BridgePolkadotBulletinGrandpa::synced_headers_grandpa_info() @@ -1039,6 +1049,9 @@ impl_runtime_apis! { bridge_to_bulletin_config::WithRococoBulletinMessagesInstance, >(lane, messages) } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeRococoBulletinMessages::compatible_relayer_version() + } } impl bp_polkadot_bulletin::ToPolkadotBulletinOutboundLaneApi for Runtime { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml index a7241cc6d10c..5ce28122e78c 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml @@ -16,6 +16,7 @@ substrate-wasm-builder = { path = "../../../../../substrate/utils/wasm-builder", codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } hex-literal = { version = "0.4.1" } log = { workspace = true } +paste = { version = "1.0", default-features = false } scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } serde = { optional = true, features = ["derive"], workspace = true, default-features = true } tuplex = { version = "0.1", default-features = false } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_rococo_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_rococo_config.rs index 425b53da30fc..e320110f065e 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_rococo_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_rococo_config.rs @@ -23,7 +23,7 @@ use crate::{ }; use bp_messages::LaneId; use bp_parachains::SingleParaStoredHeaderDataBuilder; -use bp_runtime::Chain; +use bp_runtime::{Chain, RelayerVersion}; use bridge_runtime_common::{ extensions::refund_relayer_extension::{ ActualFeeRefund, RefundBridgedMessages, RefundSignedExtensionAdapter, @@ -45,6 +45,8 @@ use frame_support::{ parameter_types, traits::{ConstU32, PalletInfoAccess}, }; +use hex_literal::hex; +use sp_core::H256; use sp_runtime::RuntimeDebug; use xcm::{ latest::prelude::*, @@ -104,6 +106,19 @@ parameter_types! { Parachain(::PARACHAIN_ID) ] ); + + pub const WithRococoCompatibleGrandpaRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("49191a792ed3ae0bb1b45cced35a916b153eefdf6805760a176b2dc3d0567d5e")), + }; + pub const WithRococoCompatibleParachainsRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("4698a08e18d00a19a793397158d64d7074555eeb782282dda25ef4b1f4fa3a66")), + }; + pub const WithRococoCompatibleMessagesRelayer: RelayerVersion = RelayerVersion { + manual: 0, + auto: H256(hex!("3531d22ffe925ea1cf562c5a59d7f5e220bc69351e1013ba2dbfdeda52f4e215")), + }; } pub const XCM_LANE_FOR_ASSET_HUB_WESTEND_TO_ASSET_HUB_ROCOCO: LaneId = LaneId([0, 0, 0, 2]); @@ -203,15 +218,16 @@ pub type OnBridgeHubWestendRefundBridgeHubRococoMessages = RefundSignedExtension >, ActualFeeRefund, PriorityBoostPerMessage, - StrOnBridgeHubWestendRefundBridgeHubRococoMessages, + StrRefundComplexRococoBridgeTransactions, >, >; -bp_runtime::generate_static_str_provider!(OnBridgeHubWestendRefundBridgeHubRococoMessages); +bp_runtime::generate_static_str_provider!(RefundComplexRococoBridgeTransactions); /// Add GRANDPA bridge pallet to track Rococo relay chain. pub type BridgeGrandpaRococoInstance = pallet_bridge_grandpa::Instance1; impl pallet_bridge_grandpa::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithRococoCompatibleGrandpaRelayer; type BridgedChain = bp_rococo::Rococo; type MaxFreeHeadersPerBlock = ConstU32<4>; type FreeHeadersInterval = ConstU32<5>; @@ -223,6 +239,7 @@ impl pallet_bridge_grandpa::Config for Runtime { pub type BridgeParachainRococoInstance = pallet_bridge_parachains::Instance1; impl pallet_bridge_parachains::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithRococoCompatibleParachainsRelayer; type WeightInfo = weights::pallet_bridge_parachains::WeightInfo; type BridgesGrandpaPalletInstance = BridgeGrandpaRococoInstance; type ParasPalletName = RococoBridgeParachainPalletName; @@ -236,6 +253,7 @@ impl pallet_bridge_parachains::Config for Runtime pub type WithBridgeHubRococoMessagesInstance = pallet_bridge_messages::Instance1; impl pallet_bridge_messages::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type CompatibleWithRelayer = WithRococoCompatibleMessagesRelayer; type WeightInfo = weights::pallet_bridge_messages::WeightInfo; type BridgedChainId = BridgeHubRococoChainId; type ActiveOutboundLanes = ActiveOutboundLanesToBridgeHubRococo; @@ -291,6 +309,10 @@ mod tests { AssertBridgeMessagesPalletConstants, AssertBridgePalletNames, AssertChainConstants, AssertCompleteBridgeConstants, }, + relayer_compatibility::{ + ensure_grandpa_relayer_compatibility, ensure_messages_relayer_compatibility, + ensure_parachains_relayer_compatibility, + }, }; use parachains_common::Balance; use testnet_parachains_constants::westend; @@ -386,5 +408,25 @@ mod tests { bp_bridge_hub_westend::WITH_BRIDGE_WESTEND_TO_ROCOCO_MESSAGES_PALLET_INDEX )] ); + + sp_io::TestExternalities::default().execute_with(|| { + ensure_grandpa_relayer_compatibility::< + Runtime, + BridgeGrandpaRococoInstance, + crate::SignedExtra, + >(); + ensure_parachains_relayer_compatibility::< + Runtime, + BridgeParachainRococoInstance, + crate::SignedExtra, + >(); + ensure_messages_relayer_compatibility::< + Runtime, + WithBridgeHubRococoMessagesInstance, + crate::SignedExtra, + _, + _, + >(); + }); } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs index 4c467010c7c8..e0f3584e0932 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs @@ -711,6 +711,10 @@ impl_runtime_apis! { fn best_finalized() -> Option> { BridgeRococoGrandpa::best_finalized() } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeRococoGrandpa::compatible_relayer_version() + } + fn free_headers_interval() -> Option { ().unwrap_or(None) } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeRococoParachains::compatible_relayer_version() + } fn free_headers_interval() -> Option { // "free interval" is not currently used for parachains None @@ -744,6 +751,9 @@ impl_runtime_apis! { bridge_to_rococo_config::WithBridgeHubRococoMessagesInstance, >(lane, messages) } + fn compatible_relayer_version() -> bp_runtime::RelayerVersion { + BridgeRococoMessages::compatible_relayer_version() + } } impl bp_bridge_hub_rococo::ToBridgeHubRococoOutboundLaneApi for Runtime { diff --git a/prdoc/pr_4256.prdoc b/prdoc/pr_4256.prdoc new file mode 100644 index 000000000000..154c0383e884 --- /dev/null +++ b/prdoc/pr_4256.prdoc @@ -0,0 +1,30 @@ +title: "Bridges: Making relayer compatible with runtime upgrades" + +doc: + - audience: Runtime Dev + description: | + Bridge pallets are extended with compatible relayer version constants. + This constant "seals" all bridge settings that may affect running relayer. + Once it changes, a new relayer version needs to be deployed. On the other + hand, we may keep running the same relayer over multiple runtime upgrades + if relayer version stays the same. + +crates: +- name: bridge-runtime-common + bump: major +- name: pallet-bridge-grandpa + bump: major +- name: pallet-bridge-messages + bump: major +- name: pallet-bridge-parachains + bump: major +- name: bp-runtime + bump: major +- name: relay-substrate-client + bump: major +- name: substrate-relay-helper + bump: major +- name: bridge-hub-rococo-runtime + bump: major +- name: bridge-hub-westend-runtime + bump: major