feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
@@ -0,0 +1,104 @@
[package]
name = "snowbridge-pallet-outbound-queue-v2"
description = "Snowbridge Outbound Queue Pallet V2"
version = "0.2.0"
authors = ["Snowfork <contact@snowfork.com>"]
edition.workspace = true
repository.workspace = true
license = "Apache-2.0"
categories = ["cryptography::cryptocurrencies"]
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[package.metadata.pezkuwi-sdk]
exclude-from-umbrella = true
[dependencies]
alloy-core = { workspace = true, features = ["sol-types"] }
codec = { features = ["derive"], workspace = true }
ethabi = { workspace = true }
hex-literal = { workspace = true, default-features = true }
scale-info = { features = ["derive"], workspace = true }
serde = { features = ["alloc", "derive"], workspace = true }
frame-benchmarking = { optional = true, workspace = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-arithmetic = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }
bp-relayers = { workspace = true }
snowbridge-beacon-primitives = { workspace = true }
snowbridge-core = { workspace = true }
snowbridge-merkle-tree = { workspace = true }
snowbridge-outbound-queue-primitives = { workspace = true }
snowbridge-verification-primitives = { workspace = true }
xcm = { workspace = true }
xcm-builder = { workspace = true }
xcm-executor = { workspace = true }
[dev-dependencies]
pallet-message-queue = { workspace = true }
snowbridge-test-utils = { workspace = true }
[features]
default = ["std"]
std = [
"alloy-core/std",
"bp-relayers/std",
"codec/std",
"ethabi/std",
"frame-benchmarking/std",
"frame-support/std",
"frame-system/std",
"pallet-message-queue/std",
"scale-info/std",
"serde/std",
"snowbridge-beacon-primitives/std",
"snowbridge-core/std",
"snowbridge-merkle-tree/std",
"snowbridge-outbound-queue-primitives/std",
"snowbridge-verification-primitives/std",
"sp-arithmetic/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
]
runtime-benchmarks = [
"bp-relayers/runtime-benchmarks",
"frame-benchmarking",
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-message-queue/runtime-benchmarks",
"snowbridge-beacon-primitives/runtime-benchmarks",
"snowbridge-core/runtime-benchmarks",
"snowbridge-merkle-tree/runtime-benchmarks",
"snowbridge-outbound-queue-primitives/runtime-benchmarks",
"snowbridge-test-utils/runtime-benchmarks",
"snowbridge-verification-primitives/runtime-benchmarks",
"sp-io/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
"xcm/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-message-queue/try-runtime",
"sp-runtime/try-runtime",
]
@@ -0,0 +1,42 @@
[package]
name = "snowbridge-outbound-queue-v2-runtime-api"
description = "Snowbridge Outbound Queue Runtime API V2"
version = "0.2.0"
authors = ["Snowfork <contact@snowfork.com>"]
edition.workspace = true
repository.workspace = true
license = "Apache-2.0"
categories = ["cryptography::cryptocurrencies"]
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[package.metadata.pezkuwi-sdk]
exclude-from-umbrella = true
[dependencies]
codec = { features = ["derive"], workspace = true }
frame-support = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
snowbridge-merkle-tree = { workspace = true }
sp-api = { workspace = true }
sp-std = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"frame-support/std",
"scale-info/std",
"snowbridge-merkle-tree/std",
"sp-api/std",
"sp-std/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"snowbridge-merkle-tree/runtime-benchmarks",
"sp-api/runtime-benchmarks",
]
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Ethereum Outbound Queue V2 Runtime API
//!
//! * `prove_message`: Generate a merkle proof for a committed message
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::traits::tokens::Balance as BalanceT;
use snowbridge_merkle_tree::MerkleProof;
sp_api::decl_runtime_apis! {
pub trait OutboundQueueV2Api<Balance> where Balance: BalanceT
{
/// Generate a merkle proof for a committed message identified by `leaf_index`.
fn prove_message(leaf_index: u64) -> Option<MerkleProof>;
}
}
@@ -0,0 +1,19 @@
// 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_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)
}
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use crate::fixture::make_submit_delivery_receipt_message;
use codec::Encode;
use frame_benchmarking::v2::*;
use frame_support::{traits::Hooks, BoundedVec};
use frame_system::RawOrigin;
use snowbridge_outbound_queue_primitives::v2::{Command, Initializer, Message};
use sp_core::{H160, H256};
#[allow(unused_imports)]
use crate::Pallet as OutboundQueue;
#[benchmarks(
where
<T as Config>::MaxMessagePayloadSize: Get<u32>,
<T as frame_system::Config>::AccountId: From<[u8; 32]>,
)]
mod benchmarks {
use super::*;
use frame_support::assert_ok;
/// Build `Upgrade` message with `MaxMessagePayloadSize`, in the worst-case.
fn build_message<T: Config>() -> (Message, OutboundMessage) {
let commands = vec![Command::Upgrade {
impl_address: H160::zero(),
impl_code_hash: H256::zero(),
initializer: Initializer {
params: core::iter::repeat_with(|| 1_u8)
.take(<T as Config>::MaxMessagePayloadSize::get() as usize)
.collect(),
maximum_required_gas: 200_000,
},
}];
let message = Message {
origin: Default::default(),
id: H256::default(),
fee: 0,
commands: BoundedVec::try_from(commands.clone()).unwrap(),
};
let wrapped_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 outbound_message = OutboundMessage {
origin: Default::default(),
nonce: 1,
topic: H256::default(),
commands: wrapped_commands.clone().try_into().unwrap(),
};
(message, outbound_message)
}
/// Initialize `MaxMessagesPerBlock` messages need to be committed, in the worst-case.
fn initialize_worst_case<T: Config>() {
for _ in 0..T::MaxMessagesPerBlock::get() {
initialize_with_one_message::<T>();
}
}
/// Initialize with a single message
fn initialize_with_one_message<T: Config>() {
let (message, outbound_message) = build_message::<T>();
let leaf = <T as Config>::Hashing::hash(&message.encode());
MessageLeaves::<T>::append(leaf);
Messages::<T>::append(outbound_message);
}
/// Benchmark for processing a message.
#[benchmark]
fn do_process_message() -> Result<(), BenchmarkError> {
let (enqueued_message, _) = build_message::<T>();
let origin = T::AggregateMessageOrigin::from([1; 32].into());
let message = enqueued_message.encode();
#[block]
{
let _ = OutboundQueue::<T>::do_process_message(origin, &message).unwrap();
}
assert_eq!(MessageLeaves::<T>::decode_len().unwrap(), 1);
Ok(())
}
/// Benchmark for producing final messages commitment, in the worst-case
#[benchmark]
fn commit() -> Result<(), BenchmarkError> {
initialize_worst_case::<T>();
#[block]
{
OutboundQueue::<T>::commit();
}
Ok(())
}
/// Benchmark for producing commitment for a single message, used to estimate the delivery
/// cost. The assumption is that cost of commit a single message is even higher than the average
/// cost of commit all messages.
#[benchmark]
fn commit_single() -> Result<(), BenchmarkError> {
initialize_with_one_message::<T>();
#[block]
{
OutboundQueue::<T>::commit();
}
Ok(())
}
/// Benchmark for `on_initialize` in the worst-case
#[benchmark]
fn on_initialize() -> Result<(), BenchmarkError> {
initialize_worst_case::<T>();
#[block]
{
OutboundQueue::<T>::on_initialize(1_u32.into());
}
Ok(())
}
/// Benchmark the entire process flow in the worst-case. This can be used to determine
/// appropriate values for the configuration parameters `MaxMessagesPerBlock` and
/// `MaxMessagePayloadSize`
#[benchmark]
fn process() -> Result<(), BenchmarkError> {
initialize_worst_case::<T>();
let origin = T::AggregateMessageOrigin::from([1; 32].into());
let (enqueued_message, _) = build_message::<T>();
let message = enqueued_message.encode();
#[block]
{
OutboundQueue::<T>::on_initialize(1_u32.into());
for _ in 0..T::MaxMessagesPerBlock::get() {
OutboundQueue::<T>::do_process_message(origin.clone(), &message).unwrap();
}
OutboundQueue::<T>::commit();
}
Ok(())
}
#[benchmark]
fn submit_delivery_receipt() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let message = make_submit_delivery_receipt_message();
T::Helper::initialize_storage(message.finalized_header, message.block_roots_root);
let receipt = DeliveryReceipt::try_from(&message.event.event_log).unwrap();
let order = PendingOrder {
nonce: receipt.nonce,
fee: 0,
block_number: frame_system::Pallet::<T>::current_block_number(),
};
<PendingOrders<T>>::insert(receipt.nonce, order);
#[block]
{
assert_ok!(OutboundQueue::<T>::submit_delivery_receipt(
RawOrigin::Signed(caller.clone()).into(),
Box::new(message.event),
));
}
Ok(())
}
impl_benchmark_test_suite!(OutboundQueue, crate::mock::new_tester(), crate::mock::Test,);
}
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
// Generated, do not edit!
// See ethereum client README.md for instructions to generate
use hex_literal::hex;
use snowbridge_beacon_primitives::{
types::deneb, AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader,
};
use snowbridge_verification_primitives::{EventFixture, EventProof, Log, Proof};
use sp_core::U256;
use sp_std::vec;
pub fn make_submit_delivery_receipt_message() -> EventFixture {
EventFixture {
event: EventProof {
event_log: Log {
address: hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305").into(),
topics: vec![
hex!("8856ab63954e6c2938803a4654fb704c8779757e7bfdbe94a578e341ec637a95").into(),
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
],
data: hex!("907b6ec7bf3f2496ef79238e0fb19e032bfe444c7ffe906bd340c6c4ffe8511f0000000000000000000000000000000000000000000000000000000000000001d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d").into(),
},
proof: Proof {
receipt_proof: (vec![
hex!("8a40611a32af2ad0ad63bf32e8c633ff209e6f701645b8f25e492327cd95d4e0").to_vec(),
], vec![
hex!("f9024e822080b9024802f9024401830d716eb9010000200000000000000000000000000000080000000000000000000000000000000000000000000004000000000000000000000000000000000000000000008000000000000000000801000000000000000000000000000000000000000000000000000000020000000008000000000800000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000800000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000020000000000200000000000000000000000000000000000000000000000000000000f90139f87a94b1185ede04202fe62d38f5db72f71e38ff3e8305f842a057f58171b8777633d03aff1e7408b96a3d910c93a7ce433a8cb7fb837dc306a6a09441dceeeffa7e032eedaccf9b7632e60e86711551a82ffbbb0dda8afd9e4ef7a0000000000000000000000000de45448ca2d57797c0bec0ee15a1e42334744219f8bb94b1185ede04202fe62d38f5db72f71e38ff3e8305f842a08856ab63954e6c2938803a4654fb704c8779757e7bfdbe94a578e341ec637a95a00000000000000000000000000000000000000000000000000000000000000000b860907b6ec7bf3f2496ef79238e0fb19e032bfe444c7ffe906bd340c6c4ffe8511f0000000000000000000000000000000000000000000000000000000000000001d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d").to_vec(),
]),
execution_proof: ExecutionProof {
header: BeaconHeader {
slot: 663,
proposer_index: 4,
parent_root: hex!("478651896411faa949cd0882bb6a82595b71d3eba74cbeb87b5eb8162c7e00f1").into(),
state_root: hex!("4191b7c2a622b8cfa31684e5e06de3b36834e403a6ded2acd32156a488f829b0").into(),
body_root: hex!("72765c81bbad6cd083314506936528719e03d33ee65927167a372febe1fbf734").into(),
},
ancestry_proof: Some(AncestryProof {
header_branch: vec![
hex!("478651896411faa949cd0882bb6a82595b71d3eba74cbeb87b5eb8162c7e00f1").into(),
hex!("87314a5200d3a22749bd98ba32af7d0546d25d47cf012dfcc85adc19ad7adfe3").into(),
hex!("e794355aa5b1743ea691e3f051f44a66956892621c0110a12980e66f70dc3d74").into(),
hex!("fe0a3f7035e5d4cc83412939c9d343caa34889bd9655a05e9cc53a09e3aa7e8c").into(),
hex!("0f472e1a66d039197fbe378845e848ec11d5bcde60ac650da812fa1f4461c603").into(),
hex!("018f388291fbd20c15691e4b118a17b870eec7beb837e91fcc839adcb5fb21fc").into(),
hex!("8a016b0d65b61e5026b9357df092bf82b52fa61af3e10d11ba7b24ae30b457ec").into(),
hex!("73af6cacce9735d5576d1defc7fc39061776004ac7d3aba3abf6dfad7b1a3a36").into(),
hex!("6116f4b671f0f26c224ab10c6c330d56c9b5e48fa6e2204035f333bc142f55d2").into(),
hex!("a08d24b9120c5faeb98654664c4a324aaa509031193dbd74930acf26285b26a6").into(),
hex!("ffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b").into(),
hex!("6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220").into(),
hex!("b7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f").into(),
],
finalized_block_root: hex!("d5793913dc57d9f5b9d50fb8c693504201d6926649834ac90337b673e66f98e0").into(),
}),
execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader {
parent_hash: hex!("35f64f37bea4538092ba578f4851d52375f7f3b2a52c1cb16f22fe512aead95d").into(),
fee_recipient: hex!("0000000000000000000000000000000000000000").into(),
state_root: hex!("dfa305877e67ab0caa827b15d573b58dfe360fe8d484d37228f8ae55ccced61c").into(),
receipts_root: hex!("8a40611a32af2ad0ad63bf32e8c633ff209e6f701645b8f25e492327cd95d4e0").into(),
logs_bloom: hex!("00200000000000000000000000000000080000000000000000000000000000000000000000000004000000000000000000000000000000000000000000008000000000000000000801000000000000000000000000000000000000000000000000000000020000000008000000000800000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000800000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000020000000000200000000000000000000000000000000000000000000000000000000").into(),
prev_randao: hex!("ebdade0d95216bdba380fce14fad97c870d8553fcf0ca37df22331b3ed498db2").into(),
block_number: 663,
gas_limit: 41857321,
gas_used: 881006,
timestamp: 1742914413,
extra_data: hex!("d983010e0c846765746888676f312e32332e348664617277696e").into(),
base_fee_per_gas: U256::from(7u64),
block_hash: hex!("c70c7e5b0a03fa9509e0b4598d65e6e0b6a2477f25064aa3221b39eee17583d4").into(),
transactions_root: hex!("065ecaa208638c4a43d080cd78a1451e406895caaa254b1ad0585c6a9e3c7ac6").into(),
withdrawals_root: hex!("792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535").into(),
blob_gas_used: 0,
excess_blob_gas: 0,
}),
execution_branch: vec![
hex!("ef46bf5d8bd654162c1a7f44949fe1f9cb2a3f356d593587a3f1f3f8da14b790").into(),
hex!("b46f0c01805fe212e15907981b757e6c496b0cb06664224655613dcec82505bb").into(),
hex!("db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").into(),
hex!("b4e39d31736a4064a84ca49c4d372696e92e7c5c7bcbb7219671c24df1dfcafa").into(),
],
}
},
},
finalized_header: BeaconHeader {
slot: 864,
proposer_index: 4,
parent_root: hex!("2839d32d7b5a1dbb9139c5fd11ea549abaac1ead425c79553b8424550fddd389").into(),
state_root: hex!("452daf2471437939f4d65af921a1cfee860b11111771fd55164b37fe25d610de").into(),
body_root: hex!("0bbd0377987d0984e7495bf61219342941e31a0b6fe790453fbc87ec92319097").into(),
},
block_roots_root: hex!("aca108d3e77ec6b010ca03df025e3b2e84f754d4642e5d8bf0ba9bdf58f42848").into(),
}
}
@@ -0,0 +1,497 @@
// 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 teyrchains via XCM, or BridgeHub itself
//! via the `snowbridge-pallet-system-v2`:
//!
//! 1. `snowbridge_outbound_queue_primitives::v2::EthereumBlobExporter::deliver`
//! 2. `snowbridge_pallet_system_v2::Pallet::send`
//!
//! The message submission pipeline works like this:
//! 1. The message is first validated via the implementation for
//! [`snowbridge_outbound_queue_primitives::v2::SendMessage::validate`]
//! 2. The message is then enqueued for later processing via the implementation for
//! [`snowbridge_outbound_queue_primitives::v2::SendMessage::deliver`]
//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
//! 4. The message queue delivers messages to this pallet via the implementation for
//! [`frame_support::traits::ProcessMessage::process_message`]
//! 5. The message is processed in `Pallet::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 bp_relayers::RewardLedger;
use codec::{Decode, FullCodec};
use frame_support::{
storage::StorageStreamIter,
traits::{tokens::Balance, EnqueueMessage, Get, ProcessMessageError},
weights::{Weight, WeightToFee},
};
use snowbridge_core::{
digest_item::SnowbridgeDigestItem,
reward::{AddTip, AddTipError},
BasicOperatingMode,
};
use snowbridge_merkle_tree::merkle_root;
use snowbridge_outbound_queue_primitives::{
v2::{
abi::{CommandWrapper, OutboundMessageWrapper},
DeliveryReceipt, GasMeter, Message, OutboundCommandWrapper, OutboundMessage,
},
EventProof, VerificationError, Verifier,
};
use sp_core::{H160, H256};
use sp_runtime::{
traits::{BlockNumberProvider, Debug, Hash},
DigestItem,
};
use sp_std::prelude::*;
pub use types::{OnNewCommitment, PendingOrder, ProcessMessageOriginOf};
pub use weights::WeightInfo;
use xcm::prelude::NetworkId;
#[cfg(feature = "runtime-benchmarks")]
use snowbridge_beacon_primitives::BeaconHeader;
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_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
#[pallet::constant]
type MaxMessagePayloadSize: Get<u32>;
/// Max number of messages processed per block
#[pallet::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 pallet
type WeightInfo: WeightInfo;
/// The verifier for delivery proof from Ethereum
type Verifier: Verifier;
/// Address of the Gateway contract
#[pallet::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 pallet.
#[pallet::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>;
}
#[pallet::event]
#[pallet::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 },
}
#[pallet::error]
pub enum Error<T> {
/// The message is too large
MessageTooLarge,
/// The pallet 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 `frame_system::Pallet::Events` storage value
#[pallet::storage]
#[pallet::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.
#[pallet::storage]
#[pallet::unbounded]
pub type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
/// The current nonce for the messages
#[pallet::storage]
pub type Nonce<T: Config> = StorageValue<_, u64, ValueQuery>;
/// Pending orders to relay
#[pallet::storage]
pub type PendingOrders<T: Config> =
StorageMap<_, Twox64Concat, u64, PendingOrder<BlockNumberFor<T>>, OptionQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<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);
}
#[pallet::call]
impl<T: Config> Pallet<T>
where
<T as frame_system::Config>::AccountId: From<[u8; 32]>,
{
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::submit_delivery_receipt())]
pub fn submit_delivery_receipt(
origin: OriginFor<T>,
event: Box<EventProof>,
) -> DispatchResult
where
<T as frame_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> 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 = SnowbridgeDigestItem::SnowbridgeV2(root).into();
// Insert merkle root into the header digest
<frame_system::Pallet<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 pallet.
/// 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: frame_system::Pallet::<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 frame_system::Config>::AccountId,
receipt: DeliveryReceipt,
) -> DispatchResult
where
<T as frame_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 Pallet<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),
}
})
}
}
}
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use frame_support::{
derive_impl, parameter_types,
traits::{Everything, Hooks},
weights::IdentityFee,
BoundedVec,
};
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use hex_literal::hex;
use scale_info::TypeInfo;
use snowbridge_core::{AgentId, AgentIdOf, ChannelId, ParaId};
use snowbridge_outbound_queue_primitives::{v2::*, Log, Proof, VerificationError, Verifier};
use snowbridge_test_utils::mock_rewards::{BridgeReward, MockRewardLedger};
use sp_core::{ConstU32, H160, H256};
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup, Keccak256},
AccountId32, BuildStorage,
};
use sp_std::marker::PhantomData;
use xcm::prelude::Here;
use xcm_executor::traits::ConvertLocation;
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>},
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type BaseCallFilter = Everything;
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 PalletInfo = PalletInfo;
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 IdleMaxServiceWeight = ();
type QueuePausedQuery = ();
}
// Mock verifier
pub struct MockVerifier;
impl Verifier for MockVerifier {
fn verify(_: &Log, _: &Proof) -> Result<(), VerificationError> {
Ok(())
}
}
const GATEWAY_ADDRESS: [u8; 20] = hex!["b1185ede04202fe62d38f5db72f71e38ff3e8305"];
const WETH: [u8; 20] = hex!["C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"];
parameter_types! {
pub const OwnParaId: ParaId = ParaId::new(1013);
pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS);
pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
pub DefaultMyRewardKind: BridgeReward = BridgeReward::Snowbridge;
}
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Copy,
MaxEncodedLen,
Clone,
Eq,
PartialEq,
TypeInfo,
Debug,
)]
pub enum AggregateMessageOrigin {
Snowbridge(ChannelId),
SnowbridgeV2(H256),
}
impl From<H256> for AggregateMessageOrigin {
fn from(hash: H256) -> Self {
Self::SnowbridgeV2(hash)
}
}
impl crate::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Verifier = MockVerifier;
type GatewayAddress = GatewayAddress;
type Hashing = Keccak256;
type MessageQueue = MessageQueue;
type MaxMessagePayloadSize = ConstU32<1024>;
type MaxMessagesPerBlock = ConstU32<20>;
type GasMeter = ConstantGasMeter;
type Balance = u128;
type WeightToFee = IdentityFee<u128>;
type WeightInfo = ();
type RewardPayment = MockRewardLedger;
type EthereumNetwork = EthereumNetwork;
type RewardKind = BridgeReward;
type DefaultRewardKind = DefaultMyRewardKind;
type OnNewCommitment = ();
#[cfg(feature = "runtime-benchmarks")]
type Helper = Test;
type AggregateMessageOrigin = AggregateMessageOrigin;
}
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 bridge_hub_root_origin() -> AgentId {
AgentIdOf::convert_location(&Here.into()).unwrap()
}
pub fn mock_governance_message<T>() -> Message
where
T: Config,
{
let _marker = PhantomData::<T>; // for clippy
Message {
origin: bridge_hub_root_origin(),
id: Default::default(),
fee: 0,
commands: BoundedVec::try_from(vec![Command::Upgrade {
impl_address: Default::default(),
impl_code_hash: Default::default(),
initializer: Initializer {
params: (0..512).map(|_| 1u8).collect::<Vec<u8>>(),
maximum_required_gas: 0,
},
}])
.unwrap(),
}
}
// 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 {
origin: Default::default(),
id: Default::default(),
fee: 0,
commands: BoundedVec::try_from(vec![Command::Upgrade {
impl_address: H160::zero(),
impl_code_hash: H256::zero(),
initializer: Initializer {
params: (0..1000).map(|_| 1u8).collect::<Vec<u8>>(),
maximum_required_gas: 0,
},
}])
.unwrap(),
}
}
pub fn mock_message(sibling_para_id: u32) -> Message {
Message {
origin: H256::from_low_u64_be(sibling_para_id as u64),
id: H256::from_low_u64_be(1),
fee: 1_000,
commands: BoundedVec::try_from(vec![Command::UnlockNativeToken {
token: H160(WETH),
recipient: H160(GATEWAY_ADDRESS),
amount: 1_000_000,
}])
.unwrap(),
}
}
pub fn mock_register_token_message(sibling_para_id: u32) -> Message {
Message {
origin: H256::from_low_u64_be(sibling_para_id as u64),
id: H256::from_low_u64_be(1),
fee: 1_000,
commands: BoundedVec::try_from(vec![Command::RegisterForeignToken {
token_id: H256::from_low_u64_be(1),
name: vec![],
symbol: vec![],
decimals: 12,
}])
.unwrap(),
}
}
#[cfg(feature = "runtime-benchmarks")]
impl<T: Config> BenchmarkHelper<T> for Test {
// not implemented since the MockVerifier is used for tests
fn initialize_storage(_: BeaconHeader, _: H256) {}
}
@@ -0,0 +1,29 @@
// 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 = T::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() {
Self::deposit_event(Event::MessagePostponed {
payload: message.to_vec(),
reason: ProcessMessageError::Overweight(weight),
});
return Err(ProcessMessageError::Overweight(weight));
}
Self::do_process_message(origin, message)
}
}
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Implementation for [`snowbridge_outbound_queue_primitives::v2::SendMessage`]
use super::*;
use codec::Encode;
use frame_support::{
ensure,
traits::{EnqueueMessage, Get},
};
use snowbridge_outbound_queue_primitives::{
v2::{Message, SendMessage},
SendError,
};
use sp_core::H256;
use sp_runtime::BoundedVec;
impl<T> SendMessage for Pallet<T>
where
T: Config,
{
type Ticket = Message;
fn validate(message: &Message) -> Result<Self::Ticket, SendError> {
// The inner payload should not be too large
let payload = message.encode();
ensure!(
payload.len() < T::MaxMessagePayloadSize::get() as usize,
SendError::MessageTooLarge
);
Ok(message.clone())
}
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError> {
let origin = ticket.origin.into();
let message =
BoundedVec::try_from(ticket.encode()).map_err(|_| SendError::MessageTooLarge)?;
T::MessageQueue::enqueue_message(message.as_bounded_slice(), origin);
Self::deposit_event(Event::MessageQueued { message: ticket.clone() });
Ok(ticket.id)
}
}
@@ -0,0 +1,348 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate::{
mock::{AggregateMessageOrigin::*, *},
*,
};
use alloy_core::primitives::FixedBytes;
use codec::Encode;
use frame_support::{
assert_err, assert_noop, assert_ok,
traits::{Hooks, ProcessMessage, ProcessMessageError, QueueFootprintQuery},
weights::WeightMeter,
BoundedVec,
};
use hex_literal::hex;
use snowbridge_core::{digest_item::SnowbridgeDigestItem, ChannelId, ParaId};
use snowbridge_outbound_queue_primitives::{
v2::{abi::OutboundMessageWrapper, Command, Initializer, SendMessage},
SendError,
};
use sp_core::{hexdisplay::HexDisplay, H256};
#[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();
assert_eq!(Nonce::<Test>::get(), 4);
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 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::SnowbridgeV2(H256::zero());
let message = Message {
origin: Default::default(),
id: Default::default(),
fee: 0,
commands: BoundedVec::try_from(vec![Command::Upgrade {
impl_address: Default::default(),
impl_code_hash: Default::default(),
initializer: Initializer {
params: (0..512).map(|_| 1u8).collect::<Vec<u8>>(),
maximum_required_gas: 0,
},
}])
.unwrap(),
};
let mut meter = WeightMeter::new();
assert_err!(
OutboundQueue::process_message(
message.encode().as_slice(),
origin,
&mut meter,
&mut [0u8; 32]
),
ProcessMessageError::Yield
);
let events = System::events();
let last_event = events.last().expect("Expected at least one event").event.clone();
match last_event {
mock::RuntimeEvent::OutboundQueue(Event::MessagePostponed {
payload: _,
reason: ProcessMessageError::Yield,
}) => {},
_ => {
panic!("Expected Event::MessagePostponed(Yield) but got {:?}", last_event);
},
}
})
}
#[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::SnowbridgeV2(H256::zero());
let message: Message = mock_message(sibling_id);
let mut meter = WeightMeter::with_limit(Weight::MAX);
Nonce::<Test>::set(u64::MAX);
let result = OutboundQueue::process_message(
message.encode().as_slice(),
origin,
&mut meter,
&mut [0u8; 32],
);
assert_err!(result, ProcessMessageError::Unsupported);
let events = System::events();
let last_event = events.last().expect("Expected at least one event").event.clone();
match last_event {
mock::RuntimeEvent::OutboundQueue(Event::MessageRejected {
id: None,
payload: _,
error: ProcessMessageError::Unsupported,
}) => {},
_ => {
panic!("Expected Event::MessageRejected(Unsupported) but got {:?}", last_event);
},
}
})
}
#[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::SnowbridgeV2(H256::zero());
let message: Message = mock_message(sibling_id);
let mut meter = WeightMeter::with_limit(Weight::from_parts(1, 1));
assert_err!(
OutboundQueue::process_message(
message.encode().as_slice(),
origin,
&mut meter,
&mut [0u8; 32]
),
ProcessMessageError::Overweight(<Test as Config>::WeightInfo::do_process_message())
);
let events = System::events();
let last_event = events.last().expect("Expected at least one event").event.clone();
match last_event {
mock::RuntimeEvent::OutboundQueue(Event::MessagePostponed {
payload: _,
reason: ProcessMessageError::Overweight(_),
}) => {},
_ => {
panic!("Expected Event::MessagePostponed(Overweight(_)) but got {:?}", last_event);
},
}
})
}
#[test]
fn governance_message_not_processed_in_same_block_when_queue_congested_with_low_priority_messages()
{
let sibling_id: u32 = 1000;
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(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64)));
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(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64)));
assert_eq!(footprint.storage.count, 40 - 20);
// and governance message does not have the chance to execute in same block
let footprint = MessageQueue::footprint(SnowbridgeV2(bridge_hub_root_origin()));
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(SnowbridgeV2(bridge_hub_root_origin()));
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(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64)));
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(SnowbridgeV2(H256::from_low_u64_be(sibling_id as u64)));
assert_eq!(footprint.storage.count, 0);
});
}
#[test]
fn encode_digest_item_with_correct_index() {
new_tester().execute_with(|| {
let digest_item: DigestItem = SnowbridgeDigestItem::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 = SnowbridgeDigestItem::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); // SnowbridgeDigestItem::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
]
);
});
}
fn encode_mock_message(message: Message) -> Vec<u8> {
let commands: Vec<CommandWrapper> = message
.commands
.into_iter()
.map(|command| CommandWrapper {
kind: command.index(),
gas: <Test as Config>::GasMeter::maximum_dispatch_gas_used_at_most(&command),
payload: Bytes::from(command.abi_encode()),
})
.collect();
// print the abi-encoded message and decode with solidity test
let committed_message = OutboundMessageWrapper {
origin: FixedBytes::from(message.origin.as_fixed_bytes()),
nonce: 1,
topic: FixedBytes::from(message.id.as_fixed_bytes()),
commands,
};
let message_abi_encoded = committed_message.abi_encode();
message_abi_encoded
}
#[test]
fn encode_unlock_message() {
let message: Message = mock_message(1000);
let message_abi_encoded = encode_mock_message(message);
println!("{}", HexDisplay::from(&message_abi_encoded));
assert_eq!(hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000030d4000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000f4240").to_vec(), message_abi_encoded)
}
#[test]
fn encode_register_pna() {
let message: Message = mock_register_token_message(1000);
let message_abi_encoded = encode_mock_message(message);
println!("{}", HexDisplay::from(&message_abi_encoded));
assert_eq!(hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000124f80000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec(), message_abi_encoded)
}
#[test]
fn test_add_tip_cumulative() {
new_tester().execute_with(|| {
let nonce = 1;
let initial_fee = 1000;
let additional_fee = 500;
let current_block = System::block_number();
let order = PendingOrder { nonce, fee: initial_fee, block_number: current_block };
PendingOrders::<Test>::insert(nonce, order);
assert_ok!(OutboundQueue::add_tip(nonce, additional_fee));
let order_after = PendingOrders::<Test>::get(nonce).unwrap();
assert_eq!(order_after.fee, initial_fee + additional_fee);
});
}
#[test]
fn test_add_tip_fails_no_pending_order() {
new_tester().execute_with(|| {
let nonce = 42;
let amount = 1000;
assert_noop!(OutboundQueue::add_tip(nonce, amount), AddTipError::UnknownMessage);
});
}
#[test]
fn test_add_tip_fails_amount_zero() {
new_tester().execute_with(|| {
let nonce = 1;
let initial_fee = 1000;
let zero_amount = 0;
let current_block = System::block_number();
let order = PendingOrder { nonce, fee: initial_fee, block_number: current_block };
PendingOrders::<Test>::insert(nonce, order);
assert_noop!(OutboundQueue::add_tip(nonce, zero_amount), AddTipError::AmountZero);
// Verify the original fee is unchanged
let order_after = PendingOrders::<Test>::get(nonce).unwrap();
assert_eq!(order_after.fee, initial_fee);
});
}
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::Pallet;
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::traits::ProcessMessage;
use scale_info::TypeInfo;
pub use snowbridge_merkle_tree::MerkleProof;
use sp_core::H256;
use sp_runtime::RuntimeDebug;
use sp_std::prelude::*;
pub type ProcessMessageOriginOf<T> = <Pallet<T> as ProcessMessage>::Origin;
/// Pending order
#[derive(Encode, Decode, TypeInfo, Clone, Eq, PartialEq, RuntimeDebug, MaxEncodedLen)]
pub struct PendingOrder<BlockNumber> {
/// The nonce used to identify the message
pub nonce: u64,
/// The block number in which the message was committed
pub block_number: BlockNumber,
/// The fee in Ether provided by the user to incentivize message delivery
#[codec(compact)]
pub fee: u128,
}
/// Hook that will be called when a new message commitment is constructed.
pub trait OnNewCommitment {
fn on_new_commitment(commitment: H256);
}
impl OnNewCommitment for () {
fn on_new_commitment(_commitment: H256) {}
}
@@ -0,0 +1,104 @@
//! 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-pezkuwichain-dev")`, DB CACHE: `1024`
// Executed Command:
// target/release/pezkuwi-teyrchain
// benchmark
// pallet
// --chain=bridge-hub-pezkuwichain-dev
// --pallet=snowbridge-pallet-outbound-queue
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --template
// ../teyrchain/templates/module-weight-template.hbs
// --output
// ../teyrchain/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;
fn submit_delivery_receipt() -> Weight;
fn on_initialize() -> Weight;
fn process() -> 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))
}
fn submit_delivery_receipt() -> Weight {
Weight::from_parts(70_000_000, 0)
.saturating_add(Weight::from_parts(0, 3601))
.saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(2))
}
fn on_initialize() -> Weight {
Weight::from_parts(5_000_000, 0)
.saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(5))
}
fn process() -> Weight {
Weight::from_parts(506_000_000, 0)
.saturating_add(Weight::from_parts(0, 1493))
.saturating_add(RocksDbWeight::get().reads(1))
.saturating_add(RocksDbWeight::get().writes(35))
}
}