diff --git a/Cargo.lock b/Cargo.lock index 232ece995f9f..0864801af261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19733,8 +19733,11 @@ dependencies = [ name = "snowbridge-router-primitives" version = "0.16.0" dependencies = [ + "alloy-primitives", + "alloy-sol-types", "frame-support", "hex-literal", + "impl-trait-for-tuples", "log", "parity-scale-codec", "scale-info", diff --git a/bridges/snowbridge/pallets/inbound-queue/src/envelope.rs b/bridges/snowbridge/pallets/inbound-queue/src/envelope.rs deleted file mode 100644 index 31a8992442d8..000000000000 --- a/bridges/snowbridge/pallets/inbound-queue/src/envelope.rs +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2023 Snowfork -use snowbridge_core::{inbound::Log, ChannelId}; - -use sp_core::{RuntimeDebug, H160, H256}; -use sp_std::prelude::*; - -use alloy_primitives::B256; -use alloy_sol_types::{sol, SolEvent}; - -sol! { - event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload); -} - -/// An inbound message that has had its outer envelope decoded. -#[derive(Clone, RuntimeDebug)] -pub struct Envelope { - /// The address of the outbound queue on Ethereum that emitted this message as an event log - pub gateway: H160, - /// The message Channel - pub channel_id: ChannelId, - /// A nonce for enforcing replay protection and ordering. - pub nonce: u64, - /// An id for tracing the message on its route (has no role in bridge consensus) - pub message_id: H256, - /// The inner payload generated from the source application. - pub payload: Vec, -} - -#[derive(Copy, Clone, RuntimeDebug)] -pub struct EnvelopeDecodeError; - -impl TryFrom<&Log> for Envelope { - type Error = EnvelopeDecodeError; - - fn try_from(log: &Log) -> Result { - let topics: Vec = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect(); - - let event = OutboundMessageAccepted::decode_log(topics, &log.data, true) - .map_err(|_| EnvelopeDecodeError)?; - - Ok(Self { - gateway: log.address, - channel_id: ChannelId::from(event.channel_id.as_ref()), - nonce: event.nonce, - message_id: H256::from(event.message_id.as_ref()), - payload: event.payload, - }) - } -} diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs index 423b92b9fae0..614330221aa9 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs @@ -23,8 +23,6 @@ //! parachain. #![cfg_attr(not(feature = "std"), no_std)] -mod envelope; - #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -35,9 +33,9 @@ mod mock; #[cfg(test)] mod test; +pub mod xcm_message_processor; -use codec::{Decode, DecodeAll, Encode}; -use envelope::Envelope; +use codec::{Decode, Encode}; use frame_support::{ traits::{ fungible::{Inspect, Mutate}, @@ -48,7 +46,7 @@ use frame_support::{ }; use frame_system::ensure_signed; use scale_info::TypeInfo; -use sp_core::H160; +use sp_core::{H160, H256}; use sp_runtime::traits::Zero; use sp_std::vec; use xcm::prelude::{ @@ -62,7 +60,7 @@ use snowbridge_core::{ StaticLookup, }; use snowbridge_router_primitives::inbound::{ - ConvertMessage, ConvertMessageError, VersionedMessage, + envelope::Envelope, ConvertMessage, ConvertMessageError, MessageProcessor, VersionedXcmMessage, }; use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError}; @@ -84,7 +82,6 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use sp_core::H256; #[pallet::pallet] pub struct Pallet(_); @@ -139,6 +136,9 @@ pub mod pallet { /// To withdraw and deposit an asset. type AssetTransactor: TransactAsset; + + /// Process the message that was submitted + type MessageProcessor: MessageProcessor; } #[pallet::hooks] @@ -205,10 +205,12 @@ pub mod pallet { XcmpSendError::NotApplicable => Error::::Send(SendError::NotApplicable), XcmpSendError::Unroutable => Error::::Send(SendError::NotRoutable), XcmpSendError::Transport(_) => Error::::Send(SendError::Transport), - XcmpSendError::DestinationUnsupported => - Error::::Send(SendError::DestinationUnsupported), - XcmpSendError::ExceedsMaxMessageSize => - Error::::Send(SendError::ExceedsMaxMessageSize), + XcmpSendError::DestinationUnsupported => { + Error::::Send(SendError::DestinationUnsupported) + }, + XcmpSendError::ExceedsMaxMessageSize => { + Error::::Send(SendError::ExceedsMaxMessageSize) + }, XcmpSendError::MissingArgument => Error::::Send(SendError::MissingArgument), XcmpSendError::Fees => Error::::Send(SendError::Fees), } @@ -251,7 +253,7 @@ pub mod pallet { // Verify message nonce >::try_mutate(envelope.channel_id, |nonce| -> DispatchResult { if *nonce == u64::MAX { - return Err(Error::::MaxNonceReached.into()) + return Err(Error::::MaxNonceReached.into()); } if envelope.nonce != nonce.saturating_add(1) { Err(Error::::InvalidNonce.into()) @@ -275,34 +277,7 @@ pub mod pallet { T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?; } - // Decode payload into `VersionedMessage` - let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref()) - .map_err(|_| Error::::InvalidPayload)?; - - // Decode message into XCM - let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?; - - log::info!( - target: LOG_TARGET, - "💫 xcm decoded as {:?} with fee {:?}", - xcm, - fee - ); - - // Burning fees for teleport - Self::burn_fees(channel.para_id, fee)?; - - // Attempt to send XCM to a dest parachain - let message_id = Self::send_xcm(xcm, channel.para_id)?; - - Self::deposit_event(Event::MessageReceived { - channel_id: envelope.channel_id, - nonce: envelope.nonce, - message_id, - fee_burned: fee, - }); - - Ok(()) + T::MessageProcessor::process_message(channel, envelope) } /// Halt or resume all pallet operations. May only be called by root. @@ -322,7 +297,7 @@ pub mod pallet { impl Pallet { pub fn do_convert( message_id: H256, - message: VersionedMessage, + message: VersionedXcmMessage, ) -> Result<(Xcm<()>, BalanceOf), Error> { let (xcm, fee) = T::MessageConverter::convert(message_id, message) .map_err(|e| Error::::ConvertMessage(e))?; diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs index 3e67d5ab738b..cecda7251e63 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs @@ -16,12 +16,13 @@ use snowbridge_router_primitives::inbound::MessageToXcm; use sp_core::{H160, H256}; use sp_runtime::{ traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify}, - BuildStorage, FixedU128, MultiSignature, + BuildStorage, FixedU128, MultiSignature, DispatchError, }; use sp_std::{convert::From, default::Default}; use xcm::{latest::SendXcm, prelude::*}; use xcm_executor::AssetsInHolding; +use crate::xcm_message_processor::XcmMessageProcessor; use crate::{self as inbound_queue}; type Block = frame_system::mocking::MockBlock; @@ -167,10 +168,10 @@ impl StaticLookup for MockChannelLookup { type Target = Channel; fn lookup(channel_id: Self::Source) -> Option { - if channel_id != - hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into() + if channel_id + != hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into() { - return None + return None; } Some(Channel { agent_id: H256::zero(), para_id: ASSET_HUB_PARAID.into() }) } @@ -218,6 +219,30 @@ impl MaybeEquivalence for MockTokenIdConvert { } } +pub struct DummyPrefix; + +impl MessageProcessor for DummyPrefix { + fn can_process_message(_channel: &Channel, _envelope: &Envelope) -> bool { + false + } + + fn process_message(_channel: Channel, _envelope: Envelope) -> Result<(), DispatchError> { + panic!("DummyPrefix::process_message shouldn't be called"); + } +} + +pub struct DummySuffix; + +impl MessageProcessor for DummySuffix { + fn can_process_message(_channel: &Channel, _envelope: &Envelope) -> bool { + true + } + + fn process_message(_channel: Channel, _envelope: Envelope) -> Result<(), DispatchError> { + panic!("DummySuffix::process_message shouldn't be called"); + } +} + impl inbound_queue::Config for Test { type RuntimeEvent = RuntimeEvent; type Verifier = MockVerifier; @@ -243,6 +268,7 @@ impl inbound_queue::Config for Test { type LengthToFee = IdentityFee; type MaxMessageSize = ConstU32<1024>; type AssetTransactor = SuccessfulTransactor; + type MessageProcessor = (DummyPrefix, XcmMessageProcessor, DummySuffix); // We are passively testing if implementation of MessageProcessor trait works correctly for tuple } pub fn last_events(n: usize) -> Vec { diff --git a/bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs b/bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs new file mode 100644 index 000000000000..1f1ea4ea7663 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue/src/xcm_message_processor.rs @@ -0,0 +1,48 @@ +use crate::{Error, Event, LOG_TARGET}; +use codec::DecodeAll; +use core::marker::PhantomData; +use snowbridge_core::Channel; +use snowbridge_router_primitives::inbound::envelope::Envelope; +use snowbridge_router_primitives::inbound::{MessageProcessor, VersionedXcmMessage}; +use sp_runtime::DispatchError; + +pub struct XcmMessageProcessor(PhantomData); + +impl MessageProcessor for XcmMessageProcessor +where + T: crate::Config, +{ + fn can_process_message(_channel: &Channel, envelope: &Envelope) -> bool { + VersionedXcmMessage::decode_all(&mut envelope.payload.as_ref()).is_ok() + } + + fn process_message(channel: Channel, envelope: Envelope) -> Result<(), DispatchError> { + // Decode message into XCM + let (xcm, fee) = match VersionedXcmMessage::decode_all(&mut envelope.payload.as_ref()) { + Ok(message) => crate::Pallet::::do_convert(envelope.message_id, message)?, + Err(_) => return Err(Error::::InvalidPayload.into()), + }; + + log::info!( + target: LOG_TARGET, + "💫 xcm decoded as {:?} with fee {:?}", + xcm, + fee + ); + + // Burning fees for teleport + crate::Pallet::::burn_fees(channel.para_id, fee)?; + + // Attempt to send XCM to a dest parachain + let message_id = crate::Pallet::::send_xcm(xcm, channel.para_id)?; + + crate::Pallet::::deposit_event(Event::MessageReceived { + channel_id: envelope.channel_id, + nonce: envelope.nonce, + message_id, + fee_burned: fee, + }); + + Ok(()) + } +} diff --git a/bridges/snowbridge/primitives/ethereum/Cargo.toml b/bridges/snowbridge/primitives/ethereum/Cargo.toml index 9df19e313ab5..1fd5dc8b566c 100644 --- a/bridges/snowbridge/primitives/ethereum/Cargo.toml +++ b/bridges/snowbridge/primitives/ethereum/Cargo.toml @@ -18,14 +18,14 @@ codec = { features = ["derive"], workspace = true } scale-info = { features = ["derive"], workspace = true } ethbloom = { workspace = true } ethereum-types = { features = ["codec", "rlp", "serialize"], workspace = true } -hex-literal = { workspace = true } +hex-literal = { workspace = true, default-features = false } parity-bytes = { workspace = true } rlp = { workspace = true } sp-io.workspace = true sp-std.workspace = true sp-runtime.workspace = true -ethabi = { workspace = true } +ethabi = { workspace = true, default-features = false } [dev-dependencies] wasm-bindgen-test = { workspace = true } diff --git a/bridges/snowbridge/primitives/router/Cargo.toml b/bridges/snowbridge/primitives/router/Cargo.toml index e9a171cdff3f..2c0b80900a9b 100644 --- a/bridges/snowbridge/primitives/router/Cargo.toml +++ b/bridges/snowbridge/primitives/router/Cargo.toml @@ -12,7 +12,10 @@ categories = ["cryptography::cryptocurrencies"] workspace = true [dependencies] +alloy-primitives = { features = ["rlp"], workspace = true } +alloy-sol-types = { workspace = true } codec = { workspace = true } +impl-trait-for-tuples = { workspace = true, default-features = false } scale-info = { features = ["derive"], workspace = true } log = { workspace = true } frame-support.workspace = true diff --git a/bridges/snowbridge/primitives/router/src/inbound/envelope.rs b/bridges/snowbridge/primitives/router/src/inbound/envelope.rs new file mode 100644 index 000000000000..ee7f4189983e --- /dev/null +++ b/bridges/snowbridge/primitives/router/src/inbound/envelope.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use snowbridge_core::{inbound::Log, ChannelId}; + +use sp_core::{RuntimeDebug, H160, H256}; +use sp_std::prelude::*; + +use alloy_primitives::B256; +use alloy_sol_types::{sol, SolEvent}; + +sol! { + event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload); +} + +/// An inbound message that has had its outer envelope decoded. +#[derive(Clone, RuntimeDebug)] +pub struct Envelope { + /// The address of the outbound queue on Ethereum that emitted this message as an event log + pub gateway: H160, + /// The message Channel + pub channel_id: ChannelId, + /// A nonce for enforcing replay protection and ordering. + pub nonce: u64, + /// An id for tracing the message on its route (has no role in bridge consensus) + pub message_id: H256, + /// The inner payload generated from the source application. + pub payload: Vec, +} + +#[derive(Copy, Clone, RuntimeDebug)] +pub struct EnvelopeDecodeError; + +impl TryFrom<&Log> for Envelope { + type Error = EnvelopeDecodeError; + + fn try_from(log: &Log) -> Result { + let topics: Vec = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect(); + + let event = OutboundMessageAccepted::decode_log(topics, &log.data, true) + .map_err(|_| EnvelopeDecodeError)?; + + Ok(Self { + gateway: log.address, + channel_id: ChannelId::from(event.channel_id.as_ref()), + nonce: event.nonce, + message_id: H256::from(event.message_id.as_ref()), + payload: event.payload, + }) + } +} diff --git a/bridges/snowbridge/primitives/router/src/inbound/mod.rs b/bridges/snowbridge/primitives/router/src/inbound/mod.rs index a10884b45531..cad88efb6d86 100644 --- a/bridges/snowbridge/primitives/router/src/inbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/inbound/mod.rs @@ -4,18 +4,20 @@ #[cfg(test)] mod tests; +pub mod envelope; use codec::{Decode, Encode}; use core::marker::PhantomData; use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError}; use scale_info::TypeInfo; -use snowbridge_core::TokenId; +use snowbridge_core::{Channel, TokenId}; use sp_core::{Get, RuntimeDebug, H160, H256}; use sp_io::hashing::blake2_256; -use sp_runtime::{traits::MaybeEquivalence, MultiAddress}; +use sp_runtime::{DispatchError, traits::MaybeEquivalence, MultiAddress}; use sp_std::prelude::*; use xcm::prelude::{Junction::AccountKey20, *}; use xcm_executor::traits::ConvertLocation; +use crate::inbound::envelope::Envelope; const MINIMUM_DEPOSIT: u128 = 1; @@ -23,7 +25,7 @@ const MINIMUM_DEPOSIT: u128 = 1; /// we may want to evolve the protocol so that the ethereum side sends XCM messages directly. /// Instead having BridgeHub transcode the messages into XCM. #[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum VersionedMessage { +pub enum VersionedXcmMessage { V1(MessageV1), } @@ -141,10 +143,7 @@ pub trait ConvertMessage { type Balance: BalanceT + From; type AccountId; /// Converts a versioned message into an XCM message and an optional topicID - fn convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; + fn convert(message_id: H256, message: VersionedXcmMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; } pub type CallIndex = [u8; 2]; @@ -181,12 +180,9 @@ impl< type Balance = Balance; type AccountId = AccountId; - fn convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { + fn convert(message_id: H256, message: VersionedXcmMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { use Command::*; - use VersionedMessage::*; + use VersionedXcmMessage::*; match message { V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) => Ok(Self::convert_register_token(message_id, chain_id, token, fee)), @@ -469,3 +465,41 @@ impl GlobalConsensusEthereumConvertsFor { (b"ethereum-chain", chain_id).using_encoded(blake2_256) } } + + +pub trait MessageProcessor { + /// Lightweight function to check if this processor can handle the message + fn can_process_message(channel: &Channel, envelope: &Envelope) -> bool; + /// Process the message + fn process_message(channel: Channel, envelope: Envelope) -> Result<(), DispatchError>; +} + +#[impl_trait_for_tuples::impl_for_tuples(10)] +impl MessageProcessor for Tuple { + fn can_process_message(channel: &Channel, envelope: &Envelope) -> bool { + for_tuples!( #( + match Tuple::can_process_message(&channel, &envelope) { + true => { + return true; + }, + _ => {} + } + )* ); + + false + } + + + fn process_message(channel: Channel, envelope: Envelope) -> Result<(), DispatchError> { + for_tuples!( #( + match Tuple::can_process_message(&channel, &envelope) { + true => { + return Tuple::process_message(channel, envelope) + }, + _ => {} + } + )* ); + + Err(DispatchError::Other("No handler for message found")) + } +} diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs index d91a0c6895f9..4d2121a0d614 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs @@ -25,7 +25,7 @@ use snowbridge_pallet_inbound_queue_fixtures::{ }; use snowbridge_pallet_system; use snowbridge_router_primitives::inbound::{ - Command, Destination, GlobalConsensusEthereumConvertsFor, MessageV1, VersionedMessage, + Command, Destination, GlobalConsensusEthereumConvertsFor, MessageV1, VersionedXcmMessage, }; use sp_core::H256; use sp_runtime::{DispatchError::Token, TokenError::FundsUnavailable}; @@ -532,7 +532,7 @@ fn register_weth_token_in_asset_hub_fail_for_insufficient_fee() { type EthereumInboundQueue = ::EthereumInboundQueue; let message_id: H256 = [0; 32].into(); - let message = VersionedMessage::V1(MessageV1 { + let message = VersionedXcmMessage::V1(MessageV1 { chain_id: CHAIN_ID, command: Command::RegisterToken { token: WETH.into(), @@ -599,7 +599,7 @@ fn send_token_from_ethereum_to_asset_hub_with_fee(account_id: [u8; 32], fee: u12 type EthereumInboundQueue = ::EthereumInboundQueue; let message_id: H256 = [0; 32].into(); - let message = VersionedMessage::V1(MessageV1 { + let message = VersionedXcmMessage::V1(MessageV1 { chain_id: CHAIN_ID, command: Command::SendToken { token: WETH.into(),