diff --git a/core/packages/test/.envrc-example b/core/packages/test/.envrc-example index efef567956de4..a5918d1752d3f 100644 --- a/core/packages/test/.envrc-example +++ b/core/packages/test/.envrc-example @@ -9,6 +9,7 @@ source_up_if_exists # export BEEFY_START_BLOCK= # Start block to configure beefy client, default value: 1 # export RELAYCHAIN_ENDPOINT= # Relaychain endpoint, default value: ws://localhost:9944 # export PARACHAIN_RUNTIME= # Runtime type of parachain should be one of snowbase|snowblink|snowbridge +# export BRIDGEHUB_PALLETS_OWNER= # Pubkey of bridge owner which can be used to halt/resume the bridge pallets ## Eth config for production diff --git a/core/packages/test/.gitignore b/core/packages/test/.gitignore index d81f76a1c390e..6d151bfe70897 100644 --- a/core/packages/test/.gitignore +++ b/core/packages/test/.gitignore @@ -19,3 +19,4 @@ rococo-local.json .pnp.* ethereum-goerli testdata +.env diff --git a/core/packages/test/scripts/configure-bridgehub.sh b/core/packages/test/scripts/configure-bridgehub.sh index b623ce0b99b99..47e679f351062 100755 --- a/core/packages/test/scripts/configure-bridgehub.sh +++ b/core/packages/test/scripts/configure-bridgehub.sh @@ -17,7 +17,31 @@ config_inbound_queue() local pallet="30" local callindex="01" local payload="0x$pallet$callindex$(address_for OutboundQueue | cut -c3-)" - send_governance_transact_from_relaychain $bridgehub_para_id "$payload" 180000000000 900000 + send_governance_transact_from_relaychain $bridgehub_para_id "$payload" +} + +config_pallet_owner() +{ + local owner=$(echo $bridgehub_pallets_owner | cut -c3-) + local option="01" + + # config owner of inbound queue + local pallet="30" + local callindex="03" + local payload="0x$pallet$callindex$option$owner" + send_governance_transact_from_relaychain $bridgehub_para_id "$payload" + + # config owner of outbound queue + local pallet="31" + local callindex="00" + local payload="0x$pallet$callindex$option$owner" + send_governance_transact_from_relaychain $bridgehub_para_id "$payload" + + # config owner of beacon client owner + local pallet="32" + local callindex="03" + local payload="0x$pallet$callindex$option$owner" + send_governance_transact_from_relaychain $bridgehub_para_id "$payload" } wait_beacon_chain_ready() @@ -45,6 +69,7 @@ configure_bridgehub() { fund_accounts config_inbound_queue + config_pallet_owner wait_beacon_chain_ready config_beacon_checkpoint } diff --git a/core/packages/test/scripts/set-env.sh b/core/packages/test/scripts/set-env.sh index 1e33147a359aa..7a3c15480c13e 100755 --- a/core/packages/test/scripts/set-env.sh +++ b/core/packages/test/scripts/set-env.sh @@ -46,6 +46,7 @@ beacon_endpoint_http="${BEACON_HTTP_ENDPOINT:-http://127.0.0.1:9596}" bridgehub_ws_url="${BRIDGEHUB_WS_URL:-ws://127.0.0.1:11144}" bridgehub_para_id="${BRIDGEHUB_PARA_ID:-1013}" bridgehub_seed="${BRIDGEHUB_SEED:-//Alice}" +bridgehub_pallets_owner="${BRIDGEHUB_PALLETS_OWNER:-0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d}" statemine_ws_url="${STATEMINE_WS_URL:-ws://127.0.0.1:12144}" statemine_para_id="${STATEMINE_PARA_ID:-1000}" @@ -77,7 +78,7 @@ export PRIVATE_KEY="${DEPLOYER_ETH_KEY:-0x4e9444a6efd6d42725a250b650a781da2737ea # but for rococo-local each session is only 20 slots=120s # so relax somehow here just for quick test # for production deployment ETH_RANDAO_DELAY should be configured in a more reasonable sense -export RANDAO_COMMIT_DELAY="${ETH_RANDAO_DELAY:-6}" +export RANDAO_COMMIT_DELAY="${ETH_RANDAO_DELAY:-3}" export RANDAO_COMMIT_EXP="${ETH_RANDAO_EXP:-3}" ## ParachainClient diff --git a/core/packages/test/scripts/xcm-helper.sh b/core/packages/test/scripts/xcm-helper.sh index 8666fa9a811ab..55800ffa637f2 100644 --- a/core/packages/test/scripts/xcm-helper.sh +++ b/core/packages/test/scripts/xcm-helper.sh @@ -6,8 +6,14 @@ source scripts/set-env.sh send_governance_transact_from_relaychain() { local para_id=$1 local hex_encoded_data=$2 - local require_weight_at_most_ref_time=$3 - local require_weight_at_most_proof_size=$4 + local require_weight_at_most_ref_time=$(echo "$3") + local require_weight_at_most_proof_size=$(echo "$4") + if [ -z "${require_weight_at_most_ref_time}" ]; then + require_weight_at_most_ref_time=200000000 + fi + if [ -z "${require_weight_at_most_proof_size}" ]; then + require_weight_at_most_proof_size=12000 + fi echo " calling send_governance_transact:" echo " relay_url: ${relaychain_ws_url}" echo " relay_chain_seed: ${relaychain_sudo_seed}" diff --git a/cumulus b/cumulus index c6254276fe874..825df854624bd 160000 --- a/cumulus +++ b/cumulus @@ -1 +1 @@ -Subproject commit c6254276fe874ea89885037aac75a950c42c97a9 +Subproject commit 825df854624bdca89ae7cd47c0d77892a502f96b diff --git a/parachain/Cargo.lock b/parachain/Cargo.lock index 4be0454037edd..0bb6b50fcf310 100644 --- a/parachain/Cargo.lock +++ b/parachain/Cargo.lock @@ -284,6 +284,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bp-runtime" +version = "0.1.0" +source = "git+https://github.com/Snowfork/cumulus.git?branch=snowbridge#1a8a815c224fa51a5df00a92eb28235b42defda6" +dependencies = [ + "frame-support", + "frame-system", + "hash-db", + "impl-trait-for-tuples", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-state-machine", + "sp-std", + "sp-trie", + "trie-db", +] + [[package]] name = "bs58" version = "0.4.0" @@ -2656,6 +2678,7 @@ dependencies = [ name = "snowbridge-ethereum-beacon-client" version = "0.0.1" dependencies = [ + "bp-runtime", "byte-slice-cast", "frame-benchmarking", "frame-support", @@ -2686,6 +2709,7 @@ dependencies = [ name = "snowbridge-inbound-queue" version = "0.1.1" dependencies = [ + "bp-runtime", "ethabi-decode", "frame-benchmarking", "frame-support", @@ -2713,6 +2737,7 @@ dependencies = [ name = "snowbridge-outbound-queue" version = "0.1.1" dependencies = [ + "bp-runtime", "ethabi-decode", "frame-benchmarking", "frame-support", diff --git a/parachain/pallets/control/src/benchmarking.rs b/parachain/pallets/control/src/benchmarking.rs index 8da7609e825b2..76a564442c1c6 100644 --- a/parachain/pallets/control/src/benchmarking.rs +++ b/parachain/pallets/control/src/benchmarking.rs @@ -14,11 +14,10 @@ mod benchmarks { #[benchmark] fn upgrade() -> Result<(), BenchmarkError> { - let caller: T::AccountId = whitelisted_caller(); let upgrade_task = H160::repeat_byte(3); #[extrinsic_call] - _(RawOrigin::Signed(caller), upgrade_task); + _(RawOrigin::Root, upgrade_task); Ok(()) } diff --git a/parachain/pallets/control/src/lib.rs b/parachain/pallets/control/src/lib.rs index 9410b6baabc71..50a7a080a619d 100644 --- a/parachain/pallets/control/src/lib.rs +++ b/parachain/pallets/control/src/lib.rs @@ -65,7 +65,7 @@ pub mod pallet { #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::upgrade())] pub fn upgrade(origin: OriginFor, upgrade_task: H160) -> DispatchResult { - let _ = ensure_signed(origin)?; + ensure_root(origin)?; let message = OutboundMessage { id: T::MessageHasher::hash(upgrade_task.as_ref()), diff --git a/parachain/pallets/ethereum-beacon-client/Cargo.toml b/parachain/pallets/ethereum-beacon-client/Cargo.toml index 8c85b489a4ea3..e4d5ebc4c6cd8 100644 --- a/parachain/pallets/ethereum-beacon-client/Cargo.toml +++ b/parachain/pallets/ethereum-beacon-client/Cargo.toml @@ -31,6 +31,7 @@ snowbridge-core = { path = "../../primitives/core", default-features = false } snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false } primitives = { package = "snowbridge-beacon-primitives", path = "../../primitives/beacon", default-features = false } static_assertions = { version = "1.1.0" } +bp-runtime = { git = "https://github.com/Snowfork/cumulus.git", branch = "snowbridge", default-features = false } [dev-dependencies] rand = "0.8.5" @@ -58,6 +59,7 @@ std = [ "primitives/std", "ssz_rs/std", "byte-slice-cast/std", + "bp-runtime/std", ] runtime-benchmarks = [ "beacon-spec-mainnet", diff --git a/parachain/pallets/ethereum-beacon-client/src/lib.rs b/parachain/pallets/ethereum-beacon-client/src/lib.rs index cee3c01fa74c7..06b04de90eb6f 100644 --- a/parachain/pallets/ethereum-beacon-client/src/lib.rs +++ b/parachain/pallets/ethereum-beacon-client/src/lib.rs @@ -46,6 +46,8 @@ pub use pallet::*; pub use config::SLOTS_PER_HISTORICAL_ROOT; +pub const LOG_TARGET: &str = "ethereum-beacon-client"; + #[frame_support::pallet] pub mod pallet { use super::*; @@ -53,6 +55,8 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; + use bp_runtime::{BasicOperatingMode, OwnedBridgeModule}; + #[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)] #[codec(mel_bound(T: Config))] #[scale_info(skip_type_params(T))] @@ -90,7 +94,6 @@ pub mod pallet { } #[pallet::error] - #[cfg_attr(test, derive(PartialEq))] pub enum Error { SkippedSyncCommitteePeriod, /// Attested header is older than latest finalized header. @@ -119,6 +122,7 @@ pub mod pallet { InvalidSyncCommitteeUpdate, ExecutionHeaderTooFarBehind, ExecutionHeaderSkippedSlot, + BridgeModule(bp_runtime::OwnedBridgeModuleError), } /// Latest imported checkpoint root @@ -179,6 +183,28 @@ pub mod pallet { #[pallet::storage] pub type ExecutionHeaderMapping = StorageMap<_, Identity, u32, H256, ValueQuery>; + /// Optional pallet owner. + /// + /// Pallet owner has a right to halt all pallet operations and then resume them. 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`). + #[pallet::storage] + pub type PalletOwner = StorageValue<_, T::AccountId, OptionQuery>; + + /// The current operating mode of the pallet. + /// + /// Depending on the mode either all, or no transactions will be allowed. + #[pallet::storage] + pub type PalletOperatingMode = StorageValue<_, BasicOperatingMode, ValueQuery>; + + impl OwnedBridgeModule for Pallet { + const LOG_TARGET: &'static str = LOG_TARGET; + type OwnerStorage = PalletOwner; + type OperatingMode = BasicOperatingMode; + type OperatingModeStorage = PalletOperatingMode; + } + #[pallet::call] impl Pallet { #[pallet::call_index(0)] @@ -203,6 +229,7 @@ pub mod pallet { /// Submits a new finalized beacon header update. The update may contain the next /// sync committee. pub fn submit(origin: OriginFor, update: Update) -> DispatchResult { + Self::ensure_not_halted().map_err(Error::::BridgeModule)?; ensure_signed(origin)?; Self::process_update(&update)?; Ok(()) @@ -217,10 +244,30 @@ pub mod pallet { origin: OriginFor, update: ExecutionHeaderUpdate, ) -> DispatchResult { + Self::ensure_not_halted().map_err(Error::::BridgeModule)?; ensure_signed(origin)?; Self::process_execution_header_update(&update)?; Ok(()) } + + /// Change `PalletOwner`. + /// May only be called either by root, or by `PalletOwner`. + #[pallet::call_index(3)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_owner(origin: OriginFor, new_owner: Option) -> DispatchResult { + >::set_owner(origin, new_owner) + } + + /// Halt or resume all pallet operations. + /// May only be called either by root, or by `PalletOwner`. + #[pallet::call_index(4)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_operating_mode( + origin: OriginFor, + operating_mode: BasicOperatingMode, + ) -> DispatchResult { + >::set_operating_mode(origin, operating_mode) + } } impl Pallet { @@ -459,7 +506,7 @@ pub mod pallet { >::set(sync_committee_prepared); } log::info!( - target: "ethereum-beacon-client", + target: LOG_TARGET, "💫 SyncCommitteeUpdated at period {}.", update_finalized_period ); @@ -628,7 +675,7 @@ pub mod pallet { >::set(header_root); log::info!( - target: "ethereum-beacon-client", + target: LOG_TARGET, "💫 Updated latest finalized block root {} at slot {}.", header_root, slot @@ -653,7 +700,7 @@ pub mod pallet { >::insert(block_hash, header); log::trace!( - target: "ethereum-beacon-client", + target: LOG_TARGET, "💫 Updated latest execution block at {} to number {}.", block_hash, block_number diff --git a/parachain/pallets/inbound-queue/Cargo.toml b/parachain/pallets/inbound-queue/Cargo.toml index 84f39bb06affb..fc4cce2662936 100644 --- a/parachain/pallets/inbound-queue/Cargo.toml +++ b/parachain/pallets/inbound-queue/Cargo.toml @@ -26,6 +26,7 @@ sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "master" sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master", default-features = false } xcm = { git = "https://github.com/paritytech/polkadot.git", branch = "master", default-features = false } +bp-runtime = { git = "https://github.com/Snowfork/cumulus.git", branch = "snowbridge", default-features = false } snowbridge-core = { path = "../../primitives/core", default-features = false } snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false } @@ -60,6 +61,7 @@ std = [ "snowbridge-router-primitives/std", "ethabi/std", "xcm/std", + "bp-runtime/std", "snowbridge-beacon-primitives/std", "snowbridge-ethereum-beacon-client/std", ] diff --git a/parachain/pallets/inbound-queue/src/benchmarking/mod.rs b/parachain/pallets/inbound-queue/src/benchmarking/mod.rs index 1fb1e15d618ef..3f168ca62e57b 100644 --- a/parachain/pallets/inbound-queue/src/benchmarking/mod.rs +++ b/parachain/pallets/inbound-queue/src/benchmarking/mod.rs @@ -51,7 +51,7 @@ mod benchmarks { impl_benchmark_test_suite!( InboundQueue, - crate::test::new_tester(crate::H160::default()), + crate::test::new_tester::(crate::H160::default()), crate::test::Test ); diff --git a/parachain/pallets/inbound-queue/src/lib.rs b/parachain/pallets/inbound-queue/src/lib.rs index f16a0fb365a48..9c4f7e657c67b 100644 --- a/parachain/pallets/inbound-queue/src/lib.rs +++ b/parachain/pallets/inbound-queue/src/lib.rs @@ -21,6 +21,7 @@ use codec::DecodeAll; use frame_support::{ storage::bounded_btree_set::BoundedBTreeSet, traits::fungible::{Inspect, Mutate}, + DefaultNoBound, }; use frame_system::ensure_signed; use snowbridge_core::ParaId; @@ -54,6 +55,8 @@ pub enum MessageDispatchResult { pub use pallet::*; +pub const LOG_TARGET: &str = "snowbridge-inbound-queue"; + #[frame_support::pallet] pub mod pallet { @@ -63,6 +66,8 @@ pub mod pallet { use frame_system::pallet_prelude::*; use xcm::v3::SendXcm; + use bp_runtime::{BasicOperatingMode, OwnedBridgeModule}; + #[pallet::pallet] pub struct Pallet(_); @@ -112,8 +117,10 @@ pub mod pallet { InvalidNonce, /// Cannot convert location InvalidAccountConversion, - // Allow list is full. + /// Allow list is full. AllowListFull, + /// Error generated by the `OwnedBridgeModule` trait. + BridgeModule(bp_runtime::OwnedBridgeModuleError), } #[pallet::storage] @@ -124,33 +131,57 @@ pub mod pallet { #[pallet::storage] pub type Nonce = StorageMap<_, Twox64Concat, ParaId, u64, ValueQuery>; + /// Optional pallet owner. + /// + /// Pallet owner has a right to halt all pallet operations and then resume them. 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`). + #[pallet::storage] + pub type PalletOwner = StorageValue<_, T::AccountId, OptionQuery>; + + /// The current operating mode of the pallet. + /// + /// Depending on the mode either all, or no transactions will be allowed. + #[pallet::storage] + pub type PalletOperatingMode = StorageValue<_, BasicOperatingMode, ValueQuery>; + #[pallet::genesis_config] - pub struct GenesisConfig { + #[derive(DefaultNoBound)] + pub struct GenesisConfig { + /// Allow list of outbound channels from ethereum pub allowlist: Vec, - } - - impl Default for GenesisConfig { - fn default() -> Self { - Self { allowlist: Default::default() } - } + /// Initial pallet owner. + pub owner: Option, } #[pallet::genesis_build] - impl GenesisBuild for GenesisConfig { + impl GenesisBuild for GenesisConfig { fn build(&self) { let allowlist: BoundedBTreeSet = BTreeSet::from_iter(self.allowlist.clone().into_iter()) .try_into() .expect("exceeded bound"); >::put(allowlist); + if let Some(ref owner) = self.owner { + PalletOwner::::put(owner); + } } } + impl OwnedBridgeModule for Pallet { + const LOG_TARGET: &'static str = LOG_TARGET; + type OwnerStorage = PalletOwner; + type OperatingMode = BasicOperatingMode; + type OperatingModeStorage = PalletOperatingMode; + } + #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::submit())] pub fn submit(origin: OriginFor, message: Message) -> DispatchResult { + Self::ensure_not_halted().map_err(Error::::BridgeModule)?; let who = ensure_signed(origin)?; // submit message to verifier for verification let log = T::Verifier::verify(&message)?; @@ -258,5 +289,24 @@ pub mod pallet { Ok(()) } + + /// Change `PalletOwner`. + /// May only be called either by root, or by `PalletOwner`. + #[pallet::call_index(3)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_owner(origin: OriginFor, new_owner: Option) -> DispatchResult { + >::set_owner(origin, new_owner) + } + + /// Halt or resume all pallet operations. + /// May only be called either by root, or by `PalletOwner`. + #[pallet::call_index(4)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_operating_mode( + origin: OriginFor, + operating_mode: BasicOperatingMode, + ) -> DispatchResult { + >::set_operating_mode(origin, operating_mode) + } } } diff --git a/parachain/pallets/inbound-queue/src/test.rs b/parachain/pallets/inbound-queue/src/test.rs index da3cc03fbc4be..a958c42ec851a 100644 --- a/parachain/pallets/inbound-queue/src/test.rs +++ b/parachain/pallets/inbound-queue/src/test.rs @@ -166,14 +166,19 @@ fn expect_events(e: Vec) { assert_eq!(last_events(e.len()), e); } -pub fn new_tester(outbound_queue_address: H160) -> sp_io::TestExternalities { - new_tester_with_config(inbound_queue::GenesisConfig { allowlist: vec![outbound_queue_address] }) +pub fn new_tester(outbound_queue_address: H160) -> sp_io::TestExternalities { + new_tester_with_config::(inbound_queue::GenesisConfig { + allowlist: vec![outbound_queue_address], + owner: None, + }) } -pub fn new_tester_with_config(config: inbound_queue::GenesisConfig) -> sp_io::TestExternalities { +pub fn new_tester_with_config( + config: inbound_queue::GenesisConfig, +) -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); - GenesisBuild::::assimilate_storage(&config, &mut storage).unwrap(); + GenesisBuild::::assimilate_storage(&config, &mut storage).unwrap(); let mut ext: sp_io::TestExternalities = storage.into(); ext.execute_with(|| System::set_block_number(1)); @@ -209,7 +214,7 @@ use snowbridge_core::ParaId; #[test] fn test_submit() { - new_tester(OUTBOUND_QUEUE_ADDRESS.into()).execute_with(|| { + new_tester::(OUTBOUND_QUEUE_ADDRESS.into()).execute_with(|| { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); @@ -241,7 +246,7 @@ fn test_submit() { #[test] fn test_submit_with_invalid_outbound_queue() { - new_tester(H160::zero()).execute_with(|| { + new_tester::(H160::zero()).execute_with(|| { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); @@ -268,7 +273,7 @@ fn test_submit_with_invalid_outbound_queue() { #[test] fn test_submit_with_invalid_nonce() { - new_tester(OUTBOUND_QUEUE_ADDRESS.into()).execute_with(|| { + new_tester::(OUTBOUND_QUEUE_ADDRESS.into()).execute_with(|| { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); @@ -302,7 +307,7 @@ fn test_submit_with_invalid_nonce() { #[test] fn test_submit_no_funds_to_reward_relayers() { - new_tester(OUTBOUND_QUEUE_ADDRESS.into()).execute_with(|| { + new_tester::(OUTBOUND_QUEUE_ADDRESS.into()).execute_with(|| { let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); @@ -331,7 +336,7 @@ fn test_submit_no_funds_to_reward_relayers() { #[test] fn test_add_allow_list_without_root_yields_bad_origin() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let contract_address = hex!("0000000000000000000000000000000000000000").into(); let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); @@ -344,7 +349,7 @@ fn test_add_allow_list_without_root_yields_bad_origin() { #[test] fn test_add_allow_list_with_root_succeeds() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let origin = RuntimeOrigin::root(); let contract_address = hex!("0000000000000000000000000000000000000000").into(); @@ -362,7 +367,7 @@ fn test_add_allow_list_with_root_succeeds() { #[test] fn test_add_allow_list_ignores_duplicates() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let origin = RuntimeOrigin::root(); let contract_address = hex!("0000000000000000000000000000000000000000").into(); @@ -378,7 +383,7 @@ fn test_add_allow_list_ignores_duplicates() { #[test] fn test_add_allow_list_fails_when_exceeding_bounds() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let origin = RuntimeOrigin::root(); let contract_address1 = hex!("0000000000000000000000000000000000000000").into(); let contract_address2 = hex!("1000000000000000000000000000000000000000").into(); @@ -402,7 +407,7 @@ fn test_add_allow_list_fails_when_exceeding_bounds() { #[test] fn test_remove_allow_list_without_root_yields_bad_origin() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let contract_address = hex!("0000000000000000000000000000000000000000").into(); let relayer: AccountId = Keyring::Bob.into(); let origin = RuntimeOrigin::signed(relayer); @@ -415,7 +420,7 @@ fn test_remove_allow_list_without_root_yields_bad_origin() { #[test] fn test_remove_allow_list_with_root_succeeds() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let origin = RuntimeOrigin::root(); let contract_address = hex!("0000000000000000000000000000000000000000").into(); @@ -435,7 +440,7 @@ fn test_remove_allow_list_with_root_succeeds() { #[test] fn test_remove_allow_list_event_not_emitted_for_none_existent_item() { - new_tester_with_config(Default::default()).execute_with(|| { + new_tester_with_config::(Default::default()).execute_with(|| { let origin = RuntimeOrigin::root(); let contract_address = hex!("0000000000000000000000000000000000000000").into(); diff --git a/parachain/pallets/outbound-queue/Cargo.toml b/parachain/pallets/outbound-queue/Cargo.toml index ac779cf07fb52..9670f9489caf4 100644 --- a/parachain/pallets/outbound-queue/Cargo.toml +++ b/parachain/pallets/outbound-queue/Cargo.toml @@ -29,6 +29,7 @@ snowbridge-outbound-queue-merkle-tree = { path = "merkle-tree", default-features ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false } xcm = { git = "https://github.com/paritytech/polkadot.git", branch = "master", default-features = false } +bp-runtime = { git = "https://github.com/Snowfork/cumulus.git", branch = "snowbridge", default-features = false } [dev-dependencies] frame-benchmarking = { git = "https://github.com/paritytech/substrate.git", branch = "master" } @@ -53,7 +54,8 @@ std = [ "snowbridge-core/std", "snowbridge-outbound-queue-merkle-tree/std", "ethabi/std", - "xcm/std" + "xcm/std", + "bp-runtime/std", ] runtime-benchmarks = [ "snowbridge-core/runtime-benchmarks", diff --git a/parachain/pallets/outbound-queue/src/lib.rs b/parachain/pallets/outbound-queue/src/lib.rs index a92340768b4e7..2a134c4da0a7f 100644 --- a/parachain/pallets/outbound-queue/src/lib.rs +++ b/parachain/pallets/outbound-queue/src/lib.rs @@ -91,12 +91,16 @@ pub type MaxEnqueuedMessageSizeOf = pub use pallet::*; +pub const LOG_TARGET: &str = "snowbridge-outbound-queue"; + #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; + use bp_runtime::{BasicOperatingMode, OwnedBridgeModule}; + #[pallet::pallet] pub struct Pallet(_); @@ -173,6 +177,19 @@ pub mod pallet { #[pallet::storage] pub type Nonce = StorageMap<_, Twox64Concat, ParaId, u64, ValueQuery>; + /// Optional pallet owner. + /// Pallet owner has a right to halt all pallet operations and then resume them. 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`). + #[pallet::storage] + pub type PalletOwner = StorageValue<_, T::AccountId, OptionQuery>; + + /// The current operating mode of the pallet. + /// Depending on the mode either all, or no transactions will be allowed. + #[pallet::storage] + pub type PalletOperatingMode = StorageValue<_, BasicOperatingMode, ValueQuery>; + #[pallet::hooks] impl Hooks> for Pallet where @@ -191,6 +208,35 @@ pub mod pallet { } } + #[pallet::call] + impl Pallet { + /// Change `PalletOwner`. + /// May only be called either by root, or by `PalletOwner`. + #[pallet::call_index(0)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_owner(origin: OriginFor, new_owner: Option) -> DispatchResult { + >::set_owner(origin, new_owner) + } + + /// Halt or resume all pallet operations. + /// May only be called either by root, or by `PalletOwner`. + #[pallet::call_index(1)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_operating_mode( + origin: OriginFor, + operating_mode: BasicOperatingMode, + ) -> DispatchResult { + >::set_operating_mode(origin, operating_mode) + } + } + + impl OwnedBridgeModule for Pallet { + const LOG_TARGET: &'static str = LOG_TARGET; + type OwnerStorage = PalletOwner; + type OperatingMode = BasicOperatingMode; + type OperatingModeStorage = PalletOperatingMode; + } + impl Pallet { /// Generate a messages commitment and insert it into the header digest pub(crate) fn commit_messages() { @@ -272,6 +318,7 @@ pub mod pallet { } fn submit(ticket: Self::Ticket) -> Result<(), SubmitError> { + Self::ensure_not_halted().map_err(|_| SubmitError::BridgeHalted)?; T::MessageQueue::enqueue_message( ticket.message.as_bounded_slice(), AggregateMessageOrigin::Parachain(ticket.origin), diff --git a/parachain/primitives/core/src/lib.rs b/parachain/primitives/core/src/lib.rs index 60fb4cfe7bd26..9dd47d54219aa 100644 --- a/parachain/primitives/core/src/lib.rs +++ b/parachain/primitives/core/src/lib.rs @@ -64,6 +64,7 @@ pub trait Verifier { #[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug)] pub enum SubmitError { MessageTooLarge, + BridgeHalted, } /// A message which can be accepted by the [`OutboundQueue`]