referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_outbound_queue/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Pallet for committing outbound messages for delivery to Ethereum
4//!
5//! # Overview
6//!
7//! Messages come either from sibling parachains via XCM, or BridgeHub itself
8//! via the `snowbridge-pallet-system`:
9//!
10//! 1. `snowbridge_outbound_queue_primitives::v1::EthereumBlobExporter::deliver`
11//! 2. `snowbridge_pallet_system::Pallet::send`
12//!
13//! The message submission pipeline works like this:
14//! 1. The message is first validated via the implementation for
15//!    [`snowbridge_outbound_queue_primitives::v1::SendMessage::validate`]
16//! 2. The message is then enqueued for later processing via the implementation for
17//!    [`snowbridge_outbound_queue_primitives::v1::SendMessage::deliver`]
18//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
19//! 4. The message queue delivers messages back to this pallet via the implementation for
20//!    [`frame_support::traits::ProcessMessage::process_message`]
21//! 5. The message is processed in `Pallet::do_process_message`: a. Assigned a nonce b. ABI-encoded,
22//!    hashed, and stored in the `MessageLeaves` vector
23//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`.
24//! 7. This merkle root is inserted into the parachain header as a digest item
25//! 8. Offchain relayers are able to relay the message to Ethereum after: a. Generating a merkle
26//!    proof for the committed message using the `prove_message` runtime API b. Reading the actual
27//!    message content from the `Messages` vector in storage
28//!
29//! On the Ethereum side, the message root is ultimately the thing being
30//! verified by the Polkadot light client.
31//!
32//! # Message Priorities
33//!
34//! The processing of governance commands can never be halted. This effectively
35//! allows us to pause processing of normal user messages while still allowing
36//! governance commands to be sent to Ethereum.
37//!
38//! # Fees
39//!
40//! An upfront fee must be paid for delivering a message. This fee covers several
41//! components:
42//! 1. The weight of processing the message locally
43//! 2. The gas refund paid out to relayers for message submission
44//! 3. An additional reward paid out to relayers for message submission
45//!
46//! Messages are weighed to determine the maximum amount of gas they could
47//! consume on Ethereum. Using this upper bound, a final fee can be calculated.
48//!
49//! The fee calculation also requires the following parameters:
50//! * Average ETH/DOT exchange rate over some period
51//! * Max fee per unit of gas that bridge is willing to refund relayers for
52//!
53//! By design, it is expected that governance should manually update these
54//! parameters every few weeks using the `set_pricing_parameters` extrinsic in the
55//! system pallet.
56//!
57//! This is an interim measure. Once ETH/DOT liquidity pools are available in the Polkadot network,
58//! we'll use them as a source of pricing info, subject to certain safeguards.
59//!
60//! ## Fee Computation Function
61//!
62//! ```text
63//! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message))
64//! RemoteFee(Message) = MaxGasRequired(Message) * Params.MaxFeePerGas + Params.Reward
65//! RemoteFeeAdjusted(Message) = Params.Multiplier * (RemoteFee(Message) / Params.Ratio("ETH/DOT"))
66//! Fee(Message) = LocalFee(Message) + RemoteFeeAdjusted(Message)
67//! ```
68//!
69//! By design, the computed fee includes a safety factor (the `Multiplier`) to cover
70//! unfavourable fluctuations in the ETH/DOT exchange rate.
71//!
72//! ## Fee Settlement
73//!
74//! On the remote side, in the gateway contract, the relayer accrues
75//!
76//! ```text
77//! Min(GasPrice, Message.MaxFeePerGas) * GasUsed() + Message.Reward
78//! ```
79//! Or in plain english, relayers are refunded for gas consumption, using a
80//! price that is a minimum of the actual gas price, or `Message.MaxFeePerGas`.
81//!
82//! # Extrinsics
83//!
84//! * [`Call::set_operating_mode`]: Set the operating mode
85//!
86//! # Runtime API
87//!
88//! * `prove_message`: Generate a merkle proof for a committed message
89//! * `calculate_fee`: Calculate the delivery fee for a message
90#![cfg_attr(not(feature = "std"), no_std)]
91pub mod api;
92pub mod process_message_impl;
93pub mod send_message_impl;
94pub mod types;
95pub mod weights;
96
97#[cfg(feature = "runtime-benchmarks")]
98mod benchmarking;
99
100#[cfg(test)]
101mod mock;
102
103#[cfg(test)]
104mod test;
105
106use bridge_hub_common::{AggregateMessageOrigin, CustomDigestItem};
107use codec::Decode;
108use frame_support::{
109	storage::StorageStreamIter,
110	traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError},
111	weights::{Weight, WeightToFee},
112};
113use snowbridge_core::{BasicOperatingMode, ChannelId};
114use snowbridge_merkle_tree::merkle_root;
115use snowbridge_outbound_queue_primitives::v1::{
116	Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS,
117};
118use sp_core::{H256, U256};
119use sp_runtime::{
120	traits::{CheckedDiv, Hash},
121	DigestItem, Saturating,
122};
123use sp_std::prelude::*;
124pub use types::{CommittedMessage, ProcessMessageOriginOf};
125pub use weights::WeightInfo;
126
127pub use pallet::*;
128
129#[frame_support::pallet]
130pub mod pallet {
131	use super::*;
132	use frame_support::pallet_prelude::*;
133	use frame_system::pallet_prelude::*;
134	use snowbridge_core::PricingParameters;
135	use sp_arithmetic::FixedU128;
136
137	#[pallet::pallet]
138	pub struct Pallet<T>(_);
139
140	#[pallet::config]
141	pub trait Config: frame_system::Config {
142		#[allow(deprecated)]
143		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
144
145		type Hashing: Hash<Output = H256>;
146
147		type MessageQueue: EnqueueMessage<AggregateMessageOrigin>;
148
149		/// Measures the maximum gas used to execute a command on Ethereum
150		type GasMeter: GasMeter;
151
152		type Balance: Balance + From<u128>;
153
154		/// Number of decimal places in native currency
155		#[pallet::constant]
156		type Decimals: Get<u8>;
157
158		/// Max bytes in a message payload
159		#[pallet::constant]
160		type MaxMessagePayloadSize: Get<u32>;
161
162		/// Max number of messages processed per block
163		#[pallet::constant]
164		type MaxMessagesPerBlock: Get<u32>;
165
166		/// Check whether a channel exists
167		type Channels: Contains<ChannelId>;
168
169		type PricingParameters: Get<PricingParameters<Self::Balance>>;
170
171		/// Convert a weight value into a deductible fee based.
172		type WeightToFee: WeightToFee<Balance = Self::Balance>;
173
174		/// Weight information for extrinsics in this pallet
175		type WeightInfo: WeightInfo;
176	}
177
178	#[pallet::event]
179	#[pallet::generate_deposit(pub(super) fn deposit_event)]
180	pub enum Event<T: Config> {
181		/// Message has been queued and will be processed in the future
182		MessageQueued {
183			/// ID of the message. Usually the XCM message hash or a SetTopic.
184			id: H256,
185		},
186		/// Message will be committed at the end of current block. From now on, to track the
187		/// progress the message, use the `nonce` of `id`.
188		MessageAccepted {
189			/// ID of the message
190			id: H256,
191			/// The nonce assigned to this message
192			nonce: u64,
193		},
194		/// Some messages have been committed
195		MessagesCommitted {
196			/// Merkle root of the committed messages
197			root: H256,
198			/// number of committed messages
199			count: u64,
200		},
201		/// Set OperatingMode
202		OperatingModeChanged { mode: BasicOperatingMode },
203	}
204
205	#[pallet::error]
206	pub enum Error<T> {
207		/// The message is too large
208		MessageTooLarge,
209		/// The pallet is halted
210		Halted,
211		/// Invalid Channel
212		InvalidChannel,
213	}
214
215	/// Messages to be committed in the current block. This storage value is killed in
216	/// `on_initialize`, so should never go into block PoV.
217	///
218	/// Is never read in the runtime, only by offchain message relayers.
219	///
220	/// Inspired by the `frame_system::Pallet::Events` storage value
221	#[pallet::storage]
222	#[pallet::unbounded]
223	pub(super) type Messages<T: Config> = StorageValue<_, Vec<CommittedMessage>, ValueQuery>;
224
225	/// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a
226	/// merkle root during `on_finalize`. This storage value is killed in
227	/// `on_initialize`, so should never go into block PoV.
228	#[pallet::storage]
229	#[pallet::unbounded]
230	#[pallet::getter(fn message_leaves)]
231	pub(super) type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
232
233	/// The current nonce for each message origin
234	#[pallet::storage]
235	pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
236
237	/// The current operating mode of the pallet.
238	#[pallet::storage]
239	#[pallet::getter(fn operating_mode)]
240	pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
241
242	#[pallet::hooks]
243	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T>
244	where
245		T::AccountId: AsRef<[u8]>,
246	{
247		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
248			// Remove storage from previous block
249			Messages::<T>::kill();
250			MessageLeaves::<T>::kill();
251			// Reserve some weight for the `on_finalize` handler
252			T::WeightInfo::commit()
253		}
254
255		fn on_finalize(_: BlockNumberFor<T>) {
256			Self::commit();
257		}
258
259		fn integrity_test() {
260			let decimals = T::Decimals::get();
261			assert!(decimals == 10 || decimals == 12, "Decimals should be 10 or 12");
262		}
263	}
264
265	#[pallet::call]
266	impl<T: Config> Pallet<T> {
267		/// Halt or resume all pallet operations. May only be called by root.
268		#[pallet::call_index(0)]
269		#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
270		pub fn set_operating_mode(
271			origin: OriginFor<T>,
272			mode: BasicOperatingMode,
273		) -> DispatchResult {
274			ensure_root(origin)?;
275			OperatingMode::<T>::put(mode);
276			Self::deposit_event(Event::OperatingModeChanged { mode });
277			Ok(())
278		}
279	}
280
281	impl<T: Config> Pallet<T> {
282		/// Generate a messages commitment and insert it into the header digest
283		pub(crate) fn commit() {
284			let count = MessageLeaves::<T>::decode_len().unwrap_or_default() as u64;
285			if count == 0 {
286				return
287			}
288
289			// Create merkle root of messages
290			let root = merkle_root::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter());
291
292			let digest_item: DigestItem = CustomDigestItem::Snowbridge(root).into();
293
294			// Insert merkle root into the header digest
295			<frame_system::Pallet<T>>::deposit_log(digest_item);
296
297			Self::deposit_event(Event::MessagesCommitted { root, count });
298		}
299
300		/// Process a message delivered by the MessageQueue pallet
301		pub(crate) fn do_process_message(
302			_: ProcessMessageOriginOf<T>,
303			mut message: &[u8],
304		) -> Result<bool, ProcessMessageError> {
305			use ProcessMessageError::*;
306
307			// Yield if the maximum number of messages has been processed this block.
308			// This ensures that the weight of `on_finalize` has a known maximum bound.
309			ensure!(
310				MessageLeaves::<T>::decode_len().unwrap_or(0) <
311					T::MaxMessagesPerBlock::get() as usize,
312				Yield
313			);
314
315			// Decode bytes into versioned message
316			let versioned_queued_message: VersionedQueuedMessage =
317				VersionedQueuedMessage::decode(&mut message).map_err(|_| Corrupt)?;
318
319			// Convert versioned message into latest supported message version
320			let queued_message: QueuedMessage =
321				versioned_queued_message.try_into().map_err(|_| Unsupported)?;
322
323			// Obtain next nonce
324			let nonce = <Nonce<T>>::try_mutate(
325				queued_message.channel_id,
326				|nonce| -> Result<u64, ProcessMessageError> {
327					*nonce = nonce.checked_add(1).ok_or(Unsupported)?;
328					Ok(*nonce)
329				},
330			)?;
331
332			let pricing_params = T::PricingParameters::get();
333			let command = queued_message.command.index();
334			let params = queued_message.command.abi_encode();
335			let max_dispatch_gas =
336				T::GasMeter::maximum_dispatch_gas_used_at_most(&queued_message.command);
337			let reward = pricing_params.rewards.remote;
338
339			// Construct the final committed message
340			let message = CommittedMessage {
341				channel_id: queued_message.channel_id,
342				nonce,
343				command,
344				params,
345				max_dispatch_gas,
346				max_fee_per_gas: pricing_params
347					.fee_per_gas
348					.try_into()
349					.defensive_unwrap_or(u128::MAX),
350				reward: reward.try_into().defensive_unwrap_or(u128::MAX),
351				id: queued_message.id,
352			};
353
354			// ABI-encode and hash the prepared message
355			let message_abi_encoded = ethabi::encode(&[message.clone().into()]);
356			let message_abi_encoded_hash = <T as Config>::Hashing::hash(&message_abi_encoded);
357
358			Messages::<T>::append(Box::new(message));
359			MessageLeaves::<T>::append(message_abi_encoded_hash);
360
361			Self::deposit_event(Event::MessageAccepted { id: queued_message.id, nonce });
362
363			Ok(true)
364		}
365
366		/// Calculate total fee in native currency to cover all costs of delivering a message to the
367		/// remote destination. See module-level documentation for more details.
368		pub(crate) fn calculate_fee(
369			gas_used_at_most: u64,
370			params: PricingParameters<T::Balance>,
371		) -> Fee<T::Balance> {
372			// Remote fee in ether
373			let fee = Self::calculate_remote_fee(
374				gas_used_at_most,
375				params.fee_per_gas,
376				params.rewards.remote,
377			);
378
379			// downcast to u128
380			let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX);
381
382			// multiply by multiplier and convert to local currency
383			let fee = FixedU128::from_inner(fee)
384				.saturating_mul(params.multiplier)
385				.checked_div(&params.exchange_rate)
386				.expect("exchange rate is not zero; qed")
387				.into_inner();
388
389			// adjust fixed point to match local currency
390			let fee = Self::convert_from_ether_decimals(fee);
391
392			Fee::from((Self::calculate_local_fee(), fee))
393		}
394
395		/// Calculate fee in remote currency for dispatching a message on Ethereum
396		pub(crate) fn calculate_remote_fee(
397			gas_used_at_most: u64,
398			fee_per_gas: U256,
399			reward: U256,
400		) -> U256 {
401			fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward)
402		}
403
404		/// The local component of the message processing fees in native currency
405		pub(crate) fn calculate_local_fee() -> T::Balance {
406			T::WeightToFee::weight_to_fee(
407				&T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()),
408			)
409		}
410
411		// 1 DOT has 10 digits of precision
412		// 1 KSM has 12 digits of precision
413		// 1 ETH has 18 digits of precision
414		pub(crate) fn convert_from_ether_decimals(value: u128) -> T::Balance {
415			let decimals = ETHER_DECIMALS.saturating_sub(T::Decimals::get()) as u32;
416			let denom = 10u128.saturating_pow(decimals);
417			value.checked_div(denom).expect("divisor is non-zero; qed").into()
418		}
419	}
420}