pezsnowbridge_pezpallet_outbound_queue/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Pezpallet for committing outbound messages for delivery to Ethereum
4//!
5//! # Overview
6//!
7//! Messages come either from sibling teyrchains via XCM, or BridgeHub itself
8//! via the `pezsnowbridge-pezpallet-system`:
9//!
10//! 1. `pezsnowbridge_outbound_queue_primitives::v1::EthereumBlobExporter::deliver`
11//! 2. `pezsnowbridge_pezpallet_system::Pezpallet::send`
12//!
13//! The message submission pipeline works like this:
14//! 1. The message is first validated via the implementation for
15//!    [`pezsnowbridge_outbound_queue_primitives::v1::SendMessage::validate`]
16//! 2. The message is then enqueued for later processing via the implementation for
17//!    [`pezsnowbridge_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 pezpallet via the implementation for
20//!    [`pezframe_support::traits::ProcessMessage::process_message`]
21//! 5. The message is processed in `Pezpallet::do_process_message`: a. Assigned a nonce b.
22//!    ABI-encoded, 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 teyrchain 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 Pezkuwi 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/HEZ 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 pezpallet.
56//!
57//! This is an interim measure. Once ETH/HEZ liquidity pools are available in the Pezkuwi 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/HEZ"))
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/HEZ 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 pezbridge_hub_common::AggregateMessageOrigin;
107use codec::Decode;
108use pezframe_support::{
109	storage::StorageStreamIter,
110	traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError},
111	weights::{Weight, WeightToFee},
112};
113use pezsnowbridge_core::{digest_item::SnowbridgeDigestItem, BasicOperatingMode, ChannelId};
114use pezsnowbridge_merkle_tree::merkle_root;
115use pezsnowbridge_outbound_queue_primitives::v1::{
116	Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS,
117};
118use pezsp_core::{H256, U256};
119use pezsp_runtime::{
120	traits::{CheckedDiv, Hash},
121	DigestItem, Saturating,
122};
123use pezsp_std::prelude::*;
124pub use types::{CommittedMessage, ProcessMessageOriginOf};
125pub use weights::WeightInfo;
126
127pub use pezpallet::*;
128
129#[pezframe_support::pezpallet]
130pub mod pezpallet {
131	use super::*;
132	use pezframe_support::pezpallet_prelude::*;
133	use pezframe_system::pezpallet_prelude::*;
134	use pezsnowbridge_core::PricingParameters;
135	use pezsp_arithmetic::FixedU128;
136
137	#[pezpallet::pezpallet]
138	pub struct Pezpallet<T>(_);
139
140	#[pezpallet::config]
141	pub trait Config: pezframe_system::Config {
142		#[allow(deprecated)]
143		type RuntimeEvent: From<Event<Self>>
144			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
145
146		type Hashing: Hash<Output = H256>;
147
148		type MessageQueue: EnqueueMessage<AggregateMessageOrigin>;
149
150		/// Measures the maximum gas used to execute a command on Ethereum
151		type GasMeter: GasMeter;
152
153		type Balance: Balance + From<u128>;
154
155		/// Number of decimal places in native currency
156		#[pezpallet::constant]
157		type Decimals: Get<u8>;
158
159		/// Max bytes in a message payload
160		#[pezpallet::constant]
161		type MaxMessagePayloadSize: Get<u32>;
162
163		/// Max number of messages processed per block
164		#[pezpallet::constant]
165		type MaxMessagesPerBlock: Get<u32>;
166
167		/// Check whether a channel exists
168		type Channels: Contains<ChannelId>;
169
170		type PricingParameters: Get<PricingParameters<Self::Balance>>;
171
172		/// Convert a weight value into a deductible fee based.
173		type WeightToFee: WeightToFee<Balance = Self::Balance>;
174
175		/// Weight information for extrinsics in this pezpallet
176		type WeightInfo: WeightInfo;
177	}
178
179	#[pezpallet::event]
180	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
181	pub enum Event<T: Config> {
182		/// Message has been queued and will be processed in the future
183		MessageQueued {
184			/// ID of the message. Usually the XCM message hash or a SetTopic.
185			id: H256,
186		},
187		/// Message will be committed at the end of current block. From now on, to track the
188		/// progress the message, use the `nonce` of `id`.
189		MessageAccepted {
190			/// ID of the message
191			id: H256,
192			/// The nonce assigned to this message
193			nonce: u64,
194		},
195		/// Some messages have been committed
196		MessagesCommitted {
197			/// Merkle root of the committed messages
198			root: H256,
199			/// number of committed messages
200			count: u64,
201		},
202		/// Set OperatingMode
203		OperatingModeChanged { mode: BasicOperatingMode },
204	}
205
206	#[pezpallet::error]
207	pub enum Error<T> {
208		/// The message is too large
209		MessageTooLarge,
210		/// The pezpallet is halted
211		Halted,
212		/// Invalid Channel
213		InvalidChannel,
214	}
215
216	/// Messages to be committed in the current block. This storage value is killed in
217	/// `on_initialize`, so should never go into block PoV.
218	///
219	/// Is never read in the runtime, only by offchain message relayers.
220	///
221	/// Inspired by the `pezframe_system::Pezpallet::Events` storage value
222	#[pezpallet::storage]
223	#[pezpallet::unbounded]
224	pub(super) type Messages<T: Config> = StorageValue<_, Vec<CommittedMessage>, ValueQuery>;
225
226	/// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a
227	/// merkle root during `on_finalize`. This storage value is killed in
228	/// `on_initialize`, so should never go into block PoV.
229	#[pezpallet::storage]
230	#[pezpallet::unbounded]
231	#[pezpallet::getter(fn message_leaves)]
232	pub(super) type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
233
234	/// The current nonce for each message origin
235	#[pezpallet::storage]
236	pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
237
238	/// The current operating mode of the pezpallet.
239	#[pezpallet::storage]
240	#[pezpallet::getter(fn operating_mode)]
241	pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
242
243	#[pezpallet::hooks]
244	impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T>
245	where
246		T::AccountId: AsRef<[u8]>,
247	{
248		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
249			// Remove storage from previous block
250			Messages::<T>::kill();
251			MessageLeaves::<T>::kill();
252			// Reserve some weight for the `on_finalize` handler
253			T::WeightInfo::commit()
254		}
255
256		fn on_finalize(_: BlockNumberFor<T>) {
257			Self::commit();
258		}
259
260		fn integrity_test() {
261			let decimals = T::Decimals::get();
262			assert!(decimals == 10 || decimals == 12, "Decimals should be 10 or 12");
263		}
264	}
265
266	#[pezpallet::call]
267	impl<T: Config> Pezpallet<T> {
268		/// Halt or resume all pezpallet operations. May only be called by root.
269		#[pezpallet::call_index(0)]
270		#[pezpallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
271		pub fn set_operating_mode(
272			origin: OriginFor<T>,
273			mode: BasicOperatingMode,
274		) -> DispatchResult {
275			ensure_root(origin)?;
276			OperatingMode::<T>::put(mode);
277			Self::deposit_event(Event::OperatingModeChanged { mode });
278			Ok(())
279		}
280	}
281
282	impl<T: Config> Pezpallet<T> {
283		/// Generate a messages commitment and insert it into the header digest
284		pub(crate) fn commit() {
285			let count = MessageLeaves::<T>::decode_len().unwrap_or_default() as u64;
286			if count == 0 {
287				return;
288			}
289
290			// Create merkle root of messages
291			let root = merkle_root::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter());
292
293			let digest_item: DigestItem = SnowbridgeDigestItem::Snowbridge(root).into();
294
295			// Insert merkle root into the header digest
296			<pezframe_system::Pezpallet<T>>::deposit_log(digest_item);
297
298			Self::deposit_event(Event::MessagesCommitted { root, count });
299		}
300
301		/// Process a message delivered by the MessageQueue pezpallet
302		pub(crate) fn do_process_message(
303			_: ProcessMessageOriginOf<T>,
304			mut message: &[u8],
305		) -> Result<bool, ProcessMessageError> {
306			use ProcessMessageError::*;
307
308			// Yield if the maximum number of messages has been processed this block.
309			// This ensures that the weight of `on_finalize` has a known maximum bound.
310			ensure!(
311				MessageLeaves::<T>::decode_len().unwrap_or(0)
312					< T::MaxMessagesPerBlock::get() as usize,
313				Yield
314			);
315
316			// Decode bytes into versioned message
317			let versioned_queued_message: VersionedQueuedMessage =
318				VersionedQueuedMessage::decode(&mut message).map_err(|_| Corrupt)?;
319
320			// Convert versioned message into latest supported message version
321			let queued_message: QueuedMessage =
322				versioned_queued_message.try_into().map_err(|_| Unsupported)?;
323
324			// Obtain next nonce
325			let nonce = <Nonce<T>>::try_mutate(
326				queued_message.channel_id,
327				|nonce| -> Result<u64, ProcessMessageError> {
328					*nonce = nonce.checked_add(1).ok_or(Unsupported)?;
329					Ok(*nonce)
330				},
331			)?;
332
333			let pricing_params = T::PricingParameters::get();
334			let command = queued_message.command.index();
335			let params = queued_message.command.abi_encode();
336			let max_dispatch_gas =
337				T::GasMeter::maximum_dispatch_gas_used_at_most(&queued_message.command);
338			let reward = pricing_params.rewards.remote;
339
340			// Construct the final committed message
341			let message = CommittedMessage {
342				channel_id: queued_message.channel_id,
343				nonce,
344				command,
345				params,
346				max_dispatch_gas,
347				max_fee_per_gas: pricing_params
348					.fee_per_gas
349					.try_into()
350					.defensive_unwrap_or(u128::MAX),
351				reward: reward.try_into().defensive_unwrap_or(u128::MAX),
352				id: queued_message.id,
353			};
354
355			// ABI-encode and hash the prepared message
356			let message_abi_encoded = ethabi::encode(&[message.clone().into()]);
357			let message_abi_encoded_hash = <T as Config>::Hashing::hash(&message_abi_encoded);
358
359			Messages::<T>::append(Box::new(message));
360			MessageLeaves::<T>::append(message_abi_encoded_hash);
361
362			Self::deposit_event(Event::MessageAccepted { id: queued_message.id, nonce });
363
364			Ok(true)
365		}
366
367		/// Calculate total fee in native currency to cover all costs of delivering a message to the
368		/// remote destination. See module-level documentation for more details.
369		pub(crate) fn calculate_fee(
370			gas_used_at_most: u64,
371			params: PricingParameters<T::Balance>,
372		) -> Fee<T::Balance> {
373			// Remote fee in ether
374			let fee = Self::calculate_remote_fee(
375				gas_used_at_most,
376				params.fee_per_gas,
377				params.rewards.remote,
378			);
379
380			// downcast to u128
381			let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX);
382
383			// multiply by multiplier and convert to local currency
384			let fee = FixedU128::from_inner(fee)
385				.saturating_mul(params.multiplier)
386				.checked_div(&params.exchange_rate)
387				.expect("exchange rate is not zero; qed")
388				.into_inner();
389
390			// adjust fixed point to match local currency
391			let fee = Self::convert_from_ether_decimals(fee);
392
393			Fee::from((Self::calculate_local_fee(), fee))
394		}
395
396		/// Calculate fee in remote currency for dispatching a message on Ethereum
397		pub(crate) fn calculate_remote_fee(
398			gas_used_at_most: u64,
399			fee_per_gas: U256,
400			reward: U256,
401		) -> U256 {
402			fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward)
403		}
404
405		/// The local component of the message processing fees in native currency
406		pub(crate) fn calculate_local_fee() -> T::Balance {
407			T::WeightToFee::weight_to_fee(
408				&T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()),
409			)
410		}
411
412		// 1 HEZ has 10 digits of precision
413		// 1 KSM has 12 digits of precision
414		// 1 ETH has 18 digits of precision
415		pub(crate) fn convert_from_ether_decimals(value: u128) -> T::Balance {
416			let decimals = ETHER_DECIMALS.saturating_sub(T::Decimals::get()) as u32;
417			let denom = 10u128.saturating_pow(decimals);
418			value.checked_div(denom).expect("divisor is non-zero; qed").into()
419		}
420	}
421}