diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 358b788f..90ce3996 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -1257,6 +1257,24 @@ dependencies = [ "trie-db", ] +[[package]] +name = "bridge-hub-common" +version = "0.13.1" +dependencies = [ + "cumulus-primitives-core", + "frame-support", + "pallet-message-queue", + "parity-scale-codec", + "scale-info", + "snowbridge-core", + "sp-core", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + [[package]] name = "bs58" version = "0.5.1" @@ -2349,6 +2367,7 @@ dependencies = [ name = "datahaven-runtime" version = "0.1.0" dependencies = [ + "bridge-hub-common", "datahaven-runtime-common", "dhp-bridge", "fp-account", @@ -2376,6 +2395,7 @@ dependencies = [ "pallet-grandpa", "pallet-identity", "pallet-im-online", + "pallet-message-queue", "pallet-mmr", "pallet-multisig", "pallet-offences", @@ -2396,8 +2416,12 @@ dependencies = [ "serde_json", "snowbridge-beacon-primitives", "snowbridge-inbound-queue-primitives", + "snowbridge-merkle-tree", + "snowbridge-outbound-queue-primitives", + "snowbridge-outbound-queue-v2-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-inbound-queue-v2", + "snowbridge-pallet-outbound-queue-v2", "snowbridge-verification-primitives", "sp-api", "sp-block-builder", @@ -2429,6 +2453,7 @@ dependencies = [ "frame-support", "polkadot-primitives", "polkadot-runtime-common", + "staging-xcm", ] [[package]] @@ -11755,6 +11780,21 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "snowbridge-merkle-tree" +version = "0.2.0" +dependencies = [ + "array-bytes", + "hex", + "hex-literal 0.3.4", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-crypto-hashing 0.1.0 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2412)", + "sp-runtime", + "sp-tracing", +] + [[package]] name = "snowbridge-milagro-bls" version = "1.5.4" @@ -11796,6 +11836,21 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "snowbridge-outbound-queue-v2-runtime-api" +version = "0.2.0" +dependencies = [ + "frame-support", + "parity-scale-codec", + "scale-info", + "snowbridge-core", + "snowbridge-merkle-tree", + "snowbridge-outbound-queue-primitives", + "sp-api", + "sp-std", + "staging-xcm", +] + [[package]] name = "snowbridge-pallet-ethereum-client" version = "0.2.0" @@ -11880,6 +11935,37 @@ dependencies = [ "sp-std", ] +[[package]] +name = "snowbridge-pallet-outbound-queue-v2" +version = "0.2.0" +dependencies = [ + "alloy-core", + "bp-relayers", + "bridge-hub-common", + "ethabi-decode", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal 0.3.4", + "pallet-message-queue", + "parity-scale-codec", + "scale-info", + "serde", + "snowbridge-core", + "snowbridge-inbound-queue-primitives", + "snowbridge-merkle-tree", + "snowbridge-outbound-queue-primitives", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + [[package]] name = "snowbridge-test-utils" version = "0.1.0" diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 4226571a..a6950088 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -10,6 +10,7 @@ members = [ "node", "pallets/ethereum-client", "pallets/inbound-queue-v2", + "pallets/outbound-queue-v2", "pallets/validator-set", "primitives/bridge", "runtime", @@ -28,6 +29,7 @@ dhp-bridge = { path = "./primitives/bridge", default-features = false } alloy-core = { version = "0.8.15", default-features = false } alloy-primitives = { version = "0.4.2", default-features = false } alloy-sol-types = { version = "0.4.2", default-features = false } +array-bytes = { version = "6.2.2", default-features = false } async-trait = { version = "0.1.42" } blake2-rfc = { version = "0.2.18", default-features = false } byte-slice-cast = { version = "1.2.1", default-features = false } @@ -63,6 +65,7 @@ tracing = { version = "0.1.37", default-features = false } # Polkadot bp-relayers = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } +cumulus-primitives-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } frame-executive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } @@ -79,6 +82,7 @@ pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = pallet-grandpa = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-identity = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-im-online = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } +pallet-message-queue = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-multisig = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-offences = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-parameters = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } @@ -118,6 +122,7 @@ sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = " sp-consensus-babe = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } +sp-crypto-hashing = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } @@ -130,6 +135,7 @@ sp-staking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "sta sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-storage = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } +sp-tracing = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sp-version = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } @@ -149,6 +155,8 @@ sc-consensus-beefy-rpc = { git = "https://github.com/paritytech/polkadot-sdk", b sp-consensus-beefy = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } # Snowbridge +bridge-hub-common = { path = "primitives/snowbridge/bridge-hub-common", default-features = false } +snowbridge-merkle-tree = { path = "primitives/snowbridge/merkle-tree", default-features = false } snowbridge-beacon-primitives = { path = "primitives/snowbridge/beacon", default-features = false } snowbridge-core = { path = "primitives/snowbridge/core", default-features = false } snowbridge-ethereum = { path = "primitives/snowbridge/ethereum", default-features = false } @@ -158,6 +166,8 @@ snowbridge-inbound-queue-primitives = { path = "primitives/snowbridge/inbound-qu snowbridge-outbound-queue-primitives = { path = "primitives/snowbridge/outbound-queue", default-features = false } snowbridge-pallet-inbound-queue-v2 = { path = "pallets/inbound-queue-v2", default-features = false } snowbridge-pallet-inbound-queue-v2-fixtures = { path = "pallets/inbound-queue-v2/fixtures", default-features = false } +snowbridge-pallet-outbound-queue-v2 = { path = "pallets/outbound-queue-v2", default-features = false } +snowbridge-outbound-queue-v2-runtime-api = { path = "pallets/outbound-queue-v2/runtime-api", default-features = false } snowbridge-test-utils = { path = "primitives/snowbridge/test-utils", default-features = false } snowbridge-verification-primitives = { path = "primitives/snowbridge/verification", default-features = false } diff --git a/operator/pallets/outbound-queue-v2/Cargo.toml b/operator/pallets/outbound-queue-v2/Cargo.toml new file mode 100644 index 00000000..1fcb4904 --- /dev/null +++ b/operator/pallets/outbound-queue-v2/Cargo.toml @@ -0,0 +1,93 @@ +[package] +name = "snowbridge-pallet-outbound-queue-v2" +description = "Snowbridge Outbound Queue Pallet V2" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[package.metadata.polkadot-sdk] +exclude-from-umbrella = true + +[dependencies] +alloy-core = { workspace = true, features = ["sol-types"] } +codec = { features = ["derive"], workspace = true } +ethabi = { workspace = true } +hex-literal = { workspace = true, default-features = true } +scale-info = { features = ["derive"], workspace = true } +serde = { features = ["alloc", "derive"], workspace = true } + +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-arithmetic = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +bp-relayers = { workspace = true } +bridge-hub-common = { workspace = true } + +snowbridge-core = { workspace = true } +snowbridge-merkle-tree = { workspace = true } +snowbridge-inbound-queue-primitives = { workspace = true } +snowbridge-outbound-queue-primitives = { workspace = true } + +xcm = { workspace = true } +xcm-builder = { workspace = true } +xcm-executor = { workspace = true } + +[dev-dependencies] +pallet-message-queue = { workspace = true } +sp-keyring = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "alloy-core/std", + "bp-relayers/std", + "bridge-hub-common/std", + "codec/std", + "ethabi/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "pallet-message-queue/std", + "scale-info/std", + "serde/std", + "snowbridge-core/std", + "snowbridge-merkle-tree/std", + "snowbridge-outbound-queue-primitives/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] +runtime-benchmarks = [ + "bridge-hub-common/runtime-benchmarks", + "frame-benchmarking", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", + "snowbridge-core/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-message-queue/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/operator/pallets/outbound-queue-v2/runtime-api/Cargo.toml b/operator/pallets/outbound-queue-v2/runtime-api/Cargo.toml new file mode 100644 index 00000000..abb6cc54 --- /dev/null +++ b/operator/pallets/outbound-queue-v2/runtime-api/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "snowbridge-outbound-queue-v2-runtime-api" +description = "Snowbridge Outbound Queue Runtime API V2" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[package.metadata.polkadot-sdk] +exclude-from-umbrella = true + +[dependencies] +codec = { features = ["derive"], workspace = true } +frame-support = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +snowbridge-core = { workspace = true } +snowbridge-merkle-tree = { workspace = true } +snowbridge-outbound-queue-primitives = { workspace = true } +sp-api = { workspace = true } +sp-std = { workspace = true } +xcm = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "scale-info/std", + "snowbridge-core/std", + "snowbridge-merkle-tree/std", + "snowbridge-outbound-queue-primitives/std", + "sp-api/std", + "sp-std/std", + "xcm/std", +] diff --git a/operator/pallets/outbound-queue-v2/runtime-api/src/lib.rs b/operator/pallets/outbound-queue-v2/runtime-api/src/lib.rs new file mode 100644 index 00000000..276ce47e --- /dev/null +++ b/operator/pallets/outbound-queue-v2/runtime-api/src/lib.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork + +//! Ethereum Outbound Queue V2 Runtime API +//! +//! * `prove_message`: Generate a merkle proof for a committed message + +#![cfg_attr(not(feature = "std"), no_std)] +use frame_support::traits::tokens::Balance as BalanceT; +use snowbridge_merkle_tree::MerkleProof; + +sp_api::decl_runtime_apis! { + pub trait OutboundQueueV2Api where Balance: BalanceT + { + /// Generate a merkle proof for a committed message identified by `leaf_index`. + fn prove_message(leaf_index: u64) -> Option; + } +} diff --git a/operator/pallets/outbound-queue-v2/src/api.rs b/operator/pallets/outbound-queue-v2/src/api.rs new file mode 100644 index 00000000..9a60a361 --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/api.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Helpers for implementing runtime api + +use crate::{Config, MessageLeaves}; +use frame_support::storage::StorageStreamIter; +use snowbridge_merkle_tree::{merkle_proof, MerkleProof}; + +pub fn prove_message(leaf_index: u64) -> Option +where + T: Config, +{ + if !MessageLeaves::::exists() { + return None; + } + let proof = + merkle_proof::<::Hashing, _>(MessageLeaves::::stream_iter(), leaf_index); + Some(proof) +} diff --git a/operator/pallets/outbound-queue-v2/src/benchmarking.rs b/operator/pallets/outbound-queue-v2/src/benchmarking.rs new file mode 100644 index 00000000..bbf739ab --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/benchmarking.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use super::*; + +use bridge_hub_common::AggregateMessageOrigin; +use codec::Encode; +use frame_benchmarking::v2::*; +use frame_support::{traits::Hooks, BoundedVec}; +use snowbridge_outbound_queue_primitives::v2::{Command, Initializer, Message}; +use sp_core::{H160, H256}; + +#[allow(unused_imports)] +use crate::Pallet as OutboundQueue; + +#[benchmarks( + where + ::MaxMessagePayloadSize: Get, +)] +mod benchmarks { + use super::*; + + /// Build `Upgrade` message with `MaxMessagePayloadSize`, in the worst-case. + fn build_message() -> (Message, OutboundMessage) { + let commands = vec![Command::Upgrade { + impl_address: H160::zero(), + impl_code_hash: H256::zero(), + initializer: Initializer { + params: core::iter::repeat_with(|| 1_u8) + .take(::MaxMessagePayloadSize::get() as usize) + .collect(), + maximum_required_gas: 200_000, + }, + }]; + let message = Message { + origin: Default::default(), + id: H256::default(), + fee: 0, + commands: BoundedVec::try_from(commands.clone()).unwrap(), + }; + let wrapped_commands: Vec = commands + .into_iter() + .map(|command| OutboundCommandWrapper { + kind: command.index(), + gas: T::GasMeter::maximum_dispatch_gas_used_at_most(&command), + payload: command.abi_encode(), + }) + .collect(); + let outbound_message = OutboundMessage { + origin: Default::default(), + nonce: 1, + topic: H256::default(), + commands: wrapped_commands.clone().try_into().unwrap(), + }; + (message, outbound_message) + } + + /// Initialize `MaxMessagesPerBlock` messages need to be committed, in the worst-case. + fn initialize_worst_case() { + for _ in 0..T::MaxMessagesPerBlock::get() { + initialize_with_one_message::(); + } + } + + /// Initialize with a single message + fn initialize_with_one_message() { + let (message, outbound_message) = build_message::(); + let leaf = ::Hashing::hash(&message.encode()); + MessageLeaves::::append(leaf); + Messages::::append(outbound_message); + } + + /// Benchmark for processing a message. + #[benchmark] + fn do_process_message() -> Result<(), BenchmarkError> { + let (enqueued_message, _) = build_message::(); + let origin = AggregateMessageOrigin::SnowbridgeV2([1; 32].into()); + let message = enqueued_message.encode(); + + #[block] + { + let _ = OutboundQueue::::do_process_message(origin, &message).unwrap(); + } + + assert_eq!(MessageLeaves::::decode_len().unwrap(), 1); + + Ok(()) + } + + /// Benchmark for producing final messages commitment, in the worst-case + #[benchmark] + fn commit() -> Result<(), BenchmarkError> { + initialize_worst_case::(); + + #[block] + { + OutboundQueue::::commit(); + } + + Ok(()) + } + + /// Benchmark for producing commitment for a single message, used to estimate the delivery + /// cost. The assumption is that cost of commit a single message is even higher than the average + /// cost of commit all messages. + #[benchmark] + fn commit_single() -> Result<(), BenchmarkError> { + initialize_with_one_message::(); + + #[block] + { + OutboundQueue::::commit(); + } + + Ok(()) + } + + /// Benchmark for `on_initialize` in the worst-case + #[benchmark] + fn on_initialize() -> Result<(), BenchmarkError> { + initialize_worst_case::(); + #[block] + { + OutboundQueue::::on_initialize(1_u32.into()); + } + Ok(()) + } + + /// Benchmark the entire process flow in the worst-case. This can be used to determine + /// appropriate values for the configuration parameters `MaxMessagesPerBlock` and + /// `MaxMessagePayloadSize` + #[benchmark] + fn process() -> Result<(), BenchmarkError> { + initialize_worst_case::(); + let origin = AggregateMessageOrigin::SnowbridgeV2([1; 32].into()); + let (enqueued_message, _) = build_message::(); + let message = enqueued_message.encode(); + + #[block] + { + OutboundQueue::::on_initialize(1_u32.into()); + for _ in 0..T::MaxMessagesPerBlock::get() { + OutboundQueue::::do_process_message(origin, &message).unwrap(); + } + OutboundQueue::::commit(); + } + + Ok(()) + } + + impl_benchmark_test_suite!(OutboundQueue, crate::mock::new_tester(), crate::mock::Test,); +} diff --git a/operator/pallets/outbound-queue-v2/src/lib.rs b/operator/pallets/outbound-queue-v2/src/lib.rs new file mode 100644 index 00000000..77e987d6 --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/lib.rs @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Pallet for committing outbound messages for delivery to Ethereum +//! +//! # Overview +//! +//! Messages come either from sibling parachains via XCM, or BridgeHub itself +//! via the `snowbridge-pallet-system-v2`: +//! +//! 1. `snowbridge_outbound_queue_primitives::v2::EthereumBlobExporter::deliver` +//! 2. `snowbridge_pallet_system_v2::Pallet::send` +//! +//! The message submission pipeline works like this: +//! 1. The message is first validated via the implementation for +//! [`snowbridge_outbound_queue_primitives::v2::SendMessage::validate`] +//! 2. The message is then enqueued for later processing via the implementation for +//! [`snowbridge_outbound_queue_primitives::v2::SendMessage::deliver`] +//! 3. The underlying message queue is implemented by [`Config::MessageQueue`] +//! 4. The message queue delivers messages to this pallet via the implementation for +//! [`frame_support::traits::ProcessMessage::process_message`] +//! 5. The message is processed in `Pallet::do_process_message`: +//! a. Convert to `OutboundMessage`, and stored into the `Messages` vector storage +//! b. ABI-encode the `OutboundMessage` and store the committed Keccak256 hash in `MessageLeaves` +//! c. Generate `PendingOrder` with assigned nonce and fee attached, stored into the +//! `PendingOrders` map storage, with nonce as the key +//! d. Increment nonce and update the `Nonce` storage +//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`. +//! At the beginning of the next block, both `Messages` and `MessageLeaves` are dropped so that +//! state at each block only holds the messages processed in that block. +//! 7. This merkle root is inserted into the parachain header as a digest item +//! 8. Offchain relayers are able to relay the message to Ethereum after: +//! a. Generating a merkle proof for the committed message using the `prove_message` runtime API +//! b. Reading the actual message content from the `Messages` vector in storage +//! 9. On the Ethereum side, the message root is ultimately the thing being verified by the Beefy +//! light client. +//! 10. When the message has been verified and executed, the relayer will call the extrinsic +//! `submit_delivery_receipt` to: +//! a. Verify the message with proof for a transaction receipt containing the event log, +//! same as the inbound queue verification flow +//! b. Fetch the pending order by nonce of the message, pay reward with fee attached in the order +//! c. Remove the order from `PendingOrders` map storage by nonce +//! +//! +//! # Extrinsics +//! +//! * [`Call::submit_delivery_receipt`]: Submit delivery proof +//! +//! # Runtime API +//! +//! * `prove_message`: Generate a merkle proof for a committed message +#![cfg_attr(not(feature = "std"), no_std)] +pub mod api; +pub mod process_message_impl; +pub mod send_message_impl; +pub mod types; +pub mod weights; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod test; + +use alloy_core::{ + primitives::{Bytes, FixedBytes}, + sol_types::SolValue, +}; +use bridge_hub_common::{AggregateMessageOrigin, CustomDigestItem}; +use codec::Decode; +use frame_support::{ + storage::StorageStreamIter, + traits::{tokens::Balance, EnqueueMessage, Get, ProcessMessageError}, + weights::{Weight, WeightToFee}, +}; +use snowbridge_core::{BasicOperatingMode, TokenId}; +use snowbridge_inbound_queue_primitives::RewardLedger; +use snowbridge_merkle_tree::merkle_root; +use snowbridge_outbound_queue_primitives::{ + v2::{ + abi::{CommandWrapper, OutboundMessageWrapper}, + DeliveryReceipt, GasMeter, Message, OutboundCommandWrapper, OutboundMessage, + }, + EventProof, VerificationError, Verifier, +}; +use sp_core::{H160, H256}; +use sp_runtime::{ + traits::{BlockNumberProvider, Hash, MaybeEquivalence}, + DigestItem, +}; +use sp_std::prelude::*; +pub use types::{OnNewCommitment, PendingOrder, ProcessMessageOriginOf}; +pub use weights::WeightInfo; +use xcm::latest::{Location, NetworkId}; +type DeliveryReceiptOf = DeliveryReceipt<::AccountId>; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type Hashing: Hash; + + type MessageQueue: EnqueueMessage; + + /// Measures the maximum gas used to execute a command on Ethereum + type GasMeter: GasMeter; + + type Balance: Balance + From; + + /// Max bytes in a message payload + #[pallet::constant] + type MaxMessagePayloadSize: Get; + + /// Max number of messages processed per block + #[pallet::constant] + type MaxMessagesPerBlock: Get; + + /// Hook that is called whenever there is a new commitment. + type OnNewCommitment: OnNewCommitment; + + /// Convert a weight value into a deductible fee based. + type WeightToFee: WeightToFee; + + /// Weight information for extrinsics in this pallet + type WeightInfo: WeightInfo; + + /// The verifier for delivery proof from Ethereum + type Verifier: Verifier; + + /// Address of the Gateway contract + #[pallet::constant] + type GatewayAddress: Get; + /// Reward discriminator type. + type RewardKind: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone; + /// The default RewardKind discriminator for rewards allocated to relayers from this pallet. + #[pallet::constant] + type DefaultRewardKind: Get; + /// Relayer reward payment. + type RewardPayment: RewardLedger; + /// Ethereum NetworkId + type EthereumNetwork: Get; + type ConvertAssetId: MaybeEquivalence; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Message has been queued and will be processed in the future + MessageQueued { + /// The message + message: Message, + }, + /// Message will be committed at the end of current block. From now on, to track the + /// progress the message, use the `nonce` or the `id`. + MessageAccepted { + /// ID of the message + id: H256, + /// The nonce assigned to this message + nonce: u64, + }, + /// Some messages have been committed + MessagesCommitted { + /// Merkle root of the committed messages + root: H256, + /// number of committed messages + count: u64, + }, + /// Set OperatingMode + OperatingModeChanged { mode: BasicOperatingMode }, + /// Delivery Proof received + MessageDeliveryProofReceived { nonce: u64 }, + } + + #[pallet::error] + pub enum Error { + /// The message is too large + MessageTooLarge, + /// The pallet is halted + Halted, + /// Invalid Channel + InvalidChannel, + /// Invalid Envelope + InvalidEnvelope, + /// Message verification error + Verification(VerificationError), + /// Invalid Gateway + InvalidGateway, + /// Pending nonce does not exist + InvalidPendingNonce, + /// Reward payment failed + RewardPaymentFailed, + } + + /// Messages to be committed in the current block. This storage value is killed in + /// `on_initialize`, so will not end up bloating state. + /// + /// Is never read in the runtime, only by offchain message relayers. + /// Because of this, it will never go into the PoV of a block. + /// + /// Inspired by the `frame_system::Pallet::Events` storage value + #[pallet::storage] + #[pallet::unbounded] + pub(super) 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 `on_initialize`, so state + /// at each block contains only root hash of messages processed in that block. This also means + /// it doesn't have to be included in PoV. + #[pallet::storage] + #[pallet::unbounded] + pub(super) type MessageLeaves = StorageValue<_, Vec, ValueQuery>; + + /// The current nonce for the messages + #[pallet::storage] + pub type Nonce = StorageValue<_, u64, ValueQuery>; + + /// Pending orders to relay + #[pallet::storage] + pub type PendingOrders = + StorageMap<_, Twox64Concat, u64, PendingOrder>, OptionQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_: BlockNumberFor) -> Weight { + // Remove storage from previous block + Messages::::kill(); + MessageLeaves::::kill(); + // Reserve some weight for the `on_finalize` handler + T::WeightInfo::on_initialize() + T::WeightInfo::commit() + } + + fn on_finalize(_: BlockNumberFor) { + Self::commit(); + } + } + + #[pallet::call] + impl Pallet + where + T::AccountId: From<[u8; 32]>, + { + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::submit_delivery_receipt())] + pub fn submit_delivery_receipt( + origin: OriginFor, + event: Box, + ) -> DispatchResult { + let relayer = ensure_signed(origin)?; + + // submit message to verifier for verification + T::Verifier::verify(&event.event_log, &event.proof) + .map_err(|e| Error::::Verification(e))?; + + let receipt = DeliveryReceiptOf::::try_from(&event.event_log) + .map_err(|_| Error::::InvalidEnvelope)?; + + Self::process_delivery_receipt(relayer, receipt) + } + } + + impl Pallet { + /// Generate a messages commitment and insert it into the header digest + pub(crate) fn commit() { + let count = MessageLeaves::::decode_len().unwrap_or_default() as u64; + if count == 0 { + return; + } + + // Create merkle root of messages + let root = merkle_root::<::Hashing, _>(MessageLeaves::::stream_iter()); + + let digest_item: DigestItem = CustomDigestItem::SnowbridgeV2(root).into(); + + // Insert merkle root into the header digest + >::deposit_log(digest_item); + + T::OnNewCommitment::on_new_commitment(root); + + Self::deposit_event(Event::MessagesCommitted { root, count }); + } + + /// Process a message delivered by the MessageQueue pallet + pub(crate) fn do_process_message( + _: ProcessMessageOriginOf, + mut message: &[u8], + ) -> Result { + use ProcessMessageError::*; + + // Yield if the maximum number of messages has been processed this block. + // This ensures that the weight of `on_finalize` has a known maximum bound. + ensure!( + MessageLeaves::::decode_len().unwrap_or(0) + < T::MaxMessagesPerBlock::get() as usize, + Yield + ); + + let nonce = Nonce::::get(); + + // Decode bytes into Message + let Message { + origin, + id, + fee, + commands, + } = Message::decode(&mut message).map_err(|_| Corrupt)?; + + // Convert it to OutboundMessage and save into Messages storage + let commands: Vec = commands + .into_iter() + .map(|command| OutboundCommandWrapper { + kind: command.index(), + gas: T::GasMeter::maximum_dispatch_gas_used_at_most(&command), + payload: command.abi_encode(), + }) + .collect(); + let outbound_message = OutboundMessage { + origin, + nonce, + topic: id, + commands: commands.clone().try_into().map_err(|_| Corrupt)?, + }; + Messages::::append(outbound_message); + + // Convert it to an OutboundMessageWrapper (in ABI format), hash it using Keccak256 to + // generate a committed hash, and store it in MessageLeaves storage which can be + // verified on Ethereum later. + let abi_commands: Vec = commands + .into_iter() + .map(|command| CommandWrapper { + kind: command.kind, + gas: command.gas, + payload: Bytes::from(command.payload), + }) + .collect(); + let committed_message = OutboundMessageWrapper { + origin: FixedBytes::from(origin.as_fixed_bytes()), + nonce, + topic: FixedBytes::from(id.as_fixed_bytes()), + commands: abi_commands, + }; + let message_abi_encoded_hash = + ::Hashing::hash(&committed_message.abi_encode()); + MessageLeaves::::append(message_abi_encoded_hash); + + // Generate `PendingOrder` with fee attached in the message, stored + // into the `PendingOrders` map storage, with assigned nonce as the key. + // When the message is processed on ethereum side, the relayer will send the nonce + // back with delivery proof, only after that the order can + // be resolved and the fee will be rewarded to the relayer. + let order = PendingOrder { + nonce, + fee, + block_number: frame_system::Pallet::::current_block_number(), + }; + >::insert(nonce, order); + + Nonce::::set(nonce.checked_add(1).ok_or(Unsupported)?); + + Self::deposit_event(Event::MessageAccepted { id, nonce }); + + Ok(true) + } + + /// Process a delivery receipt from a relayer, to allocate the relayer reward. + pub fn process_delivery_receipt( + relayer: ::AccountId, + receipt: DeliveryReceiptOf, + ) -> DispatchResult + where + ::AccountId: From<[u8; 32]>, + { + // Verify that the message was submitted from the known Gateway contract + ensure!( + T::GatewayAddress::get() == receipt.gateway, + Error::::InvalidGateway + ); + + let nonce = receipt.nonce; + + let order = >::get(nonce).ok_or(Error::::InvalidPendingNonce)?; + + if order.fee > 0 { + // Pay relayer reward + T::RewardPayment::register_reward(&relayer, T::DefaultRewardKind::get(), order.fee); + } + + >::remove(nonce); + + Self::deposit_event(Event::MessageDeliveryProofReceived { nonce }); + + Ok(()) + } + + /// The local component of the message processing fees in native currency + pub(crate) fn calculate_local_fee() -> T::Balance { + T::WeightToFee::weight_to_fee( + &T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()), + ) + } + } +} diff --git a/operator/pallets/outbound-queue-v2/src/mock.rs b/operator/pallets/outbound-queue-v2/src/mock.rs new file mode 100644 index 00000000..39ee8c91 --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/mock.rs @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use super::*; + +use frame_support::{ + derive_impl, parameter_types, + traits::{Everything, Hooks}, + weights::IdentityFee, + BoundedVec, +}; + +use codec::{DecodeWithMemTracking, Encode, MaxEncodedLen}; +use hex_literal::hex; +use scale_info::TypeInfo; +use snowbridge_core::{ + gwei, meth, + pricing::{PricingParameters, Rewards}, + AgentId, AgentIdOf, ParaId, +}; +use snowbridge_outbound_queue_primitives::{v2::*, Log, Proof, VerificationError, Verifier}; +use sp_core::{ConstU32, H160, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup, Keccak256}, + AccountId32, BuildStorage, FixedU128, +}; +use sp_std::marker::PhantomData; +use xcm::prelude::Here; +use xcm_executor::traits::ConvertLocation; + +type Block = frame_system::mocking::MockBlock; +type AccountId = AccountId32; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Storage, Event}, + MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event}, + OutboundQueue: crate::{Pallet, Storage, Event}, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type PalletInfo = PalletInfo; + type Nonce = u64; + type Block = Block; +} + +parameter_types! { + pub const HeapSize: u32 = 32 * 1024; + pub const MaxStale: u32 = 32; + pub static ServiceWeight: Option = Some(Weight::from_parts(100, 100)); +} + +impl pallet_message_queue::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type MessageProcessor = OutboundQueue; + type Size = u32; + type QueueChangeHandler = (); + type HeapSize = HeapSize; + type MaxStale = MaxStale; + type ServiceWeight = ServiceWeight; + type IdleMaxServiceWeight = (); + type QueuePausedQuery = (); +} + +// Mock verifier +pub struct MockVerifier; + +impl Verifier for MockVerifier { + fn verify(_: &Log, _: &Proof) -> Result<(), VerificationError> { + Ok(()) + } +} + +const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"]; +const WETH: [u8; 20] = hex!["C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"]; + +parameter_types! { + pub const OwnParaId: ParaId = ParaId::new(1013); + pub Parameters: PricingParameters = PricingParameters { + exchange_rate: FixedU128::from_rational(1, 400), + fee_per_gas: gwei(20), + rewards: Rewards { local: DOT, remote: meth(1) }, + multiplier: FixedU128::from_rational(4, 3), + }; + pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); + pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 }; + pub DefaultMyRewardKind: BridgeReward = BridgeReward::Snowbridge; +} + +pub const DOT: u128 = 10_000_000_000; + +/// Showcasing that we can handle multiple different rewards with the same pallet. +#[derive( + Clone, + Copy, + Debug, + Decode, + DecodeWithMemTracking, + Encode, + Eq, + MaxEncodedLen, + PartialEq, + TypeInfo, +)] +pub enum BridgeReward { + /// Rewards for Snowbridge. + Snowbridge, +} + +impl RewardLedger<::AccountId, BridgeReward, u128> for () { + fn register_reward( + _relayer: &::AccountId, + _reward: BridgeReward, + _reward_balance: u128, + ) { + } +} + +impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Verifier = MockVerifier; + type GatewayAddress = GatewayAddress; + type Hashing = Keccak256; + type MessageQueue = MessageQueue; + type MaxMessagePayloadSize = ConstU32<1024>; + type MaxMessagesPerBlock = ConstU32<20>; + type GasMeter = ConstantGasMeter; + type Balance = u128; + type WeightToFee = IdentityFee; + type WeightInfo = (); + type RewardPayment = (); + type ConvertAssetId = (); + type EthereumNetwork = EthereumNetwork; + type RewardKind = BridgeReward; + type DefaultRewardKind = DefaultMyRewardKind; + type OnNewCommitment = (); +} + +fn setup() { + System::set_block_number(1); +} + +pub fn new_tester() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext: sp_io::TestExternalities = storage.into(); + ext.execute_with(setup); + ext +} + +pub fn run_to_end_of_next_block() { + // finish current block + MessageQueue::on_finalize(System::block_number()); + OutboundQueue::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + // start next block + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + OutboundQueue::on_initialize(System::block_number()); + MessageQueue::on_initialize(System::block_number()); + // finish next block + MessageQueue::on_finalize(System::block_number()); + OutboundQueue::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); +} + +pub fn bridge_hub_root_origin() -> AgentId { + AgentIdOf::convert_location(&Here.into()).unwrap() +} + +pub fn mock_governance_message() -> Message +where + T: Config, +{ + let _marker = PhantomData::; // for clippy + + Message { + origin: bridge_hub_root_origin(), + id: Default::default(), + fee: 0, + commands: BoundedVec::try_from(vec![Command::Upgrade { + impl_address: Default::default(), + impl_code_hash: Default::default(), + initializer: Initializer { + params: (0..512).map(|_| 1u8).collect::>(), + maximum_required_gas: 0, + }, + }]) + .unwrap(), + } +} + +// Message should fail validation as it is too large +pub fn mock_invalid_governance_message() -> Message +where + T: Config, +{ + let _marker = PhantomData::; // for clippy + + Message { + origin: Default::default(), + id: Default::default(), + fee: 0, + commands: BoundedVec::try_from(vec![Command::Upgrade { + impl_address: H160::zero(), + impl_code_hash: H256::zero(), + initializer: Initializer { + params: (0..1000).map(|_| 1u8).collect::>(), + maximum_required_gas: 0, + }, + }]) + .unwrap(), + } +} + +pub fn mock_message(sibling_para_id: u32) -> Message { + Message { + origin: H256::from_low_u64_be(sibling_para_id as u64), + id: H256::from_low_u64_be(1), + fee: 1_000, + commands: BoundedVec::try_from(vec![Command::UnlockNativeToken { + token: H160(WETH), + recipient: H160(GATEWAY_ADDRESS), + amount: 1_000_000, + }]) + .unwrap(), + } +} + +pub fn mock_register_token_message(sibling_para_id: u32) -> Message { + Message { + origin: H256::from_low_u64_be(sibling_para_id as u64), + id: H256::from_low_u64_be(1), + fee: 1_000, + commands: BoundedVec::try_from(vec![Command::RegisterForeignToken { + token_id: H256::from_low_u64_be(1), + name: vec![], + symbol: vec![], + decimals: 12, + }]) + .unwrap(), + } +} diff --git a/operator/pallets/outbound-queue-v2/src/process_message_impl.rs b/operator/pallets/outbound-queue-v2/src/process_message_impl.rs new file mode 100644 index 00000000..93385cce --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/process_message_impl.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Implementation for [`frame_support::traits::ProcessMessage`] +use super::*; +use crate::weights::WeightInfo; +use frame_support::{ + traits::{ProcessMessage, ProcessMessageError}, + weights::WeightMeter, +}; + +impl ProcessMessage for Pallet { + type Origin = AggregateMessageOrigin; + fn process_message( + message: &[u8], + origin: Self::Origin, + meter: &mut WeightMeter, + _: &mut [u8; 32], + ) -> Result { + let weight = T::WeightInfo::do_process_message(); + if meter.try_consume(weight).is_err() { + return Err(ProcessMessageError::Overweight(weight)); + } + Self::do_process_message(origin, message) + } +} diff --git a/operator/pallets/outbound-queue-v2/src/send_message_impl.rs b/operator/pallets/outbound-queue-v2/src/send_message_impl.rs new file mode 100644 index 00000000..24eba56d --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/send_message_impl.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Implementation for [`snowbridge_outbound_queue_primitives::v2::SendMessage`] +use super::*; +use bridge_hub_common::AggregateMessageOrigin; +use codec::Encode; +use frame_support::{ + ensure, + traits::{EnqueueMessage, Get}, +}; +use snowbridge_outbound_queue_primitives::{ + v2::{Message, SendMessage}, + SendError, SendMessageFeeProvider, +}; +use sp_core::H256; +use sp_runtime::BoundedVec; + +impl SendMessage for Pallet +where + T: Config, +{ + type Ticket = Message; + + fn validate(message: &Message) -> Result { + // The inner payload should not be too large + let payload = message.encode(); + ensure!( + payload.len() < T::MaxMessagePayloadSize::get() as usize, + SendError::MessageTooLarge + ); + + Ok(message.clone()) + } + + fn deliver(ticket: Self::Ticket) -> Result { + let origin = AggregateMessageOrigin::SnowbridgeV2(ticket.origin); + + let message = + BoundedVec::try_from(ticket.encode()).map_err(|_| SendError::MessageTooLarge)?; + + T::MessageQueue::enqueue_message(message.as_bounded_slice(), origin); + Self::deposit_event(Event::MessageQueued { + message: ticket.clone(), + }); + Ok(ticket.id) + } +} + +impl SendMessageFeeProvider for Pallet { + type Balance = T::Balance; + + /// The local component of the message processing fees in native currency + fn local_fee() -> Self::Balance { + Self::calculate_local_fee() + } +} diff --git a/operator/pallets/outbound-queue-v2/src/test.rs b/operator/pallets/outbound-queue-v2/src/test.rs new file mode 100644 index 00000000..f27759cc --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/test.rs @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use crate::{mock::*, *}; +use alloy_core::primitives::FixedBytes; + +use frame_support::{ + assert_err, assert_noop, assert_ok, + traits::{Hooks, ProcessMessage, ProcessMessageError}, + weights::WeightMeter, + BoundedVec, +}; + +use codec::Encode; +use hex_literal::hex; +use snowbridge_core::{ChannelId, ParaId}; +use snowbridge_outbound_queue_primitives::{ + v2::{abi::OutboundMessageWrapper, Command, Initializer, SendMessage}, + SendError, +}; +use sp_core::{hexdisplay::HexDisplay, H256}; + +#[test] +fn submit_messages_and_commit() { + new_tester().execute_with(|| { + for para_id in 1000..1004 { + let message = mock_message(para_id); + let ticket = OutboundQueue::validate(&message).unwrap(); + assert_ok!(OutboundQueue::deliver(ticket)); + } + + ServiceWeight::set(Some(Weight::MAX)); + run_to_end_of_next_block(); + + assert_eq!(Nonce::::get(), 4); + + let digest = System::digest(); + let digest_items = digest.logs(); + assert!(digest_items.len() == 1 && digest_items[0].as_other().is_some()); + assert_eq!(Messages::::decode_len(), Some(4)); + }); +} + +#[test] +fn submit_message_fail_too_large() { + new_tester().execute_with(|| { + let message = mock_invalid_governance_message::(); + assert_err!( + OutboundQueue::validate(&message), + SendError::MessageTooLarge + ); + }); +} + +#[test] +fn commit_exits_early_if_no_processed_messages() { + new_tester().execute_with(|| { + // on_finalize should do nothing, nor should it panic + OutboundQueue::on_finalize(System::block_number()); + + let digest = System::digest(); + let digest_items = digest.logs(); + assert_eq!(digest_items.len(), 0); + }); +} + +#[test] +fn process_message_yields_on_max_messages_per_block() { + new_tester().execute_with(|| { + for _ in 0..::MaxMessagesPerBlock::get() { + MessageLeaves::::append(H256::zero()) + } + + let _channel_id: ChannelId = ParaId::from(1000).into(); + let origin = AggregateMessageOrigin::SnowbridgeV2(H256::zero()); + let message = Message { + origin: Default::default(), + id: Default::default(), + fee: 0, + commands: BoundedVec::try_from(vec![Command::Upgrade { + impl_address: Default::default(), + impl_code_hash: Default::default(), + initializer: Initializer { + params: (0..512).map(|_| 1u8).collect::>(), + maximum_required_gas: 0, + }, + }]) + .unwrap(), + }; + + let mut meter = WeightMeter::new(); + + assert_noop!( + OutboundQueue::process_message( + message.encode().as_slice(), + origin, + &mut meter, + &mut [0u8; 32] + ), + ProcessMessageError::Yield + ); + }) +} + +#[test] +fn process_message_fails_on_max_nonce_reached() { + new_tester().execute_with(|| { + let sibling_id = 1000; + let _channel_id: ChannelId = ParaId::from(sibling_id).into(); + let origin = AggregateMessageOrigin::SnowbridgeV2(H256::zero()); + let message: Message = mock_message(sibling_id); + + let mut meter = WeightMeter::with_limit(Weight::MAX); + + Nonce::::set(u64::MAX); + + let result = OutboundQueue::process_message( + message.encode().as_slice(), + origin, + &mut meter, + &mut [0u8; 32], + ); + assert_err!(result, ProcessMessageError::Unsupported) + }) +} + +#[test] +fn process_message_fails_on_overweight_message() { + new_tester().execute_with(|| { + let sibling_id = 1000; + let _channel_id: ChannelId = ParaId::from(sibling_id).into(); + let origin = AggregateMessageOrigin::SnowbridgeV2(H256::zero()); + let message: Message = mock_message(sibling_id); + let mut meter = WeightMeter::with_limit(Weight::from_parts(1, 1)); + assert_noop!( + OutboundQueue::process_message( + message.encode().as_slice(), + origin, + &mut meter, + &mut [0u8; 32] + ), + ProcessMessageError::Overweight(::WeightInfo::do_process_message()) + ); + }) +} + +#[test] +fn governance_message_not_processed_in_same_block_when_queue_congested_with_low_priority_messages() +{ + use AggregateMessageOrigin::*; + + let sibling_id: u32 = 1000; + + new_tester().execute_with(|| { + // submit a lot of low priority messages from asset_hub which will need multiple blocks to + // execute(20 messages for each block so 40 required at least 2 blocks) + let max_messages = 40; + for _ in 0..max_messages { + // submit low priority message + let message = mock_message(sibling_id); + let ticket = OutboundQueue::validate(&message).unwrap(); + OutboundQueue::deliver(ticket).unwrap(); + } + + let footprint = + MessageQueue::footprint(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64))); + assert_eq!(footprint.storage.count, (max_messages) as u64); + + let message = mock_governance_message::(); + let ticket = OutboundQueue::validate(&message).unwrap(); + OutboundQueue::deliver(ticket).unwrap(); + + // move to next block + ServiceWeight::set(Some(Weight::MAX)); + run_to_end_of_next_block(); + + // first process 20 messages from sibling channel + let footprint = + MessageQueue::footprint(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64))); + assert_eq!(footprint.storage.count, 40 - 20); + + // and governance message does not have the chance to execute in same block + let footprint = MessageQueue::footprint(SnowbridgeV2(bridge_hub_root_origin())); + assert_eq!(footprint.storage.count, 1); + + // move to next block + ServiceWeight::set(Some(Weight::MAX)); + run_to_end_of_next_block(); + + // now governance message get executed in this block + let footprint = MessageQueue::footprint(SnowbridgeV2(bridge_hub_root_origin())); + assert_eq!(footprint.storage.count, 0); + + // and this time process 19 messages from sibling channel so we have 1 message left + let footprint = + MessageQueue::footprint(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64))); + assert_eq!(footprint.storage.count, 1); + + // move to the next block, the last 1 message from sibling channel get executed + ServiceWeight::set(Some(Weight::MAX)); + run_to_end_of_next_block(); + let footprint = + MessageQueue::footprint(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64))); + assert_eq!(footprint.storage.count, 0); + }); +} + +#[test] +fn encode_digest_item_with_correct_index() { + new_tester().execute_with(|| { + let digest_item: DigestItem = CustomDigestItem::Snowbridge(H256::default()).into(); + let enum_prefix = match digest_item { + DigestItem::Other(data) => data[0], + _ => u8::MAX, + }; + assert_eq!(enum_prefix, 0); + }); +} + +#[test] +fn encode_digest_item() { + new_tester().execute_with(|| { + let digest_item: DigestItem = CustomDigestItem::Snowbridge([5u8; 32].into()).into(); + let digest_item_raw = digest_item.encode(); + assert_eq!(digest_item_raw[0], 0); // DigestItem::Other + assert_eq!(digest_item_raw[2], 0); // CustomDigestItem::Snowbridge + assert_eq!( + digest_item_raw, + [ + 0, 132, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5 + ] + ); + }); +} + +fn encode_mock_message(message: Message) -> Vec { + let commands: Vec = message + .commands + .into_iter() + .map(|command| CommandWrapper { + kind: command.index(), + gas: ::GasMeter::maximum_dispatch_gas_used_at_most(&command), + payload: Bytes::from(command.abi_encode()), + }) + .collect(); + + // print the abi-encoded message and decode with solidity test + let committed_message = OutboundMessageWrapper { + origin: FixedBytes::from(message.origin.as_fixed_bytes()), + nonce: 1, + topic: FixedBytes::from(message.id.as_fixed_bytes()), + commands, + }; + let message_abi_encoded = committed_message.abi_encode(); + message_abi_encoded +} + +#[test] +fn encode_unlock_message() { + let message: Message = mock_message(1000); + let message_abi_encoded = encode_mock_message(message); + println!("{}", HexDisplay::from(&message_abi_encoded)); + assert_eq!(hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000eda338e4dc46038493b885327842fd3e301cab3900000000000000000000000000000000000000000000000000000000000f4240").to_vec(), message_abi_encoded) +} + +#[test] +fn encode_register_pna() { + let message: Message = mock_register_token_message(1000); + let message_abi_encoded = encode_mock_message(message); + println!("{}", HexDisplay::from(&message_abi_encoded)); + assert_eq!(hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000124f80000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec(), message_abi_encoded) +} diff --git a/operator/pallets/outbound-queue-v2/src/types.rs b/operator/pallets/outbound-queue-v2/src/types.rs new file mode 100644 index 00000000..dccc9a72 --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/types.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use super::Pallet; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::ProcessMessage; +use scale_info::TypeInfo; +pub use snowbridge_merkle_tree::MerkleProof; +use sp_core::H256; +use sp_runtime::RuntimeDebug; +use sp_std::prelude::*; + +pub type ProcessMessageOriginOf = as ProcessMessage>::Origin; + +/// Pending order +#[derive(Encode, Decode, TypeInfo, Clone, Eq, PartialEq, RuntimeDebug, MaxEncodedLen)] +pub struct PendingOrder { + /// The nonce used to identify the message + pub nonce: u64, + /// The block number in which the message was committed + pub block_number: BlockNumber, + /// The fee in Ether provided by the user to incentivize message delivery + #[codec(compact)] + pub fee: u128, +} + +/// Hook that will be called when a 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/operator/pallets/outbound-queue-v2/src/weights.rs b/operator/pallets/outbound-queue-v2/src/weights.rs new file mode 100644 index 00000000..725026ac --- /dev/null +++ b/operator/pallets/outbound-queue-v2/src/weights.rs @@ -0,0 +1,104 @@ + +//! Autogenerated weights for `snowbridge-pallet-outbound-queue` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-10-19, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `192.168.1.7`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-rococo-dev")`, DB CACHE: `1024` + +// Executed Command: +// target/release/polkadot-parachain +// benchmark +// pallet +// --chain=bridge-hub-rococo-dev +// --pallet=snowbridge-pallet-outbound-queue +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --template +// ../parachain/templates/module-weight-template.hbs +// --output +// ../parachain/pallets/outbound-queue/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `snowbridge-pallet-outbound-queue`. +pub trait WeightInfo { + fn do_process_message() -> Weight; + fn commit() -> Weight; + fn commit_single() -> Weight; + fn submit_delivery_receipt() -> Weight; + fn on_initialize() -> Weight; + fn process() -> Weight; +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: EthereumOutboundQueue MessageLeaves (r:1 w:1) + /// Proof Skipped: EthereumOutboundQueue MessageLeaves (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: EthereumOutboundQueue PendingHighPriorityMessageCount (r:1 w:1) + /// Proof: EthereumOutboundQueue PendingHighPriorityMessageCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue Nonce (r:1 w:1) + /// Proof: EthereumOutboundQueue Nonce (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + /// Storage: EthereumOutboundQueue Messages (r:1 w:1) + /// Proof Skipped: EthereumOutboundQueue Messages (max_values: Some(1), max_size: None, mode: Measured) + fn do_process_message() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `3485` + // Minimum execution time: 39_000_000 picoseconds. + Weight::from_parts(39_000_000, 3485) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: EthereumOutboundQueue MessageLeaves (r:1 w:0) + /// Proof Skipped: EthereumOutboundQueue MessageLeaves (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: System Digest (r:1 w:1) + /// Proof Skipped: System Digest (max_values: Some(1), max_size: None, mode: Measured) + fn commit() -> Weight { + // Proof Size summary in bytes: + // Measured: `1094` + // Estimated: `2579` + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(28_000_000, 2579) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + + fn commit_single() -> Weight { + // Proof Size summary in bytes: + // Measured: `1094` + // Estimated: `2579` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(9_000_000, 1586) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + + fn submit_delivery_receipt() -> Weight { + Weight::from_parts(70_000_000, 0) + .saturating_add(Weight::from_parts(0, 3601)) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(2)) + } + + fn on_initialize() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(5)) + } + + fn process() -> Weight { + Weight::from_parts(506_000_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + .saturating_add(RocksDbWeight::get().reads(1)) + .saturating_add(RocksDbWeight::get().writes(35)) + } +} diff --git a/operator/primitives/snowbridge/bridge-hub-common/Cargo.toml b/operator/primitives/snowbridge/bridge-hub-common/Cargo.toml new file mode 100644 index 00000000..f4ecc574 --- /dev/null +++ b/operator/primitives/snowbridge/bridge-hub-common/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "bridge-hub-common" +version = "0.13.1" +authors.workspace = true +edition.workspace = true +description = "Bridge hub common utilities" +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true + +[dependencies] +codec = { features = ["derive"], workspace = true } +cumulus-primitives-core.workspace = true +frame-support.workspace = true +pallet-message-queue.workspace = true +scale-info = { features = ["derive"], workspace = true } +snowbridge-core.workspace = true +sp-core.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +xcm-builder.workspace = true +xcm-executor.workspace = true +xcm.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "cumulus-primitives-core/std", + "frame-support/std", + "pallet-message-queue/std", + "scale-info/std", + "snowbridge-core/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] + +runtime-benchmarks = [ + "cumulus-primitives-core/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", + "snowbridge-core/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] diff --git a/operator/primitives/snowbridge/bridge-hub-common/src/digest_item.rs b/operator/primitives/snowbridge/bridge-hub-common/src/digest_item.rs new file mode 100644 index 00000000..85ff8804 --- /dev/null +++ b/operator/primitives/snowbridge/bridge-hub-common/src/digest_item.rs @@ -0,0 +1,37 @@ +// 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. +//! Custom digest items + +use codec::{Decode, Encode}; +use sp_core::{RuntimeDebug, H256}; +use sp_runtime::generic::DigestItem; + +/// Custom header digest items, inserted as DigestItem::Other +#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, RuntimeDebug)] +pub enum CustomDigestItem { + #[codec(index = 0)] + /// Merkle root of outbound Snowbridge messages. + Snowbridge(H256), + #[codec(index = 1)] + /// Merkle root of outbound Snowbridge V2 messages. + SnowbridgeV2(H256), +} + +/// Convert custom application digest item into a concrete digest item +impl From for DigestItem { + fn from(val: CustomDigestItem) -> Self { + DigestItem::Other(val.encode()) + } +} diff --git a/operator/primitives/snowbridge/bridge-hub-common/src/lib.rs b/operator/primitives/snowbridge/bridge-hub-common/src/lib.rs new file mode 100644 index 00000000..cb284a24 --- /dev/null +++ b/operator/primitives/snowbridge/bridge-hub-common/src/lib.rs @@ -0,0 +1,24 @@ +// 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. +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod digest_item; +pub mod message_queue; +pub mod xcm_version; + +pub use digest_item::CustomDigestItem; +pub use message_queue::{ + AggregateMessageOrigin, BridgeHubDualMessageRouter, BridgeHubMessageRouter, +}; diff --git a/operator/primitives/snowbridge/bridge-hub-common/src/message_queue.rs b/operator/primitives/snowbridge/bridge-hub-common/src/message_queue.rs new file mode 100644 index 00000000..2f0a6d09 --- /dev/null +++ b/operator/primitives/snowbridge/bridge-hub-common/src/message_queue.rs @@ -0,0 +1,182 @@ +// 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. +//! Runtime configuration for MessageQueue pallet +use codec::{Decode, Encode, MaxEncodedLen}; +use core::marker::PhantomData; +use cumulus_primitives_core::{AggregateMessageOrigin as CumulusAggregateMessageOrigin, ParaId}; +use frame_support::{ + traits::{ProcessMessage, ProcessMessageError, QueueFootprint, QueuePausedQuery}, + weights::WeightMeter, +}; +use pallet_message_queue::OnQueueChanged; +use scale_info::TypeInfo; +use snowbridge_core::ChannelId; +use sp_core::H256; +use xcm::latest::prelude::{Junction, Location}; + +/// The aggregate origin of an inbound message. +/// This is specialized for BridgeHub, as the snowbridge-outbound-queue-pallet is also using +/// the shared MessageQueue pallet. +#[derive(Encode, Decode, Copy, MaxEncodedLen, Clone, Eq, PartialEq, TypeInfo, Debug)] +pub enum AggregateMessageOrigin { + /// The message came from the para-chain itself. + Here, + /// The message came from the relay-chain. + /// + /// This is used by the DMP queue. + Parent, + /// The message came from a sibling para-chain. + /// + /// This is used by the HRMP queue. + Sibling(ParaId), + /// The message came from a snowbridge channel. + /// + /// This is used by Snowbridge inbound queue. + Snowbridge(ChannelId), + SnowbridgeV2(H256), +} + +impl From for Location { + fn from(origin: AggregateMessageOrigin) -> Self { + use AggregateMessageOrigin::*; + match origin { + Here => Location::here(), + Parent => Location::parent(), + Sibling(id) => Location::new(1, Junction::Parachain(id.into())), + // NOTE: We don't need this conversion for Snowbridge. However, we have to + // implement it anyway as xcm_builder::ProcessXcmMessage requires it. + _ => Location::default(), + } + } +} + +impl From for AggregateMessageOrigin { + fn from(origin: CumulusAggregateMessageOrigin) -> Self { + match origin { + CumulusAggregateMessageOrigin::Here => Self::Here, + CumulusAggregateMessageOrigin::Parent => Self::Parent, + CumulusAggregateMessageOrigin::Sibling(id) => Self::Sibling(id), + } + } +} + +#[cfg(feature = "runtime-benchmarks")] +impl From for AggregateMessageOrigin { + fn from(x: u32) -> Self { + match x { + 0 => Self::Here, + 1 => Self::Parent, + p => Self::Sibling(ParaId::from(p)), + } + } +} + +/// Routes messages to either the XCMP or Snowbridge processor. +pub struct BridgeHubMessageRouter( + PhantomData<(XcmpProcessor, SnowbridgeProcessor)>, +) +where + XcmpProcessor: ProcessMessage, + SnowbridgeProcessor: ProcessMessage; +impl ProcessMessage + for BridgeHubMessageRouter +where + XcmpProcessor: ProcessMessage, + SnowbridgeProcessor: ProcessMessage, +{ + type Origin = AggregateMessageOrigin; + fn process_message( + message: &[u8], + origin: Self::Origin, + meter: &mut WeightMeter, + id: &mut [u8; 32], + ) -> Result { + use AggregateMessageOrigin::*; + match origin { + Here | Parent | Sibling(_) => { + XcmpProcessor::process_message(message, origin, meter, id) + } + Snowbridge(_) => SnowbridgeProcessor::process_message(message, origin, meter, id), + SnowbridgeV2(_) => Err(ProcessMessageError::Unsupported), + } + } +} + +/// Routes messages to either the XCMP|Snowbridge V1 processor|Snowbridge V2 processor +pub struct BridgeHubDualMessageRouter( + PhantomData<(XcmpProcessor, SnowbridgeProcessor, SnowbridgeProcessorV2)>, +) +where + XcmpProcessor: ProcessMessage, + SnowbridgeProcessor: ProcessMessage; + +impl ProcessMessage + for BridgeHubDualMessageRouter +where + XcmpProcessor: ProcessMessage, + SnowbridgeProcessor: ProcessMessage, + SnowbridgeProcessorV2: ProcessMessage, +{ + type Origin = AggregateMessageOrigin; + + fn process_message( + message: &[u8], + origin: Self::Origin, + meter: &mut WeightMeter, + id: &mut [u8; 32], + ) -> Result { + use AggregateMessageOrigin::*; + match origin { + Here | Parent | Sibling(_) => { + XcmpProcessor::process_message(message, origin, meter, id) + } + Snowbridge(_) => SnowbridgeProcessor::process_message(message, origin, meter, id), + SnowbridgeV2(_) => SnowbridgeProcessorV2::process_message(message, origin, meter, id), + } + } +} + +/// Narrow the scope of the `Inner` query from `AggregateMessageOrigin` to `ParaId`. +/// +/// All non-`Sibling` variants will be ignored. +pub struct NarrowOriginToSibling(PhantomData); +impl> QueuePausedQuery + for NarrowOriginToSibling +{ + fn is_paused(origin: &AggregateMessageOrigin) -> bool { + match origin { + AggregateMessageOrigin::Sibling(id) => Inner::is_paused(id), + _ => false, + } + } +} + +impl> OnQueueChanged + for NarrowOriginToSibling +{ + fn on_queue_changed(origin: AggregateMessageOrigin, fp: QueueFootprint) { + if let AggregateMessageOrigin::Sibling(id) = origin { + Inner::on_queue_changed(id, fp) + } + } +} + +/// Convert a sibling `ParaId` to an `AggregateMessageOrigin`. +pub struct ParaIdToSibling; +impl sp_runtime::traits::Convert for ParaIdToSibling { + fn convert(para_id: ParaId) -> AggregateMessageOrigin { + AggregateMessageOrigin::Sibling(para_id) + } +} diff --git a/operator/primitives/snowbridge/bridge-hub-common/src/xcm_version.rs b/operator/primitives/snowbridge/bridge-hub-common/src/xcm_version.rs new file mode 100644 index 00000000..be42fd73 --- /dev/null +++ b/operator/primitives/snowbridge/bridge-hub-common/src/xcm_version.rs @@ -0,0 +1,44 @@ +// 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. + +//! Custom XCM implementation. + +use frame_support::traits::Get; +use xcm::{ + latest::prelude::*, + prelude::{GetVersion, XcmVersion}, +}; + +/// Adapter for the implementation of `GetVersion`, which attempts to find the minimal +/// configured XCM version between the destination `dest` and the bridge hub location provided as +/// `Get`. +pub struct XcmVersionOfDestAndRemoteBridge( + sp_std::marker::PhantomData<(Version, RemoteBridge)>, +); +impl> GetVersion + for XcmVersionOfDestAndRemoteBridge +{ + fn get_version_for(dest: &Location) -> Option { + let dest_version = Version::get_version_for(dest); + let bridge_hub_version = Version::get_version_for(&RemoteBridge::get()); + + match (dest_version, bridge_hub_version) { + (Some(dv), Some(bhv)) => Some(sp_std::cmp::min(dv, bhv)), + (Some(dv), None) => Some(dv), + (None, Some(bhv)) => Some(bhv), + (None, None) => None, + } + } +} diff --git a/operator/primitives/snowbridge/merkle-tree/Cargo.toml b/operator/primitives/snowbridge/merkle-tree/Cargo.toml new file mode 100644 index 00000000..f2948a35 --- /dev/null +++ b/operator/primitives/snowbridge/merkle-tree/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "snowbridge-merkle-tree" +description = "Snowbridge Merkle Tree" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[package.metadata.polkadot-sdk] +exclude-from-umbrella = true + +[dependencies] +codec = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } + +[dev-dependencies] +array-bytes = { workspace = true, default-features = true } +hex = { workspace = true, default-features = true } +hex-literal = { workspace = true, default-features = true } +sp-crypto-hashing = { workspace = true, default-features = true } +sp-tracing = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = ["codec/std", "scale-info/std", "sp-core/std", "sp-runtime/std"] diff --git a/operator/primitives/snowbridge/merkle-tree/README.md b/operator/primitives/snowbridge/merkle-tree/README.md new file mode 100644 index 00000000..a9c17ad4 --- /dev/null +++ b/operator/primitives/snowbridge/merkle-tree/README.md @@ -0,0 +1,3 @@ +# Merkle-Tree Primitives + +Contains the custom merkle tree implementation optimized for Ethereum. diff --git a/operator/primitives/snowbridge/merkle-tree/src/lib.rs b/operator/primitives/snowbridge/merkle-tree/src/lib.rs new file mode 100644 index 00000000..e580ba03 --- /dev/null +++ b/operator/primitives/snowbridge/merkle-tree/src/lib.rs @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +// SPDX-FileCopyrightText: 2021-2022 Parity Technologies (UK) Ltd. +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs)] + +//! This crate implements a simple binary Merkle Tree utilities required for inter-op with Ethereum +//! bridge & Solidity contract. +//! +//! The implementation is optimised for usage within Substrate Runtime and supports no-std +//! compilation targets. +//! +//! Merkle Tree is constructed from arbitrary-length leaves, that are initially hashed using the +//! same `\[`Hasher`\]` as the inner nodes. +//! Inner nodes are created by concatenating child hashes and hashing again. The implementation +//! does not perform any sorting of the input data (leaves) nor when inner nodes are created. +//! +//! If the number of leaves is not even, last leaf (hash of) is promoted to the upper layer. + +#[cfg(not(feature = "std"))] +extern crate alloc; +#[cfg(not(feature = "std"))] +use alloc::vec; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::{RuntimeDebug, H256}; +use sp_runtime::traits::Hash; + +/// Construct a root hash of a Binary Merkle Tree created from given leaves. +/// +/// See crate-level docs for details about Merkle Tree construction. +/// +/// In case an empty list of leaves is passed the function returns a 0-filled hash. +pub fn merkle_root(leaves: I) -> H256 +where + H: Hash, + I: Iterator, +{ + merkelize::(leaves, &mut ()) +} + +fn merkelize(leaves: I, visitor: &mut V) -> H256 +where + H: Hash, + V: Visitor, + I: Iterator, +{ + let upper = Vec::with_capacity(leaves.size_hint().0); + let mut next = match merkelize_row::(leaves, upper, visitor) { + Ok(root) => return root, + Err(next) if next.is_empty() => return H256::default(), + Err(next) => next, + }; + + let mut upper = Vec::with_capacity((next.len() + 1) / 2); + loop { + visitor.move_up(); + + match merkelize_row::(next.drain(..), upper, visitor) { + Ok(root) => return root, + Err(t) => { + // swap collections to avoid allocations + upper = next; + next = t; + } + }; + } +} + +/// A generated merkle proof. +/// +/// The structure contains all necessary data to later on verify the proof and the leaf itself. +#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)] +pub struct MerkleProof { + /// Root hash of generated merkle tree. + pub root: H256, + /// Proof items (does not contain the leaf hash, nor the root obviously). + /// + /// This vec contains all inner node hashes necessary to reconstruct the root hash given the + /// leaf hash. + pub proof: Vec, + /// Number of leaves in the original tree. + /// + /// This is needed to detect a case where we have an odd number of leaves that "get promoted" + /// to upper layers. + pub number_of_leaves: u64, + /// Index of the leaf the proof is for (0-based). + pub leaf_index: u64, + /// Leaf content (hashed). + pub leaf: H256, +} + +/// A trait of object inspecting merkle root creation. +/// +/// It can be passed to [`merkelize_row`] or [`merkelize`] functions and will be notified +/// about tree traversal. +trait Visitor { + /// We are moving one level up in the tree. + fn move_up(&mut self); + + /// We are creating an inner node from given `left` and `right` nodes. + /// + /// Note that in case of last odd node in the row `right` might be empty. + /// The method will also visit the `root` hash (level 0). + /// + /// The `index` is an index of `left` item. + fn visit(&mut self, index: u64, left: &Option, right: &Option); +} + +/// No-op implementation of the visitor. +impl Visitor for () { + fn move_up(&mut self) {} + fn visit(&mut self, _index: u64, _left: &Option, _right: &Option) {} +} + +/// Construct a Merkle Proof for leaves given by indices. +/// +/// The function constructs a (partial) Merkle Tree first and stores all elements required +/// to prove the requested item (leaf) given the root hash. +/// +/// Both the Proof and the Root Hash are returned. +/// +/// # Panic +/// +/// The function will panic if given `leaf_index` is greater than the number of leaves. +pub fn merkle_proof(leaves: I, leaf_index: u64) -> MerkleProof +where + H: Hash, + I: Iterator, +{ + let mut leaf = None; + let mut hashes = vec![]; + let mut number_of_leaves = 0; + for (idx, l) in (0u64..).zip(leaves) { + // count the leaves + number_of_leaves = idx + 1; + hashes.push(l); + // find the leaf for the proof + if idx == leaf_index { + leaf = Some(l); + } + } + + /// The struct collects a proof for single leaf. + struct ProofCollection { + proof: Vec, + position: u64, + } + + impl ProofCollection { + fn new(position: u64) -> Self { + ProofCollection { + proof: Default::default(), + position, + } + } + } + + impl Visitor for ProofCollection { + fn move_up(&mut self) { + self.position /= 2; + } + + fn visit(&mut self, index: u64, left: &Option, right: &Option) { + // we are at left branch - right goes to the proof. + if self.position == index { + if let Some(right) = right { + self.proof.push(*right); + } + } + // we are at right branch - left goes to the proof. + if self.position == index + 1 { + if let Some(left) = left { + self.proof.push(*left); + } + } + } + } + + let mut collect_proof = ProofCollection::new(leaf_index); + + let root = merkelize::(hashes.into_iter(), &mut collect_proof); + let leaf = leaf.expect("Requested `leaf_index` is greater than number of leaves."); + + MerkleProof { + root, + proof: collect_proof.proof, + number_of_leaves, + leaf_index, + leaf, + } +} + +/// Leaf node for proof verification. +/// +/// Can be either a value that needs to be hashed first, +/// or the hash itself. +#[derive(Debug, PartialEq, Eq)] +pub enum Leaf<'a> { + /// Leaf content. + Value(&'a [u8]), + /// Hash of the leaf content. + Hash(H256), +} + +impl<'a, T: AsRef<[u8]>> From<&'a T> for Leaf<'a> { + fn from(v: &'a T) -> Self { + Leaf::Value(v.as_ref()) + } +} + +impl<'a> From for Leaf<'a> { + fn from(v: H256) -> Self { + Leaf::Hash(v) + } +} + +/// Verify Merkle Proof correctness versus given root hash. +/// +/// The proof is NOT expected to contain leaf hash as the first +/// element, but only all adjacent nodes required to eventually by process of +/// concatenating and hashing end up with given root hash. +/// +/// The proof must not contain the root hash. +pub fn verify_proof<'a, H, P, L>( + root: &'a H256, + proof: P, + number_of_leaves: u64, + leaf_index: u64, + leaf: L, +) -> bool +where + H: Hash, + P: IntoIterator, + L: Into>, +{ + if leaf_index >= number_of_leaves { + return false; + } + + let leaf_hash = match leaf.into() { + Leaf::Value(content) => ::hash(content), + Leaf::Hash(hash) => hash, + }; + + let hash_len = ::LENGTH; + let mut combined = [0_u8; 64]; + let computed = proof.into_iter().fold(leaf_hash, |a, b| { + if a < b { + combined[..hash_len].copy_from_slice(a.as_ref()); + combined[hash_len..].copy_from_slice(b.as_ref()); + } else { + combined[..hash_len].copy_from_slice(b.as_ref()); + combined[hash_len..].copy_from_slice(a.as_ref()); + } + ::hash(&combined) + }); + + root == &computed +} + +/// Processes a single row (layer) of a tree by taking pairs of elements, +/// concatenating them, hashing and placing into resulting vector. +/// +/// In case only one element is provided it is returned via `Ok` result, in any other case (also an +/// empty iterator) an `Err` with the inner nodes of upper layer is returned. +fn merkelize_row( + mut iter: I, + mut next: Vec, + visitor: &mut V, +) -> Result> +where + H: Hash, + V: Visitor, + I: Iterator, +{ + next.clear(); + + let hash_len = ::LENGTH; + let mut index = 0; + let mut combined = vec![0_u8; hash_len * 2]; + loop { + let a = iter.next(); + let b = iter.next(); + visitor.visit(index, &a, &b); + + index += 2; + match (a, b) { + (Some(a), Some(b)) => { + if a < b { + combined[..hash_len].copy_from_slice(a.as_ref()); + combined[hash_len..].copy_from_slice(b.as_ref()); + } else { + combined[..hash_len].copy_from_slice(b.as_ref()); + combined[hash_len..].copy_from_slice(a.as_ref()); + } + + next.push(::hash(&combined)); + } + // Odd number of items. Promote the item to the upper layer. + (Some(a), None) if !next.is_empty() => { + next.push(a); + } + // Last item = root. + (Some(a), None) => return Ok(a), + // Finish up, no more items. + _ => return Err(next), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use sp_crypto_hashing::keccak_256; + use sp_runtime::traits::Keccak256; + + fn make_leaves(count: u64) -> Vec { + (0..count) + .map(|i| keccak_256(&i.to_le_bytes()).into()) + .collect() + } + + #[test] + fn should_generate_empty_root() { + // given + sp_tracing::init_for_tests(); + let data = vec![]; + + // when + let out = merkle_root::(data.into_iter()); + + // then + assert_eq!( + hex::encode(out), + "0000000000000000000000000000000000000000000000000000000000000000" + ); + } + + #[test] + fn should_generate_single_root() { + // given + sp_tracing::init_for_tests(); + let data = make_leaves(1); + + // when + let out = merkle_root::(data.into_iter()); + + // then + assert_eq!( + hex::encode(out), + "011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce" + ); + } + + #[test] + fn should_generate_root_pow_2() { + // given + sp_tracing::init_for_tests(); + let data = make_leaves(2); + + // when + let out = merkle_root::(data.into_iter()); + + // then + assert_eq!( + hex::encode(out), + "e497bd1c13b13a60af56fa0d2703517c232fde213ad20d2c3dd60735c6604512" + ); + } + + #[test] + fn should_generate_root_complex() { + sp_tracing::init_for_tests(); + let test = |root, data: Vec| { + assert_eq!( + array_bytes::bytes2hex("", merkle_root::(data.into_iter()).as_ref()), + root + ); + }; + + test( + "816cc37bd8d39f7b0851838ebc875faf2afe58a03e95aca3b1333b3693f39dd3", + make_leaves(3), + ); + + test( + "7501ea976cb92f305cca65ab11254589ea28bb8b59d3161506350adaa237d22f", + make_leaves(4), + ); + + test( + "d26ba4eb398747bdd39255b1fadb99b803ce39696021b3b0bff7301ac146ee4e", + make_leaves(10), + ); + } + + #[test] + #[ignore] + fn should_generate_and_verify_proof() { + // given + sp_tracing::init_for_tests(); + let data: Vec = make_leaves(3); + + // when + let proof0 = merkle_proof::(data.clone().into_iter(), 0); + assert!(verify_proof::( + &proof0.root, + proof0.proof.clone(), + data.len() as u64, + proof0.leaf_index, + &data[0], + )); + + let proof1 = merkle_proof::(data.clone().into_iter(), 1); + assert!(verify_proof::( + &proof1.root, + proof1.proof, + data.len() as u64, + proof1.leaf_index, + &proof1.leaf, + )); + + let proof2 = merkle_proof::(data.clone().into_iter(), 2); + assert!(verify_proof::( + &proof2.root, + proof2.proof, + data.len() as u64, + proof2.leaf_index, + &proof2.leaf + )); + + // then + assert_eq!(hex::encode(proof0.root), hex::encode(proof1.root)); + assert_eq!(hex::encode(proof2.root), hex::encode(proof1.root)); + + assert!(!verify_proof::( + &H256::from_slice(&hex!( + "fb3b3be94be9e983ba5e094c9c51a7d96a4fa2e5d8e891df00ca89ba05bb1239" + )), + proof0.proof, + data.len() as u64, + proof0.leaf_index, + &proof0.leaf + )); + + assert!(!verify_proof::( + &proof0.root, + vec![], + data.len() as u64, + proof0.leaf_index, + &proof0.leaf + )); + } + + #[test] + #[should_panic] + fn should_panic_on_invalid_leaf_index() { + sp_tracing::init_for_tests(); + merkle_proof::(make_leaves(1).into_iter(), 5); + } +} diff --git a/operator/runtime/Cargo.toml b/operator/runtime/Cargo.toml index be773eb9..4d03501d 100644 --- a/operator/runtime/Cargo.toml +++ b/operator/runtime/Cargo.toml @@ -13,6 +13,7 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] +bridge-hub-common = { workspace = true, optional = true } codec = { features = ["derive"], workspace = true } dhp-bridge = { workspace = true } datahaven-runtime-common = { workspace = true } @@ -41,6 +42,7 @@ pallet-evm-chain-id = { workspace = true } pallet-grandpa = { workspace = true } pallet-identity = { workspace = true } pallet-im-online = { workspace = true } +pallet-message-queue = { workspace = true } pallet-mmr = { workspace = true } pallet-multisig = { workspace = true } pallet-offences = { workspace = true } @@ -60,8 +62,12 @@ scale-info = { features = ["derive", "serde"], workspace = true } serde_json = { workspace = true, default-features = false, features = ["alloc"] } snowbridge-beacon-primitives = { workspace = true } snowbridge-inbound-queue-primitives = { workspace = true } +snowbridge-outbound-queue-primitives = { workspace = true } snowbridge-pallet-ethereum-client = { workspace = true } snowbridge-pallet-inbound-queue-v2 = { workspace = true } +snowbridge-pallet-outbound-queue-v2 = { workspace = true } +snowbridge-merkle-tree = { workspace = true } +snowbridge-outbound-queue-v2-runtime-api = { workspace = true } snowbridge-verification-primitives = { workspace = true } sp-api = { workspace = true } sp-block-builder = { workspace = true } @@ -112,6 +118,7 @@ std = [ "pallet-grandpa/std", "pallet-identity/std", "pallet-im-online/std", + "pallet-message-queue/std", "pallet-mmr/std", "pallet-multisig/std", "pallet-offences/std", @@ -131,11 +138,14 @@ std = [ "serde_json/std", "snowbridge-beacon-primitives/std", "snowbridge-inbound-queue-primitives/std", + "snowbridge-outbound-queue-primitives/std", "snowbridge-pallet-ethereum-client/std", "snowbridge-pallet-inbound-queue-v2/std", + "snowbridge-pallet-outbound-queue-v2/std", + "snowbridge-merkle-tree/std", + "snowbridge-outbound-queue-v2-runtime-api/std", "dhp-bridge/std", "snowbridge-verification-primitives/std", - "sp-api/std", "sp-block-builder/std", "sp-consensus-babe/std", @@ -156,6 +166,7 @@ std = [ ] runtime-benchmarks = [ + "bridge-hub-common", "datahaven-runtime-common/runtime-benchmarks", "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", @@ -168,6 +179,7 @@ runtime-benchmarks = [ "pallet-grandpa/runtime-benchmarks", "pallet-identity/runtime-benchmarks", "pallet-im-online/runtime-benchmarks", + "pallet-message-queue/runtime-benchmarks", "pallet-mmr/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-offences/runtime-benchmarks", @@ -179,10 +191,11 @@ runtime-benchmarks = [ "pallet-utility/runtime-benchmarks", "polkadot-primitives/runtime-benchmarks", "polkadot-runtime-common/runtime-benchmarks", - "polkadot-primitives/runtime-benchmarks", + "polkadot-primitives/runtime-benchmarks", "snowbridge-inbound-queue-primitives/runtime-benchmarks", "snowbridge-pallet-ethereum-client/runtime-benchmarks", "snowbridge-pallet-inbound-queue-v2/runtime-benchmarks", + "snowbridge-pallet-outbound-queue-v2/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -202,6 +215,7 @@ try-runtime = [ "pallet-grandpa/try-runtime", "pallet-identity/try-runtime", "pallet-im-online/try-runtime", + "pallet-message-queue/try-runtime", "pallet-mmr/try-runtime", "pallet-multisig/try-runtime", "pallet-offences/try-runtime", @@ -216,6 +230,7 @@ try-runtime = [ "polkadot-runtime-common/try-runtime", "snowbridge-pallet-ethereum-client/try-runtime", "snowbridge-pallet-inbound-queue-v2/try-runtime", + "snowbridge-pallet-outbound-queue-v2/try-runtime", "sp-runtime/try-runtime", ] diff --git a/operator/runtime/common/Cargo.toml b/operator/runtime/common/Cargo.toml index 488f629a..0647ee89 100644 --- a/operator/runtime/common/Cargo.toml +++ b/operator/runtime/common/Cargo.toml @@ -8,18 +8,20 @@ edition.workspace = true frame-support.workspace = true polkadot-primitives.workspace = true polkadot-runtime-common.workspace = true +xcm = { workspace = true } [features] default = ["std"] std = [ "frame-support/std", "polkadot-primitives/std", - "polkadot-runtime-common/std" + "polkadot-runtime-common/std", + "xcm/std" ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", "polkadot-primitives/runtime-benchmarks", - "polkadot-runtime-common/runtime-benchmarks" + "polkadot-runtime-common/runtime-benchmarks", ] # Set timing constants (e.g. session period) to faster versions to speed up testing. diff --git a/operator/runtime/common/src/lib.rs b/operator/runtime/common/src/lib.rs index 3f103891..41a2d7e9 100644 --- a/operator/runtime/common/src/lib.rs +++ b/operator/runtime/common/src/lib.rs @@ -17,6 +17,5 @@ #![cfg_attr(not(feature = "std"), no_std)] pub mod constants; -pub use constants::gas; -pub use constants::time; +pub use constants::*; pub mod impl_on_charge_evm_transaction; diff --git a/operator/runtime/src/apis.rs b/operator/runtime/src/apis.rs index a7968ceb..dc9ae26a 100644 --- a/operator/runtime/src/apis.rs +++ b/operator/runtime/src/apis.rs @@ -487,6 +487,12 @@ impl_runtime_apis! { } } + impl snowbridge_outbound_queue_v2_runtime_api::OutboundQueueV2Api for Runtime { + fn prove_message(leaf_index: u64) -> Option { + snowbridge_pallet_outbound_queue_v2::api::prove_message::(leaf_index) + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( diff --git a/operator/runtime/src/configs/mod.rs b/operator/runtime/src/configs/mod.rs index 33e9e666..7747d501 100644 --- a/operator/runtime/src/configs/mod.rs +++ b/operator/runtime/src/configs/mod.rs @@ -42,7 +42,7 @@ use frame_support::{ }, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, - IdentityFee, Weight, + IdentityFee, RuntimeDbWeight, Weight, }, }; use frame_system::{ @@ -63,11 +63,12 @@ use pallet_transaction_payment::{ use polkadot_primitives::Moment; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_inbound_queue_primitives::RewardLedger; +use snowbridge_outbound_queue_primitives::v2::ConstantGasMeter; use sp_consensus_beefy::{ ecdsa_crypto::AuthorityId as BeefyId, mmr::{BeefyDataProvider, MmrLeafVersion}, }; -use sp_core::{crypto::KeyTypeId, H160, H256, U256}; +use sp_core::{crypto::KeyTypeId, Get, H160, H256, U256}; use sp_runtime::{ traits::{ConvertInto, IdentityLookup, Keccak256, One, OpaqueKeys, UniqueSaturatedInto}, FixedPointNumber, Perbill, @@ -78,17 +79,21 @@ use sp_std::{ prelude::*, }; use sp_version::RuntimeVersion; +use xcm::latest::NetworkId; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, Hash, Historical, ImOnline, Nonce, Offences, OriginCaller, - PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, - Timestamp, ValidatorSet, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, - UNIT, VERSION, + EthereumBeaconClient, EvmChainId, Hash, Historical, ImOnline, MessageQueue, Nonce, Offences, + OriginCaller, OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, + RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, + Signature, System, Timestamp, ValidatorSet, EXISTENTIAL_DEPOSIT, SLOT_DURATION, + STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use runtime_params::RuntimeParameters; +#[cfg(feature = "runtime-benchmarks")] +use bridge_hub_common::AggregateMessageOrigin; + const EVM_CHAIN_ID: u64 = 1289; const SS58_FORMAT: u16 = EVM_CHAIN_ID as u16; @@ -493,6 +498,34 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } +parameter_types! { + /// Amount of weight that can be spent per block to service messages. + /// + /// # WARNING + /// + /// This is not a good value for para-chains since the `Scheduler` already uses up to 80% block weight. + pub MessageQueueServiceWeight: Weight = Perbill::from_percent(20) * RuntimeBlockWeights::get().max_block; + pub const MessageQueueHeapSize: u32 = 32 * 1024; + pub const MessageQueueMaxStale: u32 = 96; +} + +impl pallet_message_queue::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + #[cfg(not(feature = "runtime-benchmarks"))] + type MessageProcessor = OutboundQueueV2; + #[cfg(feature = "runtime-benchmarks")] + type MessageProcessor = + pallet_message_queue::mock_helpers::NoopMessageProcessor; + type Size = u32; + type QueueChangeHandler = (); + type QueuePausedQuery = (); + type HeapSize = MessageQueueHeapSize; + type MaxStale = MessageQueueMaxStale; + type ServiceWeight = MessageQueueServiceWeight; + type IdleMaxServiceWeight = MessageQueueServiceWeight; + type WeightInfo = (); +} + //╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ //║ FRONTIER (EVM) PALLETS ║ //╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ @@ -523,7 +556,7 @@ impl FeeCalculator for TransactionPaymentAsGasPrice { .saturating_mul_int((WEIGHT_FEE).saturating_mul(WEIGHT_PER_GAS as u128)); ( min_gas_price.into(), - ::DbWeight::get().reads(1), + <::DbWeight as Get>::get().reads(1), ) } } @@ -691,6 +724,34 @@ impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { type Helper = Runtime; } +parameter_types! { + /// Network and location for the Ethereum chain. + /// Using the Sepolia Ethereum testnet, with chain ID 11155111. + /// + /// + pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 }; +} + +impl snowbridge_pallet_outbound_queue_v2::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Hashing = Keccak256; + type MessageQueue = MessageQueue; + type GasMeter = ConstantGasMeter; + type Balance = Balance; + type MaxMessagePayloadSize = ConstU32<2048>; + type MaxMessagesPerBlock = ConstU32<32>; + type OnNewCommitment = (); + type WeightToFee = IdentityFee; + type Verifier = EthereumBeaconClient; + type GatewayAddress = runtime_params::dynamic_params::runtime_config::EthereumGatewayAddress; + type RewardKind = (); + type DefaultRewardKind = DefaultRewardKind; + type RewardPayment = DummyRewardPayment; + type EthereumNetwork = EthereumNetwork; + type ConvertAssetId = (); + type WeightInfo = (); +} + //╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ //║ STORAGEHUB PALLETS ║ //╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ diff --git a/operator/runtime/src/lib.rs b/operator/runtime/src/lib.rs index 393751c4..6e97374b 100644 --- a/operator/runtime/src/lib.rs +++ b/operator/runtime/src/lib.rs @@ -307,29 +307,35 @@ mod runtime { #[runtime::pallet_index(36)] pub type Sudo = pallet_sudo; + + #[runtime::pallet_index(37)] + pub type MessageQueue = pallet_message_queue; // ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝ // ╔════════════════════ Frontier (EVM) Pallets ═════════════════════╗ - #[runtime::pallet_index(40)] + #[runtime::pallet_index(50)] pub type Ethereum = pallet_ethereum; - #[runtime::pallet_index(41)] + #[runtime::pallet_index(51)] pub type Evm = pallet_evm; - #[runtime::pallet_index(42)] + #[runtime::pallet_index(52)] pub type EvmChainId = pallet_evm_chain_id; // ╚════════════════════ Frontier (EVM) Pallets ═════════════════════╝ // ╔══════════════════════ Snowbridge Pallets ═══════════════════════╗ - #[runtime::pallet_index(50)] + #[runtime::pallet_index(60)] pub type EthereumBeaconClient = snowbridge_pallet_ethereum_client; - #[runtime::pallet_index(51)] + #[runtime::pallet_index(61)] pub type InboundQueueV2 = snowbridge_pallet_inbound_queue_v2; + + #[runtime::pallet_index(62)] + pub type OutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; // ╚══════════════════════ Snowbridge Pallets ═══════════════════════╝ // ╔══════════════════════ StorageHub Pallets ═══════════════════════╗ - // Start with index 60 + // Start with index 70 // ╚══════════════════════ StorageHub Pallets ═══════════════════════╝ // ╔═══════════════════ DataHaven-specific Pallets ══════════════════╗