Skip to main content

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