Files
pezkuwi-sdk/pezbridges/pezsnowbridge/pezpallets/outbound-queue-v2/src/lib.rs
T
pezkuwichain 57fef835e3 fix(ci): resolve all quick-checks failures
- Remove missing cli crate from workspace members
- Fix TOML array syntax errors in pvf and benchmarking-cli Cargo.toml
- Fix Rust import ordering with cargo fmt
- Fix feature propagation with zepter (try-runtime, runtime-benchmarks, std)
2026-01-04 17:22:12 +03:00

500 lines
16 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Pezpallet for committing outbound messages for delivery to Ethereum
//!
//! # Overview
//!
//! Messages come either from sibling teyrchains via XCM, or BridgeHub itself
//! via the `pezsnowbridge-pezpallet-system-v2`:
//!
//! 1. `pezsnowbridge_outbound_queue_primitives::v2::EthereumBlobExporter::deliver`
//! 2. `pezsnowbridge_pezpallet_system_v2::Pezpallet::send`
//!
//! The message submission pipeline works like this:
//! 1. The message is first validated via the implementation for
//! [`pezsnowbridge_outbound_queue_primitives::v2::SendMessage::validate`]
//! 2. The message is then enqueued for later processing via the implementation for
//! [`pezsnowbridge_outbound_queue_primitives::v2::SendMessage::deliver`]
//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
//! 4. The message queue delivers messages to this pezpallet via the implementation for
//! [`pezframe_support::traits::ProcessMessage::process_message`]
//! 5. The message is processed in `Pezpallet::do_process_message`:
//! a. Convert to `OutboundMessage`, and stored into the `Messages` vector storage
//! b. ABI-encode the `OutboundMessage` and store the committed Keccak256 hash in `MessageLeaves`
//! c. Generate `PendingOrder` with assigned nonce and fee attached, stored into the
//! `PendingOrders` map storage, with nonce as the key
//! d. Increment nonce and update the `Nonce` storage
//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`.
//! At the beginning of the next block, both `Messages` and `MessageLeaves` are dropped so that
//! state at each block only holds the messages processed in that block.
//! 7. This merkle root is inserted into the teyrchain header as a digest item
//! 8. Offchain relayers are able to relay the message to Ethereum after:
//! a. Generating a merkle proof for the committed message using the `prove_message` runtime API
//! b. Reading the actual message content from the `Messages` vector in storage
//! 9. On the Ethereum side, the message root is ultimately the thing being verified by the Beefy
//! light client.
//! 10. When the message has been verified and executed, the relayer will call the extrinsic
//! `submit_delivery_receipt` to:
//! a. Verify the message with proof for a transaction receipt containing the event log,
//! same as the inbound queue verification flow
//! b. Fetch the pending order by nonce of the message, pay reward with fee attached in the order
//! c. Remove the order from `PendingOrders` map storage by nonce
//!
//!
//! # Extrinsics
//!
//! * [`Call::submit_delivery_receipt`]: Submit delivery proof
//!
//! # Runtime API
//!
//! * `prove_message`: Generate a merkle proof for a committed message
#![cfg_attr(not(feature = "std"), no_std)]
pub mod api;
pub mod process_message_impl;
pub mod send_message_impl;
pub mod types;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod test;
#[cfg(feature = "runtime-benchmarks")]
mod fixture;
use alloy_core::{
primitives::{Bytes, FixedBytes},
sol_types::SolValue,
};
use codec::{Decode, FullCodec};
use pezbp_relayers::RewardLedger;
use pezframe_support::{
storage::StorageStreamIter,
traits::{tokens::Balance, EnqueueMessage, Get, ProcessMessageError},
weights::{Weight, WeightToFee},
};
use pezsnowbridge_core::{
digest_item::SnowbridgeDigestItem,
reward::{AddTip, AddTipError},
BasicOperatingMode,
};
use pezsnowbridge_merkle_tree::merkle_root;
use pezsnowbridge_outbound_queue_primitives::{
v2::{
abi::{CommandWrapper, OutboundMessageWrapper},
DeliveryReceipt, GasMeter, Message, OutboundCommandWrapper, OutboundMessage,
},
EventProof, VerificationError, Verifier,
};
use pezsp_core::{H160, H256};
use pezsp_runtime::{
traits::{BlockNumberProvider, Debug, Hash},
DigestItem,
};
use pezsp_std::prelude::*;
pub use types::{OnNewCommitment, PendingOrder, ProcessMessageOriginOf};
pub use weights::WeightInfo;
use xcm::prelude::NetworkId;
#[cfg(feature = "runtime-benchmarks")]
use pezsnowbridge_beacon_primitives::BeaconHeader;
pub use pezpallet::*;
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
#[pezpallet::pezpallet]
pub struct Pezpallet<T>(_);
#[pezpallet::config]
pub trait Config: pezframe_system::Config {
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
type Hashing: Hash<Output = H256>;
type AggregateMessageOrigin: FullCodec
+ MaxEncodedLen
+ Clone
+ Eq
+ PartialEq
+ TypeInfo
+ Debug
+ From<H256>;
type MessageQueue: EnqueueMessage<Self::AggregateMessageOrigin>;
/// Measures the maximum gas used to execute a command on Ethereum
type GasMeter: GasMeter;
type Balance: Balance + From<u128>;
/// Max bytes in a message payload
#[pezpallet::constant]
type MaxMessagePayloadSize: Get<u32>;
/// Max number of messages processed per block
#[pezpallet::constant]
type MaxMessagesPerBlock: Get<u32>;
/// Hook that is called whenever there is a new commitment.
type OnNewCommitment: OnNewCommitment;
/// Convert a weight value into a deductible fee based.
type WeightToFee: WeightToFee<Balance = Self::Balance>;
/// Weight information for extrinsics in this pezpallet
type WeightInfo: WeightInfo;
/// The verifier for delivery proof from Ethereum
type Verifier: Verifier;
/// Address of the Gateway contract
#[pezpallet::constant]
type GatewayAddress: Get<H160>;
/// Reward discriminator type.
type RewardKind: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone;
/// The default RewardKind discriminator for rewards allocated to relayers from this
/// pezpallet.
#[pezpallet::constant]
type DefaultRewardKind: Get<Self::RewardKind>;
/// Relayer reward payment.
type RewardPayment: RewardLedger<Self::AccountId, Self::RewardKind, u128>;
/// Ethereum NetworkId
type EthereumNetwork: Get<NetworkId>;
#[cfg(feature = "runtime-benchmarks")]
type Helper: BenchmarkHelper<Self>;
}
#[pezpallet::event]
#[pezpallet::generate_deposit(pub fn deposit_event)]
pub enum Event<T: Config> {
/// Message has been queued and will be processed in the future
MessageQueued {
/// The message
message: Message,
},
/// Message will be committed at the end of current block. From now on, to track the
/// progress the message, use the `nonce` or the `id`.
MessageAccepted {
/// ID of the message
id: H256,
/// The nonce assigned to this message
nonce: u64,
},
/// Message was not committed due to some failure condition, like an overweight message.
MessageRejected {
/// ID of the message, if known (e.g. if a message is corrupt, the ID will not be
/// known).
id: Option<H256>,
/// The payload of the message. Useful for debugging purposes if the message
/// cannot be decoded.
payload: Vec<u8>,
/// The error that was returned.
error: ProcessMessageError,
},
/// Message was not committed due to being overweight or the current block is full.
MessagePostponed {
/// The payload of the message. Useful for debugging purposes if the message
/// cannot be decoded.
payload: Vec<u8>,
/// The error that was returned.
reason: ProcessMessageError,
},
/// Some messages have been committed
MessagesCommitted {
/// Merkle root of the committed messages
root: H256,
/// number of committed messages
count: u64,
},
/// Set OperatingMode
OperatingModeChanged { mode: BasicOperatingMode },
/// Delivery Proof received
MessageDelivered { nonce: u64 },
}
#[pezpallet::error]
pub enum Error<T> {
/// The message is too large
MessageTooLarge,
/// The pezpallet is halted
Halted,
/// Invalid Channel
InvalidChannel,
/// Invalid Envelope
InvalidEnvelope,
/// Message verification error
Verification(VerificationError),
/// Invalid Gateway
InvalidGateway,
/// Pending nonce does not exist
InvalidPendingNonce,
/// Reward payment failed
RewardPaymentFailed,
}
/// Messages to be committed in the current block. This storage value is killed in
/// `on_initialize`, so will not end up bloating state.
///
/// Is never read in the runtime, only by offchain message relayers.
/// Because of this, it will never go into the PoV of a block.
///
/// Inspired by the `pezframe_system::Pezpallet::Events` storage value
#[pezpallet::storage]
#[pezpallet::unbounded]
pub type Messages<T: Config> = StorageValue<_, Vec<OutboundMessage>, ValueQuery>;
/// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a
/// merkle root during `on_finalize`. This storage value is killed in `on_initialize`, so state
/// at each block contains only root hash of messages processed in that block. This also means
/// it doesn't have to be included in PoV.
#[pezpallet::storage]
#[pezpallet::unbounded]
pub type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
/// The current nonce for the messages
#[pezpallet::storage]
pub type Nonce<T: Config> = StorageValue<_, u64, ValueQuery>;
/// Pending orders to relay
#[pezpallet::storage]
pub type PendingOrders<T: Config> =
StorageMap<_, Twox64Concat, u64, PendingOrder<BlockNumberFor<T>>, OptionQuery>;
#[pezpallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
// Remove storage from previous block
Messages::<T>::kill();
MessageLeaves::<T>::kill();
// Reserve some weight for the `on_finalize` handler
T::WeightInfo::on_initialize() + T::WeightInfo::commit()
}
fn on_finalize(_: BlockNumberFor<T>) {
Self::commit();
}
}
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<T> {
fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256);
}
#[pezpallet::call]
impl<T: Config> Pezpallet<T>
where
<T as pezframe_system::Config>::AccountId: From<[u8; 32]>,
{
#[pezpallet::call_index(1)]
#[pezpallet::weight(T::WeightInfo::submit_delivery_receipt())]
pub fn submit_delivery_receipt(
origin: OriginFor<T>,
event: Box<EventProof>,
) -> DispatchResult
where
<T as pezframe_system::Config>::AccountId: From<[u8; 32]>,
{
let relayer = ensure_signed(origin)?;
// submit message to verifier for verification
T::Verifier::verify(&event.event_log, &event.proof)
.map_err(|e| Error::<T>::Verification(e))?;
let receipt = DeliveryReceipt::try_from(&event.event_log)
.map_err(|_| Error::<T>::InvalidEnvelope)?;
Self::process_delivery_receipt(relayer, receipt)
}
}
impl<T: Config> Pezpallet<T> {
/// Generate a messages commitment and insert it into the header digest
pub(crate) fn commit() {
let count = MessageLeaves::<T>::decode_len().unwrap_or_default() as u64;
if count == 0 {
return;
}
// Create merkle root of messages
let root = merkle_root::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter());
let digest_item: DigestItem = SnowbridgeDigestItem::SnowbridgeV2(root).into();
// Insert merkle root into the header digest
<pezframe_system::Pezpallet<T>>::deposit_log(digest_item);
T::OnNewCommitment::on_new_commitment(root);
Self::deposit_event(Event::MessagesCommitted { root, count });
}
/// Process a message delivered by the MessageQueue pezpallet.
/// IMPORTANT!! This method does not roll back storage changes on error.
pub(crate) fn do_process_message(
_: ProcessMessageOriginOf<T>,
mut message: &[u8],
) -> Result<bool, ProcessMessageError> {
use ProcessMessageError::*;
// Yield if the maximum number of messages has been processed this block.
// This ensures that the weight of `on_finalize` has a known maximum bound.
let current_len = MessageLeaves::<T>::decode_len().unwrap_or(0);
if current_len >= T::MaxMessagesPerBlock::get() as usize {
Self::deposit_event(Event::MessagePostponed {
payload: message.to_vec(),
reason: Yield,
});
return Err(Yield);
}
// Decode bytes into Message
let Message { origin, id, fee, commands } =
Message::decode(&mut message).map_err(|_| {
Self::deposit_event(Event::MessageRejected {
id: None,
payload: message.to_vec(),
error: Corrupt,
});
Corrupt
})?;
// Convert it to OutboundMessage and save into Messages storage
let commands: Vec<OutboundCommandWrapper> = commands
.into_iter()
.map(|command| OutboundCommandWrapper {
kind: command.index(),
gas: T::GasMeter::maximum_dispatch_gas_used_at_most(&command),
payload: command.abi_encode(),
})
.collect();
let nonce = <Nonce<T>>::get().checked_add(1).ok_or_else(|| {
Self::deposit_event(Event::MessageRejected {
id: None,
payload: message.to_vec(),
error: Unsupported,
});
Unsupported
})?;
let outbound_message = OutboundMessage {
origin,
nonce,
topic: id,
commands: commands.clone().try_into().map_err(|_| {
Self::deposit_event(Event::MessageRejected {
id: Some(id),
payload: message.to_vec(),
error: Corrupt,
});
Corrupt
})?,
};
Messages::<T>::append(outbound_message);
// Convert it to an OutboundMessageWrapper (in ABI format), hash it using Keccak256 to
// generate a committed hash, and store it in MessageLeaves storage which can be
// verified on Ethereum later.
let abi_commands: Vec<CommandWrapper> = commands
.into_iter()
.map(|command| CommandWrapper {
kind: command.kind,
gas: command.gas,
payload: Bytes::from(command.payload),
})
.collect();
let committed_message = OutboundMessageWrapper {
origin: FixedBytes::from(origin.as_fixed_bytes()),
nonce,
topic: FixedBytes::from(id.as_fixed_bytes()),
commands: abi_commands,
};
let message_abi_encoded_hash =
<T as Config>::Hashing::hash(&committed_message.abi_encode());
MessageLeaves::<T>::append(message_abi_encoded_hash);
// Generate `PendingOrder` with fee attached in the message, stored
// into the `PendingOrders` map storage, with assigned nonce as the key.
// When the message is processed on ethereum side, the relayer will send the nonce
// back with delivery proof, only after that the order can
// be resolved and the fee will be rewarded to the relayer.
let order = PendingOrder {
nonce,
fee,
block_number: pezframe_system::Pezpallet::<T>::current_block_number(),
};
<PendingOrders<T>>::insert(nonce, order);
<Nonce<T>>::set(nonce);
Self::deposit_event(Event::MessageAccepted { id, nonce });
Ok(true)
}
/// Process a delivery receipt from a relayer, to allocate the relayer reward.
pub fn process_delivery_receipt(
relayer: <T as pezframe_system::Config>::AccountId,
receipt: DeliveryReceipt,
) -> DispatchResult
where
<T as pezframe_system::Config>::AccountId: From<[u8; 32]>,
{
// Verify that the message was submitted from the known Gateway contract
ensure!(T::GatewayAddress::get() == receipt.gateway, Error::<T>::InvalidGateway);
let reward_account = if receipt.reward_address == [0u8; 32] {
relayer
} else {
receipt.reward_address.into()
};
let nonce = receipt.nonce;
let order = <PendingOrders<T>>::get(nonce).ok_or(Error::<T>::InvalidPendingNonce)?;
if order.fee > 0 {
// Pay relayer reward
T::RewardPayment::register_reward(
&reward_account,
T::DefaultRewardKind::get(),
order.fee,
);
}
<PendingOrders<T>>::remove(nonce);
Self::deposit_event(Event::MessageDelivered { nonce });
Ok(())
}
}
impl<T: Config> AddTip for Pezpallet<T> {
fn add_tip(nonce: u64, amount: u128) -> Result<(), AddTipError> {
ensure!(amount > 0, AddTipError::AmountZero);
PendingOrders::<T>::try_mutate_exists(nonce, |maybe_order| -> Result<(), AddTipError> {
match maybe_order {
Some(order) => {
order.fee = order.fee.saturating_add(amount);
Ok(())
},
None => Err(AddTipError::UnknownMessage),
}
})
}
}
}