diff --git a/bridges/modules/message-lane/Cargo.toml b/bridges/modules/message-lane/Cargo.toml index e9313a4d70..41f844fc9f 100644 --- a/bridges/modules/message-lane/Cargo.toml +++ b/bridges/modules/message-lane/Cargo.toml @@ -8,6 +8,7 @@ license = "GPL-3.0-or-later WITH Classpath-exception-2.0" [dependencies] codec = { package = "parity-scale-codec", version = "1.3.1", default-features = false } +serde = { version = "1.0.101", optional = true, features = ["derive"] } # Bridge dependencies @@ -33,6 +34,7 @@ std = [ "codec/std", "frame-support/std", "frame-system/std", + "serde", "sp-runtime/std", "sp-std/std", ] diff --git a/bridges/modules/message-lane/src/lib.rs b/bridges/modules/message-lane/src/lib.rs index 169270efe0..9f6c1d6d70 100644 --- a/bridges/modules/message-lane/src/lib.rs +++ b/bridges/modules/message-lane/src/lib.rs @@ -39,10 +39,13 @@ use bp_message_lane::{ }; use codec::{Decode, Encode}; use frame_support::{ - decl_error, decl_event, decl_module, decl_storage, sp_runtime::DispatchResult, traits::Get, weights::Weight, + decl_error, decl_event, decl_module, decl_storage, + traits::Get, + weights::{DispatchClass, Weight}, Parameter, StorageMap, }; -use frame_system::ensure_signed; +use frame_system::{ensure_signed, RawOrigin}; +use sp_runtime::{traits::BadOrigin, DispatchResult}; use sp_std::{cell::RefCell, marker::PhantomData, prelude::*}; mod inbound_lane; @@ -120,6 +123,8 @@ type MessagesDeliveryProofOf = <>::TargetHeaderChain as Targ decl_error! { pub enum Error for Module, I: Instance> { + /// All pallet operations are halted. + Halted, /// Message has been treated as invalid by chain verifier. MessageRejectedByChainVerifier, /// Message has been treated as invalid by lane verifier. @@ -137,6 +142,15 @@ decl_error! { decl_storage! { trait Store for Module, I: Instance = DefaultInstance> as MessageLane { + /// Optional pallet owner. + /// + /// Pallet owner has a right to halt all pallet operations and then resume it. If it is + /// `None`, then there are no direct ways to halt/resume pallet operations, but other + /// runtime methods may still be used to do that (i.e. democracy::referendum to update halt + /// flag directly or call the `halt_operations`). + pub ModuleOwner get(fn module_owner) config(): Option; + /// If true, all pallet transactions are failed immediately. + pub IsHalted get(fn is_halted) config(): bool; /// Map of lane id => inbound lane data. pub InboundLanes: map hasher(blake2_128_concat) LaneId => InboundLaneData; /// Map of lane id => outbound lane data. @@ -144,6 +158,9 @@ decl_storage! { /// All queued outbound messages. pub OutboundMessages: map hasher(blake2_128_concat) MessageKey => Option>; } + add_extra_genesis { + config(phantom): sp_std::marker::PhantomData; + } } decl_event!( @@ -164,6 +181,36 @@ decl_module! { /// Deposit one of this module's events by using the default implementation. fn deposit_event() = default; + /// Change `ModuleOwner`. + /// + /// May only be called either by root, or by `ModuleOwner`. + #[weight = (T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational)] + pub fn set_owner(origin, new_owner: Option) { + ensure_owner_or_root::(origin)?; + match new_owner { + Some(new_owner) => ModuleOwner::::put(new_owner), + None => ModuleOwner::::kill(), + } + } + + /// Halt all pallet operations. Operations may be resumed using `resume_operations` call. + /// + /// May only be called either by root, or by `ModuleOwner`. + #[weight = (T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational)] + pub fn halt_operations(origin) { + ensure_owner_or_root::(origin)?; + IsHalted::::put(true); + } + + /// Resume all pallet operations. May be called even if pallet is halted. + /// + /// May only be called either by root, or by `ModuleOwner`. + #[weight = (T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational)] + pub fn resume_operations(origin) { + ensure_owner_or_root::(origin)?; + IsHalted::::put(false); + } + /// Send message over lane. #[weight = 0] // TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/78) pub fn send_message( @@ -172,6 +219,7 @@ decl_module! { payload: T::OutboundPayload, delivery_and_dispatch_fee: T::OutboundMessageFee, ) -> DispatchResult { + ensure_operational::()?; let submitter = ensure_signed(origin)?; // let's first check if message can be delivered to target chain @@ -248,6 +296,7 @@ decl_module! { proof: MessagesProofOf, dispatch_weight: Weight, ) -> DispatchResult { + ensure_operational::()?; let _ = ensure_signed(origin)?; // verify messages proof && convert proof into messages @@ -324,6 +373,8 @@ decl_module! { /// Receive messages delivery proof from bridged chain. #[weight = 0] // TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/78) pub fn receive_messages_delivery_proof(origin, proof: MessagesDeliveryProofOf) -> DispatchResult { + ensure_operational::()?; + let confirmation_relayer = ensure_signed(origin)?; let (lane_id, lane_data) = T::TargetHeaderChain::verify_messages_delivery_proof(proof).map_err(|err| { frame_support::debug::trace!( @@ -401,6 +452,24 @@ impl, I: Instance> Module { } } +/// Ensure that the origin is either root, or `ModuleOwner`. +fn ensure_owner_or_root, I: Instance>(origin: T::Origin) -> Result<(), BadOrigin> { + match origin.into() { + Ok(RawOrigin::Root) => Ok(()), + Ok(RawOrigin::Signed(ref signer)) if Some(signer) == Module::::module_owner().as_ref() => Ok(()), + _ => Err(BadOrigin), + } +} + +/// Ensure that the pallet is in operational mode (not halted). +fn ensure_operational, I: Instance>() -> Result<(), Error> { + if IsHalted::::get() { + Err(Error::::Halted) + } else { + Ok(()) + } +} + /// Creates new inbound lane object, backed by runtime storage. fn inbound_lane, I: Instance>(lane_id: LaneId) -> InboundLane> { InboundLane::new(RuntimeInboundLaneStorage { @@ -536,6 +605,7 @@ mod tests { }; use frame_support::{assert_noop, assert_ok}; use frame_system::{EventRecord, Module as System, Phase}; + use sp_runtime::DispatchError; fn send_regular_message() { System::::set_block_number(1); @@ -587,6 +657,108 @@ mod tests { ); } + #[test] + fn pallet_owner_may_change_owner() { + run_test(|| { + ModuleOwner::::put(2); + + assert_ok!(Module::::set_owner(Origin::root(), Some(1))); + assert_noop!( + Module::::halt_operations(Origin::signed(2)), + DispatchError::BadOrigin, + ); + assert_ok!(Module::::halt_operations(Origin::root())); + + assert_ok!(Module::::set_owner(Origin::signed(1), None)); + assert_noop!( + Module::::resume_operations(Origin::signed(1)), + DispatchError::BadOrigin, + ); + assert_noop!( + Module::::resume_operations(Origin::signed(2)), + DispatchError::BadOrigin, + ); + assert_ok!(Module::::resume_operations(Origin::root())); + }); + } + + #[test] + fn pallet_may_be_halted_by_root() { + run_test(|| { + assert_ok!(Module::::halt_operations(Origin::root())); + assert_ok!(Module::::resume_operations(Origin::root())); + }); + } + + #[test] + fn pallet_may_be_halted_by_owner() { + run_test(|| { + ModuleOwner::::put(2); + + assert_ok!(Module::::halt_operations(Origin::signed(2))); + assert_ok!(Module::::resume_operations(Origin::signed(2))); + + assert_noop!( + Module::::halt_operations(Origin::signed(1)), + DispatchError::BadOrigin, + ); + assert_noop!( + Module::::resume_operations(Origin::signed(1)), + DispatchError::BadOrigin, + ); + + assert_ok!(Module::::halt_operations(Origin::signed(2))); + assert_noop!( + Module::::resume_operations(Origin::signed(1)), + DispatchError::BadOrigin, + ); + }); + } + + #[test] + fn pallet_rejects_transactions_if_halted() { + run_test(|| { + // send message first to be able to check that delivery_proof fails later + send_regular_message(); + + IsHalted::::put(true); + + assert_noop!( + Module::::send_message( + Origin::signed(1), + TEST_LANE_ID, + REGULAR_PAYLOAD, + REGULAR_PAYLOAD.1, + ), + Error::::Halted, + ); + + assert_noop!( + Module::::receive_messages_proof( + Origin::signed(1), + TEST_RELAYER_A, + Ok(vec![message(2, REGULAR_PAYLOAD)]).into(), + REGULAR_PAYLOAD.1, + ), + Error::::Halted, + ); + + assert_noop!( + Module::::receive_messages_delivery_proof( + Origin::signed(1), + Ok(( + TEST_LANE_ID, + InboundLaneData { + latest_received_nonce: 1, + ..Default::default() + } + )), + ), + Error::::Halted, + ); + }); + } + #[test] fn send_message_works() { run_test(|| {