c343223ccd
- 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
422 lines
14 KiB
Rust
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(¶ms.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()
|
|
}
|
|
}
|
|
}
|