// 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`: //! //! 1. `snowbridge_router_primitives::outbound::EthereumBlobExporter::deliver` //! 2. `snowbridge_pallet_system::Pallet::send` //! //! The message submission pipeline works like this: //! 1. The message is first validated via the implementation for //! [`snowbridge_core::outbound::SendMessage::validate`] //! 2. The message is then enqueued for later processing via the implementation for //! [`snowbridge_core::outbound::SendMessage::deliver`] //! 3. The underlying message queue is implemented by [`Config::MessageQueue`] //! 4. The message queue delivers messages back to this pallet via the implementation for //! [`frame_support::traits::ProcessMessage::process_message`] //! 5. The message is processed in `Pallet::do_process_message`: a. Assigned a nonce b. ABI-encoded, //! hashed, and stored in the `MessageLeaves` vector //! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`. //! 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 //! //! On the Ethereum side, the message root is ultimately the thing being //! verified by the Polkadot light client. //! //! # Message Priorities //! //! The processing of governance commands can never be halted. This effectively //! allows us to pause processing of normal user messages while still allowing //! governance commands to be sent to Ethereum. //! //! # Fees //! //! An upfront fee must be paid for delivering a message. This fee covers several //! components: //! 1. The weight of processing the message locally //! 2. The gas refund paid out to relayers for message submission //! 3. An additional reward paid out to relayers for message submission //! //! Messages are weighed to determine the maximum amount of gas they could //! consume on Ethereum. Using this upper bound, a final fee can be calculated. //! //! The fee calculation also requires the following parameters: //! * ETH/DOT exchange rate //! * Ether fee per unit of gas //! //! By design, it is expected that governance should manually update these //! parameters every few weeks using the `set_pricing_parameters` extrinsic in the //! system pallet. //! //! ## Fee Computation Function //! //! ```text //! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message)) //! RemoteFee(Message) = MaxGasRequired(Message) * FeePerGas + Reward //! Fee(Message) = LocalFee(Message) + (RemoteFee(Message) / Ratio("ETH/DOT")) //! ``` //! //! By design, the computed fee is always going to conservative, to cover worst-case //! costs of dispatch on Ethereum. In future iterations of the design, we will optimize //! this, or provide a mechanism to asynchronously refund a portion of collected fees. //! //! # Extrinsics //! //! * [`Call::set_operating_mode`]: Set the operating mode //! //! # Runtime API //! //! * `prove_message`: Generate a merkle proof for a committed message //! * `calculate_fee`: Calculate the delivery fee for a 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 bridge_hub_common::{AggregateMessageOrigin, CustomDigestItem}; use codec::Decode; use frame_support::{ storage::StorageStreamIter, traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError}, weights::{Weight, WeightToFee}, }; use snowbridge_core::{ outbound::{Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS}, BasicOperatingMode, ChannelId, }; use snowbridge_outbound_queue_merkle_tree::merkle_root; pub use snowbridge_outbound_queue_merkle_tree::MerkleProof; use sp_core::{H256, U256}; use sp_runtime::{ traits::{CheckedDiv, Hash}, DigestItem, }; use sp_std::prelude::*; pub use types::{CommittedMessage, ProcessMessageOriginOf}; pub use weights::WeightInfo; pub use pallet::*; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use snowbridge_core::PricingParameters; use sp_arithmetic::FixedU128; #[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; /// Number of decimal places in native currency #[pallet::constant] type Decimals: Get; /// Max bytes in a message payload #[pallet::constant] type MaxMessagePayloadSize: Get; /// Max number of messages processed per block #[pallet::constant] type MaxMessagesPerBlock: Get; /// Check whether a channel exists type Channels: Contains; type PricingParameters: Get>; /// Convert a weight value into a deductible fee based. type WeightToFee: WeightToFee; /// Weight information for extrinsics in this pallet type WeightInfo: WeightInfo; } #[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 { /// ID of the message. Usually the XCM message hash or a SetTopic. id: H256, }, /// Message will be committed at the end of current block. From now on, to track the /// progress the message, use the `nonce` of `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 }, } #[pallet::error] pub enum Error { /// The message is too large MessageTooLarge, /// The pallet is halted Halted, /// Invalid Channel InvalidChannel, } /// Messages to be committed in the current block. This storage value is killed in /// `on_initialize`, so should never go into block PoV. /// /// Is never read in the runtime, only by offchain message relayers. /// /// 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 should never go into block PoV. #[pallet::storage] #[pallet::unbounded] #[pallet::getter(fn message_leaves)] pub(super) type MessageLeaves = StorageValue<_, Vec, ValueQuery>; /// The current nonce for each message origin #[pallet::storage] pub type Nonce = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>; /// The current operating mode of the pallet. #[pallet::storage] #[pallet::getter(fn operating_mode)] pub type OperatingMode = StorageValue<_, BasicOperatingMode, ValueQuery>; #[pallet::hooks] impl Hooks> for Pallet where T::AccountId: AsRef<[u8]>, { fn on_initialize(_: BlockNumberFor) -> Weight { // Remove storage from previous block Messages::::kill(); MessageLeaves::::kill(); // Reserve some weight for the `on_finalize` handler T::WeightInfo::commit() } fn on_finalize(_: BlockNumberFor) { Self::commit(); } fn integrity_test() { let decimals = T::Decimals::get(); assert!(decimals == 10 || decimals == 12, "Decimals should be 10 or 12"); } } #[pallet::call] impl Pallet { /// Halt or resume all pallet operations. May only be called by root. #[pallet::call_index(0)] #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] pub fn set_operating_mode( origin: OriginFor, mode: BasicOperatingMode, ) -> DispatchResult { ensure_root(origin)?; OperatingMode::::put(mode); Self::deposit_event(Event::OperatingModeChanged { mode }); Ok(()) } } 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::Snowbridge(root).into(); // Insert merkle root into the header digest >::deposit_log(digest_item); 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 ); // Decode bytes into versioned message let versioned_queued_message: VersionedQueuedMessage = VersionedQueuedMessage::decode(&mut message).map_err(|_| Corrupt)?; // Convert versioned message into latest supported message version let queued_message: QueuedMessage = versioned_queued_message.try_into().map_err(|_| Unsupported)?; // Obtain next nonce let nonce = >::try_mutate( queued_message.channel_id, |nonce| -> Result { *nonce = nonce.checked_add(1).ok_or(Unsupported)?; Ok(*nonce) }, )?; let pricing_params = T::PricingParameters::get(); let command = queued_message.command.index(); let params = queued_message.command.abi_encode(); let max_dispatch_gas = T::GasMeter::maximum_dispatch_gas_used_at_most(&queued_message.command); let reward = pricing_params.rewards.remote; // Construct the final committed message let message = CommittedMessage { channel_id: queued_message.channel_id, nonce, command, params, max_dispatch_gas, max_fee_per_gas: pricing_params .fee_per_gas .try_into() .defensive_unwrap_or(u128::MAX), reward: reward.try_into().defensive_unwrap_or(u128::MAX), id: queued_message.id, }; // ABI-encode and hash the prepared message let message_abi_encoded = ethabi::encode(&[message.clone().into()]); let message_abi_encoded_hash = ::Hashing::hash(&message_abi_encoded); Messages::::append(Box::new(message)); MessageLeaves::::append(message_abi_encoded_hash); Self::deposit_event(Event::MessageAccepted { id: queued_message.id, nonce }); Ok(true) } /// Calculate total fee in native currency to cover all costs of delivering a message to the /// remote destination. See module-level documentation for more details. pub(crate) fn calculate_fee( gas_used_at_most: u64, params: PricingParameters, ) -> Fee { // Remote fee in ether let fee = Self::calculate_remote_fee( gas_used_at_most, params.fee_per_gas, params.rewards.remote, ); // downcast to u128 let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX); // convert to local currency let fee = FixedU128::from_inner(fee) .checked_div(¶ms.exchange_rate) .expect("exchange rate is not zero; qed") .into_inner(); // adjust fixed point to match local currency let fee = Self::convert_from_ether_decimals(fee); Fee::from((Self::calculate_local_fee(), fee)) } /// Calculate fee in remote currency for dispatching a message on Ethereum pub(crate) fn calculate_remote_fee( gas_used_at_most: u64, fee_per_gas: U256, reward: U256, ) -> U256 { fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward) } /// 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()), ) } // 1 DOT has 10 digits of precision // 1 KSM has 12 digits of precision // 1 ETH has 18 digits of precision pub(crate) fn convert_from_ether_decimals(value: u128) -> T::Balance { let decimals = ETHER_DECIMALS.saturating_sub(T::Decimals::get()) as u32; let denom = 10u128.saturating_pow(decimals); value.checked_div(denom).expect("divisor is non-zero; qed").into() } } }