Files
pezkuwi-sdk/pezbridges/pezsnowbridge/pezpallets/outbound-queue/src/lib.rs
T
pezkuwichain c343223ccd rebrand: kusama → dicle
- Replace all kusama/Kusama references with dicle/Dicle
- Rename weight files from ksm_size to dcl_size
- Update papi-tests files from ksm to dcl
- Remove chain-specs/kusama.json files
- cargo check --workspace successful (Finished output)
- Update MAINNET_ROADMAP.md: FAZ 8 completed
2026-01-07 09:41:15 +03:00

422 lines
14 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`:
//!
//! 1. `pezsnowbridge_outbound_queue_primitives::v1::EthereumBlobExporter::deliver`
//! 2. `pezsnowbridge_pezpallet_system::Pezpallet::send`
//!
//! The message submission pipeline works like this:
//! 1. The message is first validated via the implementation for
//! [`pezsnowbridge_outbound_queue_primitives::v1::SendMessage::validate`]
//! 2. The message is then enqueued for later processing via the implementation for
//! [`pezsnowbridge_outbound_queue_primitives::v1::SendMessage::deliver`]
//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
//! 4. The message queue delivers messages back to this pezpallet via the implementation for
//! [`pezframe_support::traits::ProcessMessage::process_message`]
//! 5. The message is processed in `Pezpallet::do_process_message`: a. Assigned a nonce b.
//! ABI-encoded, hashed, and stored in the `MessageLeaves` vector
//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`.
//! 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
//!
//! On the Ethereum side, the message root is ultimately the thing being
//! verified by the Pezkuwi light client.
//!
//! # Message Priorities
//!
//! The processing of governance commands can never be halted. This effectively
//! allows us to pause processing of normal user messages while still allowing
//! governance commands to be sent to Ethereum.
//!
//! # Fees
//!
//! An upfront fee must be paid for delivering a message. This fee covers several
//! components:
//! 1. The weight of processing the message locally
//! 2. The gas refund paid out to relayers for message submission
//! 3. An additional reward paid out to relayers for message submission
//!
//! Messages are weighed to determine the maximum amount of gas they could
//! consume on Ethereum. Using this upper bound, a final fee can be calculated.
//!
//! The fee calculation also requires the following parameters:
//! * Average ETH/HEZ exchange rate over some period
//! * Max fee per unit of gas that bridge is willing to refund relayers for
//!
//! By design, it is expected that governance should manually update these
//! parameters every few weeks using the `set_pricing_parameters` extrinsic in the
//! system pezpallet.
//!
//! This is an interim measure. Once ETH/HEZ liquidity pools are available in the Pezkuwi network,
//! we'll use them as a source of pricing info, subject to certain safeguards.
//!
//! ## Fee Computation Function
//!
//! ```text
//! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message))
//! RemoteFee(Message) = MaxGasRequired(Message) * Params.MaxFeePerGas + Params.Reward
//! RemoteFeeAdjusted(Message) = Params.Multiplier * (RemoteFee(Message) / Params.Ratio("ETH/HEZ"))
//! Fee(Message) = LocalFee(Message) + RemoteFeeAdjusted(Message)
//! ```
//!
//! By design, the computed fee includes a safety factor (the `Multiplier`) to cover
//! unfavourable fluctuations in the ETH/HEZ exchange rate.
//!
//! ## Fee Settlement
//!
//! On the remote side, in the gateway contract, the relayer accrues
//!
//! ```text
//! Min(GasPrice, Message.MaxFeePerGas) * GasUsed() + Message.Reward
//! ```
//! Or in plain english, relayers are refunded for gas consumption, using a
//! price that is a minimum of the actual gas price, or `Message.MaxFeePerGas`.
//!
//! # Extrinsics
//!
//! * [`Call::set_operating_mode`]: Set the operating mode
//!
//! # Runtime API
//!
//! * `prove_message`: Generate a merkle proof for a committed message
//! * `calculate_fee`: Calculate the delivery fee for a 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;
use codec::Decode;
use pezbridge_hub_common::AggregateMessageOrigin;
use pezframe_support::{
storage::StorageStreamIter,
traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError},
weights::{Weight, WeightToFee},
};
use pezsnowbridge_core::{digest_item::SnowbridgeDigestItem, BasicOperatingMode, ChannelId};
use pezsnowbridge_merkle_tree::merkle_root;
use pezsnowbridge_outbound_queue_primitives::v1::{
Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS,
};
use pezsp_core::{H256, U256};
use pezsp_runtime::{
traits::{CheckedDiv, Hash},
DigestItem, Saturating,
};
use pezsp_std::prelude::*;
pub use types::{CommittedMessage, ProcessMessageOriginOf};
pub use weights::WeightInfo;
pub use pezpallet::*;
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
use pezsnowbridge_core::PricingParameters;
use pezsp_arithmetic::FixedU128;
#[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 MessageQueue: EnqueueMessage<AggregateMessageOrigin>;
/// Measures the maximum gas used to execute a command on Ethereum
type GasMeter: GasMeter;
type Balance: Balance + From<u128>;
/// Number of decimal places in native currency
#[pezpallet::constant]
type Decimals: Get<u8>;
/// 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>;
/// Check whether a channel exists
type Channels: Contains<ChannelId>;
type PricingParameters: Get<PricingParameters<Self::Balance>>;
/// 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;
}
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Message has been queued and will be processed in the future
MessageQueued {
/// ID of the message. Usually the XCM message hash or a SetTopic.
id: H256,
},
/// Message will be committed at the end of current block. From now on, to track the
/// progress the message, use the `nonce` of `id`.
MessageAccepted {
/// ID of the message
id: H256,
/// The nonce assigned to this message
nonce: u64,
},
/// 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 },
}
#[pezpallet::error]
pub enum Error<T> {
/// The message is too large
MessageTooLarge,
/// The pezpallet is halted
Halted,
/// Invalid Channel
InvalidChannel,
}
/// Messages to be committed in the current block. This storage value is killed in
/// `on_initialize`, so should never go into block PoV.
///
/// Is never read in the runtime, only by offchain message relayers.
///
/// Inspired by the `pezframe_system::Pezpallet::Events` storage value
#[pezpallet::storage]
#[pezpallet::unbounded]
pub(super) type Messages<T: Config> = StorageValue<_, Vec<CommittedMessage>, 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 should never go into block PoV.
#[pezpallet::storage]
#[pezpallet::unbounded]
#[pezpallet::getter(fn message_leaves)]
pub(super) type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
/// The current nonce for each message origin
#[pezpallet::storage]
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
/// The current operating mode of the pezpallet.
#[pezpallet::storage]
#[pezpallet::getter(fn operating_mode)]
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pezpallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T>
where
T::AccountId: AsRef<[u8]>,
{
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::commit()
}
fn on_finalize(_: BlockNumberFor<T>) {
Self::commit();
}
fn integrity_test() {
let decimals = T::Decimals::get();
assert!(decimals == 10 || decimals == 12, "Decimals should be 10 or 12");
}
}
#[pezpallet::call]
impl<T: Config> Pezpallet<T> {
/// Halt or resume all pezpallet operations. May only be called by root.
#[pezpallet::call_index(0)]
#[pezpallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_operating_mode(
origin: OriginFor<T>,
mode: BasicOperatingMode,
) -> DispatchResult {
ensure_root(origin)?;
OperatingMode::<T>::put(mode);
Self::deposit_event(Event::OperatingModeChanged { mode });
Ok(())
}
}
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::Snowbridge(root).into();
// Insert merkle root into the header digest
<pezframe_system::Pezpallet<T>>::deposit_log(digest_item);
Self::deposit_event(Event::MessagesCommitted { root, count });
}
/// Process a message delivered by the MessageQueue pezpallet
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.
ensure!(
MessageLeaves::<T>::decode_len().unwrap_or(0)
< T::MaxMessagesPerBlock::get() as usize,
Yield
);
// Decode bytes into versioned message
let versioned_queued_message: VersionedQueuedMessage =
VersionedQueuedMessage::decode(&mut message).map_err(|_| Corrupt)?;
// Convert versioned message into latest supported message version
let queued_message: QueuedMessage =
versioned_queued_message.try_into().map_err(|_| Unsupported)?;
// Obtain next nonce
let nonce = <Nonce<T>>::try_mutate(
queued_message.channel_id,
|nonce| -> Result<u64, ProcessMessageError> {
*nonce = nonce.checked_add(1).ok_or(Unsupported)?;
Ok(*nonce)
},
)?;
let pricing_params = T::PricingParameters::get();
let command = queued_message.command.index();
let params = queued_message.command.abi_encode();
let max_dispatch_gas =
T::GasMeter::maximum_dispatch_gas_used_at_most(&queued_message.command);
let reward = pricing_params.rewards.remote;
// Construct the final committed message
let message = CommittedMessage {
channel_id: queued_message.channel_id,
nonce,
command,
params,
max_dispatch_gas,
max_fee_per_gas: pricing_params
.fee_per_gas
.try_into()
.defensive_unwrap_or(u128::MAX),
reward: reward.try_into().defensive_unwrap_or(u128::MAX),
id: queued_message.id,
};
// ABI-encode and hash the prepared message
let message_abi_encoded = ethabi::encode(&[message.clone().into()]);
let message_abi_encoded_hash = <T as Config>::Hashing::hash(&message_abi_encoded);
Messages::<T>::append(Box::new(message));
MessageLeaves::<T>::append(message_abi_encoded_hash);
Self::deposit_event(Event::MessageAccepted { id: queued_message.id, nonce });
Ok(true)
}
/// Calculate total fee in native currency to cover all costs of delivering a message to the
/// remote destination. See module-level documentation for more details.
pub(crate) fn calculate_fee(
gas_used_at_most: u64,
params: PricingParameters<T::Balance>,
) -> Fee<T::Balance> {
// Remote fee in ether
let fee = Self::calculate_remote_fee(
gas_used_at_most,
params.fee_per_gas,
params.rewards.remote,
);
// downcast to u128
let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX);
// multiply by multiplier and convert to local currency
let fee = FixedU128::from_inner(fee)
.saturating_mul(params.multiplier)
.checked_div(&params.exchange_rate)
.expect("exchange rate is not zero; qed")
.into_inner();
// adjust fixed point to match local currency
let fee = Self::convert_from_ether_decimals(fee);
Fee::from((Self::calculate_local_fee(), fee))
}
/// Calculate fee in remote currency for dispatching a message on Ethereum
pub(crate) fn calculate_remote_fee(
gas_used_at_most: u64,
fee_per_gas: U256,
reward: U256,
) -> U256 {
fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward)
}
/// The local component of the message processing fees in native currency
pub(crate) fn calculate_local_fee() -> T::Balance {
T::WeightToFee::weight_to_fee(
&T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()),
)
}
// 1 HEZ has 10 digits of precision
// 1 DCL has 12 digits of precision
// 1 ETH has 18 digits of precision
pub(crate) fn convert_from_ether_decimals(value: u128) -> T::Balance {
let decimals = ETHER_DECIMALS.saturating_sub(T::Decimals::get()) as u32;
let denom = 10u128.saturating_pow(decimals);
value.checked_div(denom).expect("divisor is non-zero; qed").into()
}
}
}