pezsnowbridge_pezpallet_outbound_queue_v2/
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-v2`:
9//!
10//! 1. `pezsnowbridge_outbound_queue_primitives::v2::EthereumBlobExporter::deliver`
11//! 2. `pezsnowbridge_pezpallet_system_v2::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::v2::SendMessage::validate`]
16//! 2. The message is then enqueued for later processing via the implementation for
17//!    [`pezsnowbridge_outbound_queue_primitives::v2::SendMessage::deliver`]
18//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
19//! 4. The message queue delivers messages 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`:
22//! 	a. Convert to `OutboundMessage`, and stored into the `Messages` vector storage
23//! 	b. ABI-encode the `OutboundMessage` and store the committed Keccak256 hash in `MessageLeaves`
24//! 	c. Generate `PendingOrder` with assigned nonce and fee attached, stored into the
25//! 	   `PendingOrders` map storage, with nonce as the key
26//! 	d. Increment nonce and update the `Nonce` storage
27//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`.
28//!    At the beginning of the next block, both `Messages` and `MessageLeaves` are dropped so that
29//!    state at each block only holds the messages processed in that block.
30//! 7. This merkle root is inserted into the teyrchain header as a digest item
31//! 8. Offchain relayers are able to relay the message to Ethereum after:
32//! 	a. Generating a merkle proof for the committed message using the `prove_message` runtime API
33//! 	b. Reading the actual message content from the `Messages` vector in storage
34//! 9. On the Ethereum side, the message root is ultimately the thing being verified by the Beefy
35//!    light client.
36//! 10. When the message has been verified and executed, the relayer will call the extrinsic
37//!     `submit_delivery_receipt` to:
38//! 	a. Verify the message with proof for a transaction receipt containing the event log,
39//! 	   same as the inbound queue verification flow
40//! 	b. Fetch the pending order by nonce of the message, pay reward with fee attached in the order
41//!    	c. Remove the order from `PendingOrders` map storage by nonce
42//!
43//!
44//! # Extrinsics
45//!
46//! * [`Call::submit_delivery_receipt`]: Submit delivery proof
47//!
48//! # Runtime API
49//!
50//! * `prove_message`: Generate a merkle proof for a committed message
51#![cfg_attr(not(feature = "std"), no_std)]
52pub mod api;
53pub mod process_message_impl;
54pub mod send_message_impl;
55pub mod types;
56pub mod weights;
57
58#[cfg(feature = "runtime-benchmarks")]
59mod benchmarking;
60
61#[cfg(test)]
62mod mock;
63
64#[cfg(test)]
65mod test;
66
67#[cfg(feature = "runtime-benchmarks")]
68mod fixture;
69
70use alloy_core::{
71	primitives::{Bytes, FixedBytes},
72	sol_types::SolValue,
73};
74use pezbp_relayers::RewardLedger;
75use codec::{Decode, FullCodec};
76use pezframe_support::{
77	storage::StorageStreamIter,
78	traits::{tokens::Balance, EnqueueMessage, Get, ProcessMessageError},
79	weights::{Weight, WeightToFee},
80};
81use pezsnowbridge_core::{
82	digest_item::SnowbridgeDigestItem,
83	reward::{AddTip, AddTipError},
84	BasicOperatingMode,
85};
86use pezsnowbridge_merkle_tree::merkle_root;
87use pezsnowbridge_outbound_queue_primitives::{
88	v2::{
89		abi::{CommandWrapper, OutboundMessageWrapper},
90		DeliveryReceipt, GasMeter, Message, OutboundCommandWrapper, OutboundMessage,
91	},
92	EventProof, VerificationError, Verifier,
93};
94use pezsp_core::{H160, H256};
95use pezsp_runtime::{
96	traits::{BlockNumberProvider, Debug, Hash},
97	DigestItem,
98};
99use pezsp_std::prelude::*;
100pub use types::{OnNewCommitment, PendingOrder, ProcessMessageOriginOf};
101pub use weights::WeightInfo;
102use xcm::prelude::NetworkId;
103
104#[cfg(feature = "runtime-benchmarks")]
105use pezsnowbridge_beacon_primitives::BeaconHeader;
106
107pub use pezpallet::*;
108
109#[pezframe_support::pezpallet]
110pub mod pezpallet {
111	use super::*;
112	use pezframe_support::pezpallet_prelude::*;
113	use pezframe_system::pezpallet_prelude::*;
114
115	#[pezpallet::pezpallet]
116	pub struct Pezpallet<T>(_);
117
118	#[pezpallet::config]
119	pub trait Config: pezframe_system::Config {
120		#[allow(deprecated)]
121		type RuntimeEvent: From<Event<Self>>
122			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
123
124		type Hashing: Hash<Output = H256>;
125
126		type AggregateMessageOrigin: FullCodec
127			+ MaxEncodedLen
128			+ Clone
129			+ Eq
130			+ PartialEq
131			+ TypeInfo
132			+ Debug
133			+ From<H256>;
134
135		type MessageQueue: EnqueueMessage<Self::AggregateMessageOrigin>;
136
137		/// Measures the maximum gas used to execute a command on Ethereum
138		type GasMeter: GasMeter;
139
140		type Balance: Balance + From<u128>;
141
142		/// Max bytes in a message payload
143		#[pezpallet::constant]
144		type MaxMessagePayloadSize: Get<u32>;
145
146		/// Max number of messages processed per block
147		#[pezpallet::constant]
148		type MaxMessagesPerBlock: Get<u32>;
149
150		/// Hook that is called whenever there is a new commitment.
151		type OnNewCommitment: OnNewCommitment;
152
153		/// Convert a weight value into a deductible fee based.
154		type WeightToFee: WeightToFee<Balance = Self::Balance>;
155
156		/// Weight information for extrinsics in this pezpallet
157		type WeightInfo: WeightInfo;
158
159		/// The verifier for delivery proof from Ethereum
160		type Verifier: Verifier;
161
162		/// Address of the Gateway contract
163		#[pezpallet::constant]
164		type GatewayAddress: Get<H160>;
165		/// Reward discriminator type.
166		type RewardKind: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone;
167		/// The default RewardKind discriminator for rewards allocated to relayers from this
168		/// pezpallet.
169		#[pezpallet::constant]
170		type DefaultRewardKind: Get<Self::RewardKind>;
171		/// Relayer reward payment.
172		type RewardPayment: RewardLedger<Self::AccountId, Self::RewardKind, u128>;
173		/// Ethereum NetworkId
174		type EthereumNetwork: Get<NetworkId>;
175		#[cfg(feature = "runtime-benchmarks")]
176		type Helper: BenchmarkHelper<Self>;
177	}
178
179	#[pezpallet::event]
180	#[pezpallet::generate_deposit(pub fn deposit_event)]
181	pub enum Event<T: Config> {
182		/// Message has been queued and will be processed in the future
183		MessageQueued {
184			/// The message
185			message: Message,
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` or the `id`.
189		MessageAccepted {
190			/// ID of the message
191			id: H256,
192			/// The nonce assigned to this message
193			nonce: u64,
194		},
195		/// Message was not committed due to some failure condition, like an overweight message.
196		MessageRejected {
197			/// ID of the message, if known (e.g. if a message is corrupt, the ID will not be
198			/// known).
199			id: Option<H256>,
200			/// The payload of the message. Useful for debugging purposes if the message
201			/// cannot be decoded.
202			payload: Vec<u8>,
203			/// The error that was returned.
204			error: ProcessMessageError,
205		},
206		/// Message was not committed due to being overweight or the current block is full.
207		MessagePostponed {
208			/// The payload of the message. Useful for debugging purposes if the message
209			/// cannot be decoded.
210			payload: Vec<u8>,
211			/// The error that was returned.
212			reason: ProcessMessageError,
213		},
214		/// Some messages have been committed
215		MessagesCommitted {
216			/// Merkle root of the committed messages
217			root: H256,
218			/// number of committed messages
219			count: u64,
220		},
221		/// Set OperatingMode
222		OperatingModeChanged { mode: BasicOperatingMode },
223		/// Delivery Proof received
224		MessageDelivered { nonce: u64 },
225	}
226
227	#[pezpallet::error]
228	pub enum Error<T> {
229		/// The message is too large
230		MessageTooLarge,
231		/// The pezpallet is halted
232		Halted,
233		/// Invalid Channel
234		InvalidChannel,
235		/// Invalid Envelope
236		InvalidEnvelope,
237		/// Message verification error
238		Verification(VerificationError),
239		/// Invalid Gateway
240		InvalidGateway,
241		/// Pending nonce does not exist
242		InvalidPendingNonce,
243		/// Reward payment failed
244		RewardPaymentFailed,
245	}
246
247	/// Messages to be committed in the current block. This storage value is killed in
248	/// `on_initialize`, so will not end up bloating state.
249	///
250	/// Is never read in the runtime, only by offchain message relayers.
251	/// Because of this, it will never go into the PoV of a block.
252	///
253	/// Inspired by the `pezframe_system::Pezpallet::Events` storage value
254	#[pezpallet::storage]
255	#[pezpallet::unbounded]
256	pub type Messages<T: Config> = StorageValue<_, Vec<OutboundMessage>, ValueQuery>;
257
258	/// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a
259	/// merkle root during `on_finalize`. This storage value is killed in `on_initialize`, so state
260	/// at each block contains only root hash of messages processed in that block. This also means
261	/// it doesn't have to be included in PoV.
262	#[pezpallet::storage]
263	#[pezpallet::unbounded]
264	pub type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
265
266	/// The current nonce for the messages
267	#[pezpallet::storage]
268	pub type Nonce<T: Config> = StorageValue<_, u64, ValueQuery>;
269
270	/// Pending orders to relay
271	#[pezpallet::storage]
272	pub type PendingOrders<T: Config> =
273		StorageMap<_, Twox64Concat, u64, PendingOrder<BlockNumberFor<T>>, OptionQuery>;
274
275	#[pezpallet::hooks]
276	impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
277		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
278			// Remove storage from previous block
279			Messages::<T>::kill();
280			MessageLeaves::<T>::kill();
281			// Reserve some weight for the `on_finalize` handler
282			T::WeightInfo::on_initialize() + T::WeightInfo::commit()
283		}
284
285		fn on_finalize(_: BlockNumberFor<T>) {
286			Self::commit();
287		}
288	}
289
290	#[cfg(feature = "runtime-benchmarks")]
291	pub trait BenchmarkHelper<T> {
292		fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256);
293	}
294
295	#[pezpallet::call]
296	impl<T: Config> Pezpallet<T>
297	where
298		<T as pezframe_system::Config>::AccountId: From<[u8; 32]>,
299	{
300		#[pezpallet::call_index(1)]
301		#[pezpallet::weight(T::WeightInfo::submit_delivery_receipt())]
302		pub fn submit_delivery_receipt(
303			origin: OriginFor<T>,
304			event: Box<EventProof>,
305		) -> DispatchResult
306		where
307			<T as pezframe_system::Config>::AccountId: From<[u8; 32]>,
308		{
309			let relayer = ensure_signed(origin)?;
310
311			// submit message to verifier for verification
312			T::Verifier::verify(&event.event_log, &event.proof)
313				.map_err(|e| Error::<T>::Verification(e))?;
314
315			let receipt = DeliveryReceipt::try_from(&event.event_log)
316				.map_err(|_| Error::<T>::InvalidEnvelope)?;
317
318			Self::process_delivery_receipt(relayer, receipt)
319		}
320	}
321
322	impl<T: Config> Pezpallet<T> {
323		/// Generate a messages commitment and insert it into the header digest
324		pub(crate) fn commit() {
325			let count = MessageLeaves::<T>::decode_len().unwrap_or_default() as u64;
326			if count == 0 {
327				return;
328			}
329
330			// Create merkle root of messages
331			let root = merkle_root::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter());
332
333			let digest_item: DigestItem = SnowbridgeDigestItem::SnowbridgeV2(root).into();
334
335			// Insert merkle root into the header digest
336			<pezframe_system::Pezpallet<T>>::deposit_log(digest_item);
337
338			T::OnNewCommitment::on_new_commitment(root);
339
340			Self::deposit_event(Event::MessagesCommitted { root, count });
341		}
342
343		/// Process a message delivered by the MessageQueue pezpallet.
344		/// IMPORTANT!! This method does not roll back storage changes on error.
345		pub(crate) fn do_process_message(
346			_: ProcessMessageOriginOf<T>,
347			mut message: &[u8],
348		) -> Result<bool, ProcessMessageError> {
349			use ProcessMessageError::*;
350
351			// Yield if the maximum number of messages has been processed this block.
352			// This ensures that the weight of `on_finalize` has a known maximum bound.
353			let current_len = MessageLeaves::<T>::decode_len().unwrap_or(0);
354			if current_len >= T::MaxMessagesPerBlock::get() as usize {
355				Self::deposit_event(Event::MessagePostponed {
356					payload: message.to_vec(),
357					reason: Yield,
358				});
359				return Err(Yield);
360			}
361
362			// Decode bytes into Message
363			let Message { origin, id, fee, commands } =
364				Message::decode(&mut message).map_err(|_| {
365					Self::deposit_event(Event::MessageRejected {
366						id: None,
367						payload: message.to_vec(),
368						error: Corrupt,
369					});
370					Corrupt
371				})?;
372
373			// Convert it to OutboundMessage and save into Messages storage
374			let commands: Vec<OutboundCommandWrapper> = commands
375				.into_iter()
376				.map(|command| OutboundCommandWrapper {
377					kind: command.index(),
378					gas: T::GasMeter::maximum_dispatch_gas_used_at_most(&command),
379					payload: command.abi_encode(),
380				})
381				.collect();
382
383			let nonce = <Nonce<T>>::get().checked_add(1).ok_or_else(|| {
384				Self::deposit_event(Event::MessageRejected {
385					id: None,
386					payload: message.to_vec(),
387					error: Unsupported,
388				});
389				Unsupported
390			})?;
391
392			let outbound_message = OutboundMessage {
393				origin,
394				nonce,
395				topic: id,
396				commands: commands.clone().try_into().map_err(|_| {
397					Self::deposit_event(Event::MessageRejected {
398						id: Some(id),
399						payload: message.to_vec(),
400						error: Corrupt,
401					});
402					Corrupt
403				})?,
404			};
405			Messages::<T>::append(outbound_message);
406
407			// Convert it to an OutboundMessageWrapper (in ABI format), hash it using Keccak256 to
408			// generate a committed hash, and store it in MessageLeaves storage which can be
409			// verified on Ethereum later.
410			let abi_commands: Vec<CommandWrapper> = commands
411				.into_iter()
412				.map(|command| CommandWrapper {
413					kind: command.kind,
414					gas: command.gas,
415					payload: Bytes::from(command.payload),
416				})
417				.collect();
418			let committed_message = OutboundMessageWrapper {
419				origin: FixedBytes::from(origin.as_fixed_bytes()),
420				nonce,
421				topic: FixedBytes::from(id.as_fixed_bytes()),
422				commands: abi_commands,
423			};
424			let message_abi_encoded_hash =
425				<T as Config>::Hashing::hash(&committed_message.abi_encode());
426			MessageLeaves::<T>::append(message_abi_encoded_hash);
427
428			// Generate `PendingOrder` with fee attached in the message, stored
429			// into the `PendingOrders` map storage, with assigned nonce as the key.
430			// When the message is processed on ethereum side, the relayer will send the nonce
431			// back with delivery proof, only after that the order can
432			// be resolved and the fee will be rewarded to the relayer.
433			let order = PendingOrder {
434				nonce,
435				fee,
436				block_number: pezframe_system::Pezpallet::<T>::current_block_number(),
437			};
438			<PendingOrders<T>>::insert(nonce, order);
439
440			<Nonce<T>>::set(nonce);
441
442			Self::deposit_event(Event::MessageAccepted { id, nonce });
443
444			Ok(true)
445		}
446
447		/// Process a delivery receipt from a relayer, to allocate the relayer reward.
448		pub fn process_delivery_receipt(
449			relayer: <T as pezframe_system::Config>::AccountId,
450			receipt: DeliveryReceipt,
451		) -> DispatchResult
452		where
453			<T as pezframe_system::Config>::AccountId: From<[u8; 32]>,
454		{
455			// Verify that the message was submitted from the known Gateway contract
456			ensure!(T::GatewayAddress::get() == receipt.gateway, Error::<T>::InvalidGateway);
457
458			let reward_account = if receipt.reward_address == [0u8; 32] {
459				relayer
460			} else {
461				receipt.reward_address.into()
462			};
463
464			let nonce = receipt.nonce;
465
466			let order = <PendingOrders<T>>::get(nonce).ok_or(Error::<T>::InvalidPendingNonce)?;
467
468			if order.fee > 0 {
469				// Pay relayer reward
470				T::RewardPayment::register_reward(
471					&reward_account,
472					T::DefaultRewardKind::get(),
473					order.fee,
474				);
475			}
476
477			<PendingOrders<T>>::remove(nonce);
478
479			Self::deposit_event(Event::MessageDelivered { nonce });
480
481			Ok(())
482		}
483	}
484
485	impl<T: Config> AddTip for Pezpallet<T> {
486		fn add_tip(nonce: u64, amount: u128) -> Result<(), AddTipError> {
487			ensure!(amount > 0, AddTipError::AmountZero);
488			PendingOrders::<T>::try_mutate_exists(nonce, |maybe_order| -> Result<(), AddTipError> {
489				match maybe_order {
490					Some(order) => {
491						order.fee = order.fee.saturating_add(amount);
492						Ok(())
493					},
494					None => Err(AddTipError::UnknownMessage),
495				}
496			})
497		}
498	}
499}