From 2384372a23772d74c15f9d18c67e7fdf95135f7a Mon Sep 17 00:00:00 2001 From: librelois Date: Tue, 19 Sep 2023 15:58:32 +0200 Subject: [PATCH 01/11] add command PrecompileWasmCmd This cherry-pick should match this pull request: https://github.com/paritytech/polkadot-sdk/pull/1641 --- Cargo.lock | 7 + .../core/pvf/common/src/executor_interface.rs | 2 +- .../bin/node/cli/benches/block_production.rs | 1 + .../bin/node/cli/benches/transaction_pool.rs | 1 + substrate/bin/node/cli/src/cli.rs | 3 + substrate/bin/node/cli/src/command.rs | 7 + substrate/client/cli/Cargo.toml | 3 + substrate/client/cli/src/commands/mod.rs | 5 +- .../cli/src/commands/precompile_wasm_cmd.rs | 148 ++++++++++++ substrate/client/cli/src/config.rs | 8 + .../client/cli/src/params/import_params.rs | 16 ++ substrate/client/cli/src/runner.rs | 1 + substrate/client/executor/Cargo.toml | 1 + substrate/client/executor/benches/bench.rs | 12 +- substrate/client/executor/common/Cargo.toml | 1 + .../executor/common/src/wasm_runtime.rs | 2 +- substrate/client/executor/src/executor.rs | 24 ++ substrate/client/executor/src/lib.rs | 13 +- substrate/client/executor/src/wasm_runtime.rs | 221 ++++++++++++++++-- substrate/client/executor/wasmtime/Cargo.toml | 5 +- substrate/client/executor/wasmtime/src/lib.rs | 4 +- .../client/executor/wasmtime/src/runtime.rs | 39 +++- .../client/executor/wasmtime/src/tests.rs | 14 +- substrate/client/service/src/builder.rs | 12 +- .../service/src/client/call_executor.rs | 5 +- substrate/client/service/src/client/client.rs | 3 + substrate/client/service/src/config.rs | 5 + .../primitives/wasm-interface/src/lib.rs | 2 + .../benchmarking-cli/src/overhead/command.rs | 1 + templates/parachain/node/src/service.rs | 11 +- templates/solochain/node/src/cli.rs | 3 + templates/solochain/node/src/command.rs | 8 + 32 files changed, 536 insertions(+), 52 deletions(-) create mode 100644 substrate/client/cli/src/commands/precompile_wasm_cmd.rs diff --git a/Cargo.lock b/Cargo.lock index 2a7224f26fbe3..9d85b3bb2621a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19869,6 +19869,7 @@ dependencies = [ "rpassword", "sc-client-api", "sc-client-db", + "sc-executor", "sc-keystore", "sc-mixnet", "sc-network", @@ -19885,6 +19886,8 @@ dependencies = [ "sp-keystore", "sp-panic-handler", "sp-runtime", + "sp-state-machine", + "sp-storage 22.0.0", "sp-tracing 19.0.0", "sp-version", "tempfile", @@ -20319,6 +20322,7 @@ dependencies = [ "array-bytes 6.2.2", "assert_matches", "criterion", + "log", "num_cpus", "parity-scale-codec", "parking_lot 0.12.3", @@ -20354,6 +20358,7 @@ dependencies = [ name = "sc-executor-common" version = "0.43.0" dependencies = [ + "parity-scale-codec", "polkavm", "sc-allocator", "sp-maybe-compressed-blob", @@ -20378,6 +20383,8 @@ version = "0.43.0" dependencies = [ "anyhow", "cargo_metadata", + "cfg-if", + "libc", "log", "parity-scale-codec", "parking_lot 0.12.3", diff --git a/polkadot/node/core/pvf/common/src/executor_interface.rs b/polkadot/node/core/pvf/common/src/executor_interface.rs index f2e98f79a2276..a960d5c0fb1eb 100644 --- a/polkadot/node/core/pvf/common/src/executor_interface.rs +++ b/polkadot/node/core/pvf/common/src/executor_interface.rs @@ -191,7 +191,7 @@ pub fn prepare( executor_params: &ExecutorParams, ) -> Result, sc_executor_common::error::WasmError> { let (semantics, _) = params_to_wasmtime_semantics(executor_params); - sc_executor_wasmtime::prepare_runtime_artifact(blob, &semantics) + sc_executor_wasmtime::prepare_runtime_artifact(blob, Default::default(), &semantics) } /// Available host functions. We leave out: diff --git a/substrate/bin/node/cli/benches/block_production.rs b/substrate/bin/node/cli/benches/block_production.rs index 80f33b607b387..6198461c19249 100644 --- a/substrate/bin/node/cli/benches/block_production.rs +++ b/substrate/bin/node/cli/benches/block_production.rs @@ -110,6 +110,7 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { announce_block: true, data_path: base_path.path().into(), base_path, + wasmtime_precompiled: None, wasm_runtime_overrides: None, }; diff --git a/substrate/bin/node/cli/benches/transaction_pool.rs b/substrate/bin/node/cli/benches/transaction_pool.rs index 112aa9ea13746..290d2d60cf78d 100644 --- a/substrate/bin/node/cli/benches/transaction_pool.rs +++ b/substrate/bin/node/cli/benches/transaction_pool.rs @@ -96,6 +96,7 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { announce_block: true, data_path: base_path.path().into(), base_path, + wasmtime_precompiled: None, wasm_runtime_overrides: None, }; diff --git a/substrate/bin/node/cli/src/cli.rs b/substrate/bin/node/cli/src/cli.rs index c2187e77e7f1b..0539b94524e50 100644 --- a/substrate/bin/node/cli/src/cli.rs +++ b/substrate/bin/node/cli/src/cli.rs @@ -108,4 +108,7 @@ pub enum Subcommand { /// Db meta columns information. ChainInfo(sc_cli::ChainInfoCmd), + + /// Precompile the WASM runtime into native code + PrecompileWasm(sc_cli::PrecompileWasmCmd), } diff --git a/substrate/bin/node/cli/src/command.rs b/substrate/bin/node/cli/src/command.rs index 5f3be4d8b2159..e20f0da7a8b54 100644 --- a/substrate/bin/node/cli/src/command.rs +++ b/substrate/bin/node/cli/src/command.rs @@ -234,5 +234,12 @@ pub fn run() -> Result<()> { let runner = cli.create_runner(cmd)?; runner.sync_run(|config| cmd.run::(&config)) }, + Some(Subcommand::PrecompileWasm(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { task_manager, backend, .. } = new_partial(&config, None)?; + Ok((cmd.run(backend, config.chain_spec), task_manager)) + }) + }, } } diff --git a/substrate/client/cli/Cargo.toml b/substrate/client/cli/Cargo.toml index b122d0923b1b0..bd4ccecd717c3 100644 --- a/substrate/client/cli/Cargo.toml +++ b/substrate/client/cli/Cargo.toml @@ -68,6 +68,9 @@ sp-version.default-features = true sp-version.workspace = true thiserror = { workspace = true } tokio = { features = ["parking_lot", "rt-multi-thread", "signal"], workspace = true, default-features = true } +sc-executor = { workspace = true } +sp-storage = { workspace = true, default-features = true } +sp-state-machine = { workspace = true, default-features = true } [dev-dependencies] futures-timer = { workspace = true } diff --git a/substrate/client/cli/src/commands/mod.rs b/substrate/client/cli/src/commands/mod.rs index b63fb1d904234..27137a1f22143 100644 --- a/substrate/client/cli/src/commands/mod.rs +++ b/substrate/client/cli/src/commands/mod.rs @@ -31,6 +31,7 @@ mod insert_key; mod inspect_key; mod inspect_node_key; mod key; +mod precompile_wasm_cmd; mod purge_chain_cmd; mod revert_cmd; mod run_cmd; @@ -46,6 +47,6 @@ pub use self::{ export_state_cmd::ExportStateCmd, generate::GenerateCmd, generate_node_key::GenerateKeyCmdCommon, import_blocks_cmd::ImportBlocksCmd, insert_key::InsertKeyCmd, inspect_key::InspectKeyCmd, inspect_node_key::InspectNodeKeyCmd, - key::KeySubcommand, purge_chain_cmd::PurgeChainCmd, revert_cmd::RevertCmd, run_cmd::RunCmd, - sign::SignCmd, vanity::VanityCmd, verify::VerifyCmd, + key::KeySubcommand, precompile_wasm_cmd::PrecompileWasmCmd, purge_chain_cmd::PurgeChainCmd, + revert_cmd::RevertCmd, run_cmd::RunCmd, sign::SignCmd, vanity::VanityCmd, verify::VerifyCmd, }; diff --git a/substrate/client/cli/src/commands/precompile_wasm_cmd.rs b/substrate/client/cli/src/commands/precompile_wasm_cmd.rs new file mode 100644 index 0000000000000..ba2948a449953 --- /dev/null +++ b/substrate/client/cli/src/commands/precompile_wasm_cmd.rs @@ -0,0 +1,148 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +use crate::{ + arg_enums::{ + execution_method_from_cli, WasmExecutionMethod, WasmtimeInstantiationStrategy, + DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + }, + error::{self, Error}, + params::{DatabaseParams, PruningParams, SharedParams}, + CliConfiguration, +}; + +use clap::Parser; +use sc_client_api::{Backend, HeaderBackend, TrieCacheContext}; +use sc_executor::{ + precompile_and_serialize_versioned_wasm_runtime, HeapAllocStrategy, DEFAULT_HEAP_ALLOC_PAGES, +}; +use sc_service::ChainSpec; +use sp_core::traits::RuntimeCode; +use sp_runtime::traits::Block as BlockT; +use sp_state_machine::backend::BackendRuntimeCode; +use std::{fmt::Debug, path::PathBuf, sync::Arc}; + +/// The `precompile-wasm` command used to serialize a precompiled WASM module. +#[derive(Debug, Parser)] +pub struct PrecompileWasmCmd { + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, + + /// The default number of 64KB pages to ever allocate for Wasm execution. + /// Don't alter this unless you know what you're doing. + #[arg(long, value_name = "COUNT")] + pub default_heap_pages: Option, + + /// path to the directory where precompiled artifact will be written + #[arg()] + pub output_dir: PathBuf, + + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + /// The WASM instantiation method to use. + /// Only has an effect when `wasm-execution` is set to `compiled`. + /// The copy-on-write strategies are only supported on Linux. + /// If the copy-on-write variant of a strategy is unsupported + /// the executor will fall back to the non-CoW equivalent. + /// The fastest (and the default) strategy available is `pooling-copy-on-write`. + /// The `legacy-instance-reuse` strategy is deprecated and will + /// be removed in the future. It should only be used in case of + /// issues with the default instantiation strategy. + #[arg( + long, + value_name = "STRATEGY", + default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + value_enum, + )] + pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, +} + +impl PrecompileWasmCmd { + /// Run the precompile-wasm command + pub async fn run(&self, backend: Arc, spec: Box) -> error::Result<()> + where + B: BlockT, + BA: Backend, + { + let heap_pages = self.default_heap_pages.unwrap_or(DEFAULT_HEAP_ALLOC_PAGES); + + let blockchain_info = backend.blockchain().info(); + + if backend.have_state_at(blockchain_info.finalized_hash, blockchain_info.finalized_number) { + let state = backend.state_at( + backend.blockchain().info().finalized_hash, + TrieCacheContext::Untrusted, + )?; + + precompile_and_serialize_versioned_wasm_runtime( + HeapAllocStrategy::Static { extra_pages: heap_pages }, + &BackendRuntimeCode::new(&state).runtime_code()?, + execution_method_from_cli( + WasmExecutionMethod::Compiled, + self.wasmtime_instantiation_strategy, + ), + &self.output_dir, + ) + .map_err(|e| Error::Application(Box::new(e)))?; + } else { + let storage = spec.as_storage_builder().build_storage()?; + if let Some(wasm_bytecode) = storage.top.get(sp_storage::well_known_keys::CODE) { + let runtime_code = RuntimeCode { + code_fetcher: &sp_core::traits::WrappedRuntimeCode( + wasm_bytecode.as_slice().into(), + ), + hash: sp_core::blake2_256(&wasm_bytecode).to_vec(), + heap_pages: Some(heap_pages as u64), + }; + precompile_and_serialize_versioned_wasm_runtime( + HeapAllocStrategy::Static { extra_pages: heap_pages }, + &runtime_code, + execution_method_from_cli( + WasmExecutionMethod::Compiled, + self.wasmtime_instantiation_strategy, + ), + &self.output_dir, + ) + .map_err(|e| Error::Application(Box::new(e)))?; + } + } + + Ok(()) + } +} + +impl CliConfiguration for PrecompileWasmCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn pruning_params(&self) -> Option<&PruningParams> { + Some(&self.pruning_params) + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/client/cli/src/config.rs b/substrate/client/cli/src/config.rs index 855b6e0eb22b4..6688476c71767 100644 --- a/substrate/client/cli/src/config.rs +++ b/substrate/client/cli/src/config.rs @@ -304,6 +304,13 @@ pub trait CliConfiguration: Sized { Ok(self.import_params().map(|x| x.wasm_method()).unwrap_or_default()) } + /// Get the path where WASM precompiled artifacts live. + /// + /// By default this is `None`. + fn wasmtime_precompiled(&self) -> Option { + self.import_params().map(|x| x.wasmtime_precompiled()).unwrap_or_default() + } + /// Get the path where WASM overrides live. /// /// By default this is `None`. @@ -543,6 +550,7 @@ pub trait CliConfiguration: Sized { blocks_pruning: self.blocks_pruning()?, executor: ExecutorConfiguration { wasm_method: self.wasm_method()?, + wasmtime_precompiled: self.wasmtime_precompiled(), default_heap_pages: self.default_heap_pages()?, max_runtime_instances, runtime_cache_size, diff --git a/substrate/client/cli/src/params/import_params.rs b/substrate/client/cli/src/params/import_params.rs index 236907957df67..4efe78322eba7 100644 --- a/substrate/client/cli/src/params/import_params.rs +++ b/substrate/client/cli/src/params/import_params.rs @@ -65,6 +65,16 @@ pub struct ImportParams { )] pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, + /// Specify the path where local precompiled WASM runtimes are stored. + /// Only has an effect when `wasm-execution` is set to `compiled`. + /// + /// The precompiled runtimes must have been generated using the `precompile-runtimes` + /// subcommand with the same version of wasmtime and the exact same configuration. + /// The file name must end with the hash of the configuration. This hash must match, otherwise + /// the runtime will be recompiled. + #[arg(long, value_name = "PATH")] + pub wasmtime_precompiled: Option, + /// Specify the path where local WASM runtimes are stored. /// /// These runtimes will override on-chain runtimes when the version matches. @@ -136,6 +146,12 @@ impl ImportParams { crate::execution_method_from_cli(self.wasm_method, self.wasmtime_instantiation_strategy) } + /// Enable using precompiled WASM module with locally-stored artifacts + /// by specifying the path where artifacts are stored. + pub fn wasmtime_precompiled(&self) -> Option { + self.wasmtime_precompiled.clone() + } + /// Enable overriding on-chain WASM with locally-stored WASM /// by specifying the path where local WASM is stored. pub fn wasm_runtime_overrides(&self) -> Option { diff --git a/substrate/client/cli/src/runner.rs b/substrate/client/cli/src/runner.rs index 34c846f6e73b5..20258bd41f5e4 100644 --- a/substrate/client/cli/src/runner.rs +++ b/substrate/client/cli/src/runner.rs @@ -267,6 +267,7 @@ mod tests { .build(), ), executor: ExecutorConfiguration::default(), + wasmtime_precompiled: None, wasm_runtime_overrides: None, rpc: RpcConfiguration { addr: None, diff --git a/substrate/client/executor/Cargo.toml b/substrate/client/executor/Cargo.toml index a75a5f4257e86..971f3a3555b25 100644 --- a/substrate/client/executor/Cargo.toml +++ b/substrate/client/executor/Cargo.toml @@ -21,6 +21,7 @@ name = "bench" harness = false [dependencies] +log = { workspace = true, default-features = true } parking_lot = { workspace = true, default-features = true } schnellru = { workspace = true } tracing = { workspace = true, default-features = true } diff --git a/substrate/client/executor/benches/bench.rs b/substrate/client/executor/benches/bench.rs index 8e460955396c4..6baeab03358f5 100644 --- a/substrate/client/executor/benches/bench.rs +++ b/substrate/client/executor/benches/bench.rs @@ -67,13 +67,17 @@ fn initialize( wasm_bulk_memory: false, wasm_reference_types: false, wasm_simd: false, + module_version_strategy: Default::default(), }, }; if precompile { - let precompiled_blob = - sc_executor_wasmtime::prepare_runtime_artifact(blob, &config.semantics) - .unwrap(); + let precompiled_blob = sc_executor_wasmtime::prepare_runtime_artifact( + blob, + Default::default(), + &config.semantics, + ) + .unwrap(); // Create a fresh temporary directory to make absolutely sure // we'll use the right module. @@ -85,7 +89,7 @@ fn initialize( unsafe { sc_executor_wasmtime::create_runtime_from_artifact::< sp_io::SubstrateHostFunctions, - >(&path, config) + >(&path, Default::defaut(), config) } } else { sc_executor_wasmtime::create_runtime::(blob, config) diff --git a/substrate/client/executor/common/Cargo.toml b/substrate/client/executor/common/Cargo.toml index 4e45fc3c182c5..cbcfa108e6e05 100644 --- a/substrate/client/executor/common/Cargo.toml +++ b/substrate/client/executor/common/Cargo.toml @@ -24,6 +24,7 @@ sp-maybe-compressed-blob.default-features = true sp-maybe-compressed-blob.workspace = true sp-wasm-interface.default-features = true sp-wasm-interface.workspace = true +codec = { features = ["derive"], workspace = true } thiserror = { workspace = true } wasm-instrument = { workspace = true, default-features = true } diff --git a/substrate/client/executor/common/src/wasm_runtime.rs b/substrate/client/executor/common/src/wasm_runtime.rs index e8f429a3dbb2a..778731c964c40 100644 --- a/substrate/client/executor/common/src/wasm_runtime.rs +++ b/substrate/client/executor/common/src/wasm_runtime.rs @@ -74,7 +74,7 @@ pub trait WasmInstance: Send { /// Defines the heap pages allocation strategy the wasm runtime should use. /// /// A heap page is defined as 64KiB of memory. -#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, codec::Encode)] pub enum HeapAllocStrategy { /// Allocate a static number of heap pages. /// diff --git a/substrate/client/executor/src/executor.rs b/substrate/client/executor/src/executor.rs index 198a2a31ab8db..b59bbb7ff0633 100644 --- a/substrate/client/executor/src/executor.rs +++ b/substrate/client/executor/src/executor.rs @@ -92,6 +92,7 @@ pub struct WasmExecutorBuilder { max_runtime_instances: usize, cache_path: Option, allow_missing_host_functions: bool, + wasmtime_precompiled_path: Option, runtime_cache_size: u8, } @@ -110,6 +111,7 @@ impl WasmExecutorBuilder { runtime_cache_size: 4, allow_missing_host_functions: false, cache_path: None, + wasmtime_precompiled_path: None, } } @@ -193,6 +195,20 @@ impl WasmExecutorBuilder { self } + /// Create the wasm executor with the given `wasmtime_precompiled_path`. + /// + /// The `wasmtime_precompiled_path` is a path to a directory where the executor load precompiled + /// wasmtime modules. + /// + /// By default there is no `wasmtime_precompiled_path` given. + pub fn with_wasmtime_precompiled_path( + mut self, + wasmtime_precompiled_path: impl Into, + ) -> Self { + self.wasmtime_precompiled_path = Some(wasmtime_precompiled_path.into()); + self + } + /// Build the configured [`WasmExecutor`]. pub fn build(self) -> WasmExecutor { WasmExecutor { @@ -211,6 +227,7 @@ impl WasmExecutorBuilder { )), cache_path: self.cache_path, allow_missing_host_functions: self.allow_missing_host_functions, + wasmtime_precompiled_path: self.wasmtime_precompiled_path, phantom: PhantomData, } } @@ -234,6 +251,8 @@ pub struct WasmExecutor { cache_path: Option, /// Ignore missing function imports. allow_missing_host_functions: bool, + /// TODO + wasmtime_precompiled_path: Option, phantom: PhantomData, } @@ -247,6 +266,7 @@ impl Clone for WasmExecutor { cache: self.cache.clone(), cache_path: self.cache_path.clone(), allow_missing_host_functions: self.allow_missing_host_functions, + wasmtime_precompiled_path: self.wasmtime_precompiled_path.clone(), phantom: self.phantom, } } @@ -301,6 +321,7 @@ impl WasmExecutor { )), cache_path, allow_missing_host_functions: false, + wasmtime_precompiled_path: None, phantom: PhantomData, } } @@ -353,6 +374,7 @@ where runtime_code, ext, self.method, + self.wasmtime_precompiled_path.as_deref(), heap_alloc_strategy, self.allow_missing_host_functions, |module, instance, version, ext| { @@ -430,6 +452,8 @@ where runtime_blob, allow_missing_host_functions, self.cache_path.as_deref(), + None, + &[], ) .map_err(|e| format!("Failed to create module: {}", e))?; diff --git a/substrate/client/executor/src/lib.rs b/substrate/client/executor/src/lib.rs index 204f1ff22d74d..ab713a4aafb5c 100644 --- a/substrate/client/executor/src/lib.rs +++ b/substrate/client/executor/src/lib.rs @@ -36,17 +36,22 @@ mod executor; mod integration_tests; mod wasm_runtime; -pub use codec::Codec; #[allow(deprecated)] -pub use executor::NativeElseWasmExecutor; -pub use executor::{with_externalities_safe, NativeExecutionDispatch, WasmExecutor}; +pub use self::{ + executor::{ + with_externalities_safe, NativeElseWasmExecutor, NativeExecutionDispatch, WasmExecutor, + }, + wasm_runtime::{ + precompile_and_serialize_versioned_wasm_runtime, read_embedded_version, WasmExecutionMethod, + }, +}; +pub use codec::Codec; #[doc(hidden)] pub use sp_core::traits::Externalities; pub use sp_version::{NativeVersion, RuntimeVersion}; #[doc(hidden)] pub use sp_wasm_interface; pub use sp_wasm_interface::HostFunctions; -pub use wasm_runtime::{read_embedded_version, WasmExecutionMethod}; pub use sc_executor_common::{ error, diff --git a/substrate/client/executor/src/wasm_runtime.rs b/substrate/client/executor/src/wasm_runtime.rs index 8f189ca92388a..31e4192e078e9 100644 --- a/substrate/client/executor/src/wasm_runtime.rs +++ b/substrate/client/executor/src/wasm_runtime.rs @@ -23,7 +23,7 @@ use crate::error::{Error, WasmError}; -use codec::Decode; +use codec::{Decode, Encode}; use parking_lot::Mutex; use sc_executor_common::{ runtime_blob::RuntimeBlob, @@ -35,6 +35,7 @@ use sp_version::RuntimeVersion; use sp_wasm_interface::HostFunctions; use std::{ + io::Write, panic::AssertUnwindSafe, path::{Path, PathBuf}, sync::Arc, @@ -220,6 +221,7 @@ impl RuntimeCache { runtime_code: &'c RuntimeCode<'c>, ext: &mut dyn Externalities, wasm_method: WasmExecutionMethod, + wasmtime_precompiled: Option<&Path>, heap_alloc_strategy: HeapAllocStrategy, allow_missing_func_imports: bool, f: F, @@ -255,6 +257,8 @@ impl RuntimeCache { allow_missing_func_imports, self.max_runtime_instances, self.cache_path.as_deref(), + wasmtime_precompiled, + code_hash, ); match result { @@ -293,6 +297,8 @@ pub fn create_wasm_runtime_with_code( blob: RuntimeBlob, allow_missing_func_imports: bool, cache_path: Option<&Path>, + wasmtime_precompiled_path: Option<&Path>, + code_hash: &[u8], ) -> Result, WasmError> where H: HostFunctions, @@ -302,29 +308,200 @@ where } match wasm_method { - WasmExecutionMethod::Compiled { instantiation_strategy } => - sc_executor_wasmtime::create_runtime::( - blob, - sc_executor_wasmtime::Config { - allow_missing_func_imports, - cache_path: cache_path.map(ToOwned::to_owned), - semantics: sc_executor_wasmtime::Semantics { - heap_alloc_strategy, - instantiation_strategy, - deterministic_stack_limit: None, - canonicalize_nans: false, - parallel_compilation: true, - wasm_multi_value: false, - wasm_bulk_memory: false, - wasm_reference_types: false, - wasm_simd: false, + WasmExecutionMethod::Compiled { instantiation_strategy } => { + let semantics = sc_executor_wasmtime::Semantics { + heap_alloc_strategy, + instantiation_strategy, + deterministic_stack_limit: None, + canonicalize_nans: false, + parallel_compilation: true, + wasm_multi_value: false, + wasm_bulk_memory: false, + wasm_reference_types: false, + wasm_simd: false, + }; + if let Some(wasmtime_precompiled_dir) = wasmtime_precompiled_path { + if !wasmtime_precompiled_dir.is_dir() { + return Err(WasmError::Instantiation(format!( + "--wasmtime-precompiled is not a directory: {}", + wasmtime_precompiled_dir.display() + ))); + } + let handle_err = |e: std::io::Error| -> WasmError { + return WasmError::Instantiation(format!( + "Io error when loading wasmtime precompiled folder ({}): {}", + wasmtime_precompiled_dir.display(), + e + )); + }; + let mut maybe_compiled_artifact = None; + + let artifact_version = + compute_artifact_version(allow_missing_func_imports, code_hash, &semantics); + log::debug!( + target: "wasmtime-runtime", + "Searching for wasm hash: {}", + artifact_version.clone() + ); + + for entry in std::fs::read_dir(wasmtime_precompiled_dir).map_err(handle_err)? { + let entry = entry.map_err(handle_err)?; + if let Some(file_name) = entry.file_name().to_str() { + // We check that the artifact was generated for this specific artifact + // version and with the same wasm interface version and configuration. + if file_name.contains(&artifact_version.clone()) { + log::info!( + target: "wasmtime-runtime", + "Found precompiled wasm: {}", + file_name + ); + // We change the version check strategy to make sure that the file + // content was serialized with the exact same config as well + maybe_compiled_artifact = Some(( + entry.path(), + sc_executor_wasmtime::ModuleVersionStrategy::Custom( + artifact_version.clone(), + ), + )); + } + } else { + return Err(WasmError::Instantiation( + "wasmtime precompiled folder contain a file with invalid utf8 name" + .to_owned(), + )); + } + } + + if let Some((compiled_artifact_path, module_version_strategy)) = + maybe_compiled_artifact + { + // # Safety + // + // The file name of the artifact was checked before, + // so if the user has not renamed nor modified the file, + // it's certain that the file has been generated by + // `prepare_runtime_artifact` and with the same wasmtime + // version and configuration. + unsafe { + sc_executor_wasmtime::create_runtime_from_artifact::( + &compiled_artifact_path, + module_version_strategy, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + semantics, + }, + ) + } + .map(|runtime| -> Box { Box::new(runtime) }) + } else { + sc_executor_wasmtime::create_runtime::( + blob, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + semantics, + }, + ) + .map(|runtime| -> Box { Box::new(runtime) }) + } + } else { + sc_executor_wasmtime::create_runtime::( + blob, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + semantics, }, - }, - ) - .map(|runtime| -> Box { Box::new(runtime) }), + ) + .map(|runtime| -> Box { Box::new(runtime) }) + } + }, } } +/// Create and serialize a precompiled artifact of a wasm runtime with the given `code`. +pub fn precompile_and_serialize_versioned_wasm_runtime<'c>( + heap_alloc_strategy: HeapAllocStrategy, + runtime_code: &'c RuntimeCode<'c>, + wasm_method: WasmExecutionMethod, + wasmtime_precompiled_path: &Path, +) -> Result<(), WasmError> { + let semantics = match wasm_method { + WasmExecutionMethod::Compiled { instantiation_strategy } => + sc_executor_wasmtime::Semantics { + heap_alloc_strategy, + instantiation_strategy, + deterministic_stack_limit: None, + canonicalize_nans: false, + parallel_compilation: true, + wasm_multi_value: false, + wasm_bulk_memory: false, + wasm_reference_types: false, + wasm_simd: false, + }, + }; + + let code_hash = &runtime_code.hash; + + let artifact_version = compute_artifact_version(false, code_hash, &semantics); + log::debug!( + target: "wasmtime-runtime", + "Generated precompiled wasm hash: {}", + artifact_version.clone() + ); + + let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?; + + // The incoming code may be actually compressed. We decompress it here and then work with + // the uncompressed code from now on. + let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code.as_ref())?; + + let serialized_precompiled_wasm = sc_executor_wasmtime::prepare_runtime_artifact( + blob, + sc_executor_wasmtime::ModuleVersionStrategy::Custom(artifact_version.clone()), + &semantics, + )?; + + // Write in a file + let mut file = std::fs::File::create( + wasmtime_precompiled_path.join(format!("precompiled_wasm_{}", &artifact_version)), + ) + .map_err(|e| { + WasmError::Other(format!( + "Fail to create file 'precompiled_wasm_0x{}', I/O Error: {}", + &artifact_version, e + )) + })?; + file.write_all(&serialized_precompiled_wasm).map_err(|e| { + WasmError::Other(format!("Fail to write precompiled artifact, I/O Error: {}", e)) + })?; + + Ok(()) +} + +/// Compute a hash that aggregates all the metadata relating to the artifact that must not change +/// so that it can be reused with confidence. +fn compute_artifact_version( + allow_missing_func_imports: bool, + code_hash: &[u8], + semantics: &sc_executor_wasmtime::Semantics, +) -> String { + log::trace!( + target: "wasmtime-runtime", + "Computing wasm runtime hash [allow_missing_func_imports: {}, code_hash: {}, semantics: {:?}]", + allow_missing_func_imports, sp_core::bytes::to_hex(&code_hash, false), semantics + ); + let mut buffer = Vec::new(); + buffer.extend_from_slice(code_hash); + buffer.extend_from_slice(sp_wasm_interface::VERSION.as_bytes()); + buffer.push(allow_missing_func_imports as u8); + semantics.encode_to(&mut buffer); + + let hash = sp_core::hashing::blake2_256(&buffer); + sp_core::bytes::to_hex(&hash, false) +} + fn decode_version(mut version: &[u8]) -> Result { Decode::decode(&mut version).map_err(|_| { WasmError::Instantiation( @@ -389,6 +566,8 @@ fn create_versioned_wasm_runtime( allow_missing_func_imports: bool, max_instances: usize, cache_path: Option<&Path>, + wasmtime_precompiled: Option<&Path>, + code_hash: &[u8], ) -> Result where H: HostFunctions, @@ -408,6 +587,8 @@ where blob, allow_missing_func_imports, cache_path, + wasmtime_precompiled, + code_hash, )?; // If the runtime blob doesn't embed the runtime version then use the legacy version query diff --git a/substrate/client/executor/wasmtime/Cargo.toml b/substrate/client/executor/wasmtime/Cargo.toml index 976779631eca4..c6dc4bb67cc3c 100644 --- a/substrate/client/executor/wasmtime/Cargo.toml +++ b/substrate/client/executor/wasmtime/Cargo.toml @@ -17,6 +17,9 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] log = { workspace = true, default-features = true } +codec = { workspace = true, default-features = true } +cfg-if = { workspace = true } +libc = { workspace = true } parking_lot = { workspace = true, default-features = true } # When bumping wasmtime do not forget to also bump rustix @@ -51,9 +54,9 @@ wasmtime = { features = [ rustix = { features = ["fs", "mm", "param", "std", "use-libc"], workspace = true } [dev-dependencies] +paste = { workspace = true, default-features = true } cargo_metadata = { workspace = true } codec = { workspace = true, default-features = true } -paste = { workspace = true, default-features = true } sc-runtime-test = { workspace = true } sp-io = { default-features = true, workspace = true } tempfile = { workspace = true } diff --git a/substrate/client/executor/wasmtime/src/lib.rs b/substrate/client/executor/wasmtime/src/lib.rs index 54e978996c0ca..12b9386a6e901 100644 --- a/substrate/client/executor/wasmtime/src/lib.rs +++ b/substrate/client/executor/wasmtime/src/lib.rs @@ -39,8 +39,8 @@ mod tests; pub use runtime::{ create_runtime, create_runtime_from_artifact, create_runtime_from_artifact_bytes, - prepare_runtime_artifact, Config, DeterministicStackLimit, InstantiationStrategy, Semantics, - WasmtimeRuntime, + prepare_runtime_artifact, Config, DeterministicStackLimit, InstantiationStrategy, + ModuleVersionStrategy, Semantics, WasmtimeRuntime, }; pub use sc_executor_common::{ runtime_blob::RuntimeBlob, diff --git a/substrate/client/executor/wasmtime/src/runtime.rs b/substrate/client/executor/wasmtime/src/runtime.rs index 2e47f1469310a..a80fd044ab133 100644 --- a/substrate/client/executor/wasmtime/src/runtime.rs +++ b/substrate/client/executor/wasmtime/src/runtime.rs @@ -18,6 +18,8 @@ //! Defines the compiled Wasm runtime that uses Wasmtime internally. +pub use wasmtime::ModuleVersionStrategy; + use crate::{ host::HostState, instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper}, @@ -338,7 +340,7 @@ fn common_config(semantics: &Semantics) -> std::result::Result { /// /// We use a `Path` here instead of simply passing a byte slice to allow `wasmtime` to /// map the runtime's linear memory on supported platforms in a copy-on-write fashion. - Precompiled(&'a Path), + Precompiled { + /// Path to the precompiled artifact + compiled_artifact_path: &'a Path, + /// Configure the strategy used for versioning check in deserializing precompiled artifact + module_version_strategy: ModuleVersionStrategy, + }, /// The runtime is instantiated using a precompiled module with the given bytes. /// @@ -511,12 +518,16 @@ where /// different configuration flags. In such case the caller will receive an `Err` deterministically. pub unsafe fn create_runtime_from_artifact( compiled_artifact_path: &Path, + module_version_strategy: ModuleVersionStrategy, config: Config, ) -> std::result::Result where H: HostFunctions, { - do_create_runtime::(CodeSupplyMode::Precompiled(compiled_artifact_path), config) + do_create_runtime::( + CodeSupplyMode::Precompiled { compiled_artifact_path, module_version_strategy }, + config, + ) } /// The same as [`create_runtime`] but takes the bytes of a precompiled artifact, @@ -558,6 +569,13 @@ where replace_strategy_if_broken(&mut config.semantics.instantiation_strategy); let mut wasmtime_config = common_config(&config.semantics)?; + + if let CodeSupplyMode::Precompiled { ref module_version_strategy, .. } = code_supply_mode { + wasmtime_config.module_version(module_version_strategy.clone()).map_err(|e| { + WasmError::Other(format!("fail to apply module_version_strategy: {:#}", e)) + })?; + } + if let Some(ref cache_path) = config.cache_path { if let Err(reason) = setup_wasmtime_caching(cache_path, &mut wasmtime_config) { log::warn!( @@ -586,7 +604,7 @@ where (module, InternalInstantiationStrategy::Builtin), } }, - CodeSupplyMode::Precompiled(compiled_artifact_path) => { + CodeSupplyMode::Precompiled { compiled_artifact_path, .. } => { // SAFETY: The unsafety of `deserialize_file` is covered by this function. The // responsibilities to maintain the invariants are passed to the caller. // @@ -645,6 +663,7 @@ fn prepare_blob_for_compilation( /// can then be used for calling [`create_runtime`] avoiding long compilation times. pub fn prepare_runtime_artifact( blob: RuntimeBlob, + module_version_strategy: ModuleVersionStrategy, semantics: &Semantics, ) -> std::result::Result, WasmError> { let mut semantics = semantics.clone(); @@ -652,7 +671,13 @@ pub fn prepare_runtime_artifact( let blob = prepare_blob_for_compilation(blob, &semantics)?; - let engine = Engine::new(&common_config(&semantics)?) + let mut wasmtime_config = common_config(&semantics)?; + + wasmtime_config + .module_version(module_version_strategy) + .map_err(|e| WasmError::Other(format!("fail to apply module_version_strategy: {:#}", e)))?; + + let engine = Engine::new(&wasmtime_config) .map_err(|e| WasmError::Other(format!("cannot create the engine: {:#}", e)))?; engine diff --git a/substrate/client/executor/wasmtime/src/tests.rs b/substrate/client/executor/wasmtime/src/tests.rs index dbbf6490d292b..b5c4ab210698e 100644 --- a/substrate/client/executor/wasmtime/src/tests.rs +++ b/substrate/client/executor/wasmtime/src/tests.rs @@ -146,6 +146,7 @@ impl RuntimeBuilder { wasm_bulk_memory: false, wasm_reference_types: false, wasm_simd: false, + module_version_strategy: Default::default(), }, }; @@ -156,9 +157,17 @@ impl RuntimeBuilder { // Delay the removal of the temporary directory until we're dropped. self.tmpdir = Some(dir); - let artifact = crate::prepare_runtime_artifact(blob, &config.semantics).unwrap(); + let artifact = + crate::prepare_runtime_artifact(blob, Default::default(), &config.semantics) + .unwrap(); std::fs::write(&path, artifact).unwrap(); - unsafe { crate::create_runtime_from_artifact::(&path, config) } + unsafe { + crate::create_runtime_from_artifact::( + &path, + Default::default(), + config, + ) + } } else { crate::create_runtime::(blob, config) } @@ -473,6 +482,7 @@ fn test_instances_without_reuse_are_not_leaked() { wasm_bulk_memory: false, wasm_reference_types: false, wasm_simd: false, + module_version_strategy: Default::default(), }, }, ) diff --git a/substrate/client/service/src/builder.rs b/substrate/client/service/src/builder.rs index 1ec349fc55718..dbeeb4f5116af 100644 --- a/substrate/client/service/src/builder.rs +++ b/substrate/client/service/src/builder.rs @@ -259,6 +259,7 @@ where ClientConfig { offchain_worker_enabled: config.offchain_worker.enabled, offchain_indexing_api: config.offchain_worker.indexing_enabled, + wasmtime_precompiled: config.executor.wasmtime_precompiled.clone(), wasm_runtime_overrides: config.wasm_runtime_overrides.clone(), no_genesis: config.no_genesis(), wasm_runtime_substitutes, @@ -365,13 +366,18 @@ pub fn new_wasm_executor(config: &ExecutorConfiguration) -> Wa let strategy = config .default_heap_pages .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |p| HeapAllocStrategy::Static { extra_pages: p as _ }); - WasmExecutor::::builder() + let mut wasm_builder = WasmExecutor::::builder() .with_execution_method(config.wasm_method) .with_onchain_heap_alloc_strategy(strategy) .with_offchain_heap_alloc_strategy(strategy) .with_max_runtime_instances(config.max_runtime_instances) - .with_runtime_cache_size(config.runtime_cache_size) - .build() + .with_runtime_cache_size(config.runtime_cache_size); + + if let Some(ref wasmtime_precompiled_path) = config.wasmtime_precompiled { + wasm_builder = wasm_builder.with_wasmtime_precompiled_path(wasmtime_precompiled_path); + } + + wasm_builder.build() } /// Create an instance of default DB-backend backend. diff --git a/substrate/client/service/src/client/call_executor.rs b/substrate/client/service/src/client/call_executor.rs index 235b2a6a07ed1..c62fb1b2bd3c5 100644 --- a/substrate/client/service/src/client/call_executor.rs +++ b/substrate/client/service/src/client/call_executor.rs @@ -30,7 +30,7 @@ use sp_runtime::{ traits::{Block as BlockT, HashingFor}, }; use sp_state_machine::{backend::AsTrieBackend, OverlayedChanges, StateMachine, StorageProof}; -use std::{cell::RefCell, sync::Arc}; +use std::{cell::RefCell, path::PathBuf, sync::Arc}; /// Call executor that executes methods locally, querying all required /// data from local backend. @@ -38,6 +38,7 @@ pub struct LocalCallExecutor { backend: Arc, executor: E, code_provider: CodeProvider, + wasmtime_precompiled_path: Option, execution_extensions: Arc>, } @@ -59,6 +60,7 @@ where backend, executor, code_provider, + wasmtime_precompiled_path: client_config.wasmtime_precompiled, execution_extensions: Arc::new(execution_extensions), }) } @@ -73,6 +75,7 @@ where backend: self.backend.clone(), executor: self.executor.clone(), code_provider: self.code_provider.clone(), + wasmtime_precompiled_path: self.wasmtime_precompiled_path.clone(), execution_extensions: self.execution_extensions.clone(), } } diff --git a/substrate/client/service/src/client/client.rs b/substrate/client/service/src/client/client.rs index 298fe7907a491..b46bf207eedf3 100644 --- a/substrate/client/service/src/client/client.rs +++ b/substrate/client/service/src/client/client.rs @@ -166,6 +166,8 @@ pub struct ClientConfig { pub wasm_runtime_substitutes: HashMap, Vec>, /// Enable recording of storage proofs during block import pub enable_import_proof_recording: bool, + /// Path where precompiled wasmtime modules exist. + pub wasmtime_precompiled: Option, } impl Default for ClientConfig { @@ -177,6 +179,7 @@ impl Default for ClientConfig { no_genesis: false, wasm_runtime_substitutes: HashMap::new(), enable_import_proof_recording: false, + wasmtime_precompiled: None, } } } diff --git a/substrate/client/service/src/config.rs b/substrate/client/service/src/config.rs index 2effa4782e122..a060a7ebaaadc 100644 --- a/substrate/client/service/src/config.rs +++ b/substrate/client/service/src/config.rs @@ -349,6 +349,10 @@ pub struct RpcConfiguration { pub struct ExecutorConfiguration { /// Wasm execution method. pub wasm_method: WasmExecutionMethod, + /// Directory where local WASM precompiled artifacts live. These wasm modules + /// take precedence over runtimes when the spec and wasm config matches. Set to `None` to + /// disable (default). + pub wasmtime_precompiled: Option, /// The size of the instances cache. /// /// The default value is 8. @@ -363,6 +367,7 @@ impl Default for ExecutorConfiguration { fn default() -> Self { Self { wasm_method: WasmExecutionMethod::default(), + wasmtime_precompiled: Default::default(), max_runtime_instances: 8, default_heap_pages: None, runtime_cache_size: 2, diff --git a/substrate/primitives/wasm-interface/src/lib.rs b/substrate/primitives/wasm-interface/src/lib.rs index 5e2bd10c16853..3d634da129fe8 100644 --- a/substrate/primitives/wasm-interface/src/lib.rs +++ b/substrate/primitives/wasm-interface/src/lib.rs @@ -46,6 +46,8 @@ if_wasmtime_is_enabled! { pub use anyhow; } +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + /// Result type used by traits in this crate. pub type Result = core::result::Result; diff --git a/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs b/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs index c8b61fe92b617..cf00e01744051 100644 --- a/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs +++ b/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs @@ -521,6 +521,7 @@ impl OverheadCmd { no_genesis: false, wasm_runtime_substitutes: Default::default(), enable_import_proof_recording: chain_type.requires_proof_recording(), + wasmtime_precompiled: Default::default(), }, )?); diff --git a/templates/parachain/node/src/service.rs b/templates/parachain/node/src/service.rs index 1509241e7692b..9d65095001b57 100644 --- a/templates/parachain/node/src/service.rs +++ b/templates/parachain/node/src/service.rs @@ -86,19 +86,22 @@ pub fn new_partial(config: &Configuration) -> Result .default_heap_pages .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| HeapAllocStrategy::Static { extra_pages: h as _ }); - let executor = ParachainExecutor::builder() + let mut wasm_builder = WasmExecutor::builder() .with_execution_method(config.executor.wasm_method) .with_onchain_heap_alloc_strategy(heap_pages) .with_offchain_heap_alloc_strategy(heap_pages) .with_max_runtime_instances(config.executor.max_runtime_instances) - .with_runtime_cache_size(config.executor.runtime_cache_size) - .build(); + .with_runtime_cache_size(config.executor.runtime_cache_size); + if let Some(ref wasmtime_precompiled_path) = config.executor.wasmtime_precompiled { + wasm_builder = wasm_builder.with_wasmtime_precompiled_path(wasmtime_precompiled_path); + } + let wasm = wasm_builder.build(); let (client, backend, keystore_container, task_manager) = sc_service::new_full_parts_record_import::( config, telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), - executor, + wasm, true, )?; let client = Arc::new(client); diff --git a/templates/solochain/node/src/cli.rs b/templates/solochain/node/src/cli.rs index c09905c7949e7..f87c889454f38 100644 --- a/templates/solochain/node/src/cli.rs +++ b/templates/solochain/node/src/cli.rs @@ -49,4 +49,7 @@ pub enum Subcommand { /// Db meta columns information. ChainInfo(sc_cli::ChainInfoCmd), + + /// Precompile the WASM runtime into native code + PrecompileWasm(sc_cli::PrecompileWasmCmd), } diff --git a/templates/solochain/node/src/command.rs b/templates/solochain/node/src/command.rs index 57e6726178569..7449400ca4acd 100644 --- a/templates/solochain/node/src/command.rs +++ b/templates/solochain/node/src/command.rs @@ -181,6 +181,14 @@ pub fn run() -> sc_cli::Result<()> { let runner = cli.create_runner(cmd)?; runner.sync_run(|config| cmd.run::(&config)) }, + Some(Subcommand::PrecompileWasm(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { task_manager, backend, .. } = + service::new_partial(&config)?; + Ok((cmd.run(backend, config.chain_spec), task_manager)) + }) + }, None => { let runner = cli.create_runner(&cli.run)?; runner.run_node_until_exit(|config| async move { From dddabe3eb6fbd6becd5143ac56ccba47326e2b1c Mon Sep 17 00:00:00 2001 From: Tomasz Polaczyk Date: Tue, 12 Nov 2024 12:54:38 +0100 Subject: [PATCH 02/11] snowbridge_pallet_outbound_queue: make it more generic --- Cargo.lock | 1 + .../outbound-queue/src/benchmarking.rs | 3 +- .../pallets/outbound-queue/src/lib.rs | 21 +++++++++----- .../pallets/outbound-queue/src/mock.rs | 12 +++++++- .../src/process_message_impl.rs | 2 +- .../outbound-queue/src/send_message_impl.rs | 8 +++--- bridges/snowbridge/pallets/system/Cargo.toml | 1 + bridges/snowbridge/pallets/system/src/mock.rs | 15 +++++++++- .../src/bridge_to_ethereum_config.rs | 28 +++++++++++++------ .../src/bridge_to_ethereum_config.rs | 13 +++++++-- 10 files changed, 77 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d85b3bb2621a..a006d07746260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22608,6 +22608,7 @@ dependencies = [ name = "snowbridge-pallet-system" version = "0.18.0" dependencies = [ + "bridge-hub-common", "frame-benchmarking", "frame-support", "frame-system", diff --git a/bridges/snowbridge/pallets/outbound-queue/src/benchmarking.rs b/bridges/snowbridge/pallets/outbound-queue/src/benchmarking.rs index 1ff8df5793581..c3d0347438af0 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/benchmarking.rs @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: 2023 Snowfork use super::*; -use bridge_hub_common::AggregateMessageOrigin; use codec::Encode; use frame_benchmarking::v2::*; use snowbridge_core::ChannelId; @@ -34,7 +33,7 @@ mod benchmarks { }), }, }; - let origin = AggregateMessageOrigin::Snowbridge([1; 32].into()); + let origin = T::GetAggregateMessageOrigin::convert([1; 32].into()); let encoded_enqueued_message = enqueued_message.encode(); #[block] diff --git a/bridges/snowbridge/pallets/outbound-queue/src/lib.rs b/bridges/snowbridge/pallets/outbound-queue/src/lib.rs index e78a56389a68f..4f5e948d6435f 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/lib.rs @@ -103,8 +103,7 @@ mod mock; #[cfg(test)] mod test; -use bridge_hub_common::AggregateMessageOrigin; -use codec::Decode; +use codec::{Decode, FullCodec}; use frame_support::{ storage::StorageStreamIter, traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError}, @@ -117,7 +116,7 @@ use snowbridge_outbound_queue_primitives::v1::{ }; use sp_core::{H256, U256}; use sp_runtime::{ - traits::{CheckedDiv, Hash}, + traits::{CheckedDiv, Convert, Debug, Hash}, DigestItem, Saturating, }; use sp_std::prelude::*; @@ -144,7 +143,15 @@ pub mod pallet { type Hashing: Hash; - type MessageQueue: EnqueueMessage; + type AggregateMessageOrigin: FullCodec + + MaxEncodedLen + + Clone + + Eq + + PartialEq + + TypeInfo + + Debug; + type GetAggregateMessageOrigin: Convert; + type MessageQueue: EnqueueMessage; /// Measures the maximum gas used to execute a command on Ethereum type GasMeter: GasMeter; @@ -176,7 +183,7 @@ pub mod pallet { } #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] + #[pallet::generate_deposit(pub fn deposit_event)] pub enum Event { /// Message has been queued and will be processed in the future MessageQueued { @@ -220,7 +227,7 @@ pub mod pallet { /// Inspired by the `frame_system::Pallet::Events` storage value #[pallet::storage] #[pallet::unbounded] - pub(super) type Messages = StorageValue<_, Vec, ValueQuery>; + pub type Messages = StorageValue<_, Vec, ValueQuery>; /// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a /// merkle root during `on_finalize`. This storage value is killed in @@ -228,7 +235,7 @@ pub mod pallet { #[pallet::storage] #[pallet::unbounded] #[pallet::getter(fn message_leaves)] - pub(super) type MessageLeaves = StorageValue<_, Vec, ValueQuery>; + pub type MessageLeaves = StorageValue<_, Vec, ValueQuery>; /// The current nonce for each message origin #[pallet::storage] diff --git a/bridges/snowbridge/pallets/outbound-queue/src/mock.rs b/bridges/snowbridge/pallets/outbound-queue/src/mock.rs index 236cca7d83817..4e22ac7efc0ad 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/mock.rs @@ -2,12 +2,12 @@ // SPDX-FileCopyrightText: 2023 Snowfork use super::*; +pub use bridge_hub_common::AggregateMessageOrigin; use frame_support::{ derive_impl, parameter_types, traits::{Everything, Hooks}, weights::IdentityFee, }; - use snowbridge_core::{ gwei, meth, pricing::{PricingParameters, Rewards}, @@ -78,6 +78,14 @@ parameter_types! { }; } +pub struct GetAggregateMessageOrigin; + +impl Convert for GetAggregateMessageOrigin { + fn convert(channel_id: ChannelId) -> AggregateMessageOrigin { + AggregateMessageOrigin::Snowbridge(channel_id) + } +} + pub const DOT: u128 = 10_000_000_000; impl crate::Config for Test { @@ -91,6 +99,8 @@ impl crate::Config for Test { type Balance = u128; type PricingParameters = Parameters; type Channels = Everything; + type AggregateMessageOrigin = AggregateMessageOrigin; + type GetAggregateMessageOrigin = GetAggregateMessageOrigin; type WeightToFee = IdentityFee; type WeightInfo = (); } diff --git a/bridges/snowbridge/pallets/outbound-queue/src/process_message_impl.rs b/bridges/snowbridge/pallets/outbound-queue/src/process_message_impl.rs index 731aa6fa6d5ca..b9f4032d3de30 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/process_message_impl.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/process_message_impl.rs @@ -9,7 +9,7 @@ use frame_support::{ }; impl ProcessMessage for Pallet { - type Origin = AggregateMessageOrigin; + type Origin = T::AggregateMessageOrigin; fn process_message( message: &[u8], origin: Self::Origin, diff --git a/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs b/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs index 6d13703d864eb..f799aeec7d430 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/send_message_impl.rs @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! Implementation for [`snowbridge_outbound_queue_primitives::v1::SendMessage`] use super::*; -use bridge_hub_common::AggregateMessageOrigin; use codec::Encode; use frame_support::{ ensure, @@ -19,8 +18,9 @@ use sp_core::H256; use sp_runtime::BoundedVec; /// The maximal length of an enqueued message, as determined by the MessageQueue pallet -pub type MaxEnqueuedMessageSizeOf = - <::MessageQueue as EnqueueMessage>::MaxMessageLen; +pub type MaxEnqueuedMessageSizeOf = <::MessageQueue as EnqueueMessage< + ::AggregateMessageOrigin, +>>::MaxMessageLen; #[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound)] pub struct Ticket @@ -74,7 +74,7 @@ where } fn deliver(ticket: Self::Ticket) -> Result { - let origin = AggregateMessageOrigin::Snowbridge(ticket.channel_id); + let origin = T::GetAggregateMessageOrigin::convert(ticket.channel_id); if ticket.channel_id != PRIMARY_GOVERNANCE_CHANNEL { ensure!(!Self::operating_mode().is_halted(), SendError::Halted); diff --git a/bridges/snowbridge/pallets/system/Cargo.toml b/bridges/snowbridge/pallets/system/Cargo.toml index 8c2744112e62e..e03cc5f260db4 100644 --- a/bridges/snowbridge/pallets/system/Cargo.toml +++ b/bridges/snowbridge/pallets/system/Cargo.toml @@ -34,6 +34,7 @@ xcm.workspace = true xcm-executor.workspace = true [dev-dependencies] +bridge-hub-common = { workspace = true } hex = { workspace = true, default-features = true } hex-literal = { workspace = true, default-features = true } pallet-balances = { default-features = true, workspace = true } diff --git a/bridges/snowbridge/pallets/system/src/mock.rs b/bridges/snowbridge/pallets/system/src/mock.rs index 311981a873b01..a88962663dc38 100644 --- a/bridges/snowbridge/pallets/system/src/mock.rs +++ b/bridges/snowbridge/pallets/system/src/mock.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork use crate as snowbridge_system; +use crate::ChannelId; use frame_support::{ derive_impl, parameter_types, traits::{tokens::fungible::Mutate, ConstU128, ConstU8}, @@ -16,7 +17,7 @@ use snowbridge_core::{ }; use snowbridge_outbound_queue_primitives::v1::ConstantGasMeter; use sp_runtime::{ - traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, Keccak256}, + traits::{AccountIdConversion, BlakeTwo256, Convert, IdentityLookup, Keccak256}, AccountId32, BuildStorage, FixedU128, }; use xcm::prelude::*; @@ -24,6 +25,8 @@ use xcm::prelude::*; #[cfg(feature = "runtime-benchmarks")] use crate::BenchmarkHelper; +pub use bridge_hub_common::AggregateMessageOrigin; + type Block = frame_system::mocking::MockBlock; type Balance = u128; @@ -159,6 +162,14 @@ parameter_types! { pub const OwnParaId: ParaId = ParaId::new(1013); } +pub struct GetAggregateMessageOrigin; + +impl Convert for GetAggregateMessageOrigin { + fn convert(channel_id: ChannelId) -> AggregateMessageOrigin { + AggregateMessageOrigin::Snowbridge(channel_id) + } +} + impl snowbridge_pallet_outbound_queue::Config for Test { type RuntimeEvent = RuntimeEvent; type Hashing = Keccak256; @@ -170,6 +181,8 @@ impl snowbridge_pallet_outbound_queue::Config for Test { type Balance = u128; type PricingParameters = EthereumSystem; type Channels = EthereumSystem; + type AggregateMessageOrigin = AggregateMessageOrigin; + type GetAggregateMessageOrigin = GetAggregateMessageOrigin; type WeightToFee = IdentityFee; type WeightInfo = (); } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index e09ed18250ab3..0bc1669548335 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -23,28 +23,28 @@ use crate::{ }; use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; -use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards}; +use snowbridge_core::{gwei, meth, AllowSiblingsOnly, ChannelId, PricingParameters, Rewards}; use snowbridge_inbound_queue_primitives::v1::MessageToXcm; use snowbridge_outbound_queue_primitives::v1::EthereumBlobExporter; -use sp_core::H160; -use testnet_parachains_constants::rococo::{ - currency::*, - fee::WeightToFee, - snowbridge::{EthereumLocation, EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX}, -}; - use crate::xcm_config::RelayNetwork; #[cfg(feature = "runtime-benchmarks")] use benchmark_helpers::DoNothingRouter; use bp_asset_hub_rococo::CreateForeignAssetDeposit; +use bridge_hub_common::AggregateMessageOrigin; use frame_support::{parameter_types, weights::ConstantMultiplier}; use hex_literal::hex; use pallet_xcm::EnsureXcm; +use sp_core::H160; use sp_runtime::{ - traits::{ConstU32, ConstU8, Keccak256}, + traits::{ConstU32, ConstU8, Convert, Keccak256}, FixedU128, }; +use testnet_parachains_constants::rococo::{ + currency::*, + fee::WeightToFee, + snowbridge::{EthereumLocation, EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX}, +}; use xcm::prelude::{GlobalConsensus, InteriorLocation, Location, Parachain}; /// Exports message to the Ethereum Gateway contract. @@ -103,6 +103,14 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type AssetTransactor = ::AssetTransactor; } +pub struct GetAggregateMessageOrigin; + +impl Convert for GetAggregateMessageOrigin { + fn convert(channel_id: ChannelId) -> AggregateMessageOrigin { + AggregateMessageOrigin::Snowbridge(channel_id) + } +} + impl snowbridge_pallet_outbound_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Hashing = Keccak256; @@ -116,6 +124,8 @@ impl snowbridge_pallet_outbound_queue::Config for Runtime { type WeightInfo = crate::weights::snowbridge_pallet_outbound_queue::WeightInfo; type PricingParameters = EthereumSystem; type Channels = EthereumSystem; + type AggregateMessageOrigin = AggregateMessageOrigin; + type GetAggregateMessageOrigin = GetAggregateMessageOrigin; } #[cfg(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test))] diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index 384cc7cd84ba0..2cb5af70ca194 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -30,7 +30,7 @@ use hex_literal::hex; use pallet_xcm::EnsureXcm; use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; -use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards}; +use snowbridge_core::{gwei, meth, AllowSiblingsOnly, ChannelId, PricingParameters, Rewards}; use snowbridge_inbound_queue_primitives::v2::CreateAssetCallInfo; use snowbridge_outbound_queue_primitives::{ v1::{ConstantGasMeter, EthereumBlobExporter}, @@ -38,7 +38,7 @@ use snowbridge_outbound_queue_primitives::{ }; use sp_core::H160; use sp_runtime::{ - traits::{ConstU32, ConstU8, Keccak256}, + traits::{ConstU32, ConstU8, Convert, Keccak256}, FixedU128, }; use testnet_parachains_constants::westend::{ @@ -161,6 +161,13 @@ impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { type DefaultRewardKind = SnowbridgeReward; type RewardPayment = BridgeRelayers; } +pub struct GetAggregateMessageOrigin; + +impl Convert for GetAggregateMessageOrigin { + fn convert(channel_id: ChannelId) -> AggregateMessageOrigin { + AggregateMessageOrigin::Snowbridge(channel_id) + } +} impl snowbridge_pallet_outbound_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; @@ -175,6 +182,8 @@ impl snowbridge_pallet_outbound_queue::Config for Runtime { type WeightInfo = crate::weights::snowbridge_pallet_outbound_queue::WeightInfo; type PricingParameters = EthereumSystem; type Channels = EthereumSystem; + type AggregateMessageOrigin = AggregateMessageOrigin; + type GetAggregateMessageOrigin = GetAggregateMessageOrigin; } impl snowbridge_pallet_outbound_queue_v2::Config for Runtime { From 325e000fcc3aa8ca7d671f70e7ffa31b068e6d08 Mon Sep 17 00:00:00 2001 From: Parth Date: Fri, 15 Nov 2024 14:21:17 +0400 Subject: [PATCH 03/11] Add inbound queue (#4) * modify inbound queue to support custom message processing * passively test tuple trait implementation * rename XCM to Xcm everywhere * add comment explaining MessageProcessor --- Cargo.lock | 98 +- Cargo.toml | 4 + .../pallets/inbound-queue/src/lib.rs | 47 +- .../pallets/inbound-queue/src/mock.rs | 30 +- .../src/xcm_message_processor.rs | 47 + .../primitives/inbound-queue/Cargo.toml | 3 + .../inbound-queue/src/envelope.rs | 7 +- .../primitives/inbound-queue/src/v1.rs | 91 +- .../bridge-hub-rococo/src/tests/snowbridge.rs | 892 ++++++++++++++++++ .../src/bridge_to_ethereum_config.rs | 6 +- .../src/bridge_to_ethereum_config.rs | 2 + 11 files changed, 1158 insertions(+), 69 deletions(-) create mode 100644 bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs rename bridges/snowbridge/{pallets => primitives}/inbound-queue/src/envelope.rs (91%) create mode 100644 cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs diff --git a/Cargo.lock b/Cargo.lock index a006d07746260..c44732038c260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,7 +123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9b151e38e42f1586a01369ec52a6934702731d07e8509a7307331b09f6c46dc" dependencies = [ "alloy-eips", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "alloy-serde", "alloy-trie", @@ -150,9 +150,9 @@ checksum = "ad31216895d27d307369daa1393f5850b50bbbd372478a9fa951c095c210627e" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", - "alloy-sol-types", + "alloy-sol-types 1.2.1", ] [[package]] @@ -162,9 +162,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b95b3deca680efc7e9cba781f1a1db352fa1ea50e6384a514944dcf4419e652" dependencies = [ "alloy-json-abi", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-sol-type-parser", - "alloy-sol-types", + "alloy-sol-types 1.2.1", "itoa", "serde", "serde_json", @@ -177,7 +177,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "crc", "serde", @@ -190,7 +190,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "serde", ] @@ -201,7 +201,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "k256", "serde", @@ -217,7 +217,7 @@ dependencies = [ "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "alloy-serde", "auto_impl", @@ -236,12 +236,29 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15516116086325c157c18261d768a20677f0f699348000ed391d4ad0dcb82530" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-sol-type-parser", "serde", "serde_json", ] +[[package]] +name = "alloy-primitives" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0628ec0ba5b98b3370bb6be17b12f23bfce8ee4ad83823325a20546d9b03b78" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more 0.99.17", + "hex-literal", + "itoa", + "ruint", + "tiny-keccak", +] + [[package]] name = "alloy-primitives" version = "1.3.1" @@ -297,11 +314,28 @@ version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64600fc6c312b7e0ba76f73a381059af044f4f21f43e07f51f1fa76c868fe302" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "serde", "serde_json", ] +[[package]] +name = "alloy-sol-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a98ad1696a2e17f010ae8e43e9f2a1e930ed176a8e3ff77acfeff6dfb07b42c" +dependencies = [ + "const-hex", + "dunce", + "heck 0.4.1", + "proc-macro-error", + "proc-macro2 1.0.95", + "quote 1.0.40", + "syn 2.0.98", + "syn-solidity 0.4.2", + "tiny-keccak", +] + [[package]] name = "alloy-sol-macro" version = "1.2.1" @@ -330,7 +364,7 @@ dependencies = [ "proc-macro2 1.0.95", "quote 1.0.40", "syn 2.0.98", - "syn-solidity", + "syn-solidity 1.2.1", "tiny-keccak", ] @@ -347,7 +381,7 @@ dependencies = [ "proc-macro2 1.0.95", "quote 1.0.40", "syn 2.0.98", - "syn-solidity", + "syn-solidity 1.2.1", ] [[package]] @@ -360,6 +394,17 @@ dependencies = [ "winnow 0.7.10", ] +[[package]] +name = "alloy-sol-types" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d7107bed88e8f09f0ddcc3335622d87bfb6821f3e0c7473329fb1cfad5e015" +dependencies = [ + "alloy-primitives 0.4.2", + "alloy-sol-macro 0.4.2", + "const-hex", +] + [[package]] name = "alloy-sol-types" version = "1.2.1" @@ -367,8 +412,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58377025a47d8b8426b3e4846a251f2c1991033b27f517aade368146f6ab1dfe" dependencies = [ "alloy-json-abi", - "alloy-primitives", - "alloy-sol-macro", + "alloy-primitives 1.3.1", + "alloy-sol-macro 1.2.1", "serde", ] @@ -378,7 +423,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "arrayvec 0.7.6", "derive_more 2.0.1", @@ -394,7 +439,7 @@ version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e52276fdb553d3c11563afad2898f4085165e4093604afe3d78b69afbf408f" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "darling 0.21.3", "proc-macro2 1.0.95", "quote 1.0.40", @@ -18955,7 +19000,7 @@ version = "20.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa29d9da06fe03b249b6419b33968ecdf92ad6428e2f012dc57bcd619b5d94e" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.3.1", "num_enum", "once_cell", "serde", @@ -22309,7 +22354,7 @@ version = "0.16.0" dependencies = [ "alloy-consensus", "alloy-core", - "alloy-primitives", + "alloy-primitives 1.3.1", "alloy-rlp", "ethabi-decode", "ethbloom", @@ -22331,10 +22376,13 @@ name = "snowbridge-inbound-queue-primitives" version = "0.7.0" dependencies = [ "alloy-core", + "alloy-primitives 0.4.2", + "alloy-sol-types 0.4.2", "assets-common", "frame-support", "frame-system", "hex-literal", + "impl-trait-for-tuples", "parity-scale-codec", "scale-info", "snowbridge-beacon-primitives", @@ -25419,6 +25467,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b837ef12ab88835251726eb12237655e61ec8dc8a280085d1961cdc3dfd047" +dependencies = [ + "paste", + "proc-macro2 1.0.95", + "quote 1.0.40", + "syn 2.0.98", +] + [[package]] name = "syn-solidity" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index d8e6f40fe3a12..afc80cb996934 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -643,6 +643,10 @@ alloy-core = { version = "1.2.1", default-features = false } alloy-primitives = { version = "1.2.1", default-features = false } alloy-rlp = { version = "0.3", default-features = false } alloy-trie = { version = "0.9.1", default-features = false } +# TODO: remove alloy-primitives and alloy-sol-types, they are re-exported in alloy-core +# but the version is different so it doesnt compile +alloy-primitives-042 = { package = "alloy-primitives", version = "0.4.2", default-features = false } +alloy-sol-types = { version = "0.4.2", default-features = false } always-assert = { version = "0.1" } anyhow = { version = "1.0.81", default-features = false } approx = { version = "0.5.1" } diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs index 02620844e0956..eef2f69cf604f 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs @@ -23,8 +23,6 @@ //! parachain. #![cfg_attr(not(feature = "std"), no_std)] -mod envelope; - #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -35,9 +33,9 @@ mod mock; #[cfg(test)] mod test; +pub mod xcm_message_processor; -use codec::{Decode, DecodeAll, Encode}; -use envelope::Envelope; +use codec::{Decode, Encode}; use frame_support::{ traits::{ fungible::{Inspect, Mutate}, @@ -48,7 +46,7 @@ use frame_support::{ }; use frame_system::ensure_signed; use scale_info::TypeInfo; -use sp_core::H160; +use sp_core::{H160, H256}; use sp_runtime::traits::Zero; use sp_std::vec; use xcm::prelude::{ @@ -61,12 +59,14 @@ use snowbridge_core::{ StaticLookup, }; use snowbridge_inbound_queue_primitives::{ - v1::{ConvertMessage, ConvertMessageError, VersionedMessage}, + v1::{ConvertMessage, ConvertMessageError, VersionedXcmMessage}, EventProof, VerificationError, Verifier, }; use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError}; +use snowbridge_inbound_queue_primitives::v1::{Envelope, MessageProcessor}; + pub use weights::WeightInfo; type BalanceOf = @@ -82,7 +82,6 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use sp_core::H256; #[cfg(feature = "runtime-benchmarks")] use snowbridge_inbound_queue_primitives::EventFixture; @@ -141,6 +140,9 @@ pub mod pallet { /// To withdraw and deposit an asset. type AssetTransactor: TransactAsset; + + /// Process the message that was submitted + type MessageProcessor: MessageProcessor; } #[pallet::hooks] @@ -279,34 +281,7 @@ pub mod pallet { T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?; } - // Decode payload into `VersionedMessage` - let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref()) - .map_err(|_| Error::::InvalidPayload)?; - - // Decode message into XCM - let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?; - - tracing::info!( - target: LOG_TARGET, - ?xcm, - ?fee, - "💫 xcm decoded" - ); - - // Burning fees for teleport - Self::burn_fees(channel.para_id, fee)?; - - // Attempt to send XCM to a dest parachain - let message_id = Self::send_xcm(xcm, channel.para_id)?; - - Self::deposit_event(Event::MessageReceived { - channel_id: envelope.channel_id, - nonce: envelope.nonce, - message_id, - fee_burned: fee, - }); - - Ok(()) + T::MessageProcessor::process_message(channel, envelope) } /// Halt or resume all pallet operations. May only be called by root. @@ -326,7 +301,7 @@ pub mod pallet { impl Pallet { pub fn do_convert( message_id: H256, - message: VersionedMessage, + message: VersionedXcmMessage, ) -> Result<(Xcm<()>, BalanceOf), Error> { let (xcm, fee) = T::MessageConverter::convert(message_id, message) .map_err(|e| Error::::ConvertMessage(e))?; diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs index c26fa14420bf1..d2e47a046b9f5 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs @@ -14,7 +14,7 @@ use snowbridge_inbound_queue_primitives::{v1::MessageToXcm, Log, Proof, Verifica use sp_core::{H160, H256}; use sp_runtime::{ traits::{IdentifyAccount, IdentityLookup, MaybeConvert, Verify}, - BuildStorage, FixedU128, MultiSignature, + BuildStorage, DispatchError, FixedU128, MultiSignature, }; use sp_std::{convert::From, default::Default}; use xcm::{ @@ -28,7 +28,7 @@ use snowbridge_inbound_queue_primitives::EventFixture; #[cfg(feature = "runtime-benchmarks")] use snowbridge_pallet_inbound_queue_fixtures::register_token::make_register_token_message; -use crate::{self as inbound_queue}; +use crate::{self as inbound_queue, xcm_message_processor::XcmMessageProcessor}; type Block = frame_system::mocking::MockBlock; @@ -230,6 +230,30 @@ impl MaybeConvert for MockTokenIdConvert { } } +pub struct DummyPrefix; + +impl MessageProcessor for DummyPrefix { + fn can_process_message(_channel: &Channel, _envelope: &Envelope) -> bool { + false + } + + fn process_message(_channel: Channel, _envelope: Envelope) -> Result<(), DispatchError> { + panic!("DummyPrefix::process_message shouldn't be called"); + } +} + +pub struct DummySuffix; + +impl MessageProcessor for DummySuffix { + fn can_process_message(_channel: &Channel, _envelope: &Envelope) -> bool { + true + } + + fn process_message(_channel: Channel, _envelope: Envelope) -> Result<(), DispatchError> { + panic!("DummySuffix::process_message shouldn't be called"); + } +} + impl inbound_queue::Config for Test { type RuntimeEvent = RuntimeEvent; type Verifier = MockVerifier; @@ -255,6 +279,8 @@ impl inbound_queue::Config for Test { type LengthToFee = IdentityFee; type MaxMessageSize = ConstU32<1024>; type AssetTransactor = SuccessfulTransactor; + type MessageProcessor = (DummyPrefix, XcmMessageProcessor, DummySuffix); // We are passively testing if implementation of MessageProcessor trait works correctly for + // tuple } pub fn setup() { diff --git a/bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs b/bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs new file mode 100644 index 0000000000000..41cd22e7fa829 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs @@ -0,0 +1,47 @@ +use crate::{Error, Event, LOG_TARGET}; +use codec::DecodeAll; +use core::marker::PhantomData; +use snowbridge_core::Channel; +use snowbridge_inbound_queue_primitives::v1::{Envelope, MessageProcessor, VersionedXcmMessage}; +use sp_runtime::DispatchError; + +pub struct XcmMessageProcessor(PhantomData); + +impl MessageProcessor for XcmMessageProcessor +where + T: crate::Config, +{ + fn can_process_message(_channel: &Channel, envelope: &Envelope) -> bool { + VersionedXcmMessage::decode_all(&mut envelope.payload.as_ref()).is_ok() + } + + fn process_message(channel: Channel, envelope: Envelope) -> Result<(), DispatchError> { + // Decode message into XCM + let (xcm, fee) = match VersionedXcmMessage::decode_all(&mut envelope.payload.as_ref()) { + Ok(message) => crate::Pallet::::do_convert(envelope.message_id, message)?, + Err(_) => return Err(Error::::InvalidPayload.into()), + }; + + tracing::info!( + target: LOG_TARGET, + ?xcm, + ?fee, + "💫 xcm decoded", + ); + + // Burning fees for teleport + crate::Pallet::::burn_fees(channel.para_id, fee)?; + + // Attempt to send XCM to a dest parachain + let message_id = crate::Pallet::::send_xcm(xcm, channel.para_id)?; + + crate::Pallet::::deposit_event(Event::MessageReceived { + channel_id: envelope.channel_id, + nonce: envelope.nonce, + message_id, + fee_burned: fee, + }); + + Ok(()) + } +} diff --git a/bridges/snowbridge/primitives/inbound-queue/Cargo.toml b/bridges/snowbridge/primitives/inbound-queue/Cargo.toml index d77e0b30c4b4a..44591c21f1ba2 100644 --- a/bridges/snowbridge/primitives/inbound-queue/Cargo.toml +++ b/bridges/snowbridge/primitives/inbound-queue/Cargo.toml @@ -15,12 +15,15 @@ workspace = true exclude-from-umbrella = true [dependencies] +alloy-primitives-042 = { features = ["rlp"], workspace = true } +alloy-sol-types = { workspace = true } alloy-core = { workspace = true, features = ["sol-types"] } assets-common.workspace = true codec = { workspace = true } frame-support.workspace = true frame-system.workspace = true scale-info = { features = ["derive"], workspace = true } +impl-trait-for-tuples = { workspace = true } snowbridge-beacon-primitives.workspace = true snowbridge-core.workspace = true snowbridge-verification-primitives.workspace = true diff --git a/bridges/snowbridge/pallets/inbound-queue/src/envelope.rs b/bridges/snowbridge/primitives/inbound-queue/src/envelope.rs similarity index 91% rename from bridges/snowbridge/pallets/inbound-queue/src/envelope.rs rename to bridges/snowbridge/primitives/inbound-queue/src/envelope.rs index 2ba779b410709..ad778ea1749fc 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/envelope.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/envelope.rs @@ -1,12 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork use snowbridge_core::ChannelId; -use snowbridge_inbound_queue_primitives::Log; +use snowbridge_verification_primitives::Log; use sp_core::{RuntimeDebug, H160, H256}; use sp_std::prelude::*; -use alloy_core::{primitives::B256, sol, sol_types::SolEvent}; +use alloy_primitives::B256; +use alloy_sol_types::{sol, SolEvent}; sol! { event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload); @@ -44,7 +45,7 @@ impl TryFrom<&Log> for Envelope { channel_id: ChannelId::from(event.channel_id.as_ref()), nonce: event.nonce, message_id: H256::from(event.message_id.as_ref()), - payload: event.payload.into(), + payload: event.payload, }) } } diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v1.rs b/bridges/snowbridge/primitives/inbound-queue/src/v1.rs index 6eba682f72f74..655ada3befb53 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v1.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v1.rs @@ -3,13 +3,16 @@ //! Converts messages from Ethereum to XCM messages use crate::{CallIndex, EthereumLocationsConverterFor}; +use alloy_primitives_042::B256; +use alloy_sol_types::{sol, SolEvent}; use codec::{Decode, DecodeWithMemTracking, Encode}; use core::marker::PhantomData; use frame_support::{traits::tokens::Balance as BalanceT, PalletError}; use scale_info::TypeInfo; -use snowbridge_core::TokenId; +use snowbridge_core::{Channel, ChannelId, TokenId}; +use snowbridge_verification_primitives::Log; use sp_core::{Get, RuntimeDebug, H160, H256}; -use sp_runtime::{traits::MaybeConvert, MultiAddress}; +use sp_runtime::{traits::MaybeConvert, DispatchError, MultiAddress}; use sp_std::prelude::*; use xcm::prelude::{Junction::AccountKey20, *}; @@ -19,7 +22,7 @@ const MINIMUM_DEPOSIT: u128 = 1; /// we may want to evolve the protocol so that the ethereum side sends XCM messages directly. /// Instead having BridgeHub transcode the messages into XCM. #[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum VersionedMessage { +pub enum VersionedXcmMessage { V1(MessageV1), } @@ -141,7 +144,7 @@ pub trait ConvertMessage { /// Converts a versioned message into an XCM message and an optional topicID fn convert( message_id: H256, - message: VersionedMessage, + message: VersionedXcmMessage, ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; } @@ -180,10 +183,10 @@ where fn convert( message_id: H256, - message: VersionedMessage, + message: VersionedXcmMessage, ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { use Command::*; - use VersionedMessage::*; + use VersionedXcmMessage::*; match message { V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) => Ok(Self::convert_register_token(message_id, chain_id, token, fee)), @@ -456,6 +459,47 @@ where } } +sol! { + event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload); +} + +/// An inbound message that has had its outer envelope decoded. +#[derive(Clone, RuntimeDebug)] +pub struct Envelope { + /// The address of the outbound queue on Ethereum that emitted this message as an event log + pub gateway: H160, + /// The message Channel + pub channel_id: ChannelId, + /// A nonce for enforcing replay protection and ordering. + pub nonce: u64, + /// An id for tracing the message on its route (has no role in bridge consensus) + pub message_id: H256, + /// The inner payload generated from the source application. + pub payload: Vec, +} + +#[derive(Copy, Clone, RuntimeDebug)] +pub struct EnvelopeDecodeError; + +impl TryFrom<&Log> for Envelope { + type Error = EnvelopeDecodeError; + + fn try_from(log: &Log) -> Result { + let topics: Vec = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect(); + + let event = OutboundMessageAccepted::decode_log(topics, &log.data, true) + .map_err(|_| EnvelopeDecodeError)?; + + Ok(Self { + gateway: log.address, + channel_id: ChannelId::from(event.channel_id.as_ref()), + nonce: event.nonce, + message_id: H256::from(event.message_id.as_ref()), + payload: event.payload, + }) + } +} + #[cfg(test)] mod tests { use crate::{ @@ -671,3 +715,38 @@ mod tests { assert_eq!(actual_assets, Some(expected_assets)) } } +pub trait MessageProcessor { + /// Lightweight function to check if this processor can handle the message + fn can_process_message(channel: &Channel, envelope: &Envelope) -> bool; + /// Process the message + fn process_message(channel: Channel, envelope: Envelope) -> Result<(), DispatchError>; +} + +#[impl_trait_for_tuples::impl_for_tuples(10)] +impl MessageProcessor for Tuple { + fn can_process_message(channel: &Channel, envelope: &Envelope) -> bool { + for_tuples!( #( + match Tuple::can_process_message(&channel, &envelope) { + true => { + return true; + }, + _ => {} + } + )* ); + + false + } + + fn process_message(channel: Channel, envelope: Envelope) -> Result<(), DispatchError> { + for_tuples!( #( + match Tuple::can_process_message(&channel, &envelope) { + true => { + return Tuple::process_message(channel, envelope) + }, + _ => {} + } + )* ); + + Err(DispatchError::Other("No handler for message found")) + } +} diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs new file mode 100644 index 0000000000000..7125a4e8b60ee --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs @@ -0,0 +1,892 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::imports::*; +use ahr_xcm_config::UniversalLocation as AssetHubRococoUniversalLocation; +use codec::{Decode, Encode}; +use emulated_integration_tests_common::xcm_emulator::ConvertLocation; +use frame_support::pallet_prelude::TypeInfo; +use hex_literal::hex; +use rococo_westend_system_emulated_network::BridgeHubRococoParaSender as BridgeHubRococoSender; +use snowbridge_inbound_queue_primitives::{ + v1::{Command, Destination, MessageV1, VersionedMessage}, + EventFixture, +}; +use snowbridge_inbound_queue_primitives::{ + v1::{ConvertMessage, ConvertMessageError, VersionedMessage}, + EventProof, VerificationError, Verifier, +}; +use snowbridge_outbound_queue_primitives::OperatingMode; +use snowbridge_pallet_inbound_queue_fixtures::{ + register_token::make_register_token_message, send_native_eth::make_send_native_eth_message, + send_token::make_send_token_message, send_token_to_penpal::make_send_token_to_penpal_message, +}; +use snowbridge_pallet_system; +use sp_core::H256; +use sp_runtime::{DispatchError::Token, TokenError::FundsUnavailable}; +use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork; + +const INITIAL_FUND: u128 = 5_000_000_000 * ROCOCO_ED; +pub const CHAIN_ID: u64 = 11155111; +const TREASURY_ACCOUNT: [u8; 32] = + hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000"); +pub const WETH: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); +const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); +const INSUFFICIENT_XCM_FEE: u128 = 1000; +const XCM_FEE: u128 = 4_000_000_000; + +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub enum ControlCall { + #[codec(index = 3)] + CreateAgent, + #[codec(index = 4)] + CreateChannel { mode: OperatingMode }, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub enum SnowbridgeControl { + #[codec(index = 83)] + Control(ControlCall), +} + +pub fn send_inbound_message(fixture: EventFixture) -> DispatchResult { + EthereumBeaconClient::store_finalized_header( + fixture.finalized_header, + fixture.block_roots_root, + ) + .unwrap(); + EthereumInboundQueue::submit( + BridgeHubRococoRuntimeOrigin::signed(BridgeHubRococoSender::get()), + fixture.event, + ) +} + +/// Create an agent on Ethereum. An agent is a representation of an entity in the Polkadot +/// ecosystem (like a parachain) on Ethereum. +#[test] +#[ignore] +fn create_agent() { + let origin_para: u32 = 1001; + // Fund the origin parachain sovereign account so that it can pay execution fees. + BridgeHubRococo::fund_para_sovereign(origin_para.into(), INITIAL_FUND); + + let sudo_origin = ::RuntimeOrigin::root(); + let destination = Rococo::child_location_of(BridgeHubRococo::para_id()).into(); + + let create_agent_call = SnowbridgeControl::Control(ControlCall::CreateAgent {}); + // Construct XCM to create an agent for para 1001 + let remote_xcm = VersionedXcm::from(Xcm(vec![ + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + DescendOrigin(Parachain(origin_para).into()), + Transact { + origin_kind: OriginKind::Xcm, + call: create_agent_call.encode().into(), + fallback_max_weight: None, + }, + ])); + + // Rococo Global Consensus + // Send XCM message from Relay Chain to Bridge Hub source Parachain + Rococo::execute_with(|| { + assert_ok!(::XcmPallet::send( + sudo_origin, + bx!(destination), + bx!(remote_xcm), + )); + + type RuntimeEvent = ::RuntimeEvent; + // Check that the Transact message was sent + assert_expected_events!( + Rococo, + vec![ + RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { .. }) => {}, + ] + ); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that a message was sent to Ethereum to create the agent + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::EthereumSystem(snowbridge_pallet_system::Event::CreateAgent { + .. + }) => {}, + ] + ); + }); +} + +/// Create a channel for a consensus system. A channel is a bidirectional messaging channel +/// between BridgeHub and Ethereum. +#[test] +#[ignore] +fn create_channel() { + let origin_para: u32 = 1001; + // Fund AssetHub sovereign account so that it can pay execution fees. + BridgeHubRococo::fund_para_sovereign(origin_para.into(), INITIAL_FUND); + + let sudo_origin = ::RuntimeOrigin::root(); + let destination: VersionedLocation = + Rococo::child_location_of(BridgeHubRococo::para_id()).into(); + + let create_agent_call = SnowbridgeControl::Control(ControlCall::CreateAgent {}); + // Construct XCM to create an agent for para 1001 + let create_agent_xcm = VersionedXcm::from(Xcm(vec![ + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + DescendOrigin(Parachain(origin_para).into()), + Transact { + origin_kind: OriginKind::Xcm, + call: create_agent_call.encode().into(), + fallback_max_weight: None, + }, + ])); + + let create_channel_call = + SnowbridgeControl::Control(ControlCall::CreateChannel { mode: OperatingMode::Normal }); + // Construct XCM to create a channel for para 1001 + let create_channel_xcm = VersionedXcm::from(Xcm(vec![ + UnpaidExecution { weight_limit: Unlimited, check_origin: None }, + DescendOrigin(Parachain(origin_para).into()), + Transact { + origin_kind: OriginKind::Xcm, + call: create_channel_call.encode().into(), + fallback_max_weight: None, + }, + ])); + + // Rococo Global Consensus + // Send XCM message from Relay Chain to Bridge Hub source Parachain + Rococo::execute_with(|| { + assert_ok!(::XcmPallet::send( + sudo_origin.clone(), + bx!(destination.clone()), + bx!(create_agent_xcm), + )); + + assert_ok!(::XcmPallet::send( + sudo_origin, + bx!(destination), + bx!(create_channel_xcm), + )); + + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + Rococo, + vec![ + RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { .. }) => {}, + ] + ); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the Channel was created + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::EthereumSystem(snowbridge_pallet_system::Event::CreateChannel { + .. + }) => {}, + ] + ); + }); +} + +/// Tests the registering of a token as an asset on AssetHub. +#[test] +fn register_weth_token_from_ethereum_to_asset_hub() { + // Fund AssetHub sovereign account so that it can pay execution fees. + BridgeHubRococo::fund_para_sovereign(AssetHubRococo::para_id().into(), INITIAL_FUND); + // Fund ethereum sovereign on AssetHub to satisfy ED + AssetHubRococo::fund_accounts(vec![(snowbridge_sovereign(), INITIAL_FUND)]); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Construct RegisterToken message and sent to inbound queue + let register_token_message = make_register_token_message(); + assert_ok!(send_inbound_message(register_token_message.clone())); + + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { .. }) => {}, + ] + ); + }); +} + +/// Tests the registering of a token as an asset on AssetHub, and then subsequently sending +/// a token from Ethereum to AssetHub. +#[test] +fn send_weth_token_from_ethereum_to_asset_hub() { + BridgeHubRococo::fund_para_sovereign(AssetHubRococo::para_id().into(), INITIAL_FUND); + // Fund ethereum sovereign and receiver on AssetHub to satisfy ED + AssetHubRococo::fund_accounts(vec![ + (snowbridge_sovereign(), INITIAL_FUND), + (AssetHubRococoReceiver::get(), INITIAL_FUND), + ]); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Construct RegisterToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_register_token_message())); + + // Construct SendToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_send_token_message())); + + // Check that the message was sent + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the token was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + ] + ); + }); +} + +/// Tests sending a token to a 3rd party parachain, called PenPal. The token reserve is +/// still located on AssetHub. +#[test] +fn send_weth_from_ethereum_to_penpal() { + let asset_hub_sovereign = BridgeHubRococo::sovereign_account_id_of(Location::new( + 1, + [Parachain(AssetHubRococo::para_id().into())], + )); + // Fund AssetHub sovereign account so it can pay execution fees for the asset transfer + BridgeHubRococo::fund_accounts(vec![(asset_hub_sovereign.clone(), INITIAL_FUND)]); + + // Fund PenPal receiver (covering ED) + let native_id: Location = Parent.into(); + let receiver: AccountId = [ + 28, 189, 45, 67, 83, 10, 68, 112, 90, 208, 136, 175, 49, 62, 24, 248, 11, 83, 239, 22, 179, + 97, 119, 205, 75, 119, 184, 70, 242, 165, 240, 124, + ] + .into(); + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + native_id, + receiver, + penpal_runtime::EXISTENTIAL_DEPOSIT, + ); + + PenpalA::execute_with(|| { + assert_ok!(::System::set_storage( + ::RuntimeOrigin::root(), + vec![( + PenpalCustomizableAssetFromSystemAssetHub::key().to_vec(), + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]).encode(), + )], + )); + }); + + let ethereum_network_v5: NetworkId = EthereumNetwork::get().into(); + + // The Weth asset location, identified by the contract address on Ethereum + let weth_asset_location: Location = + (Parent, Parent, ethereum_network_v5, AccountKey20 { network: None, key: WETH }).into(); + + let origin_location = (Parent, Parent, ethereum_network_v5).into(); + + // Fund ethereum sovereign on AssetHub + let ethereum_sovereign: AccountId = + GlobalConsensusEthereumConvertsFor::::convert_location(&origin_location) + .unwrap(); + AssetHubRococo::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]); + + // Create asset on the Penpal parachain. + PenpalA::execute_with(|| { + assert_ok!(::ForeignAssets::force_create( + ::RuntimeOrigin::root(), + weth_asset_location.clone(), + asset_hub_sovereign.into(), + false, + 1000, + )); + + assert!(::ForeignAssets::asset_exists(weth_asset_location)); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Construct RegisterToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_register_token_message())); + + // Construct SendToken message to AssetHub(only for increase the nonce as the same order in + // smoke test) + assert_ok!(send_inbound_message(make_send_token_message())); + + // Construct SendToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_send_token_to_penpal_message())); + + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on AssetHub + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + PenpalA::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on PenPal + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + ] + ); + }); +} + +/// Tests the full cycle of token transfers: +/// - registering a token on AssetHub +/// - sending a token to AssetHub +/// - returning the token to Ethereum +#[test] +fn send_weth_asset_from_asset_hub_to_ethereum() { + use ahr_xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee; + let assethub_location = BridgeHubRococo::sibling_location_of(AssetHubRococo::para_id()); + let assethub_sovereign = BridgeHubRococo::sovereign_account_id_of(assethub_location); + + AssetHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + BridgeHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + AssetHubRococo::force_xcm_version( + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]), + XCM_VERSION, + ); + + BridgeHubRococo::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); + AssetHubRococo::fund_accounts(vec![ + (AssetHubRococoReceiver::get(), INITIAL_FUND), + (snowbridge_sovereign(), INITIAL_FUND), + ]); + + const WETH_AMOUNT: u128 = 1_000_000_000; + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Construct RegisterToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_register_token_message())); + + // Check that the register token message was sent using xcm + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + + // Construct SendToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_send_token_message())); + + // Check that the send token message was sent using xcm + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + // Check that AssetHub has issued the foreign asset + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + ] + ); + let assets = vec![Asset { + id: AssetId(Location::new( + 2, + [ + GlobalConsensus(Ethereum { chain_id: CHAIN_ID }), + AccountKey20 { network: None, key: WETH }, + ], + )), + fun: Fungible(WETH_AMOUNT), + }]; + let multi_assets = VersionedAssets::from(Assets::from(assets)); + + let destination = VersionedLocation::from(Location::new( + 2, + [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })], + )); + + let beneficiary = VersionedLocation::from(Location::new( + 0, + [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }], + )); + + let free_balance_before = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Send the Weth back to Ethereum + ::PolkadotXcm::limited_reserve_transfer_assets( + RuntimeOrigin::signed(AssetHubRococoReceiver::get()), + Box::new(destination), + Box::new(beneficiary), + Box::new(multi_assets), + 0, + Unlimited, + ) + .unwrap(); + let free_balance_after = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Assert at least DefaultBridgeHubEthereumBaseFee charged from the sender + let free_balance_diff = free_balance_before - free_balance_after; + assert!(free_balance_diff > DefaultBridgeHubEthereumBaseFee::get()); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the transfer token back to Ethereum message was queue in the Ethereum + // Outbound Queue + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued {..}) => {}, + ] + ); + let events = BridgeHubRococo::events(); + // Check that the local fee was credited to the Snowbridge sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount: _amount }) + if *who == TREASURY_ACCOUNT.into() + )), + "Snowbridge sovereign takes local fee." + ); + // Check that the remote fee was credited to the AssetHub sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount: _amount }) + if *who == assethub_sovereign + )), + "AssetHub sovereign takes remote fee." + ); + }); +} + +/// Tests the full cycle of eth transfers: +/// - sending a token to AssetHub +/// - returning the token to Ethereum +#[test] +fn send_eth_asset_from_asset_hub_to_ethereum_and_back() { + let ethereum_network: NetworkId = EthereumNetwork::get().into(); + let origin_location = (Parent, Parent, ethereum_network).into(); + + use ahr_xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee; + let assethub_location = BridgeHubRococo::sibling_location_of(AssetHubRococo::para_id()); + let assethub_sovereign = BridgeHubRococo::sovereign_account_id_of(assethub_location); + let ethereum_sovereign: AccountId = AssetHubRococo::execute_with(|| { + ExternalConsensusLocationsConverterFor::< + AssetHubRococoUniversalLocation, + AccountId, + >::convert_location(&origin_location) + .unwrap() + }); + + AssetHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + BridgeHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + AssetHubRococo::force_xcm_version(origin_location.clone(), XCM_VERSION); + + BridgeHubRococo::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); + AssetHubRococo::fund_accounts(vec![ + (AssetHubRococoReceiver::get(), INITIAL_FUND), + (ethereum_sovereign.clone(), INITIAL_FUND), + ]); + + const ETH_AMOUNT: u128 = 1_000_000_000_000_000_000; + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + // Set the gateway. This is needed because new fixtures use a different gateway address. + assert_ok!(::System::set_storage( + RuntimeOrigin::root(), + vec![( + EthereumGatewayAddress::key().to_vec(), + sp_core::H160(hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d")).encode(), + )], + )); + + // Construct SendToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_send_native_eth_message())); + + // Check that the send token message was sent using xcm + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let _issued_event = RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { + asset_id: origin_location.clone(), + owner: AssetHubRococoReceiver::get().into(), + amount: ETH_AMOUNT, + }); + // Check that AssetHub has issued the foreign asset + assert_expected_events!( + AssetHubRococo, + vec![ + _issued_event => {}, + ] + ); + let assets = + vec![Asset { id: AssetId(origin_location.clone()), fun: Fungible(ETH_AMOUNT) }]; + let multi_assets = VersionedAssets::from(Assets::from(assets)); + + let destination = origin_location.clone().into(); + + let beneficiary = VersionedLocation::from(Location::new( + 0, + [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }], + )); + + let free_balance_before = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Send the Weth back to Ethereum + ::PolkadotXcm::limited_reserve_transfer_assets( + RuntimeOrigin::signed(AssetHubRococoReceiver::get()), + Box::new(destination), + Box::new(beneficiary), + Box::new(multi_assets), + 0, + Unlimited, + ) + .unwrap(); + + let _burned_event = RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { + asset_id: origin_location.clone(), + owner: AssetHubRococoReceiver::get().into(), + balance: ETH_AMOUNT, + }); + // Check that AssetHub has issued the foreign asset + let _destination = origin_location.clone(); + assert_expected_events!( + AssetHubRococo, + vec![ + _burned_event => {}, + RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent { + destination: _destination, .. + }) => {}, + ] + ); + + let free_balance_after = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Assert at least DefaultBridgeHubEthereumBaseFee charged from the sender + let free_balance_diff = free_balance_before - free_balance_after; + assert!(free_balance_diff > DefaultBridgeHubEthereumBaseFee::get()); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the transfer token back to Ethereum message was queue in the Ethereum + // Outbound Queue + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageAccepted {..}) => {}, + RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued {..}) => {}, + ] + ); + + let events = BridgeHubRococo::events(); + // Check that the local fee was credited to the Snowbridge sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount: _amount }) + if *who == TREASURY_ACCOUNT.into() + )), + "Snowbridge sovereign takes local fee." + ); + // Check that the remote fee was credited to the AssetHub sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount: _amount }) + if *who == assethub_sovereign + )), + "AssetHub sovereign takes remote fee." + ); + }); +} + +#[test] +fn send_token_from_ethereum_to_asset_hub_fail_for_insufficient_fund() { + // Insufficient fund + BridgeHubRococo::fund_para_sovereign(AssetHubRococo::para_id().into(), 1_000); + + BridgeHubRococo::execute_with(|| { + assert_err!(send_inbound_message(make_register_token_message()), Token(FundsUnavailable)); + }); +} + +#[test] +fn register_weth_token_in_asset_hub_fail_for_insufficient_fee() { + BridgeHubRococo::fund_para_sovereign(AssetHubRococo::para_id().into(), INITIAL_FUND); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type EthereumInboundQueue = + ::EthereumInboundQueue; + let message_id: H256 = [0; 32].into(); + let message = VersionedXcmMessage::V1(MessageV1 { + chain_id: CHAIN_ID, + command: Command::RegisterToken { + token: WETH.into(), + // Insufficient fee which should trigger the trap + fee: INSUFFICIENT_XCM_FEE, + }, + }); + let (xcm, _) = EthereumInboundQueue::do_convert(message_id, message).unwrap(); + let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubRococo::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { success:false, .. }) => {}, + ] + ); + }); +} + +fn send_weth_from_ethereum_to_asset_hub_with_fee(account_id: [u8; 32], fee: u128) { + let ethereum_network_v5: NetworkId = EthereumNetwork::get().into(); + let weth_asset_location: Location = + Location::new(2, [ethereum_network_v5.into(), AccountKey20 { network: None, key: WETH }]); + // Fund asset hub sovereign on bridge hub + let asset_hub_sovereign = BridgeHubRococo::sovereign_account_id_of(Location::new( + 1, + [Parachain(AssetHubRococo::para_id().into())], + )); + BridgeHubRococo::fund_accounts(vec![(asset_hub_sovereign.clone(), INITIAL_FUND)]); + + // Register WETH + AssetHubRococo::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + weth_asset_location.clone().try_into().unwrap(), + asset_hub_sovereign.into(), + false, + 1, + )); + + assert!(::ForeignAssets::asset_exists( + weth_asset_location.clone().try_into().unwrap(), + )); + }); + + // Send WETH to an existent account on asset hub + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + type EthereumInboundQueue = + ::EthereumInboundQueue; + let message_id: H256 = [0; 32].into(); + let message = VersionedXcmMessage::V1(MessageV1 { + chain_id: CHAIN_ID, + command: Command::SendToken { + token: WETH.into(), + destination: Destination::AccountId32 { id: account_id }, + amount: 1_000_000, + fee, + }, + }); + let (xcm, _) = EthereumInboundQueue::do_convert(message_id, message).unwrap(); + assert_ok!(EthereumInboundQueue::send_xcm(xcm, AssetHubRococo::para_id().into())); + + // Check that the message was sent + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); +} + +#[test] +fn send_weth_from_ethereum_to_existent_account_on_asset_hub() { + send_weth_from_ethereum_to_asset_hub_with_fee(AssetHubRococoSender::get().into(), XCM_FEE); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the token was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + ] + ); + }); +} + +#[test] +fn send_weth_from_ethereum_to_non_existent_account_on_asset_hub() { + send_weth_from_ethereum_to_asset_hub_with_fee([1; 32], XCM_FEE); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the token was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + ] + ); + }); +} + +#[test] +fn send_weth_from_ethereum_to_non_existent_account_on_asset_hub_with_insufficient_fee() { + send_weth_from_ethereum_to_asset_hub_with_fee([1; 32], INSUFFICIENT_XCM_FEE); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the message was not processed successfully due to insufficient fee + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { success:false, .. }) => {}, + ] + ); + }); +} + +#[test] +fn send_weth_from_ethereum_to_non_existent_account_on_asset_hub_with_sufficient_fee_but_do_not_satisfy_ed( +) { + // On AH the xcm fee is 26_789_690 and the ED is 3_300_000 + send_weth_from_ethereum_to_asset_hub_with_fee([1; 32], 30_000_000); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the message was not processed successfully due to insufficient ED + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { success:false, .. }) => {}, + ] + ); + }); +} + +#[test] +fn create_foreign_asset_deposit_is_equal_to_asset_hub_foreign_asset_pallet_deposit() { + let asset_hub_deposit = asset_hub_rococo_runtime::ForeignAssetsAssetDeposit::get(); + let bridge_hub_deposit = bp_asset_hub_rococo::CreateForeignAssetDeposit::get(); + assert!( + bridge_hub_deposit >= + asset_hub_deposit, + "The BridgeHub asset creation deposit must be equal to or larger than the asset creation deposit configured on BridgeHub" + ); +} + +pub fn snowbridge_sovereign() -> sp_runtime::AccountId32 { + use asset_hub_rococo_runtime::xcm_config::UniversalLocation as AssetHubWestendUniversalLocation; + let ethereum_sovereign: AccountId = AssetHubRococo::execute_with(|| { + ExternalConsensusLocationsConverterFor::< + AssetHubWestendUniversalLocation, + [u8; 32], + >::convert_location(&Location::new( + 2, + [xcm::v5::Junction::GlobalConsensus(EthereumNetwork::get())], + )) + .unwrap() + .into() + }); + + ethereum_sovereign +} diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index 0bc1669548335..2a5a3abadb280 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -28,8 +28,6 @@ use snowbridge_inbound_queue_primitives::v1::MessageToXcm; use snowbridge_outbound_queue_primitives::v1::EthereumBlobExporter; use crate::xcm_config::RelayNetwork; -#[cfg(feature = "runtime-benchmarks")] -use benchmark_helpers::DoNothingRouter; use bp_asset_hub_rococo::CreateForeignAssetDeposit; use bridge_hub_common::AggregateMessageOrigin; use frame_support::{parameter_types, weights::ConstantMultiplier}; @@ -80,7 +78,7 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { #[cfg(not(feature = "runtime-benchmarks"))] type XcmSender = XcmRouter; #[cfg(feature = "runtime-benchmarks")] - type XcmSender = DoNothingRouter; + type XcmSender = benchmark_helpers::DoNothingRouter; type ChannelLookup = EthereumSystem; type GatewayAddress = EthereumGatewayAddress; #[cfg(feature = "runtime-benchmarks")] @@ -101,6 +99,8 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue::WeightInfo; type PricingParameters = EthereumSystem; type AssetTransactor = ::AssetTransactor; + type MessageProcessor = + snowbridge_pallet_inbound_queue::xcm_message_processor::XcmMessageProcessor; } pub struct GetAggregateMessageOrigin; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index 2cb5af70ca194..c8ae63c109601 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -128,6 +128,8 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue::WeightInfo; type PricingParameters = EthereumSystem; type AssetTransactor = ::AssetTransactor; + type MessageProcessor = + snowbridge_pallet_inbound_queue::xcm_message_processor::XcmMessageProcessor; } impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { From af0f774aaa1b0803a848ae410b26a7f8b4003930 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 8 Jan 2025 16:23:56 +0400 Subject: [PATCH 04/11] Add new commitment hook (#6) * add on_new_commitment hook Missing dependencies xcm-emulator --- bridges/snowbridge/pallets/outbound-queue/src/lib.rs | 6 +++++- bridges/snowbridge/pallets/outbound-queue/src/mock.rs | 1 + bridges/snowbridge/pallets/outbound-queue/src/types.rs | 9 +++++++++ bridges/snowbridge/pallets/system/src/mock.rs | 1 + .../bridge-hub-rococo/src/bridge_to_ethereum_config.rs | 1 + .../bridge-hub-westend/src/bridge_to_ethereum_config.rs | 1 + 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/bridges/snowbridge/pallets/outbound-queue/src/lib.rs b/bridges/snowbridge/pallets/outbound-queue/src/lib.rs index 4f5e948d6435f..c5728b1544a14 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/lib.rs @@ -120,7 +120,7 @@ use sp_runtime::{ DigestItem, Saturating, }; use sp_std::prelude::*; -pub use types::{CommittedMessage, ProcessMessageOriginOf}; +pub use types::{CommittedMessage, OnNewCommitment, ProcessMessageOriginOf}; pub use weights::WeightInfo; pub use pallet::*; @@ -175,6 +175,8 @@ pub mod pallet { type PricingParameters: Get>; + type OnNewCommitment: OnNewCommitment; + /// Convert a weight value into a deductible fee based. type WeightToFee: WeightToFee; @@ -296,6 +298,8 @@ pub mod pallet { // Create merkle root of messages let root = merkle_root::<::Hashing, _>(MessageLeaves::::stream_iter()); + T::OnNewCommitment::on_new_commitment(root); + let digest_item: DigestItem = SnowbridgeDigestItem::Snowbridge(root).into(); // Insert merkle root into the header digest diff --git a/bridges/snowbridge/pallets/outbound-queue/src/mock.rs b/bridges/snowbridge/pallets/outbound-queue/src/mock.rs index 4e22ac7efc0ad..2b4fad25f6936 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/mock.rs @@ -102,6 +102,7 @@ impl crate::Config for Test { type AggregateMessageOrigin = AggregateMessageOrigin; type GetAggregateMessageOrigin = GetAggregateMessageOrigin; type WeightToFee = IdentityFee; + type OnNewCommitment = (); type WeightInfo = (); } diff --git a/bridges/snowbridge/pallets/outbound-queue/src/types.rs b/bridges/snowbridge/pallets/outbound-queue/src/types.rs index 76d32fab462a4..181894499243d 100644 --- a/bridges/snowbridge/pallets/outbound-queue/src/types.rs +++ b/bridges/snowbridge/pallets/outbound-queue/src/types.rs @@ -55,3 +55,12 @@ impl From for Token { ]) } } + +/// Hook that will be called when new message commitment is constructed +pub trait OnNewCommitment { + fn on_new_commitment(commitment: H256); +} + +impl OnNewCommitment for () { + fn on_new_commitment(_commitment: H256) {} +} diff --git a/bridges/snowbridge/pallets/system/src/mock.rs b/bridges/snowbridge/pallets/system/src/mock.rs index a88962663dc38..d14d1a5d19e56 100644 --- a/bridges/snowbridge/pallets/system/src/mock.rs +++ b/bridges/snowbridge/pallets/system/src/mock.rs @@ -184,6 +184,7 @@ impl snowbridge_pallet_outbound_queue::Config for Test { type AggregateMessageOrigin = AggregateMessageOrigin; type GetAggregateMessageOrigin = GetAggregateMessageOrigin; type WeightToFee = IdentityFee; + type OnNewCommitment = (); type WeightInfo = (); } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index 2a5a3abadb280..042e12e2278ad 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -126,6 +126,7 @@ impl snowbridge_pallet_outbound_queue::Config for Runtime { type Channels = EthereumSystem; type AggregateMessageOrigin = AggregateMessageOrigin; type GetAggregateMessageOrigin = GetAggregateMessageOrigin; + type OnNewCommitment = (); } #[cfg(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test))] diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index c8ae63c109601..283e241c8d06b 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -186,6 +186,7 @@ impl snowbridge_pallet_outbound_queue::Config for Runtime { type Channels = EthereumSystem; type AggregateMessageOrigin = AggregateMessageOrigin; type GetAggregateMessageOrigin = GetAggregateMessageOrigin; + type OnNewCommitment = (); } impl snowbridge_pallet_outbound_queue_v2::Config for Runtime { From 76dae27a9fca43d8ce082877f6f11ba8eef6510e Mon Sep 17 00:00:00 2001 From: girazoki Date: Tue, 28 Jan 2025 13:44:47 +0100 Subject: [PATCH 05/11] remove bandersnatch from sp-core --- Cargo.lock | 1 - Cargo.toml | 1 - substrate/primitives/core/Cargo.toml | 6 +----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c44732038c260..f22e0fd99e87f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23261,7 +23261,6 @@ dependencies = [ name = "sp-core" version = "39.0.0" dependencies = [ - "ark-vrf", "array-bytes 6.2.2", "bip39", "bitflags 1.3.2", diff --git a/Cargo.toml b/Cargo.toml index afc80cb996934..a3b198e7cc331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1537,7 +1537,6 @@ overflow-checks = true # # This list is ordered alphabetically. [profile.dev.package] -ark-vrf = { opt-level = 3 } blake2 = { opt-level = 3 } blake2b_simd = { opt-level = 3 } chacha20poly1305 = { opt-level = 3 } diff --git a/substrate/primitives/core/Cargo.toml b/substrate/primitives/core/Cargo.toml index a2784490555df..bc3e09f667010 100644 --- a/substrate/primitives/core/Cargo.toml +++ b/substrate/primitives/core/Cargo.toml @@ -68,9 +68,6 @@ secp256k1 = { features = ["alloc", "recovery"], optional = true, workspace = tru sha2 = { optional = true, workspace = true } w3f-bls = { optional = true, workspace = true } -# bandersnatch crypto -ark-vrf = { optional = true, workspace = true, features = ["bandersnatch", "ring"] } - [target.'cfg(not(substrate_runtime))'.dependencies] sp-externalities.workspace = true sp-externalities.default-features = false @@ -85,7 +82,6 @@ serde_json = { workspace = true, default-features = true } [features] default = ["std"] std = [ - "ark-vrf?/std", "bip39/rand", "bip39/std", "blake2/std", @@ -158,4 +154,4 @@ bls-experimental = ["sha2", "w3f-bls"] # This feature adds Bandersnatch crypto primitives. # It should not be used in production since the implementation and interface may still # be subject to significant changes. -bandersnatch-experimental = ["ark-vrf"] +bandersnatch-experimental = [] From b7163644e5861e9c3d5e0bf8e4a866aae2887328 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 5 Feb 2025 00:46:26 -0300 Subject: [PATCH 06/11] Add reward processor in snowbridge inbound queue --- .../pallets/inbound-queue/src/lib.rs | 54 ++++++++++++++----- .../pallets/inbound-queue/src/mock.rs | 7 ++- .../pallets/inbound-queue/src/test.rs | 6 +-- .../src/bridge_to_ethereum_config.rs | 16 +++--- .../src/bridge_to_ethereum_config.rs | 2 + 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs index eef2f69cf604f..713d777236c37 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs @@ -37,6 +37,7 @@ pub mod xcm_message_processor; use codec::{Decode, Encode}; use frame_support::{ + pallet_prelude::DispatchResult, traits::{ fungible::{Inspect, Mutate}, tokens::{Fortitude, Preservation}, @@ -76,6 +77,41 @@ pub use pallet::*; pub const LOG_TARGET: &str = "snowbridge-inbound-queue"; +pub trait RewardProcessor { + fn process_reward(who: T::AccountId, channel: Channel, message: EventProof) -> DispatchResult; +} + +impl RewardProcessor for () { + fn process_reward( + _who: T::AccountId, + _channel: Channel, + _message: EventProof, + ) -> DispatchResult { + Ok(()) + } +} + +pub struct RewardThroughSovereign(sp_std::marker::PhantomData); + +impl RewardProcessor for RewardThroughSovereign { + fn process_reward(who: T::AccountId, channel: Channel, event: EventProof) -> DispatchResult { + let length = event.encode().len() as u32; + let delivery_cost = pallet::Pallet::::calculate_delivery_cost(length); + let sovereign_account: T::AccountId = sibling_sovereign_account::(channel.para_id); + + let amount = T::Token::reducible_balance( + &sovereign_account, + Preservation::Preserve, + Fortitude::Polite, + ) + .min(delivery_cost); + if !amount.is_zero() { + T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?; + } + + Ok(()) + } +} #[frame_support::pallet] pub mod pallet { use super::*; @@ -143,6 +179,9 @@ pub mod pallet { /// Process the message that was submitted type MessageProcessor: MessageProcessor; + + /// Process the reward to the relayer + type RewardProcessor: RewardProcessor; } #[pallet::hooks] @@ -267,20 +306,7 @@ pub mod pallet { } })?; - // Reward relayer from the sovereign account of the destination parachain, only if funds - // are available - let sovereign_account = sibling_sovereign_account::(channel.para_id); - let delivery_cost = Self::calculate_delivery_cost(event.encode().len() as u32); - let amount = T::Token::reducible_balance( - &sovereign_account, - Preservation::Preserve, - Fortitude::Polite, - ) - .min(delivery_cost); - if !amount.is_zero() { - T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?; - } - + T::RewardProcessor::process_reward(who, channel.clone(), event)?; T::MessageProcessor::process_message(channel, envelope) } diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs index d2e47a046b9f5..2e3678fdc065e 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs @@ -2,6 +2,10 @@ // SPDX-FileCopyrightText: 2023 Snowfork use super::*; +use crate::{ + xcm_message_processor::XcmMessageProcessor, + {self as inbound_queue}, +}; use frame_support::{derive_impl, parameter_types, traits::ConstU32, weights::IdentityFee}; use hex_literal::hex; use snowbridge_beacon_primitives::{ @@ -28,8 +32,6 @@ use snowbridge_inbound_queue_primitives::EventFixture; #[cfg(feature = "runtime-benchmarks")] use snowbridge_pallet_inbound_queue_fixtures::register_token::make_register_token_message; -use crate::{self as inbound_queue, xcm_message_processor::XcmMessageProcessor}; - type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( @@ -281,6 +283,7 @@ impl inbound_queue::Config for Test { type AssetTransactor = SuccessfulTransactor; type MessageProcessor = (DummyPrefix, XcmMessageProcessor, DummySuffix); // We are passively testing if implementation of MessageProcessor trait works correctly for // tuple + type RewardProcessor = RewardThroughSovereign; } pub fn setup() { diff --git a/bridges/snowbridge/pallets/inbound-queue/src/test.rs b/bridges/snowbridge/pallets/inbound-queue/src/test.rs index 60065ccb15b24..bf7c66aeef8e8 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/test.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/test.rs @@ -22,7 +22,7 @@ fn test_submit_happy_path() { let origin = RuntimeOrigin::signed(relayer.clone()); - // Submit event proof + // Submit message let event = EventProof { event_log: mock_event_log(), proof: Proof { @@ -72,7 +72,7 @@ fn test_submit_xcm_invalid_channel() { println!("account: {}", sovereign_account); let _ = Balances::mint_into(&sovereign_account, 10000); - // Submit event proof + // Submit message let event = EventProof { event_log: mock_event_log_invalid_channel(), proof: Proof { @@ -97,7 +97,7 @@ fn test_submit_with_invalid_gateway() { let sovereign_account = sibling_sovereign_account::(ASSET_HUB_PARAID.into()); let _ = Balances::mint_into(&sovereign_account, 10000); - // Submit event proof + // Submit message let event = EventProof { event_log: mock_event_log_invalid_gateway(), proof: Proof { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index 042e12e2278ad..c74080fefccda 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -17,22 +17,23 @@ #[cfg(not(feature = "runtime-benchmarks"))] use crate::XcmRouter; use crate::{ - xcm_config, xcm_config::UniversalLocation, Balances, EthereumInboundQueue, - EthereumOutboundQueue, EthereumSystem, MessageQueue, Runtime, RuntimeEvent, TransactionByteFee, - TreasuryAccount, + xcm_config, + xcm_config::{RelayNetwork, UniversalLocation}, + Balances, EthereumInboundQueue, EthereumOutboundQueue, EthereumSystem, MessageQueue, Runtime, + RuntimeEvent, TransactionByteFee, TreasuryAccount, }; +use bridge_hub_common::AggregateMessageOrigin; +use frame_support::{parameter_types, weights::ConstantMultiplier}; +use pallet_xcm::EnsureXcm; use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::{gwei, meth, AllowSiblingsOnly, ChannelId, PricingParameters, Rewards}; use snowbridge_inbound_queue_primitives::v1::MessageToXcm; use snowbridge_outbound_queue_primitives::v1::EthereumBlobExporter; -use crate::xcm_config::RelayNetwork; use bp_asset_hub_rococo::CreateForeignAssetDeposit; -use bridge_hub_common::AggregateMessageOrigin; -use frame_support::{parameter_types, weights::ConstantMultiplier}; use hex_literal::hex; -use pallet_xcm::EnsureXcm; +use snowbridge_pallet_inbound_queue::RewardThroughSovereign; use sp_core::H160; use sp_runtime::{ traits::{ConstU32, ConstU8, Convert, Keccak256}, @@ -101,6 +102,7 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type AssetTransactor = ::AssetTransactor; type MessageProcessor = snowbridge_pallet_inbound_queue::xcm_message_processor::XcmMessageProcessor; + type RewardProcessor = RewardThroughSovereign; } pub struct GetAggregateMessageOrigin; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index 283e241c8d06b..e3ade0ce04b31 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -36,6 +36,7 @@ use snowbridge_outbound_queue_primitives::{ v1::{ConstantGasMeter, EthereumBlobExporter}, v2::{ConstantGasMeter as ConstantGasMeterV2, EthereumBlobExporter as EthereumBlobExporterV2}, }; +use snowbridge_pallet_inbound_queue::RewardThroughSovereign; use sp_core::H160; use sp_runtime::{ traits::{ConstU32, ConstU8, Convert, Keccak256}, @@ -130,6 +131,7 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type AssetTransactor = ::AssetTransactor; type MessageProcessor = snowbridge_pallet_inbound_queue::xcm_message_processor::XcmMessageProcessor; + type RewardProcessor = RewardThroughSovereign; } impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { From 027154779eaa11e59b1d05b8926a50567bc9ea28 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 4 Jul 2025 13:56:59 -0300 Subject: [PATCH 07/11] Re introduce snowbridge v1 removed extrinsics (#10) --- .../pallets/system/src/benchmarking.rs | 47 +++++++- bridges/snowbridge/pallets/system/src/lib.rs | 104 +++++++++++++++++- .../snowbridge/pallets/system/src/weights.rs | 40 +++++++ .../outbound-queue/src/v1/message.rs | 20 +++- .../src/weights/snowbridge_pallet_system.rs | 40 +++++++ .../src/weights/snowbridge_pallet_system.rs | 40 +++++++ 6 files changed, 288 insertions(+), 3 deletions(-) diff --git a/bridges/snowbridge/pallets/system/src/benchmarking.rs b/bridges/snowbridge/pallets/system/src/benchmarking.rs index cba1483a193cd..824eac26cdae1 100644 --- a/bridges/snowbridge/pallets/system/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/system/src/benchmarking.rs @@ -7,11 +7,19 @@ use super::*; use crate::Pallet as SnowbridgeControl; use frame_benchmarking::v2::*; use frame_system::RawOrigin; -use snowbridge_core::eth; +use snowbridge_core::{eth, sibling_sovereign_account}; use snowbridge_outbound_queue_primitives::OperatingMode; use sp_runtime::SaturatedConversion; use xcm::prelude::*; +#[allow(clippy::result_large_err)] +fn fund_sovereign_account(para_id: ParaId) -> Result<(), BenchmarkError> { + let amount: BalanceOf = (10_000_000_000_000_u64).saturated_into::().saturated_into(); + let sovereign_account = sibling_sovereign_account::(para_id); + T::Token::mint_into(&sovereign_account, amount)?; + Ok(()) +} + #[benchmarks] mod benchmarks { use super::*; @@ -53,6 +61,43 @@ mod benchmarks { Ok(()) } + #[benchmark] + fn force_update_channel() -> Result<(), BenchmarkError> { + let origin_para_id = 2000; + let origin_location = Location::new(1, [Parachain(origin_para_id)]); + let _origin = T::Helper::make_xcm_origin(origin_location.clone()); + let _channel_id: ChannelId = ParaId::from(origin_para_id).into(); + + fund_sovereign_account::(origin_para_id.into())?; + let (para_id, agent_id) = ensure_sibling::(&origin_location)?; + Agents::::insert(agent_id, ()); + let channel_id: ChannelId = para_id.into(); + let channel = Channel { agent_id, para_id }; + Channels::::insert(channel_id, channel); + + #[extrinsic_call] + _(RawOrigin::Root, channel_id, OperatingMode::RejectingOutboundMessages); + + Ok(()) + } + + #[benchmark] + fn force_transfer_native_from_agent() -> Result<(), BenchmarkError> { + let origin_para_id = 2000; + let origin_location = Location::new(1, [Parachain(origin_para_id)]); + let _origin = T::Helper::make_xcm_origin(origin_location.clone()); + fund_sovereign_account::(origin_para_id.into())?; + let (_para_id, agent_id) = ensure_sibling::(&origin_location)?; + Agents::::insert(agent_id, ()); + + let versioned_location: VersionedLocation = origin_location.into(); + + #[extrinsic_call] + _(RawOrigin::Root, Box::new(versioned_location), H160::default(), 1); + + Ok(()) + } + #[benchmark] fn set_token_transfer_fees() -> Result<(), BenchmarkError> { #[extrinsic_call] diff --git a/bridges/snowbridge/pallets/system/src/lib.rs b/bridges/snowbridge/pallets/system/src/lib.rs index d277186bd5b9d..ece0c7a5f0874 100644 --- a/bridges/snowbridge/pallets/system/src/lib.rs +++ b/bridges/snowbridge/pallets/system/src/lib.rs @@ -52,7 +52,10 @@ use snowbridge_outbound_queue_primitives::{ }; use sp_core::{RuntimeDebug, H160, H256}; use sp_io::hashing::blake2_256; -use sp_runtime::{traits::MaybeConvert, DispatchError, SaturatedConversion}; +use sp_runtime::{ + traits::{BadOrigin, MaybeConvert}, + DispatchError, SaturatedConversion, +}; use sp_std::prelude::*; use xcm::prelude::*; use xcm_executor::traits::ConvertLocation; @@ -67,6 +70,20 @@ pub type BalanceOf = pub type AccountIdOf = ::AccountId; pub type PricingParametersOf = PricingParametersRecord>; +/// Ensure origin location is a sibling +fn ensure_sibling(location: &Location) -> Result<(ParaId, H256), DispatchError> +where + T: Config, +{ + match location.unpack() { + (1, [Parachain(para_id)]) => { + let agent_id = agent_id_of::(location)?; + Ok(((*para_id).into(), agent_id)) + }, + _ => Err(BadOrigin.into()), + } +} + /// Hash the location to produce an agent id pub fn agent_id_of(location: &Location) -> Result { T::AgentIdOf::convert_location(location).ok_or(Error::::LocationConversionFailed.into()) @@ -333,6 +350,69 @@ pub mod pallet { Ok(()) } + /// Sends a message to the Gateway contract to update an arbitrary channel + /// + /// Fee required: No + /// + /// - `origin`: Must be root + /// - `channel_id`: ID of channel + /// - `mode`: Initial operating mode of the channel + /// - `outbound_fee`: Fee charged to users for sending outbound messages to Polkadot + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::force_update_channel())] + pub fn force_update_channel( + origin: OriginFor, + channel_id: ChannelId, + mode: OperatingMode, + ) -> DispatchResult { + ensure_root(origin)?; + + ensure!(Channels::::contains_key(channel_id), Error::::NoChannel); + + let command = Command::UpdateChannel { channel_id, mode }; + Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::::No)?; + + Self::deposit_event(Event::::UpdateChannel { channel_id, mode }); + Ok(()) + } + + /// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`. + /// + /// Privileged. Can only be called by root. + /// + /// Fee required: No + /// + /// - `origin`: Must be root + /// - `location`: Location used to resolve the agent + /// - `recipient`: Recipient of funds + /// - `amount`: Amount to transfer + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::force_transfer_native_from_agent())] + pub fn force_transfer_native_from_agent( + origin: OriginFor, + location: Box, + recipient: H160, + amount: u128, + ) -> DispatchResult { + ensure_root(origin)?; + + // Ensure that location is some consensus system on a sibling parachain + let location: Location = + (*location).try_into().map_err(|_| Error::::UnsupportedLocationVersion)?; + let (_, agent_id) = + ensure_sibling::(&location).map_err(|_| Error::::InvalidLocation)?; + + let pays_fee = PaysFee::::No; + + Self::do_transfer_native_from_agent( + agent_id, + PRIMARY_GOVERNANCE_CHANNEL, + recipient, + amount, + pays_fee, + ) + } + /// Sends a message to the Gateway contract to update fee related parameters for /// token transfers. /// @@ -433,6 +513,28 @@ pub mod pallet { Ok(()) } + /// Issue a `Command::TransferNativeFromAgent` command. The command will be sent on the + /// channel `channel_id` + pub fn do_transfer_native_from_agent( + agent_id: H256, + channel_id: ChannelId, + recipient: H160, + amount: u128, + pays_fee: PaysFee, + ) -> DispatchResult { + ensure!(Agents::::contains_key(agent_id), Error::::NoAgent); + + let command = Command::TransferNativeFromAgent { agent_id, recipient, amount }; + Self::send(channel_id, command, pays_fee)?; + + Self::deposit_event(Event::::TransferNativeFromAgent { + agent_id, + recipient, + amount, + }); + Ok(()) + } + /// Initializes agents and channels. pub fn initialize(para_id: ParaId, asset_hub_para_id: ParaId) -> Result<(), DispatchError> { // Asset Hub diff --git a/bridges/snowbridge/pallets/system/src/weights.rs b/bridges/snowbridge/pallets/system/src/weights.rs index 7b2513775601e..6d56a6a71cda9 100644 --- a/bridges/snowbridge/pallets/system/src/weights.rs +++ b/bridges/snowbridge/pallets/system/src/weights.rs @@ -37,6 +37,8 @@ pub trait WeightInfo { fn set_token_transfer_fees() -> Weight; fn set_pricing_parameters() -> Weight; fn register_token() -> Weight; + fn force_update_channel() -> Weight; + fn force_transfer_native_from_agent() -> Weight; } // For backwards compatibility and tests. @@ -60,6 +62,25 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + /// Storage: EthereumSystem Channels (r:1 w:0) + /// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: MessageQueue BookStateFor (r:2 w:2) + /// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + /// Storage: MessageQueue ServiceHead (r:1 w:0) + /// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen) + /// Storage: MessageQueue Pages (r:0 w:1) + /// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen) + fn force_update_channel() -> Weight { + // Proof Size summary in bytes: + // Measured: `256` + // Estimated: `6044` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(41_000_000, 6044) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } /// Storage: ParachainInfo ParachainId (r:1 w:0) /// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) @@ -79,6 +100,25 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + /// Storage: EthereumSystem Agents (r:1 w:0) + /// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: MessageQueue BookStateFor (r:2 w:2) + /// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + /// Storage: MessageQueue ServiceHead (r:1 w:0) + /// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen) + /// Storage: MessageQueue Pages (r:0 w:1) + /// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen) + fn force_transfer_native_from_agent() -> Weight { + // Proof Size summary in bytes: + // Measured: `252` + // Estimated: `6044` + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(42_000_000, 6044) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } /// Storage: ParachainInfo ParachainId (r:1 w:0) /// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) diff --git a/bridges/snowbridge/primitives/outbound-queue/src/v1/message.rs b/bridges/snowbridge/primitives/outbound-queue/src/v1/message.rs index c5a1ebb391072..fcc7cf8d5b8b3 100644 --- a/bridges/snowbridge/primitives/outbound-queue/src/v1/message.rs +++ b/bridges/snowbridge/primitives/outbound-queue/src/v1/message.rs @@ -6,7 +6,7 @@ use crate::{OperatingMode, SendError, SendMessageFeeProvider}; use codec::{Decode, DecodeWithMemTracking, Encode}; use ethabi::Token; use scale_info::TypeInfo; -use snowbridge_core::{pricing::UD60x18, ChannelId}; +use snowbridge_core::{pricing::UD60x18, AgentId, ChannelId}; use sp_arithmetic::traits::{BaseArithmetic, Unsigned}; use sp_core::{RuntimeDebug, H160, H256, U256}; use sp_std::{borrow::ToOwned, vec, vec::Vec}; @@ -74,11 +74,15 @@ pub enum Command { /// Optionally invoke an initializer in the implementation contract initializer: Option, }, + /// An UpdateChannel message was sent to the Gateway + UpdateChannel { channel_id: ChannelId, mode: OperatingMode }, /// Set the global operating mode of the Gateway contract SetOperatingMode { /// The new operating mode mode: OperatingMode, }, + /// An TransferNativeFromAgent message was sent to the Gateway + TransferNativeFromAgent { agent_id: AgentId, recipient: H160, amount: u128 }, /// Set token fees of the Gateway contract SetTokenTransferFees { /// The fee(DOT) for the cost of creating asset on AssetHub @@ -136,7 +140,9 @@ impl Command { match self { Command::AgentExecute { .. } => 0, Command::Upgrade { .. } => 1, + Command::UpdateChannel { .. } => 4, Command::SetOperatingMode { .. } => 5, + Command::TransferNativeFromAgent { .. } => 6, Command::SetTokenTransferFees { .. } => 7, Command::SetPricingParameters { .. } => 8, Command::UnlockNativeToken { .. } => 9, @@ -158,8 +164,18 @@ impl Command { Token::FixedBytes(impl_code_hash.as_bytes().to_owned()), initializer.clone().map_or(Token::Bytes(vec![]), |i| Token::Bytes(i.params)), ])]), + Command::UpdateChannel { channel_id, mode } => ethabi::encode(&[Token::Tuple(vec![ + Token::FixedBytes(channel_id.as_ref().to_owned()), + Token::Uint(U256::from((*mode) as u64)), + ])]), Command::SetOperatingMode { mode } => ethabi::encode(&[Token::Tuple(vec![Token::Uint(U256::from((*mode) as u64))])]), + Command::TransferNativeFromAgent { agent_id, recipient, amount } => + ethabi::encode(&[Token::Tuple(vec![ + Token::FixedBytes(agent_id.as_bytes().to_owned()), + Token::Address(*recipient), + Token::Uint(U256::from(*amount)), + ])]), Command::SetTokenTransferFees { create_asset_xcm, transfer_asset_xcm, @@ -360,6 +376,8 @@ impl GasMeter for ConstantGasMeter { // the the initializer is called. 50_000 + initializer_max_gas }, + Command::UpdateChannel { .. } => 50_000, + Command::TransferNativeFromAgent { .. } => 60_000, Command::SetTokenTransferFees { .. } => 60_000, Command::SetPricingParameters { .. } => 60_000, Command::UnlockNativeToken { .. } => 200_000, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs index 1ce2329a97618..a1df98bde966f 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs @@ -70,6 +70,26 @@ impl snowbridge_pallet_system::WeightInfo for WeightInf .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: EthereumSystem Channels (r:1 w:0) + /// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: MessageQueue BookStateFor (r:2 w:2) + /// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + /// Storage: MessageQueue ServiceHead (r:1 w:0) + /// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen) + /// Storage: MessageQueue Pages (r:0 w:1) + /// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen) + fn force_update_channel() -> Weight { + // Proof Size summary in bytes: + // Measured: `256` + // Estimated: `6044` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(41_000_000, 0) + .saturating_add(Weight::from_parts(0, 6044)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } /// Storage: `EthereumSystem::Channels` (r:1 w:0) /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) /// Storage: `EthereumSystem::PricingParameters` (r:1 w:0) @@ -90,6 +110,26 @@ impl snowbridge_pallet_system::WeightInfo for WeightInf .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: EthereumSystem Agents (r:1 w:0) + /// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: MessageQueue BookStateFor (r:2 w:2) + /// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + /// Storage: MessageQueue ServiceHead (r:1 w:0) + /// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen) + /// Storage: MessageQueue Pages (r:0 w:1) + /// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen) + fn force_transfer_native_from_agent() -> Weight { + // Proof Size summary in bytes: + // Measured: `252` + // Estimated: `6044` + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(42_000_000, 0) + .saturating_add(Weight::from_parts(0, 6044)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } /// Storage: `EthereumSystem::Channels` (r:1 w:0) /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs index fd1080de5a794..3513cd92b6918 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs @@ -70,6 +70,26 @@ impl snowbridge_pallet_system::WeightInfo for WeightInf .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: EthereumSystem Channels (r:1 w:0) + /// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: MessageQueue BookStateFor (r:2 w:2) + /// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + /// Storage: MessageQueue ServiceHead (r:1 w:0) + /// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen) + /// Storage: MessageQueue Pages (r:0 w:1) + /// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen) + fn force_update_channel() -> Weight { + // Proof Size summary in bytes: + // Measured: `256` + // Estimated: `6044` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(41_000_000, 0) + .saturating_add(Weight::from_parts(0, 6044)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } /// Storage: `EthereumSystem::Channels` (r:1 w:0) /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) /// Storage: `EthereumSystem::PricingParameters` (r:1 w:0) @@ -90,6 +110,26 @@ impl snowbridge_pallet_system::WeightInfo for WeightInf .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: EthereumSystem Agents (r:1 w:0) + /// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: MessageQueue BookStateFor (r:2 w:2) + /// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + /// Storage: MessageQueue ServiceHead (r:1 w:0) + /// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen) + /// Storage: MessageQueue Pages (r:0 w:1) + /// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen) + fn force_transfer_native_from_agent() -> Weight { + // Proof Size summary in bytes: + // Measured: `252` + // Estimated: `6044` + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(42_000_000, 0) + .saturating_add(Weight::from_parts(0, 6044)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } /// Storage: `EthereumSystem::Channels` (r:1 w:0) /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) From bb579b8dcad86865bc9c17331d4dddc27ac3be8f Mon Sep 17 00:00:00 2001 From: girazoki Date: Wed, 1 Oct 2025 10:49:15 +0200 Subject: [PATCH 08/11] change back to have a consistent location to token id --- .../snowbridge/pallets/system-v2/src/lib.rs | 14 ++++++-- .../snowbridge/pallets/system-v2/src/tests.rs | 5 +++ bridges/snowbridge/pallets/system/src/lib.rs | 17 ++++++++- .../outbound-queue/src/v1/converter/mod.rs | 12 +++---- .../outbound-queue/src/v1/converter/tests.rs | 7 ++-- .../src/v2/converter/convert.rs | 9 +++-- .../outbound-queue/src/v2/converter/mod.rs | 4 +-- .../outbound-queue/src/v2/converter/tests.rs | 7 ++-- prdoc/pr_8106.prdoc | 36 +++++++++++++++++++ 9 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 prdoc/pr_8106.prdoc diff --git a/bridges/snowbridge/pallets/system-v2/src/lib.rs b/bridges/snowbridge/pallets/system-v2/src/lib.rs index 095b263f8f257..9f7bfccbfb609 100644 --- a/bridges/snowbridge/pallets/system-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/system-v2/src/lib.rs @@ -43,10 +43,10 @@ use snowbridge_outbound_queue_primitives::{ v2::{Command, Initializer, Message, SendMessage}, OperatingMode, SendError, }; -use snowbridge_pallet_system::ForeignToNativeId; +use snowbridge_pallet_system::{ForeignToNativeId, NativeToForeignId}; use sp_core::{H160, H256}; use sp_io::hashing::blake2_256; -use sp_runtime::traits::MaybeConvert; +use sp_runtime::traits::{MaybeConvert, MaybeEquivalence}; use sp_std::prelude::*; use xcm::prelude::*; use xcm_executor::traits::ConvertLocation; @@ -227,6 +227,7 @@ pub mod pallet { .ok_or(Error::::LocationConversionFailed)?; if !ForeignToNativeId::::contains_key(token_id) { + NativeToForeignId::::insert(location.clone(), token_id); ForeignToNativeId::::insert(token_id, location.clone()); } @@ -318,4 +319,13 @@ pub mod pallet { snowbridge_pallet_system::Pallet::::maybe_convert(foreign_id) } } + + impl MaybeEquivalence for Pallet { + fn convert(foreign_id: &TokenId) -> Option { + snowbridge_pallet_system::Pallet::::convert(foreign_id) + } + fn convert_back(location: &Location) -> Option { + snowbridge_pallet_system::Pallet::::convert_back(location) + } + } } diff --git a/bridges/snowbridge/pallets/system-v2/src/tests.rs b/bridges/snowbridge/pallets/system-v2/src/tests.rs index 2475dfa67c399..d92a7aa249c78 100644 --- a/bridges/snowbridge/pallets/system-v2/src/tests.rs +++ b/bridges/snowbridge/pallets/system-v2/src/tests.rs @@ -138,6 +138,11 @@ fn register_all_tokens_succeeds() { let foreign_token_id = EthereumSystemV2::location_to_message_origin(tc.native.clone()).unwrap(); + assert_eq!( + NativeToForeignId::::get(reanchored_location.clone()), + Some(foreign_token_id) + ); + assert_eq!( ForeignToNativeId::::get(foreign_token_id), Some(reanchored_location.clone()) diff --git a/bridges/snowbridge/pallets/system/src/lib.rs b/bridges/snowbridge/pallets/system/src/lib.rs index ece0c7a5f0874..42e7f29935495 100644 --- a/bridges/snowbridge/pallets/system/src/lib.rs +++ b/bridges/snowbridge/pallets/system/src/lib.rs @@ -53,7 +53,7 @@ use snowbridge_outbound_queue_primitives::{ use sp_core::{RuntimeDebug, H160, H256}; use sp_io::hashing::blake2_256; use sp_runtime::{ - traits::{BadOrigin, MaybeConvert}, + traits::{BadOrigin, MaybeConvert, MaybeEquivalence}, DispatchError, SaturatedConversion, }; use sp_std::prelude::*; @@ -250,6 +250,11 @@ pub mod pallet { pub type ForeignToNativeId = StorageMap<_, Blake2_128Concat, TokenId, Location, OptionQuery>; + /// Lookup table for native location relative to ethereum to foreign token ID + #[pallet::storage] + pub type NativeToForeignId = + StorageMap<_, Blake2_128Concat, Location, TokenId, OptionQuery>; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -591,6 +596,7 @@ pub mod pallet { .ok_or(Error::::LocationConversionFailed)?; if !ForeignToNativeId::::contains_key(token_id) { + NativeToForeignId::::insert(location.clone(), token_id); ForeignToNativeId::::insert(token_id, location.clone()); } @@ -636,4 +642,13 @@ pub mod pallet { ForeignToNativeId::::get(foreign_id) } } + + impl MaybeEquivalence for Pallet { + fn convert(foreign_id: &TokenId) -> Option { + ForeignToNativeId::::get(foreign_id) + } + fn convert_back(location: &Location) -> Option { + NativeToForeignId::::get(location) + } + } } diff --git a/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/mod.rs b/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/mod.rs index 31a9f223d495e..7603213ac3c1b 100644 --- a/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/mod.rs +++ b/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/mod.rs @@ -11,9 +11,9 @@ use codec::{Decode, Encode}; use super::message::{Command, Message, SendMessage}; use frame_support::{ensure, traits::Get}; -use snowbridge_core::{AgentId, ChannelId, ParaId, TokenId, TokenIdOf}; +use snowbridge_core::{AgentId, ChannelId, ParaId, TokenId}; use sp_core::{H160, H256}; -use sp_runtime::traits::MaybeConvert; +use sp_runtime::traits::MaybeEquivalence; use sp_std::{iter::Peekable, marker::PhantomData, prelude::*}; use xcm::prelude::*; use xcm_executor::traits::{ConvertLocation, ExportXcm}; @@ -48,7 +48,7 @@ where EthereumNetwork: Get, OutboundQueue: SendMessage, AgentHashedDescription: ConvertLocation, - ConvertAssetId: MaybeConvert, + ConvertAssetId: MaybeEquivalence, { type Ticket = (Vec, XcmHash); @@ -194,7 +194,7 @@ struct XcmConverter<'a, ConvertAssetId, Call> { } impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call> where - ConvertAssetId: MaybeConvert, + ConvertAssetId: MaybeEquivalence, { fn new(message: &'a Xcm, ethereum_network: NetworkId, agent_id: AgentId) -> Self { Self { @@ -411,9 +411,7 @@ where // transfer amount must be greater than 0. ensure!(amount > 0, ZeroAssetTransfer); - let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?; - - ConvertAssetId::maybe_convert(token_id).ok_or(InvalidAsset)?; + let token_id = ConvertAssetId::convert_back(&asset_id).ok_or(InvalidAsset)?; // Check if there is a SetTopic and skip over it if found. let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?; diff --git a/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/tests.rs b/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/tests.rs index 9ce5bb8b29d30..158c4604a9435 100644 --- a/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/tests.rs +++ b/bridges/snowbridge/primitives/outbound-queue/src/v1/converter/tests.rs @@ -62,10 +62,13 @@ impl SendMessageFeeProvider for MockErrOutboundQueue { } pub struct MockTokenIdConvert; -impl MaybeConvert for MockTokenIdConvert { - fn maybe_convert(_id: TokenId) -> Option { +impl MaybeEquivalence for MockTokenIdConvert { + fn convert(_id: &TokenId) -> Option { Some(Location::new(1, [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH))])) } + fn convert_back(_loc: &Location) -> Option { + Some(1) + } } #[test] diff --git a/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/convert.rs b/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/convert.rs index f64554e42756a..06f9f1c63057d 100644 --- a/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/convert.rs +++ b/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/convert.rs @@ -5,7 +5,7 @@ use codec::DecodeAll; use core::slice::Iter; use frame_support::{ensure, BoundedVec}; -use snowbridge_core::{AgentIdOf, TokenId, TokenIdOf}; +use snowbridge_core::{AgentIdOf, TokenId}; use crate::v2::{ message::{Command, Message}, @@ -14,7 +14,7 @@ use crate::v2::{ use crate::v2::convert::XcmConverterError::{AssetResolutionFailed, FilterDoesNotConsumeAllAssets}; use sp_core::H160; -use sp_runtime::traits::MaybeConvert; +use sp_runtime::traits::MaybeEquivalence; use sp_std::{iter::Peekable, marker::PhantomData, prelude::*}; use xcm::prelude::*; use xcm_executor::traits::ConvertLocation; @@ -64,7 +64,7 @@ pub struct XcmConverter<'a, ConvertAssetId, Call> { } impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call> where - ConvertAssetId: MaybeConvert, + ConvertAssetId: MaybeEquivalence, { pub fn new(message: &'a Xcm, ethereum_network: NetworkId) -> Self { Self { @@ -173,8 +173,7 @@ where ensure!(amount > 0, ZeroAssetTransfer); // Ensure PNA already registered - let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?; - ConvertAssetId::maybe_convert(token_id).ok_or(InvalidAsset)?; + let token_id = ConvertAssetId::convert_back(&asset_id).ok_or(InvalidAsset)?; commands.push(Command::MintForeignToken { token_id, recipient, amount }); } diff --git a/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/mod.rs b/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/mod.rs index 0c856aa4f31d4..8b6f81e558d43 100644 --- a/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/mod.rs +++ b/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/mod.rs @@ -15,7 +15,7 @@ use frame_support::{ traits::{Contains, Get, ProcessMessageError}, }; use snowbridge_core::{ParaId, TokenId}; -use sp_runtime::traits::MaybeConvert; +use sp_runtime::traits::MaybeEquivalence; use sp_std::{marker::PhantomData, ops::ControlFlow, prelude::*}; use xcm::prelude::*; use xcm_builder::{CreateMatcher, ExporterFor, MatchXcm}; @@ -53,7 +53,7 @@ where UniversalLocation: Get, EthereumNetwork: Get, OutboundQueue: SendMessage, - ConvertAssetId: MaybeConvert, + ConvertAssetId: MaybeEquivalence, AssetHubParaId: Get, { type Ticket = (Vec, XcmHash); diff --git a/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/tests.rs b/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/tests.rs index bb5a88c73c809..e5baa10b90d83 100644 --- a/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/tests.rs +++ b/bridges/snowbridge/primitives/outbound-queue/src/v2/converter/tests.rs @@ -62,10 +62,13 @@ impl SendMessageFeeProvider for MockErrOutboundQueue { } pub struct MockTokenIdConvert; -impl MaybeConvert for MockTokenIdConvert { - fn maybe_convert(_id: TokenId) -> Option { +impl MaybeEquivalence for MockTokenIdConvert { + fn convert(_id: &TokenId) -> Option { Some(Location::new(1, [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH))])) } + fn convert_back(_loc: &Location) -> Option { + Some(1) + } } #[test] diff --git a/prdoc/pr_8106.prdoc b/prdoc/pr_8106.prdoc new file mode 100644 index 0000000000000..c4ee2ec78fd55 --- /dev/null +++ b/prdoc/pr_8106.prdoc @@ -0,0 +1,36 @@ +title: 'Snowbridge V2: Add generic AggregateMessageOrigin' +doc: +- audience: Runtime Dev + description: |- + # Description + + This PR introduces a generic `AggregateMessageOrigin` in Snowbridge V2 to provide a more flexible mechanism for message-queue origin recognition and handling. This approach aims to support broader use cases beyond parachains. By enabling custom origins, scenarios like solo chains, can build specialized message routing and processing making use of this configuration. + + ## Motivation + + - **Enhanced Integration**: While Snowbridge traditionally focuses on bridging parachains, there is also a need to send messages from various substrate-based chains (e.g., solo chains) and in different contexts. This change expands the protocol's ability to handle multiple, distinct origin types. + + - **Custom Command Structures**: Some projects need to incorporate custom Snowbridge commands or message structures, which cannot be processed by the default outbound queue. By introducing a more generic origin, these commands can be cleanly differentiated and securely processed. + + - **Flexibility**: Parachains, solo chains, or any environment running Snowbridge benefit from a uniform pattern to handle both common and custom message processing. + + ## Use Case + + Consider a scenario where you need to send messages to Ethereum directly from your runtime: + + - You have your own set of commands or data structures different from those provided by the traditional Snowbridge outbound queue. + - You want to introduce a custom message processor that specifically handles your commands. + - You need a distinct origin to differentiate your messages from standard Snowbridge traffic. + + By using the `AggregateMessageOrigin`, you can verify whether a message comes from a parachain, from Snowbridge itself, or from a custom origin. This is especially relevant in solo chain contexts, where you might not rely on the parachain assumptions but still want to leverage a robust bridging framework. +crates: +- name: snowbridge-pallet-outbound-queue-v2 + bump: major +- name: bridge-hub-westend-runtime + bump: minor +- name: bridge-hub-common + bump: major +- name: snowbridge-core + bump: minor +- name: snowbridge-pallet-outbound-queue + bump: patch From 9bc7c7ed860fe4bf13e1bce1f0e36d74dcc3ca26 Mon Sep 17 00:00:00 2001 From: girazoki Date: Mon, 3 Nov 2025 14:12:41 +0100 Subject: [PATCH 09/11] https://github.com/paritytech/polkadot-sdk/pull/8175 by agus --- .../pallets/inbound-queue-v2/src/lib.rs | 70 ++--------- .../pallets/inbound-queue-v2/src/mock.rs | 73 +++++++++--- .../pallets/inbound-queue-v2/src/test.rs | 11 +- .../primitives/inbound-queue/Cargo.toml | 4 +- .../inbound-queue/src/v2/converter.rs | 22 ++-- .../inbound-queue/src/v2/message.rs | 36 +++--- .../primitives/inbound-queue/src/v2/mod.rs | 2 + .../inbound-queue/src/v2/processor.rs | 110 ++++++++++++++++++ .../primitives/inbound-queue/src/v2/traits.rs | 59 +++++++++- .../src/tests/snowbridge_v2_inbound.rs | 18 +-- .../tests/snowbridge_v2_inbound_to_rococo.rs | 8 +- .../src/bridge_to_ethereum_config.rs | 80 +++++++++---- 12 files changed, 345 insertions(+), 148 deletions(-) create mode 100644 bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs index b82139bf5629f..8ab4c667d8736 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs @@ -44,13 +44,12 @@ use snowbridge_core::{ BasicOperatingMode, }; use snowbridge_inbound_queue_primitives::{ - v2::{ConvertMessage, ConvertMessageError, Message}, + v2::{ConvertMessageError, Message, MessageProcessor, MessageProcessorError}, EventProof, VerificationError, Verifier, }; use sp_core::H160; -use sp_runtime::traits::TryConvert; use sp_std::prelude::*; -use xcm::prelude::{ExecuteXcm, Junction::*, Location, SendXcm, *}; +use xcm::latest::SendError; pub use pallet::*; @@ -84,17 +83,11 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The verifier for inbound messages from Ethereum. type Verifier: Verifier; - /// XCM message sender. - type XcmSender: SendXcm; - /// Handler for XCM fees. - type XcmExecutor: ExecuteXcm; /// Address of the Gateway contract. #[pallet::constant] type GatewayAddress: Get; - /// AssetHub parachain ID. - type AssetHubParaId: Get; - /// Convert a command from Ethereum to an XCM message. - type MessageConverter: ConvertMessage; + /// Process the message that was submitted. + type MessageProcessor: MessageProcessor; #[cfg(feature = "runtime-benchmarks")] type Helper: BenchmarkHelper; /// Reward discriminator type. @@ -104,8 +97,6 @@ pub mod pallet { type DefaultRewardKind: Get; /// Relayer reward payment. type RewardPayment: RewardLedger; - /// AccountId to Location converter - type AccountToLocation: for<'a> TryConvert<&'a Self::AccountId, Location>; type WeightInfo: WeightInfo; } @@ -127,22 +118,10 @@ pub mod pallet { pub enum Error { /// Message came from an invalid outbound channel on the Ethereum side. InvalidGateway, - /// Account could not be converted to bytes - InvalidAccount, /// Message has an invalid envelope. InvalidMessage, /// Message has an unexpected nonce. InvalidNonce, - /// Fee provided is invalid. - InvalidFee, - /// Message has an invalid payload. - InvalidPayload, - /// Message channel is invalid - InvalidChannel, - /// The max nonce for the type has been reached - MaxNonceReached, - /// Cannot convert location - InvalidAccountConversion, /// Invalid network specified InvalidNetwork, /// Pallet is halted @@ -242,19 +221,14 @@ pub mod pallet { // Verify the message has not been processed ensure!(!Nonce::::get(nonce), Error::::InvalidNonce); - let xcm = - T::MessageConverter::convert(message).map_err(|error| Error::::from(error))?; - - // Forward XCM to AH - let dest = Location::new(1, [Parachain(T::AssetHubParaId::get())]); - // Mark message as received Nonce::::set(nonce); - let message_id = - Self::send_xcm(dest.clone(), &relayer, xcm.clone()).map_err(|error| { - tracing::error!(target: LOG_TARGET, ?error, ?dest, ?xcm, "XCM send failed with error"); - Error::::from(error) + let message_id = T::MessageProcessor::process_message(relayer.clone(), message) + .map_err(|e| match e { + MessageProcessorError::ProcessMessage(e) => e, + MessageProcessorError::ConvertMessage(e) => Error::::from(e).into(), + MessageProcessorError::SendMessage(e) => Error::::from(e).into(), })?; // Pay relayer reward @@ -264,35 +238,11 @@ pub mod pallet { T::RewardPayment::register_reward(&relayer, T::DefaultRewardKind::get(), total_tip); } + // Emit event with the message_id Self::deposit_event(Event::MessageReceived { nonce, message_id }); Ok(()) } - - fn send_xcm( - dest: Location, - fee_payer: &T::AccountId, - xcm: Xcm<()>, - ) -> Result { - let (ticket, fee) = validate_send::(dest, xcm)?; - let fee_payer = T::AccountToLocation::try_convert(fee_payer).map_err(|err| { - tracing::error!( - target: LOG_TARGET, - ?err, - "Failed to convert account to XCM location", - ); - SendError::NotApplicable - })?; - T::XcmExecutor::charge_fees(fee_payer.clone(), fee.clone()).map_err(|error| { - tracing::error!( - target: LOG_TARGET, - ?error, - "Charging fees failed with error", - ); - SendError::Fees - })?; - T::XcmSender::deliver(ticket) - } } impl AddTip for Pallet { diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs index 0da332f59e347..f08bafa159349 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs @@ -3,19 +3,19 @@ use super::*; use crate::{self as inbound_queue_v2}; -use frame_support::{derive_impl, parameter_types, traits::ConstU32}; +use frame_support::{derive_impl, parameter_types}; use hex_literal::hex; use snowbridge_beacon_primitives::{ types::deneb, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader, }; use snowbridge_core::{ParaId, TokenId}; use snowbridge_inbound_queue_primitives::{ - v2::{CreateAssetCallInfo, MessageToXcm}, + v2::{CreateAssetCallInfo, MessageProcessorError, MessageToXcm, XcmMessageProcessor}, Log, Proof, VerificationError, }; use sp_core::H160; use sp_runtime::{ - traits::{IdentityLookup, MaybeConvert}, + traits::{IdentityLookup, MaybeConvert, TryConvert}, BuildStorage, }; use sp_std::{convert::From, default::Default, marker::PhantomData}; @@ -116,29 +116,68 @@ parameter_types! { set_reserves_call: SetReservesCallIndex::get(), }; pub AssetHubParaId: ParaId = ParaId::from(1000); + pub TargetLocation: Location = Location::new(1, [Parachain(AssetHubParaId::get().into())]); +} + +pub struct DummyPrefix; + +impl MessageProcessor for DummyPrefix { + fn can_process_message(_relayer: &AccountId, _message: &Message) -> bool { + false + } + + fn process_message( + _relayer: AccountId, + _message: Message, + ) -> Result<[u8; 32], MessageProcessorError> { + panic!("DummyPrefix::process_message shouldn't be called"); + } +} + +pub struct DummySuffix; + +impl MessageProcessor for DummySuffix { + fn can_process_message(_relayer: &AccountId, _message: &Message) -> bool { + true + } + + fn process_message( + _relayer: AccountId, + _message: Message, + ) -> Result<[u8; 32], MessageProcessorError> { + panic!("DummySuffix::process_message shouldn't be called"); + } } impl inbound_queue_v2::Config for Test { type RuntimeEvent = RuntimeEvent; type Verifier = MockVerifier; - type XcmSender = MockXcmSender; - type XcmExecutor = MockXcmExecutor; type GatewayAddress = GatewayAddress; - type AssetHubParaId = ConstU32<1000>; - type MessageConverter = MessageToXcm< - CreateAssetCall, - EthereumNetwork, - LocalNetwork, - GatewayAddress, - InboundQueueLocation, - AssetHubParaId, - MockTokenIdConvert, - AccountId, - >; + // Passively test that the implementation of MessageProcessor trait works correctly for tuple + type MessageProcessor = ( + DummyPrefix, + XcmMessageProcessor< + Test, + MockXcmSender, + MockXcmExecutor, + MessageToXcm< + CreateAssetCall, + EthereumNetwork, + LocalNetwork, + GatewayAddress, + InboundQueueLocation, + AssetHubParaId, + MockTokenIdConvert, + AccountId, + >, + MockAccountLocationConverter, + TargetLocation, + >, + DummySuffix, + ); #[cfg(feature = "runtime-benchmarks")] type Helper = Test; type WeightInfo = (); - type AccountToLocation = MockAccountLocationConverter; type RewardKind = BridgeReward; type DefaultRewardKind = SnowbridgeReward; type RewardPayment = MockRewardLedger; diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs index 65b084ea11c28..fe08daeaba803 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs @@ -5,13 +5,14 @@ use super::*; use crate::{mock::*, Error}; use codec::Encode; use frame_support::{assert_noop, assert_ok}; -use snowbridge_inbound_queue_primitives::{v2::XcmPayload, EventProof, Proof}; +use snowbridge_inbound_queue_primitives::{v2::Payload, EventProof, Proof}; use snowbridge_test_utils::{ mock_rewards::{RegisteredRewardAmount, RegisteredRewardsCount}, mock_xcm::{set_charge_fees_override, set_sender_override}, }; use sp_keyring::sr25519::Keyring; use sp_runtime::DispatchError; +use xcm::prelude::*; #[test] fn test_submit_happy_path() { @@ -334,7 +335,7 @@ fn zero_reward_does_not_register_reward() { Message { nonce: 0, assets: vec![], - xcm: XcmPayload::Raw(vec![]), + payload: Payload::Raw(vec![]), claimer: None, execution_fee: 1_000_000_000, relayer_fee: 0, @@ -408,7 +409,7 @@ fn inbound_tip_is_paid_out_to_relayer() { Message { nonce, assets: vec![], - xcm: XcmPayload::Raw(vec![]), + payload: Payload::Raw(vec![]), claimer: None, execution_fee: 1_000_000_000, relayer_fee, @@ -453,7 +454,7 @@ fn relayer_fee_paid_out_when_no_tip_exists() { Message { nonce, assets: vec![], - xcm: XcmPayload::Raw(vec![]), + payload: Payload::Raw(vec![]), claimer: None, execution_fee: 1_000_000_000, relayer_fee, @@ -499,7 +500,7 @@ fn tip_paid_out_when_no_relayer_fee() { Message { nonce, assets: vec![], - xcm: XcmPayload::Raw(vec![]), + payload: Payload::Raw(vec![]), claimer: None, execution_fee: 1_000_000_000, relayer_fee: 0, diff --git a/bridges/snowbridge/primitives/inbound-queue/Cargo.toml b/bridges/snowbridge/primitives/inbound-queue/Cargo.toml index 44591c21f1ba2..b367cf7a0f1ba 100644 --- a/bridges/snowbridge/primitives/inbound-queue/Cargo.toml +++ b/bridges/snowbridge/primitives/inbound-queue/Cargo.toml @@ -15,15 +15,15 @@ workspace = true exclude-from-umbrella = true [dependencies] +alloy-core = { workspace = true, features = ["sol-types"] } alloy-primitives-042 = { features = ["rlp"], workspace = true } alloy-sol-types = { workspace = true } -alloy-core = { workspace = true, features = ["sol-types"] } assets-common.workspace = true codec = { workspace = true } frame-support.workspace = true frame-system.workspace = true -scale-info = { features = ["derive"], workspace = true } impl-trait-for-tuples = { workspace = true } +scale-info = { features = ["derive"], workspace = true } snowbridge-beacon-primitives.workspace = true snowbridge-core.workspace = true snowbridge-verification-primitives.workspace = true diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/converter.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/converter.rs index c96f3b8e63063..ff67ac2956c1f 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v2/converter.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/converter.rs @@ -136,9 +136,9 @@ where Location::new(0, [AccountId32 { network: None, id: bridge_owner.clone().into() }]) }); - let mut remote_xcm: Xcm<()> = match &message.xcm { - XcmPayload::Raw(raw) => Self::decode_raw_xcm(raw), - XcmPayload::CreateAsset { token, network } => Self::make_create_asset_xcm( + let mut remote_xcm: Xcm<()> = match &message.payload { + Payload::Raw(raw) => Self::decode_raw_xcm(raw), + Payload::CreateAsset { token, network } => Self::make_create_asset_xcm( token, *network, message.value, @@ -522,7 +522,7 @@ mod tests { nonce: 0, origin, assets, - xcm: XcmPayload::Raw(versioned_xcm.encode()), + payload: Payload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, @@ -659,7 +659,7 @@ mod tests { nonce: 0, origin, assets, - xcm: XcmPayload::Raw(versioned_xcm.encode()), + payload: Payload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, @@ -711,7 +711,7 @@ mod tests { nonce: 0, origin, assets, - xcm: XcmPayload::Raw(versioned_xcm.encode()), + payload: Payload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, @@ -748,7 +748,7 @@ mod tests { nonce: 0, origin, assets, - xcm: XcmPayload::Raw(versioned_xcm.encode()), + payload: Payload::Raw(versioned_xcm.encode()), claimer, value, execution_fee, @@ -817,7 +817,7 @@ mod tests { nonce: 0, origin, assets, - xcm: XcmPayload::Raw(versioned_xcm), + payload: Payload::Raw(versioned_xcm), claimer: Some(claimer.encode()), value, execution_fee, @@ -853,7 +853,7 @@ mod tests { nonce: 0, origin, assets: vec![], - xcm: XcmPayload::Raw(versioned_xcm.encode()), + payload: Payload::Raw(versioned_xcm.encode()), claimer: None, value, execution_fee, @@ -890,7 +890,7 @@ mod tests { nonce: 0, origin, assets: vec![], - xcm: XcmPayload::Raw(vec![]), + payload: Payload::Raw(vec![]), claimer: None, value, execution_fee, @@ -931,7 +931,7 @@ mod tests { nonce: 0, origin, assets: vec![], - xcm: XcmPayload::Raw(versioned_xcm.encode()), + payload: Payload::Raw(versioned_xcm.encode()), claimer: None, value, execution_fee, diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/message.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/message.rs index 177f62ab0ef58..9b78acad87f41 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v2/message.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/message.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! Converts messages from Ethereum to XCM messages -use crate::{v2::IGatewayV2::Payload, Log}; +use crate::{v2::IGatewayV2::Payload as GatewayV2Payload, Log}; use alloy_core::{ primitives::B256, sol, @@ -81,8 +81,8 @@ impl core::fmt::Debug for IGatewayV2::Xcm { } #[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] -pub enum XcmPayload { - /// Represents raw XCM bytes +pub enum Payload { + /// Raw bytes payload. Commonly used to represent raw XCM bytes Raw(Vec), /// A token registration template CreateAsset { token: H160, network: Network }, @@ -108,7 +108,7 @@ pub struct Message { /// The assets sent from Ethereum (ERC-20s). pub assets: Vec, /// The command originating from the Gateway contract. - pub xcm: XcmPayload, + pub payload: Payload, /// The claimer in the case that funds get trapped. Expected to be an XCM::v5::Location. pub claimer: Option>, /// Native ether bridged over from Ethereum @@ -152,27 +152,27 @@ impl TryFrom<&Log> for Message { let event = IGatewayV2::OutboundMessageAccepted::decode_raw_log_validate(topics, &log.data) .map_err(|_| MessageDecodeError)?; - let payload = event.payload; + let event_payload = event.payload; - let substrate_assets = Self::extract_assets(&payload)?; + let substrate_assets = Self::extract_assets(&event_payload)?; - let xcm = XcmPayload::try_from(&payload)?; + let message_payload = Payload::try_from(&event_payload)?; let mut claimer = None; - if payload.claimer.len() > 0 { - claimer = Some(payload.claimer.to_vec()); + if event_payload.claimer.len() > 0 { + claimer = Some(event_payload.claimer.to_vec()); } let message = Message { gateway: log.address, nonce: event.nonce, - origin: H160::from(payload.origin.as_ref()), + origin: H160::from(event_payload.origin.as_ref()), assets: substrate_assets, - xcm, + payload: message_payload, claimer, - value: payload.value, - execution_fee: payload.executionFee, - relayer_fee: payload.relayerFee, + value: event_payload.value, + execution_fee: event_payload.executionFee, + relayer_fee: event_payload.relayerFee, }; Ok(message) @@ -191,12 +191,12 @@ impl Message { } } -impl TryFrom<&IGatewayV2::Payload> for XcmPayload { +impl TryFrom<&IGatewayV2::Payload> for Payload { type Error = MessageDecodeError; - fn try_from(payload: &Payload) -> Result { + fn try_from(payload: &GatewayV2Payload) -> Result { let xcm = match payload.xcm.kind { - 0 => XcmPayload::Raw(payload.xcm.data.to_vec()), + 0 => Payload::Raw(payload.xcm.data.to_vec()), 1 => { let create_asset = IGatewayV2::XcmCreateAsset::abi_decode_validate(&payload.xcm.data) @@ -206,7 +206,7 @@ impl TryFrom<&IGatewayV2::Payload> for XcmPayload { 0 => Network::Polkadot, _ => return Err(MessageDecodeError), }; - XcmPayload::CreateAsset { token: H160::from(create_asset.token.as_ref()), network } + Payload::CreateAsset { token: H160::from(create_asset.token.as_ref()), network } }, _ => return Err(MessageDecodeError), }; diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/mod.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/mod.rs index 98207c7944a5e..b4784eec91c8b 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v2/mod.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/mod.rs @@ -4,10 +4,12 @@ pub mod converter; pub mod message; +pub mod processor; pub mod traits; pub use converter::*; pub use message::*; +pub use processor::*; pub use traits::*; const LOG_TARGET: &str = "snowbridge-inbound-queue-primitives"; diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs new file mode 100644 index 0000000000000..926912f06a66f --- /dev/null +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Message processor for inbound queue v2 + +use super::*; +use frame_support::traits::Get; +use sp_runtime::traits::TryConvert; +use sp_std::marker::PhantomData; +use xcm::prelude::*; + +/// A message processor that converts messages to XCM and forwards them to AssetHub +/// Generic parameters: T = pallet Config, Sender = XCM sender, Executor = fee handler, +/// Converter = message converter, AccountToLocation = account-to-location converter +pub struct XcmMessageProcessor( + pub PhantomData<(T, Sender, Executor, Converter, AccountToLocation, TargetLocation)>, +); + +impl + MessageProcessor + for XcmMessageProcessor +where + T: frame_system::Config, + Sender: SendXcm, + Executor: ExecuteXcm, + Converter: ConvertMessage, + AccountToLocation: for<'a> TryConvert<&'a AccountId, Location>, + TargetLocation: Get, +{ + fn can_process_message(_relayer: &AccountId, _message: &Message) -> bool { + true + } + + fn process_message( + relayer: AccountId, + message: Message, + ) -> Result<[u8; 32], MessageProcessorError> { + // Process the message and return its ID + let id = Self::process_xcm(relayer, message)?; + Ok(id) + } +} + +impl + XcmMessageProcessor +where + T: frame_system::Config, + Sender: SendXcm, + Executor: ExecuteXcm, + Converter: ConvertMessage, + AccountToLocation: for<'a> TryConvert<&'a T::AccountId, Location>, + TargetLocation: Get, +{ + /// Process a message and return the message ID + pub fn process_xcm( + who: T::AccountId, + message: Message, + ) -> Result { + // Convert the message to XCM + let xcm = Converter::convert(message).map_err(|error| { + tracing::error!(target: LOG_TARGET, ?error, "XCM conversion failed with error"); + MessageProcessorError::ConvertMessage(error) + })?; + + // Forward XCM to a target location + let dest = TargetLocation::get(); + let message_id = Self::send_xcm(dest.clone(), &who, xcm.clone()).map_err(|error| { + tracing::error!(target: LOG_TARGET, ?error, ?dest, ?xcm, "XCM send failed with error"); + MessageProcessorError::SendMessage(error) + })?; + + // Return the message_id + Ok(message_id) + } +} + +impl + XcmMessageProcessor +where + T: frame_system::Config, + Sender: SendXcm, + Executor: ExecuteXcm, + Converter: ConvertMessage, + AccountToLocation: for<'a> TryConvert<&'a T::AccountId, Location>, + TargetLocation: Get, +{ + fn send_xcm( + dest: Location, + fee_payer: &T::AccountId, + xcm: Xcm<()>, + ) -> Result { + let fee_payer = AccountToLocation::try_convert(fee_payer).map_err(|err| { + tracing::error!( + target: LOG_TARGET, + ?err, + "Failed to convert account to XCM location", + ); + SendError::NotApplicable + })?; + let (ticket, fee) = validate_send::(dest, xcm)?; + Executor::charge_fees(fee_payer, fee).map_err(|error| { + tracing::error!( + target: LOG_TARGET, + ?error, + "Charging fees failed with error", + ); + SendError::Fees + })?; + Sender::deliver(ticket) + } +} diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs index 3ad07bd0d6f4f..c9e26b05ec1dd 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs @@ -3,7 +3,8 @@ // SPDX-FileCopyrightText: 2021-2025 Parity Technologies (UK) Ltd. use super::Message; use sp_core::RuntimeDebug; -use xcm::latest::Xcm; +use sp_runtime::DispatchError; +use xcm::latest::{SendError, Xcm}; /// Converts an inbound message from Ethereum to an XCM message that can be /// executed on a parachain. @@ -21,3 +22,59 @@ pub enum ConvertMessageError { /// Invalid network specified (not from Ethereum) InvalidNetwork, } + +/// Reason why a message processor failed. +#[derive(Clone, RuntimeDebug, PartialEq)] +pub enum MessageProcessorError { + /// Message processing failed. + ProcessMessage(DispatchError), + /// Message conversion failed. + ConvertMessage(ConvertMessageError), + /// Message sending failed. + SendMessage(SendError), +} + +/// Trait to define the logic for checking and processing inbound messages. +pub trait MessageProcessor { + /// Lightweight function to check if this processor can handle the message + fn can_process_message(relayer: &AccountId, message: &Message) -> bool; + /// Process the message and return the message ID + fn process_message( + relayer: AccountId, + message: Message, + ) -> Result<[u8; 32], MessageProcessorError>; +} + +#[impl_trait_for_tuples::impl_for_tuples(10)] +impl MessageProcessor for Tuple { + fn can_process_message(relayer: &AccountId, message: &Message) -> bool { + for_tuples!( #( + match Tuple::can_process_message(&relayer, &message) { + true => { + return true; + }, + _ => {} + } + )* ); + + false + } + + fn process_message( + relayer: AccountId, + message: Message, + ) -> Result<[u8; 32], MessageProcessorError> { + for_tuples!( #( + match Tuple::can_process_message(&relayer, &message) { + true => { + return Tuple::process_message(relayer, message) + }, + _ => {} + } + )* ); + + Err(MessageProcessorError::ProcessMessage(DispatchError::Other( + "No handler found for message!", + ))) + } +} diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound.rs index 026db448d7271..4b120d036703f 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound.rs @@ -38,7 +38,7 @@ use rococo_westend_system_emulated_network::penpal_emulated_chain::PARA_ID_B; use snowbridge_core::{reward::MessageId, AssetMetadata, TokenIdOf}; use snowbridge_inbound_queue_primitives::v2::{ EthereumAsset::{ForeignTokenERC20, NativeTokenERC20}, - Message, Network, XcmPayload, + Message, Network, Payload, }; use sp_core::{H160, H256}; use sp_io::hashing::blake2_256; @@ -79,7 +79,7 @@ fn register_token_v2() { nonce: 1, origin, assets: vec![], - xcm: XcmPayload::CreateAsset { token, network: Network::Polkadot }, + payload: Payload::CreateAsset { token, network: Network::Polkadot }, claimer: Some(claimer_bytes), // Used to pay the asset creation deposit. value: 9_000_000_000_000u128, @@ -190,7 +190,7 @@ fn send_token_v2() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 1_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -300,7 +300,7 @@ fn send_weth_v2() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 3_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -457,7 +457,7 @@ fn register_and_send_token_in_one_transaction_fails() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 3_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -610,7 +610,7 @@ fn send_token_to_penpal_v2() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 3_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -794,7 +794,7 @@ fn send_foreign_erc20_token_back_to_polkadot() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 1_500_000_000_000u128, execution_fee: 3_500_000_000_000u128, @@ -890,7 +890,7 @@ fn invalid_xcm_traps_funds_on_ah() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(instructions.to_vec()), + payload: Payload::Raw(instructions.to_vec()), claimer: Some(claimer_bytes), value: 1_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -957,7 +957,7 @@ fn invalid_claimer_does_not_fail_the_message() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), // Set an invalid claimer claimer: Some(hex!("2b7ce7bc7e87e4d6619da21487c7a53f").to_vec()), value: 1_500_000_000_000u128, diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound_to_rococo.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound_to_rococo.rs index acb067dfa9736..ed993e401cca9 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound_to_rococo.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2_inbound_to_rococo.rs @@ -35,7 +35,7 @@ use hex_literal::hex; use snowbridge_core::TokenIdOf; use snowbridge_inbound_queue_primitives::v2::{ EthereumAsset::{ForeignTokenERC20, NativeTokenERC20}, - Message, XcmPayload, + Message, Payload, }; use sp_core::{H160, H256}; use xcm::opaque::latest::AssetTransferFilter::{ReserveDeposit, ReserveWithdraw}; @@ -151,7 +151,7 @@ fn send_token_to_rococo_v2() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 3_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -313,7 +313,7 @@ fn send_ether_to_rococo_v2() { nonce: 1, origin, assets: vec![], - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 6_500_000_000_000u128, execution_fee: 1_500_000_000_000u128, @@ -509,7 +509,7 @@ fn send_roc_from_ethereum_to_rococo() { nonce: 1, origin, assets, - xcm: XcmPayload::Raw(versioned_message_xcm.encode()), + payload: Payload::Raw(versioned_message_xcm.encode()), claimer: Some(claimer_bytes), value: 9_500_000_000_000u128, execution_fee: 3_500_000_000_000u128, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index e3ade0ce04b31..e1821e3cbf189 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -31,7 +31,9 @@ use pallet_xcm::EnsureXcm; use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::{gwei, meth, AllowSiblingsOnly, ChannelId, PricingParameters, Rewards}; -use snowbridge_inbound_queue_primitives::v2::CreateAssetCallInfo; +use snowbridge_inbound_queue_primitives::v2::{ + CreateAssetCallInfo, MessageToXcm, XcmMessageProcessor as InboundXcmMessageProcessor, +}; use snowbridge_outbound_queue_primitives::{ v1::{ConstantGasMeter, EthereumBlobExporter}, v2::{ConstantGasMeter as ConstantGasMeterV2, EthereumBlobExporter as EthereumBlobExporterV2}, @@ -99,6 +101,7 @@ parameter_types! { set_reserves_call: SetReservesCallIndex::get(), }; pub SnowbridgeFrontendLocation: Location = Location::new(1, [Parachain(ASSET_HUB_ID), PalletInstance(FRONTEND_PALLET_INDEX)]); + pub TargetLocation: Location = Location::new(1, [Parachain(AssetHubParaId::get().into())]); } impl snowbridge_pallet_inbound_queue::Config for Runtime { @@ -134,20 +137,11 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type RewardProcessor = RewardThroughSovereign; } -impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type Verifier = EthereumBeaconClient; - #[cfg(not(feature = "runtime-benchmarks"))] - type XcmSender = crate::XcmRouter; - #[cfg(feature = "runtime-benchmarks")] - type XcmSender = benchmark_helpers::DoNothingRouter; - type GatewayAddress = EthereumGatewayAddress; - #[cfg(feature = "runtime-benchmarks")] - type Helper = Runtime; - type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue_v2::WeightInfo; - type AssetHubParaId = AssetHubParaId; - type XcmExecutor = XcmExecutor; - type MessageConverter = snowbridge_inbound_queue_primitives::v2::MessageToXcm< +pub type XcmMessageProcessor = InboundXcmMessageProcessor< + Runtime, + crate::XcmRouter, + XcmExecutor, + MessageToXcm< CreateAssetCall, EthereumNetwork, RelayNetwork, @@ -156,14 +150,28 @@ impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { AssetHubParaId, EthereumSystem, AccountId, - >; - type AccountToLocation = xcm_builder::AliasesIntoAccountId32< + >, + xcm_builder::AliasesIntoAccountId32< xcm_config::RelayNetwork, ::AccountId, - >; + >, + TargetLocation, +>; + +impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Verifier = EthereumBeaconClient; + type GatewayAddress = EthereumGatewayAddress; + type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue_v2::WeightInfo; + #[cfg(feature = "runtime-benchmarks")] + type MessageProcessor = benchmark_helpers::DummyXcmProcessor; + #[cfg(not(feature = "runtime-benchmarks"))] + type MessageProcessor = XcmMessageProcessor; type RewardKind = BridgeReward; type DefaultRewardKind = SnowbridgeReward; type RewardPayment = BridgeRelayers; + #[cfg(feature = "runtime-benchmarks")] + type Helper = Runtime; } pub struct GetAggregateMessageOrigin; @@ -335,21 +343,30 @@ impl snowbridge_pallet_system_v2::Config for Runtime { #[cfg(feature = "runtime-benchmarks")] pub mod benchmark_helpers { use crate::{ - bridge_to_ethereum_config::EthereumGatewayAddress, vec, EthereumBeaconClient, Runtime, - RuntimeOrigin, System, + bridge_to_ethereum_config::{ + CreateAssetCall, EthereumGatewayAddress, InboundQueueV2Location, TargetLocation, + }, + vec, + xcm_config::{RelayNetwork, XcmConfig}, + EthereumBeaconClient, EthereumSystem, Runtime, RuntimeOrigin, System, }; use codec::Encode; use frame_support::assert_ok; use hex_literal::hex; use snowbridge_beacon_primitives::BeaconHeader; - use snowbridge_inbound_queue_primitives::EventFixture; + use snowbridge_inbound_queue_primitives::{ + v2::{MessageToXcm, XcmMessageProcessor as InboundXcmMessageProcessor}, + EventFixture, + }; use snowbridge_pallet_inbound_queue::BenchmarkHelper; use snowbridge_pallet_inbound_queue_fixtures::register_token::make_register_token_message; use snowbridge_pallet_inbound_queue_v2::BenchmarkHelper as InboundQueueBenchmarkHelperV2; use snowbridge_pallet_inbound_queue_v2_fixtures::register_token::make_register_token_message as make_register_token_message_v2; use snowbridge_pallet_outbound_queue_v2::BenchmarkHelper as OutboundQueueBenchmarkHelperV2; use sp_core::H256; + use testnet_parachains_constants::westend::snowbridge::{AssetHubParaId, EthereumNetwork}; use xcm::latest::{Assets, Location, SendError, SendResult, SendXcm, Xcm, XcmHash}; + use xcm_executor::XcmExecutor; impl BenchmarkHelper for Runtime { fn initialize_storage() -> EventFixture { @@ -406,6 +423,27 @@ pub mod benchmark_helpers { } } + pub type DummyXcmProcessor = InboundXcmMessageProcessor< + Runtime, + DoNothingRouter, + XcmExecutor, + MessageToXcm< + CreateAssetCall, + EthereumNetwork, + RelayNetwork, + EthereumGatewayAddress, + InboundQueueV2Location, + AssetHubParaId, + EthereumSystem, + ::AccountId, + >, + xcm_builder::AliasesIntoAccountId32< + RelayNetwork, + ::AccountId, + >, + TargetLocation, + >; + impl snowbridge_pallet_system::BenchmarkHelper for () { fn make_xcm_origin(location: Location) -> RuntimeOrigin { RuntimeOrigin::from(pallet_xcm::Origin::Xcm(location)) From 477306754b7b623af228a3abf75fd16dfca2a215 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 7 Jan 2026 14:31:37 +0400 Subject: [PATCH 10/11] modify message processor trait to return worst case weight + refund excess weight (#20) Co-authored-by: girazoki --- .../pallets/inbound-queue-v2/src/lib.rs | 34 +++++++++++++------ .../pallets/inbound-queue-v2/src/mock.rs | 4 +-- .../inbound-queue/src/v2/processor.rs | 4 +-- .../primitives/inbound-queue/src/v2/traits.rs | 23 +++++++++++-- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs index 8ab4c667d8736..96e0c9ca5f1bb 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs @@ -37,6 +37,7 @@ mod test; pub use crate::weights::WeightInfo; use bp_relayers::RewardLedger; +use frame_support::dispatch::PostDispatchInfo; use frame_system::ensure_signed; use snowbridge_core::{ reward::{AddTip, AddTipError}, @@ -181,8 +182,8 @@ pub mod pallet { impl Pallet { /// Submit an inbound message originating from the Gateway contract on Ethereum #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::submit())] - pub fn submit(origin: OriginFor, event: Box) -> DispatchResult { + #[pallet::weight(T::WeightInfo::submit().saturating_add(T::MessageProcessor::worst_case_message_processor_weight()))] + pub fn submit(origin: OriginFor, event: Box) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; ensure!(!OperatingMode::::get().is_halted(), Error::::Halted); @@ -212,7 +213,10 @@ pub mod pallet { } impl Pallet { - pub fn process_message(relayer: T::AccountId, message: Message) -> DispatchResult { + pub fn process_message( + relayer: T::AccountId, + message: Message, + ) -> DispatchResultWithPostInfo { // Verify that the message was submitted from the known Gateway contract ensure!(T::GatewayAddress::get() == message.gateway, Error::::InvalidGateway); @@ -224,12 +228,14 @@ pub mod pallet { // Mark message as received Nonce::::set(nonce); - let message_id = T::MessageProcessor::process_message(relayer.clone(), message) - .map_err(|e| match e { - MessageProcessorError::ProcessMessage(e) => e, - MessageProcessorError::ConvertMessage(e) => Error::::from(e).into(), - MessageProcessorError::SendMessage(e) => Error::::from(e).into(), - })?; + let (message_id, maybe_corrected_weight) = + T::MessageProcessor::process_message(relayer.clone(), message).map_err( + |e| match e { + MessageProcessorError::ProcessMessage(e) => e, + MessageProcessorError::ConvertMessage(e) => Error::::from(e).into(), + MessageProcessorError::SendMessage(e) => Error::::from(e).into(), + }, + )?; // Pay relayer reward let tip = Tips::::take(nonce).unwrap_or_default(); @@ -241,7 +247,15 @@ pub mod pallet { // Emit event with the message_id Self::deposit_event(Event::MessageReceived { nonce, message_id }); - Ok(()) + if let Some(corrected_weight) = maybe_corrected_weight { + Ok(PostDispatchInfo { + actual_weight: Some(corrected_weight.saturating_add(T::WeightInfo::submit())), + ..Default::default() + }) + } else { + // Pays fees and non-corrected-weight + Ok(().into()) + } } } diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs index f08bafa159349..126aec5fbfbaf 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs @@ -129,7 +129,7 @@ impl MessageProcessor for DummyPrefix { fn process_message( _relayer: AccountId, _message: Message, - ) -> Result<[u8; 32], MessageProcessorError> { + ) -> Result<([u8; 32], Option), MessageProcessorError> { panic!("DummyPrefix::process_message shouldn't be called"); } } @@ -144,7 +144,7 @@ impl MessageProcessor for DummySuffix { fn process_message( _relayer: AccountId, _message: Message, - ) -> Result<[u8; 32], MessageProcessorError> { + ) -> Result<([u8; 32], Option), MessageProcessorError> { panic!("DummySuffix::process_message shouldn't be called"); } } diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs index 926912f06a66f..d7e28eb9c62fa 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/processor.rs @@ -33,10 +33,10 @@ where fn process_message( relayer: AccountId, message: Message, - ) -> Result<[u8; 32], MessageProcessorError> { + ) -> Result<([u8; 32], Option), MessageProcessorError> { // Process the message and return its ID let id = Self::process_xcm(relayer, message)?; - Ok(id) + Ok((id, None)) } } diff --git a/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs b/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs index c9e26b05ec1dd..c8fbce78c13f9 100644 --- a/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs +++ b/bridges/snowbridge/primitives/inbound-queue/src/v2/traits.rs @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: 2021-2025 Parity Technologies (UK) Ltd. use super::Message; use sp_core::RuntimeDebug; -use sp_runtime::DispatchError; +use sp_runtime::{DispatchError, Weight}; use xcm::latest::{SendError, Xcm}; /// Converts an inbound message from Ethereum to an XCM message that can be @@ -42,7 +42,12 @@ pub trait MessageProcessor { fn process_message( relayer: AccountId, message: Message, - ) -> Result<[u8; 32], MessageProcessorError>; + ) -> Result<([u8; 32], Option), MessageProcessorError>; + + /// Returns the worst case message processor weight + fn worst_case_message_processor_weight() -> Weight { + Weight::default() + } } #[impl_trait_for_tuples::impl_for_tuples(10)] @@ -63,7 +68,7 @@ impl MessageProcessor for Tuple { fn process_message( relayer: AccountId, message: Message, - ) -> Result<[u8; 32], MessageProcessorError> { + ) -> Result<([u8; 32], Option), MessageProcessorError> { for_tuples!( #( match Tuple::can_process_message(&relayer, &message) { true => { @@ -77,4 +82,16 @@ impl MessageProcessor for Tuple { "No handler found for message!", ))) } + + fn worst_case_message_processor_weight() -> Weight { + let mut max_weight = Weight::zero(); + + for_tuples!( #( + max_weight = max_weight.max( + Tuple::worst_case_message_processor_weight() + ); + )* ); + + max_weight + } } From afb5dc300ecda3d92320f3cf4d0f28aac5e856c5 Mon Sep 17 00:00:00 2001 From: Tomasz Polaczyk Date: Wed, 28 Jan 2026 10:57:01 +0100 Subject: [PATCH 11/11] xcm-emulator: hardcode slot_duration to 6000 Some runtimes don't have pallet_aura --- cumulus/xcm/xcm-emulator/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulus/xcm/xcm-emulator/src/lib.rs b/cumulus/xcm/xcm-emulator/src/lib.rs index 44fd383dc263a..e88f3dd530d52 100644 --- a/cumulus/xcm/xcm-emulator/src/lib.rs +++ b/cumulus/xcm/xcm-emulator/src/lib.rs @@ -712,7 +712,7 @@ macro_rules! decl_test_parachains { // Initialze `System`. let digest = ::DigestProvider::convert((block_number, relay_block_number)); - let slot_duration = $crate::pallet_aura::Pallet::<$runtime::Runtime>::slot_duration(); + let slot_duration: u64 = 6000; ::System::initialize(&block_number, &parent_head_data.hash(), &digest); // Process `on_initialize` for all pallets except `System`.