referrerpolicy=no-referrer-when-downgrade

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