Removes Snowbridge parachain directory (#3186)

Removes the `bridges/snowbridge/parachain` directory and moves
everything up to under `snowbridge` directly. We are cleaning up our
local dev env after merging our crates into the polkadot-sdk.

---------

Co-authored-by: claravanstaden <Cats 4 life!>
This commit is contained in:
Clara van Staden
2024-02-02 19:08:36 +02:00
committed by GitHub
parent 700d5f85b7
commit 2ab3f03f0b
122 changed files with 199 additions and 529 deletions
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Helpers for implementing runtime api
use crate::{Config, MessageLeaves};
use frame_support::storage::StorageStreamIter;
use snowbridge_core::outbound::{Message, SendMessage};
use snowbridge_outbound_queue_merkle_tree::{merkle_proof, MerkleProof};
pub fn prove_message<T>(leaf_index: u64) -> Option<MerkleProof>
where
T: Config,
{
if !MessageLeaves::<T>::exists() {
return None
}
let proof =
merkle_proof::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter(), leaf_index);
Some(proof)
}
pub fn calculate_fee<T>(message: Message) -> Option<T::Balance>
where
T: Config,
{
match crate::Pallet::<T>::validate(&message) {
Ok((_, fees)) => Some(fees.total()),
_ => None,
}
}
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use bridge_hub_common::AggregateMessageOrigin;
use codec::Encode;
use frame_benchmarking::v2::*;
use snowbridge_core::{
outbound::{Command, Initializer},
ChannelId,
};
use sp_core::{H160, H256};
#[allow(unused_imports)]
use crate::Pallet as OutboundQueue;
#[benchmarks(
where
<T as Config>::MaxMessagePayloadSize: Get<u32>,
)]
mod benchmarks {
use super::*;
/// Benchmark for processing a message.
#[benchmark]
fn do_process_message() -> Result<(), BenchmarkError> {
let enqueued_message = QueuedMessage {
id: H256::zero(),
channel_id: ChannelId::from([1; 32]),
command: Command::Upgrade {
impl_address: H160::zero(),
impl_code_hash: H256::zero(),
initializer: Some(Initializer {
params: [7u8; 256].into_iter().collect(),
maximum_required_gas: 200_000,
}),
},
};
let origin = AggregateMessageOrigin::Snowbridge([1; 32].into());
let encoded_enqueued_message = enqueued_message.encode();
#[block]
{
let _ = OutboundQueue::<T>::do_process_message(origin, &encoded_enqueued_message);
}
assert_eq!(MessageLeaves::<T>::decode_len().unwrap(), 1);
Ok(())
}
/// Benchmark for producing final messages commitment
#[benchmark]
fn commit() -> Result<(), BenchmarkError> {
// Assume worst case, where `MaxMessagesPerBlock` messages need to be committed.
for i in 0..T::MaxMessagesPerBlock::get() {
let leaf_data: [u8; 1] = [i as u8];
let leaf = <T as Config>::Hashing::hash(&leaf_data);
MessageLeaves::<T>::append(leaf);
}
#[block]
{
OutboundQueue::<T>::commit();
}
Ok(())
}
/// Benchmark for producing commitment for a single message
#[benchmark]
fn commit_single() -> Result<(), BenchmarkError> {
let leaf = <T as Config>::Hashing::hash(&[100; 1]);
MessageLeaves::<T>::append(leaf);
#[block]
{
OutboundQueue::<T>::commit();
}
Ok(())
}
impl_benchmark_test_suite!(OutboundQueue, crate::mock::new_tester(), crate::mock::Test,);
}
@@ -0,0 +1,406 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Pallet for committing outbound messages for delivery to Ethereum
//!
//! # Overview
//!
//! Messages come either from sibling parachains via XCM, or BridgeHub itself
//! via the `snowbridge-pallet-system`:
//!
//! 1. `snowbridge_router_primitives::outbound::EthereumBlobExporter::deliver`
//! 2. `snowbridge_pallet_system::Pallet::send`
//!
//! The message submission pipeline works like this:
//! 1. The message is first validated via the implementation for
//! [`snowbridge_core::outbound::SendMessage::validate`]
//! 2. The message is then enqueued for later processing via the implementation for
//! [`snowbridge_core::outbound::SendMessage::deliver`]
//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
//! 4. The message queue delivers messages back to this pallet via the implementation for
//! [`frame_support::traits::ProcessMessage::process_message`]
//! 5. The message is processed in `Pallet::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 parachain 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 Polkadot 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:
//! * ETH/DOT exchange rate
//! * Ether fee per unit of gas
//!
//! By design, it is expected that governance should manually update these
//! parameters every few weeks using the `set_pricing_parameters` extrinsic in the
//! system pallet.
//!
//! ## Fee Computation Function
//!
//! ```text
//! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message))
//! RemoteFee(Message) = MaxGasRequired(Message) * FeePerGas + Reward
//! Fee(Message) = LocalFee(Message) + (RemoteFee(Message) / Ratio("ETH/DOT"))
//! ```
//!
//! By design, the computed fee is always going to conservative, to cover worst-case
//! costs of dispatch on Ethereum. In future iterations of the design, we will optimize
//! this, or provide a mechanism to asynchronously refund a portion of collected fees.
//!
//! # 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 bridge_hub_common::{AggregateMessageOrigin, CustomDigestItem};
use codec::Decode;
use frame_support::{
storage::StorageStreamIter,
traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError},
weights::{Weight, WeightToFee},
};
use snowbridge_core::{
outbound::{Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS},
BasicOperatingMode, ChannelId,
};
use snowbridge_outbound_queue_merkle_tree::merkle_root;
pub use snowbridge_outbound_queue_merkle_tree::MerkleProof;
use sp_core::{H256, U256};
use sp_runtime::{
traits::{CheckedDiv, Hash},
DigestItem,
};
use sp_std::prelude::*;
pub use types::{CommittedMessage, ProcessMessageOriginOf};
pub use weights::WeightInfo;
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use snowbridge_core::PricingParameters;
use sp_arithmetic::FixedU128;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_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
#[pallet::constant]
type Decimals: Get<u8>;
/// Max bytes in a message payload
#[pallet::constant]
type MaxMessagePayloadSize: Get<u32>;
/// Max number of messages processed per block
#[pallet::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 pallet
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::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 },
}
#[pallet::error]
pub enum Error<T> {
/// The message is too large
MessageTooLarge,
/// The pallet 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 `frame_system::Pallet::Events` storage value
#[pallet::storage]
#[pallet::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.
#[pallet::storage]
#[pallet::unbounded]
#[pallet::getter(fn message_leaves)]
pub(super) type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
/// The current nonce for each message origin
#[pallet::storage]
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
/// The current operating mode of the pallet.
#[pallet::storage]
#[pallet::getter(fn operating_mode)]
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<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");
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Halt or resume all pallet operations. May only be called by root.
#[pallet::call_index(0)]
#[pallet::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> Pallet<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 = CustomDigestItem::Snowbridge(root).into();
// Insert merkle root into the header digest
<frame_system::Pallet<T>>::deposit_log(digest_item);
Self::deposit_event(Event::MessagesCommitted { root, count });
}
/// Process a message delivered by the MessageQueue pallet
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);
// convert to local currency
let fee = FixedU128::from_inner(fee)
.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 DOT has 10 digits of precision
// 1 KSM 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()
}
}
}
@@ -0,0 +1,189 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use frame_support::{
parameter_types,
traits::{Everything, Hooks},
weights::IdentityFee,
};
use snowbridge_core::{
gwei, meth,
outbound::*,
pricing::{PricingParameters, Rewards},
ParaId, PRIMARY_GOVERNANCE_CHANNEL,
};
use sp_core::{ConstU32, ConstU8, H160, H256};
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup, Keccak256},
AccountId32, BuildStorage, FixedU128,
};
use sp_std::marker::PhantomData;
type Block = frame_system::mocking::MockBlock<Test>;
type AccountId = AccountId32;
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system::{Pallet, Call, Storage, Event<T>},
MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event<T>},
OutboundQueue: crate::{Pallet, Storage, Event<T>},
}
);
parameter_types! {
pub const BlockHashCount: u64 = 250;
}
impl frame_system::Config for Test {
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type RuntimeTask = RuntimeTask;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type DbWeight = ();
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type Block = Block;
}
parameter_types! {
pub const HeapSize: u32 = 32 * 1024;
pub const MaxStale: u32 = 32;
pub static ServiceWeight: Option<Weight> = Some(Weight::from_parts(100, 100));
}
impl pallet_message_queue::Config for Test {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type MessageProcessor = OutboundQueue;
type Size = u32;
type QueueChangeHandler = ();
type HeapSize = HeapSize;
type MaxStale = MaxStale;
type ServiceWeight = ServiceWeight;
type QueuePausedQuery = ();
}
parameter_types! {
pub const OwnParaId: ParaId = ParaId::new(1013);
pub Parameters: PricingParameters<u128> = PricingParameters {
exchange_rate: FixedU128::from_rational(1, 400),
fee_per_gas: gwei(20),
rewards: Rewards { local: DOT, remote: meth(1) }
};
}
pub const DOT: u128 = 10_000_000_000;
impl crate::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Hashing = Keccak256;
type MessageQueue = MessageQueue;
type Decimals = ConstU8<12>;
type MaxMessagePayloadSize = ConstU32<1024>;
type MaxMessagesPerBlock = ConstU32<20>;
type GasMeter = ConstantGasMeter;
type Balance = u128;
type PricingParameters = Parameters;
type Channels = Everything;
type WeightToFee = IdentityFee<u128>;
type WeightInfo = ();
}
fn setup() {
System::set_block_number(1);
}
pub fn new_tester() -> sp_io::TestExternalities {
let storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
let mut ext: sp_io::TestExternalities = storage.into();
ext.execute_with(setup);
ext
}
pub fn run_to_end_of_next_block() {
// finish current block
MessageQueue::on_finalize(System::block_number());
OutboundQueue::on_finalize(System::block_number());
System::on_finalize(System::block_number());
// start next block
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
OutboundQueue::on_initialize(System::block_number());
MessageQueue::on_initialize(System::block_number());
// finish next block
MessageQueue::on_finalize(System::block_number());
OutboundQueue::on_finalize(System::block_number());
System::on_finalize(System::block_number());
}
pub fn mock_governance_message<T>() -> Message
where
T: Config,
{
let _marker = PhantomData::<T>; // for clippy
Message {
id: None,
channel_id: PRIMARY_GOVERNANCE_CHANNEL,
command: Command::Upgrade {
impl_address: H160::zero(),
impl_code_hash: H256::zero(),
initializer: None,
},
}
}
// Message should fail validation as it is too large
pub fn mock_invalid_governance_message<T>() -> Message
where
T: Config,
{
let _marker = PhantomData::<T>; // for clippy
Message {
id: None,
channel_id: PRIMARY_GOVERNANCE_CHANNEL,
command: Command::Upgrade {
impl_address: H160::zero(),
impl_code_hash: H256::zero(),
initializer: Some(Initializer {
params: (0..1000).map(|_| 1u8).collect::<Vec<u8>>(),
maximum_required_gas: 0,
}),
},
}
}
pub fn mock_message(sibling_para_id: u32) -> Message {
Message {
id: None,
channel_id: ParaId::from(sibling_para_id).into(),
command: Command::AgentExecute {
agent_id: Default::default(),
command: AgentExecuteCommand::TransferToken {
token: Default::default(),
recipient: Default::default(),
amount: 0,
},
},
}
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Implementation for [`frame_support::traits::ProcessMessage`]
use super::*;
use crate::weights::WeightInfo;
use frame_support::{
traits::{ProcessMessage, ProcessMessageError},
weights::WeightMeter,
};
impl<T: Config> ProcessMessage for Pallet<T> {
type Origin = AggregateMessageOrigin;
fn process_message(
message: &[u8],
origin: Self::Origin,
meter: &mut WeightMeter,
_: &mut [u8; 32],
) -> Result<bool, ProcessMessageError> {
let weight = T::WeightInfo::do_process_message();
if meter.try_consume(weight).is_err() {
return Err(ProcessMessageError::Overweight(weight))
}
Self::do_process_message(origin, message)
}
}
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Implementation for [`snowbridge_core::outbound::SendMessage`]
use super::*;
use bridge_hub_common::AggregateMessageOrigin;
use codec::Encode;
use frame_support::{
ensure,
traits::{EnqueueMessage, Get},
CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
use frame_system::unique;
use snowbridge_core::{
outbound::{
Fee, Message, QueuedMessage, SendError, SendMessage, SendMessageFeeProvider,
VersionedQueuedMessage,
},
ChannelId, PRIMARY_GOVERNANCE_CHANNEL,
};
use sp_core::H256;
use sp_runtime::BoundedVec;
/// The maximal length of an enqueued message, as determined by the MessageQueue pallet
pub type MaxEnqueuedMessageSizeOf<T> =
<<T as Config>::MessageQueue as EnqueueMessage<AggregateMessageOrigin>>::MaxMessageLen;
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound)]
pub struct Ticket<T>
where
T: Config,
{
pub message_id: H256,
pub channel_id: ChannelId,
pub message: BoundedVec<u8, MaxEnqueuedMessageSizeOf<T>>,
}
impl<T> SendMessage for Pallet<T>
where
T: Config,
{
type Ticket = Ticket<T>;
fn validate(
message: &Message,
) -> Result<(Self::Ticket, Fee<<Self as SendMessageFeeProvider>::Balance>), SendError> {
// The inner payload should not be too large
let payload = message.command.abi_encode();
ensure!(
payload.len() < T::MaxMessagePayloadSize::get() as usize,
SendError::MessageTooLarge
);
// Ensure there is a registered channel we can transmit this message on
ensure!(T::Channels::contains(&message.channel_id), SendError::InvalidChannel);
// Generate a unique message id unless one is provided
let message_id: H256 = message
.id
.unwrap_or_else(|| unique((message.channel_id, &message.command)).into());
let gas_used_at_most = T::GasMeter::maximum_gas_used_at_most(&message.command);
let fee = Self::calculate_fee(gas_used_at_most, T::PricingParameters::get());
let queued_message: VersionedQueuedMessage = QueuedMessage {
id: message_id,
channel_id: message.channel_id,
command: message.command.clone(),
}
.into();
// The whole message should not be too large
let encoded = queued_message.encode().try_into().map_err(|_| SendError::MessageTooLarge)?;
let ticket = Ticket { message_id, channel_id: message.channel_id, message: encoded };
Ok((ticket, fee))
}
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError> {
let origin = AggregateMessageOrigin::Snowbridge(ticket.channel_id);
if ticket.channel_id != PRIMARY_GOVERNANCE_CHANNEL {
ensure!(!Self::operating_mode().is_halted(), SendError::Halted);
}
let message = ticket.message.as_bounded_slice();
T::MessageQueue::enqueue_message(message, origin);
Self::deposit_event(Event::MessageQueued { id: ticket.message_id });
Ok(ticket.message_id)
}
}
impl<T: Config> SendMessageFeeProvider for Pallet<T> {
type Balance = T::Balance;
/// The local component of the message processing fees in native currency
fn local_fee() -> Self::Balance {
Self::calculate_local_fee()
}
}
@@ -0,0 +1,312 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate::{mock::*, *};
use frame_support::{
assert_err, assert_noop, assert_ok,
traits::{Hooks, ProcessMessage, ProcessMessageError},
weights::WeightMeter,
};
use codec::Encode;
use snowbridge_core::{
outbound::{Command, SendError, SendMessage},
ParaId, PricingParameters, Rewards,
};
use sp_arithmetic::FixedU128;
use sp_core::H256;
use sp_runtime::FixedPointNumber;
#[test]
fn submit_messages_and_commit() {
new_tester().execute_with(|| {
for para_id in 1000..1004 {
let message = mock_message(para_id);
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
assert_ok!(OutboundQueue::deliver(ticket));
}
ServiceWeight::set(Some(Weight::MAX));
run_to_end_of_next_block();
for para_id in 1000..1004 {
let origin: ParaId = (para_id as u32).into();
let channel_id: ChannelId = origin.into();
assert_eq!(Nonce::<Test>::get(channel_id), 1);
}
let digest = System::digest();
let digest_items = digest.logs();
assert!(digest_items.len() == 1 && digest_items[0].as_other().is_some());
assert_eq!(Messages::<Test>::decode_len(), Some(4));
});
}
#[test]
fn submit_message_fail_too_large() {
new_tester().execute_with(|| {
let message = mock_invalid_governance_message::<Test>();
assert_err!(OutboundQueue::validate(&message), SendError::MessageTooLarge);
});
}
#[test]
fn convert_from_ether_decimals() {
assert_eq!(
OutboundQueue::convert_from_ether_decimals(1_000_000_000_000_000_000),
1_000_000_000_000
);
}
#[test]
fn commit_exits_early_if_no_processed_messages() {
new_tester().execute_with(|| {
// on_finalize should do nothing, nor should it panic
OutboundQueue::on_finalize(System::block_number());
let digest = System::digest();
let digest_items = digest.logs();
assert_eq!(digest_items.len(), 0);
});
}
#[test]
fn process_message_yields_on_max_messages_per_block() {
new_tester().execute_with(|| {
for _ in 0..<Test as Config>::MaxMessagesPerBlock::get() {
MessageLeaves::<Test>::append(H256::zero())
}
let channel_id: ChannelId = ParaId::from(1000).into();
let origin = AggregateMessageOrigin::Snowbridge(channel_id);
let message = QueuedMessage {
id: Default::default(),
channel_id,
command: Command::Upgrade {
impl_address: Default::default(),
impl_code_hash: Default::default(),
initializer: None,
},
}
.encode();
let mut meter = WeightMeter::new();
assert_noop!(
OutboundQueue::process_message(message.as_slice(), origin, &mut meter, &mut [0u8; 32]),
ProcessMessageError::Yield
);
})
}
#[test]
fn process_message_fails_on_max_nonce_reached() {
new_tester().execute_with(|| {
let sibling_id = 1000;
let channel_id: ChannelId = ParaId::from(sibling_id).into();
let origin = AggregateMessageOrigin::Snowbridge(channel_id);
let message: QueuedMessage = QueuedMessage {
id: H256::zero(),
channel_id,
command: mock_message(sibling_id).command,
};
let versioned_queued_message: VersionedQueuedMessage = message.try_into().unwrap();
let encoded = versioned_queued_message.encode();
let mut meter = WeightMeter::with_limit(Weight::MAX);
Nonce::<Test>::set(channel_id, u64::MAX);
assert_noop!(
OutboundQueue::process_message(encoded.as_slice(), origin, &mut meter, &mut [0u8; 32]),
ProcessMessageError::Unsupported
);
})
}
#[test]
fn process_message_fails_on_overweight_message() {
new_tester().execute_with(|| {
let sibling_id = 1000;
let channel_id: ChannelId = ParaId::from(sibling_id).into();
let origin = AggregateMessageOrigin::Snowbridge(channel_id);
let message: QueuedMessage = QueuedMessage {
id: H256::zero(),
channel_id,
command: mock_message(sibling_id).command,
};
let versioned_queued_message: VersionedQueuedMessage = message.try_into().unwrap();
let encoded = versioned_queued_message.encode();
let mut meter = WeightMeter::with_limit(Weight::from_parts(1, 1));
assert_noop!(
OutboundQueue::process_message(encoded.as_slice(), origin, &mut meter, &mut [0u8; 32]),
ProcessMessageError::Overweight(<Test as Config>::WeightInfo::do_process_message())
);
})
}
// Governance messages should be able to bypass a halted operating mode
// Other message sends should fail when halted
#[test]
fn submit_upgrade_message_success_when_queue_halted() {
new_tester().execute_with(|| {
// halt the outbound queue
OutboundQueue::set_operating_mode(RuntimeOrigin::root(), BasicOperatingMode::Halted)
.unwrap();
// submit a high priority message from bridge_hub should success
let message = mock_governance_message::<Test>();
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
assert_ok!(OutboundQueue::deliver(ticket));
// submit a low priority message from asset_hub will fail as pallet is halted
let message = mock_message(1000);
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
assert_noop!(OutboundQueue::deliver(ticket), SendError::Halted);
});
}
#[test]
fn governance_message_does_not_get_the_chance_to_processed_in_same_block_when_congest_of_low_priority_sibling_messages(
) {
use snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
use AggregateMessageOrigin::*;
let sibling_id: u32 = 1000;
let sibling_channel_id: ChannelId = ParaId::from(sibling_id).into();
new_tester().execute_with(|| {
// submit a lot of low priority messages from asset_hub which will need multiple blocks to
// execute(20 messages for each block so 40 required at least 2 blocks)
let max_messages = 40;
for _ in 0..max_messages {
// submit low priority message
let message = mock_message(sibling_id);
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
OutboundQueue::deliver(ticket).unwrap();
}
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
assert_eq!(footprint.storage.count, (max_messages) as u64);
let message = mock_governance_message::<Test>();
let (ticket, _) = OutboundQueue::validate(&message).unwrap();
OutboundQueue::deliver(ticket).unwrap();
// move to next block
ServiceWeight::set(Some(Weight::MAX));
run_to_end_of_next_block();
// first process 20 messages from sibling channel
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
assert_eq!(footprint.storage.count, 40 - 20);
// and governance message does not have the chance to execute in same block
let footprint = MessageQueue::footprint(Snowbridge(PRIMARY_GOVERNANCE_CHANNEL));
assert_eq!(footprint.storage.count, 1);
// move to next block
ServiceWeight::set(Some(Weight::MAX));
run_to_end_of_next_block();
// now governance message get executed in this block
let footprint = MessageQueue::footprint(Snowbridge(PRIMARY_GOVERNANCE_CHANNEL));
assert_eq!(footprint.storage.count, 0);
// and this time process 19 messages from sibling channel so we have 1 message left
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
assert_eq!(footprint.storage.count, 1);
// move to the next block, the last 1 message from sibling channel get executed
ServiceWeight::set(Some(Weight::MAX));
run_to_end_of_next_block();
let footprint = MessageQueue::footprint(Snowbridge(sibling_channel_id));
assert_eq!(footprint.storage.count, 0);
});
}
#[test]
fn convert_local_currency() {
new_tester().execute_with(|| {
let fee: u128 = 1_000_000;
let fee1 = FixedU128::from_inner(fee).into_inner();
let fee2 = FixedU128::from(fee)
.into_inner()
.checked_div(FixedU128::accuracy())
.expect("accuracy is not zero; qed");
assert_eq!(fee, fee1);
assert_eq!(fee, fee2);
});
}
#[test]
fn encode_digest_item_with_correct_index() {
new_tester().execute_with(|| {
let digest_item: DigestItem = CustomDigestItem::Snowbridge(H256::default()).into();
let enum_prefix = match digest_item {
DigestItem::Other(data) => data[0],
_ => u8::MAX,
};
assert_eq!(enum_prefix, 0);
});
}
#[test]
fn encode_digest_item() {
new_tester().execute_with(|| {
let digest_item: DigestItem = CustomDigestItem::Snowbridge([5u8; 32].into()).into();
let digest_item_raw = digest_item.encode();
assert_eq!(digest_item_raw[0], 0); // DigestItem::Other
assert_eq!(digest_item_raw[2], 0); // CustomDigestItem::Snowbridge
assert_eq!(
digest_item_raw,
[
0, 132, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5
]
);
});
}
#[test]
fn validate_messages_with_fees() {
new_tester().execute_with(|| {
let message = mock_message(1000);
let (_, fee) = OutboundQueue::validate(&message).unwrap();
assert_eq!(fee.local, 698000000);
assert_eq!(fee.remote, 2680000000000);
});
}
#[test]
fn test_calculate_fees() {
new_tester().execute_with(|| {
let gas_used: u64 = 250000;
let illegal_price_params: PricingParameters<<Test as Config>::Balance> =
PricingParameters {
exchange_rate: FixedU128::from_rational(1, 400),
fee_per_gas: 10000_u32.into(),
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
};
let fee = OutboundQueue::calculate_fee(gas_used, illegal_price_params);
assert_eq!(fee.local, 698000000);
assert_eq!(fee.remote, 1000000);
});
}
#[test]
fn test_calculate_fees_with_valid_exchange_rate_but_remote_fee_calculated_as_zero() {
new_tester().execute_with(|| {
let gas_used: u64 = 250000;
let illegal_price_params: PricingParameters<<Test as Config>::Balance> =
PricingParameters {
exchange_rate: FixedU128::from_rational(1, 1),
fee_per_gas: 1_u32.into(),
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
};
let fee = OutboundQueue::calculate_fee(gas_used, illegal_price_params.clone());
assert_eq!(fee.local, 698000000);
// Though none zero pricing params the remote fee calculated here is invalid
// which should be avoided
assert_eq!(fee.remote, 0);
});
}
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use codec::{Decode, Encode};
use ethabi::Token;
use frame_support::traits::ProcessMessage;
use scale_info::TypeInfo;
use sp_core::H256;
use sp_runtime::RuntimeDebug;
use sp_std::prelude::*;
use super::Pallet;
use snowbridge_core::ChannelId;
pub use snowbridge_outbound_queue_merkle_tree::MerkleProof;
pub type ProcessMessageOriginOf<T> = <Pallet<T> as ProcessMessage>::Origin;
pub const LOG_TARGET: &str = "snowbridge-outbound-queue";
/// Message which has been assigned a nonce and will be committed at the end of a block
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug, TypeInfo)]
pub struct CommittedMessage {
/// Message channel
pub channel_id: ChannelId,
/// Unique nonce to prevent replaying messages
#[codec(compact)]
pub nonce: u64,
/// Command to execute in the Gateway contract
pub command: u8,
/// Params for the command
pub params: Vec<u8>,
/// Maximum gas allowed for message dispatch
#[codec(compact)]
pub max_dispatch_gas: u64,
/// Maximum fee per gas
#[codec(compact)]
pub max_fee_per_gas: u128,
/// Reward in ether for delivering this message, in addition to the gas refund
#[codec(compact)]
pub reward: u128,
/// Message ID (Used for tracing messages across route, has no role in consensus)
pub id: H256,
}
/// Convert message into an ABI-encoded form for delivery to the InboundQueue contract on Ethereum
impl From<CommittedMessage> for Token {
fn from(x: CommittedMessage) -> Token {
Token::Tuple(vec![
Token::FixedBytes(Vec::from(x.channel_id.as_ref())),
Token::Uint(x.nonce.into()),
Token::Uint(x.command.into()),
Token::Bytes(x.params.to_vec()),
Token::Uint(x.max_dispatch_gas.into()),
Token::Uint(x.max_fee_per_gas.into()),
Token::Uint(x.reward.into()),
Token::FixedBytes(Vec::from(x.id.as_ref())),
])
}
}
@@ -0,0 +1,81 @@
//! Autogenerated weights for `snowbridge-pallet-outbound-queue`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-10-19, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `192.168.1.7`, CPU: `<UNKNOWN>`
//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-rococo-dev")`, DB CACHE: `1024`
// Executed Command:
// target/release/polkadot-parachain
// benchmark
// pallet
// --chain=bridge-hub-rococo-dev
// --pallet=snowbridge-pallet-outbound-queue
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --template
// ../parachain/templates/module-weight-template.hbs
// --output
// ../parachain/pallets/outbound-queue/src/weights.rs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use core::marker::PhantomData;
/// Weight functions needed for `snowbridge-pallet-outbound-queue`.
pub trait WeightInfo {
fn do_process_message() -> Weight;
fn commit() -> Weight;
fn commit_single() -> Weight;
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: EthereumOutboundQueue MessageLeaves (r:1 w:1)
/// Proof Skipped: EthereumOutboundQueue MessageLeaves (max_values: Some(1), max_size: None, mode: Measured)
/// Storage: EthereumOutboundQueue PendingHighPriorityMessageCount (r:1 w:1)
/// Proof: EthereumOutboundQueue PendingHighPriorityMessageCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue Nonce (r:1 w:1)
/// Proof: EthereumOutboundQueue Nonce (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue Messages (r:1 w:1)
/// Proof Skipped: EthereumOutboundQueue Messages (max_values: Some(1), max_size: None, mode: Measured)
fn do_process_message() -> Weight {
// Proof Size summary in bytes:
// Measured: `42`
// Estimated: `3485`
// Minimum execution time: 39_000_000 picoseconds.
Weight::from_parts(39_000_000, 3485)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
/// Storage: EthereumOutboundQueue MessageLeaves (r:1 w:0)
/// Proof Skipped: EthereumOutboundQueue MessageLeaves (max_values: Some(1), max_size: None, mode: Measured)
/// Storage: System Digest (r:1 w:1)
/// Proof Skipped: System Digest (max_values: Some(1), max_size: None, mode: Measured)
fn commit() -> Weight {
// Proof Size summary in bytes:
// Measured: `1094`
// Estimated: `2579`
// Minimum execution time: 28_000_000 picoseconds.
Weight::from_parts(28_000_000, 2579)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
fn commit_single() -> Weight {
// Proof Size summary in bytes:
// Measured: `1094`
// Estimated: `2579`
// Minimum execution time: 9_000_000 picoseconds.
Weight::from_parts(9_000_000, 1586)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
}