diff --git a/bridges/bin/node/runtime/Cargo.toml b/bridges/bin/node/runtime/Cargo.toml index 5deeff8308..7713c621dc 100644 --- a/bridges/bin/node/runtime/Cargo.toml +++ b/bridges/bin/node/runtime/Cargo.toml @@ -45,6 +45,11 @@ version = "0.1.0" default-features = false path = "../../../modules/ethereum" +[dependencies.pallet-bridge-call-dispatch] +version = "0.1.0" +default-features = false +path = "../../../modules/call-dispatch" + [dependencies.pallet-bridge-currency-exchange] version = "0.1.0" default-features = false @@ -216,6 +221,11 @@ version = "0.1.0" default-features = false path = "../../../primitives/header-chain" +[dependencies.bp-message-lane] +version = "0.1.0" +default-features = false +path = "../../../primitives/message-lane" + # Dev Dependencies [dev-dependencies.libsecp256k1] @@ -240,6 +250,7 @@ default = ["std"] std = [ "bp-currency-exchange/std", "bp-eth-poa/std", + "bp-message-lane/std", "codec/std", "frame-benchmarking/std", "frame-executive/std", @@ -248,6 +259,7 @@ std = [ "frame-system/std", "pallet-aura/std", "pallet-balances/std", + "pallet-bridge-call-dispatch/std", "pallet-bridge-currency-exchange/std", "pallet-bridge-eth-poa/std", "pallet-grandpa/std", diff --git a/bridges/bin/node/runtime/src/lib.rs b/bridges/bin/node/runtime/src/lib.rs index 7fedc3e354..3bd039e3a5 100644 --- a/bridges/bin/node/runtime/src/lib.rs +++ b/bridges/bin/node/runtime/src/lib.rs @@ -268,6 +268,12 @@ impl pallet_bridge_currency_exchange::Trait for Runtime { type DepositInto = DepositInto; } +impl pallet_bridge_call_dispatch::Trait for Runtime { + type Event = Event; + type MessageId = (bp_message_lane::LaneId, bp_message_lane::MessageNonce); + type Call = Call; +} + pub struct DepositInto; impl bp_currency_exchange::DepositInto for DepositInto { @@ -463,6 +469,7 @@ construct_runtime!( BridgeKovan: pallet_bridge_eth_poa::::{Module, Call, Config, Storage, ValidateUnsigned}, BridgeRialtoCurrencyExchange: pallet_bridge_currency_exchange::::{Module, Call}, BridgeKovanCurrencyExchange: pallet_bridge_currency_exchange::::{Module, Call}, + BridgeCallDispatch: pallet_bridge_call_dispatch::{Module, Event}, System: frame_system::{Module, Call, Config, Storage, Event}, RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, diff --git a/bridges/modules/call-dispatch/Cargo.toml b/bridges/modules/call-dispatch/Cargo.toml new file mode 100644 index 0000000000..838f0aa6a9 --- /dev/null +++ b/bridges/modules/call-dispatch/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "pallet-bridge-call-dispatch" +description = "A Substrate Runtime module that dispatches a bridge message, treating it simply as encoded Call" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.3.1", default-features = false } +bp-message-dispatch = { path = "../../primitives/message-dispatch", default-features = false } +bp-runtime = { path = "../../primitives/runtime", default-features = false } + +# Substrate Based Dependencies +[dependencies.frame-support] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + +[dependencies.frame-system] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + +[dependencies.sp-std] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + +[dependencies.sp-runtime] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + +[dependencies.sp-core] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + +[dependencies.sp-io] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + +[features] +default = ["std"] +std = [ + "bp-message-dispatch/std", + "bp-runtime/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/bridges/modules/call-dispatch/src/lib.rs b/bridges/modules/call-dispatch/src/lib.rs new file mode 100644 index 0000000000..03da86ec0e --- /dev/null +++ b/bridges/modules/call-dispatch/src/lib.rs @@ -0,0 +1,348 @@ +// Copyright 2019-2020 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 . + +//! Runtime module which takes care of dispatching messages received over the bridge. +//! +//! The messages are interpreted directly as runtime `Call`s, we attempt to decode +//! them and then dispatch as usualy. +//! To prevent compatibility issues, the calls have to include `spec_version` as well +//! which is being checked before dispatch. +//! +//! In case of succesful dispatch event is emitted. + +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs)] + +use bp_message_dispatch::{MessageDispatch, Weight}; +use bp_runtime::{bridge_account_id, InstanceId, CALL_DISPATCH_MODULE_PREFIX}; +use frame_support::{ + decl_event, decl_module, + dispatch::{Dispatchable, Parameter}, + traits::Get, + weights::{extract_actual_weight, GetDispatchInfo}, +}; +use sp_runtime::DispatchResult; + +/// Spec version type. +pub type SpecVersion = u32; + +// TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/78) +/// Weight of single deposit_event() call. +const DEPOSIT_EVENT_WEIGHT: Weight = 0; + +/// The module configuration trait. +pub trait Trait: frame_system::Trait { + /// The overarching event type. + type Event: From> + Into<::Event>; + /// Id of the message. Whenever message is passed to the dispatch module, it emits + /// event with this id + dispatch result. Could be e.g. (LaneId, MessageNonce) if + /// it comes from message-lane module. + type MessageId: Parameter; + /// The overarching dispatch call type. + type Call: Parameter + + GetDispatchInfo + + Dispatchable< + Origin = ::Origin, + PostInfo = frame_support::dispatch::PostDispatchInfo, + >; +} + +decl_event!( + pub enum Event where + ::MessageId, + { + /// Message has been rejected by dispatcher because of spec version mismatch. + /// Last two arguments are: expected and passed spec version. + MessageVersionSpecMismatch(InstanceId, MessageId, SpecVersion, SpecVersion), + /// Message has been rejected by dispatcher because of weight mismatch. + /// Last two arguments are: expected and passed call weight. + MessageWeightMismatch(InstanceId, MessageId, Weight, Weight), + /// Message has been dispatched with given result. + MessageDispatched(InstanceId, MessageId, DispatchResult), + } +); + +decl_module! { + /// Call Dispatch FRAME Pallet. + pub struct Module for enum Call where origin: T::Origin { + /// Deposit one of this module's events by using the default implementation. + fn deposit_event() = default; + } +} + +impl MessageDispatch for Module { + type Message = (SpecVersion, Weight, ::Call); + + fn dispatch(bridge: InstanceId, id: T::MessageId, message: Self::Message) -> Weight { + let (spec_version, weight, call) = message; + + // verify spec version + // (we want it to be the same, because otherwise we may decode Call improperly) + let expected_version = ::Version::get().spec_version; + if spec_version != expected_version { + frame_support::debug::trace!( + "Message {:?}/{:?}: spec_version mismatch. Expected {:?}, got {:?}", + bridge, + id, + expected_version, + spec_version, + ); + Self::deposit_event(Event::::MessageVersionSpecMismatch( + bridge, + id, + expected_version, + spec_version, + )); + return DEPOSIT_EVENT_WEIGHT; + } + + // verify weight + // (we want passed weight to be at least equal to pre-dispatch weight of the call + // because otherwise Calls may be dispatched at lower price) + let dispatch_info = call.get_dispatch_info(); + let expected_weight = dispatch_info.weight; + if weight < expected_weight { + frame_support::debug::trace!( + "Message {:?}/{:?}: passed weight is too low. Expected at least {:?}, got {:?}", + bridge, + id, + expected_weight, + weight, + ); + Self::deposit_event(Event::::MessageWeightMismatch(bridge, id, expected_weight, weight)); + return DEPOSIT_EVENT_WEIGHT; + } + + // finally dispatch message + let origin_account = bridge_account_id(bridge, CALL_DISPATCH_MODULE_PREFIX); + let dispatch_result = call.dispatch(frame_system::RawOrigin::Signed(origin_account).into()); + let actual_call_weight = extract_actual_weight(&dispatch_result, &dispatch_info); + frame_support::debug::trace!( + "Message {:?}/{:?} has been dispatched. Result: {:?}", + bridge, + id, + dispatch_result, + ); + + Self::deposit_event(Event::::MessageDispatched( + bridge, + id, + dispatch_result.map(drop).map_err(|e| e.error), + )); + + actual_call_weight + DEPOSIT_EVENT_WEIGHT + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{impl_outer_dispatch, impl_outer_event, impl_outer_origin, parameter_types, weights::Weight}; + use frame_system::{EventRecord, Phase}; + use sp_core::H256; + use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + DispatchError, Perbill, + }; + + type AccountId = u64; + type CallDispatch = Module; + type System = frame_system::Module; + + type MessageId = [u8; 4]; + + #[derive(Clone, Eq, PartialEq)] + pub struct TestRuntime; + + mod call_dispatch { + pub use crate::Event; + } + + impl_outer_event! { + pub enum TestEvent for TestRuntime { + frame_system, + call_dispatch, + } + } + + impl_outer_origin! { + pub enum Origin for TestRuntime where system = frame_system {} + } + + impl_outer_dispatch! { + pub enum Call for TestRuntime where origin: Origin { + frame_system::System, + call_dispatch::CallDispatch, + } + } + + parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + } + + impl frame_system::Trait for TestRuntime { + type Origin = Origin; + type Index = u64; + type Call = Call; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = (); + type AvailableBlockRatio = AvailableBlockRatio; + type MaximumBlockLength = MaximumBlockLength; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); + } + + impl Trait for TestRuntime { + type Event = TestEvent; + type MessageId = MessageId; + type Call = Call; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + sp_io::TestExternalities::new(t) + } + + #[test] + fn should_succesfuly_dispatch_remark() { + new_test_ext().execute_with(|| { + let origin = b"ethb".to_owned(); + let id = [0; 4]; + let message = ( + 0, + 1_000_000_000, + Call::System(>::remark(vec![1, 2, 3])), + ); + + System::set_block_number(1); + CallDispatch::dispatch(origin, id, message); + + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: TestEvent::call_dispatch(Event::::MessageDispatched(origin, id, Ok(()))), + topics: vec![], + }], + ); + }); + } + + #[test] + fn should_fail_on_spec_version_mismatch() { + new_test_ext().execute_with(|| { + let origin = b"ethb".to_owned(); + let id = [0; 4]; + let message = ( + 69, + 1_000_000, + Call::System(>::remark(vec![1, 2, 3])), + ); + + System::set_block_number(1); + CallDispatch::dispatch(origin, id, message); + + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: TestEvent::call_dispatch(Event::::MessageVersionSpecMismatch( + origin, id, 0, 69, + )), + topics: vec![], + }], + ); + }); + } + + #[test] + fn should_fail_on_weight_mismatch() { + new_test_ext().execute_with(|| { + let origin = b"ethb".to_owned(); + let id = [0; 4]; + let message = ( + 0, + 0, + Call::System(>::remark(vec![1, 2, 3])), + ); + + System::set_block_number(1); + CallDispatch::dispatch(origin, id, message); + + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: TestEvent::call_dispatch(Event::::MessageWeightMismatch( + origin, id, 1305000, 0, + )), + topics: vec![], + }], + ); + }); + } + + #[test] + fn should_dispatch_from_non_root_origin() { + new_test_ext().execute_with(|| { + let origin = b"ethb".to_owned(); + let id = [0; 4]; + let message = ( + 0, + 1_000_000, + Call::System(>::fill_block(Perbill::from_percent(10))), + ); + + System::set_block_number(1); + CallDispatch::dispatch(origin, id, message); + + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: TestEvent::call_dispatch(Event::::MessageDispatched( + origin, + id, + Err(DispatchError::BadOrigin) + )), + topics: vec![], + }], + ); + }); + } +} diff --git a/bridges/primitives/message-dispatch/Cargo.toml b/bridges/primitives/message-dispatch/Cargo.toml new file mode 100644 index 0000000000..a6bf4b7cc5 --- /dev/null +++ b/bridges/primitives/message-dispatch/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bp-message-dispatch" +description = "Primitives of bridge messages dispatch modules." +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +bp-runtime = { path = "../runtime", default-features = false } +codec = { package = "parity-scale-codec", version = "1.3.1", default-features = false } + +[features] +default = ["std"] +std = [ + "bp-runtime/std", + "codec/std", +] diff --git a/bridges/primitives/message-dispatch/src/lib.rs b/bridges/primitives/message-dispatch/src/lib.rs new file mode 100644 index 0000000000..04b40e5972 --- /dev/null +++ b/bridges/primitives/message-dispatch/src/lib.rs @@ -0,0 +1,40 @@ +// Copyright 2019-2020 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 . + +//! A common interface for all Bridge Message Dispatch modules. + +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs)] + +use bp_runtime::InstanceId; + +/// Message dispatch weight. +pub type Weight = u64; + +/// A generic trait to dispatch arbitrary messages delivered over the bridge. +pub trait MessageDispatch { + /// A type of the message to be dispatched. + type Message: codec::Decode; + + /// Dispatches the message internally. + /// + /// `bridge` indicates instance of deployed bridge where the message came from. + /// + /// `id` is a short unique if of the message. + /// + /// Returns post-dispatch (actual) message weight. + fn dispatch(bridge: InstanceId, id: MessageId, message: Self::Message) -> Weight; +} diff --git a/bridges/primitives/runtime/Cargo.toml b/bridges/primitives/runtime/Cargo.toml index 84dd53e4d3..a0c8cb035c 100644 --- a/bridges/primitives/runtime/Cargo.toml +++ b/bridges/primitives/runtime/Cargo.toml @@ -6,6 +6,20 @@ authors = ["Parity Technologies "] edition = "2018" license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +[dependencies] +codec = { package = "parity-scale-codec", version = "1.3.1", default-features = false } + +# Substrate Based Dependencies + +[dependencies.sp-io] +version = "2.0.0-rc6" +tag = 'v2.0.0-rc6' +default-features = false +git = "https://github.com/paritytech/substrate/" + [features] default = ["std"] -std = [] +std = [ + "codec/std", + "sp-io/std", +] diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs index fa41b2126a..45097ea1b6 100644 --- a/bridges/primitives/runtime/src/lib.rs +++ b/bridges/primitives/runtime/src/lib.rs @@ -18,8 +18,29 @@ #![cfg_attr(not(feature = "std"), no_std)] +use codec::{Decode, Encode}; +use sp_io::hashing::blake2_256; + +/// Call-dispatch module prefix. +pub const CALL_DISPATCH_MODULE_PREFIX: &[u8] = b"pallet-bridge/call-dispatch"; + /// Id of deployed module instance. We have a bunch of pallets that may be used in /// different bridges. E.g. message-lane pallet may be deployed twice in the same /// runtime to bridge ThisChain with Chain1 and Chain2. Sometimes we need to be able /// to identify deployed instance dynamically. This type is used for that. pub type InstanceId = [u8; 4]; + +/// Returns id of account that acts as "system" account of given bridge instance. +/// The `module_prefix` (arbitrary slice) may be used to generate module-level +/// "system" account, so you could have separate "system" accounts for currency +/// exchange, message dispatch and other modules. +/// +/// The account is not supposed to actually exists on the chain, or to have any funds. +/// It is only used to +pub fn bridge_account_id(bridge: InstanceId, module_prefix: &[u8]) -> AccountId +where + AccountId: Decode + Default, +{ + let entropy = (module_prefix, bridge).using_encoded(blake2_256); + AccountId::decode(&mut &entropy[..]).unwrap_or_default() +}