Adds Snowbridge to Rococo runtime (#2522)

# Description

Adds Snowbridge to the Rococo bridge hub runtime. Includes config
changes required in Rococo asset hub.

---------

Co-authored-by: Alistair Singh <alistair.singh7@gmail.com>
Co-authored-by: ron <yrong1997@gmail.com>
Co-authored-by: Vincent Geddes <vincent.geddes@hey.com>
Co-authored-by: claravanstaden <Cats 4 life!>
This commit is contained in:
Clara van Staden
2023-12-21 18:06:36 +02:00
committed by GitHub
parent 9f5221cc2f
commit 18d53dbf91
151 changed files with 19379 additions and 149 deletions
@@ -0,0 +1,78 @@
[package]
name = "snowbridge-outbound-queue"
description = "Snowbridge Outbound Queue"
version = "0.1.1"
edition = "2021"
authors = ["Snowfork <contact@snowfork.com>"]
repository = "https://github.com/Snowfork/snowbridge"
license = "Apache-2.0"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
serde = { version = "1.0.188", features = ["alloc", "derive"], default-features = false }
codec = { version = "3.6.1", package = "parity-scale-codec", default-features = false, features = ["derive"] }
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
hex-literal = { version = "0.4.1", optional = true }
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking", default-features = false, optional = true }
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
sp-arithmetic = { path = "../../../../../substrate/primitives/arithmetic", default-features = false }
bridge-hub-common = { path = "../../../../../cumulus/parachains/runtimes/bridge-hubs/common", default-features = false }
snowbridge-core = { path = "../../primitives/core", features = ["serde"], default-features = false }
snowbridge-outbound-queue-merkle-tree = { path = "merkle-tree", default-features = false }
ethabi = { git = "https://github.com/snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
[dev-dependencies]
pallet-message-queue = { path = "../../../../../substrate/frame/message-queue", default-features = false }
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
hex-literal = { version = "0.4.1" }
[features]
default = ["std"]
std = [
"bridge-hub-common/std",
"codec/std",
"ethabi/std",
"frame-benchmarking/std",
"frame-support/std",
"frame-system/std",
"pallet-message-queue/std",
"scale-info/std",
"serde/std",
"snowbridge-core/std",
"snowbridge-outbound-queue-merkle-tree/std",
"sp-arithmetic/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"xcm/std",
]
runtime-benchmarks = [
"bridge-hub-common/runtime-benchmarks",
"frame-benchmarking",
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"hex-literal",
"pallet-message-queue/runtime-benchmarks",
"snowbridge-core/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-message-queue/try-runtime",
"sp-runtime/try-runtime",
]
@@ -0,0 +1,33 @@
[package]
name = "snowbridge-outbound-queue-merkle-tree"
description = "Snowbridge Outbound Queue Merkle Tree"
version = "0.1.1"
edition = "2021"
authors = ["Snowfork <contact@snowfork.com>"]
repository = "https://github.com/Snowfork/snowbridge"
license = "Apache-2.0"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { version = "3.1.5", package = "parity-scale-codec", default-features = false, features = ["derive"] }
scale-info = { version = "2.7.0", default-features = false, features = ["derive"] }
sp-core = { path = "../../../../../../substrate/primitives/core", default-features = false }
sp-runtime = { path = "../../../../../../substrate/primitives/runtime", default-features = false }
[dev-dependencies]
hex-literal = { version = "0.4.1" }
env_logger = "0.9"
hex = "0.4"
array-bytes = "4.1"
[features]
default = ["std"]
std = [
"codec/std",
"scale-info/std",
"sp-core/std",
"sp-runtime/std",
]
@@ -0,0 +1,464 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
// SPDX-FileCopyrightText: 2021-2022 Parity Technologies (UK) Ltd.
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
//! This crate implements a simple binary Merkle Tree utilities required for inter-op with Ethereum
//! bridge & Solidity contract.
//!
//! The implementation is optimised for usage within Substrate Runtime and supports no-std
//! compilation targets.
//!
//! Merkle Tree is constructed from arbitrary-length leaves, that are initially hashed using the
//! same `\[`Hasher`\]` as the inner nodes.
//! Inner nodes are created by concatenating child hashes and hashing again. The implementation
//! does not perform any sorting of the input data (leaves) nor when inner nodes are created.
//!
//! If the number of leaves is not even, last leaf (hash of) is promoted to the upper layer.
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_core::{RuntimeDebug, H256};
use sp_runtime::traits::Hash;
/// Construct a root hash of a Binary Merkle Tree created from given leaves.
///
/// See crate-level docs for details about Merkle Tree construction.
///
/// In case an empty list of leaves is passed the function returns a 0-filled hash.
pub fn merkle_root<H, I>(leaves: I) -> H256
where
H: Hash<Output = H256>,
I: Iterator<Item = H256>,
{
merkelize::<H, _, _>(leaves, &mut ())
}
fn merkelize<H, V, I>(leaves: I, visitor: &mut V) -> H256
where
H: Hash<Output = H256>,
V: Visitor,
I: Iterator<Item = H256>,
{
let upper = Vec::with_capacity(leaves.size_hint().0);
let mut next = match merkelize_row::<H, _, _>(leaves, upper, visitor) {
Ok(root) => return root,
Err(next) if next.is_empty() => return H256::default(),
Err(next) => next,
};
let mut upper = Vec::with_capacity((next.len() + 1) / 2);
loop {
visitor.move_up();
match merkelize_row::<H, _, _>(next.drain(..), upper, visitor) {
Ok(root) => return root,
Err(t) => {
// swap collections to avoid allocations
upper = next;
next = t;
},
};
}
}
/// A generated merkle proof.
///
/// The structure contains all necessary data to later on verify the proof and the leaf itself.
#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)]
pub struct MerkleProof {
/// Root hash of generated merkle tree.
pub root: H256,
/// Proof items (does not contain the leaf hash, nor the root obviously).
///
/// This vec contains all inner node hashes necessary to reconstruct the root hash given the
/// leaf hash.
pub proof: Vec<H256>,
/// Number of leaves in the original tree.
///
/// This is needed to detect a case where we have an odd number of leaves that "get promoted"
/// to upper layers.
pub number_of_leaves: u64,
/// Index of the leaf the proof is for (0-based).
pub leaf_index: u64,
/// Leaf content (hashed).
pub leaf: H256,
}
/// A trait of object inspecting merkle root creation.
///
/// It can be passed to [`merkelize_row`] or [`merkelize`] functions and will be notified
/// about tree traversal.
trait Visitor {
/// We are moving one level up in the tree.
fn move_up(&mut self);
/// We are creating an inner node from given `left` and `right` nodes.
///
/// Note that in case of last odd node in the row `right` might be empty.
/// The method will also visit the `root` hash (level 0).
///
/// The `index` is an index of `left` item.
fn visit(&mut self, index: u64, left: &Option<H256>, right: &Option<H256>);
}
/// No-op implementation of the visitor.
impl Visitor for () {
fn move_up(&mut self) {}
fn visit(&mut self, _index: u64, _left: &Option<H256>, _right: &Option<H256>) {}
}
/// Construct a Merkle Proof for leaves given by indices.
///
/// The function constructs a (partial) Merkle Tree first and stores all elements required
/// to prove the requested item (leaf) given the root hash.
///
/// Both the Proof and the Root Hash are returned.
///
/// # Panic
///
/// The function will panic if given `leaf_index` is greater than the number of leaves.
pub fn merkle_proof<H, I>(leaves: I, leaf_index: u64) -> MerkleProof
where
H: Hash<Output = H256>,
I: Iterator<Item = H256>,
{
let mut leaf = None;
let mut hashes = vec![];
let mut number_of_leaves = 0;
for (idx, l) in (0u64..).zip(leaves) {
// count the leaves
number_of_leaves = idx + 1;
hashes.push(l);
// find the leaf for the proof
if idx == leaf_index {
leaf = Some(l);
}
}
/// The struct collects a proof for single leaf.
struct ProofCollection {
proof: Vec<H256>,
position: u64,
}
impl ProofCollection {
fn new(position: u64) -> Self {
ProofCollection { proof: Default::default(), position }
}
}
impl Visitor for ProofCollection {
fn move_up(&mut self) {
self.position /= 2;
}
fn visit(&mut self, index: u64, left: &Option<H256>, right: &Option<H256>) {
// we are at left branch - right goes to the proof.
if self.position == index {
if let Some(right) = right {
self.proof.push(*right);
}
}
// we are at right branch - left goes to the proof.
if self.position == index + 1 {
if let Some(left) = left {
self.proof.push(*left);
}
}
}
}
let mut collect_proof = ProofCollection::new(leaf_index);
let root = merkelize::<H, _, _>(hashes.into_iter(), &mut collect_proof);
let leaf = leaf.expect("Requested `leaf_index` is greater than number of leaves.");
#[cfg(feature = "debug")]
log::debug!(
"[merkle_proof] Proof: {:?}",
collect_proof.proof.iter().map(hex::encode).collect::<Vec<_>>()
);
MerkleProof { root, proof: collect_proof.proof, number_of_leaves, leaf_index, leaf }
}
/// Leaf node for proof verification.
///
/// Can be either a value that needs to be hashed first,
/// or the hash itself.
#[derive(Debug, PartialEq, Eq)]
pub enum Leaf<'a> {
/// Leaf content.
Value(&'a [u8]),
/// Hash of the leaf content.
Hash(H256),
}
impl<'a, T: AsRef<[u8]>> From<&'a T> for Leaf<'a> {
fn from(v: &'a T) -> Self {
Leaf::Value(v.as_ref())
}
}
impl<'a> From<H256> for Leaf<'a> {
fn from(v: H256) -> Self {
Leaf::Hash(v)
}
}
/// Verify Merkle Proof correctness versus given root hash.
///
/// The proof is NOT expected to contain leaf hash as the first
/// element, but only all adjacent nodes required to eventually by process of
/// concatenating and hashing end up with given root hash.
///
/// The proof must not contain the root hash.
pub fn verify_proof<'a, H, P, L>(
root: &'a H256,
proof: P,
number_of_leaves: u64,
leaf_index: u64,
leaf: L,
) -> bool
where
H: Hash<Output = H256>,
P: IntoIterator<Item = H256>,
L: Into<Leaf<'a>>,
{
if leaf_index >= number_of_leaves {
return false
}
let leaf_hash = match leaf.into() {
Leaf::Value(content) => <H as Hash>::hash(content),
Leaf::Hash(hash) => hash,
};
let hash_len = <H as sp_core::Hasher>::LENGTH;
let mut combined = [0_u8; 64];
let computed = proof.into_iter().fold(leaf_hash, |a, b| {
if a < b {
combined[..hash_len].copy_from_slice(a.as_ref());
combined[hash_len..].copy_from_slice(b.as_ref());
} else {
combined[..hash_len].copy_from_slice(b.as_ref());
combined[hash_len..].copy_from_slice(a.as_ref());
}
<H as Hash>::hash(&combined)
});
root == &computed
}
/// Processes a single row (layer) of a tree by taking pairs of elements,
/// concatenating them, hashing and placing into resulting vector.
///
/// In case only one element is provided it is returned via `Ok` result, in any other case (also an
/// empty iterator) an `Err` with the inner nodes of upper layer is returned.
fn merkelize_row<H, V, I>(
mut iter: I,
mut next: Vec<H256>,
visitor: &mut V,
) -> Result<H256, Vec<H256>>
where
H: Hash<Output = H256>,
V: Visitor,
I: Iterator<Item = H256>,
{
#[cfg(feature = "debug")]
log::debug!("[merkelize_row]");
next.clear();
let hash_len = <H as sp_core::Hasher>::LENGTH;
let mut index = 0;
let mut combined = vec![0_u8; hash_len * 2];
loop {
let a = iter.next();
let b = iter.next();
visitor.visit(index, &a, &b);
#[cfg(feature = "debug")]
log::debug!(" {:?}\n {:?}", a.as_ref().map(hex::encode), b.as_ref().map(hex::encode));
index += 2;
match (a, b) {
(Some(a), Some(b)) => {
if a < b {
combined[..hash_len].copy_from_slice(a.as_ref());
combined[hash_len..].copy_from_slice(b.as_ref());
} else {
combined[..hash_len].copy_from_slice(b.as_ref());
combined[hash_len..].copy_from_slice(a.as_ref());
}
next.push(<H as Hash>::hash(&combined));
},
// Odd number of items. Promote the item to the upper layer.
(Some(a), None) if !next.is_empty() => {
next.push(a);
},
// Last item = root.
(Some(a), None) => return Ok(a),
// Finish up, no more items.
_ => {
#[cfg(feature = "debug")]
log::debug!(
"[merkelize_row] Next: {:?}",
next.iter().map(hex::encode).collect::<Vec<_>>()
);
return Err(next)
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
use sp_core::keccak_256;
use sp_runtime::traits::Keccak256;
fn make_leaves(count: u64) -> Vec<H256> {
(0..count).map(|i| keccak_256(&i.to_le_bytes()).into()).collect()
}
#[test]
fn should_generate_empty_root() {
// given
let _ = env_logger::try_init();
let data = vec![];
// when
let out = merkle_root::<Keccak256, _>(data.into_iter());
// then
assert_eq!(
hex::encode(out),
"0000000000000000000000000000000000000000000000000000000000000000"
);
}
#[test]
fn should_generate_single_root() {
// given
let _ = env_logger::try_init();
let data = make_leaves(1);
// when
let out = merkle_root::<Keccak256, _>(data.into_iter());
// then
assert_eq!(
hex::encode(out),
"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce"
);
}
#[test]
fn should_generate_root_pow_2() {
// given
let _ = env_logger::try_init();
let data = make_leaves(2);
// when
let out = merkle_root::<Keccak256, _>(data.into_iter());
// then
assert_eq!(
hex::encode(out),
"e497bd1c13b13a60af56fa0d2703517c232fde213ad20d2c3dd60735c6604512"
);
}
#[test]
fn should_generate_root_complex() {
let _ = env_logger::try_init();
let test = |root, data: Vec<H256>| {
assert_eq!(
array_bytes::bytes2hex("", merkle_root::<Keccak256, _>(data.into_iter()).as_ref()),
root
);
};
test("816cc37bd8d39f7b0851838ebc875faf2afe58a03e95aca3b1333b3693f39dd3", make_leaves(3));
test("7501ea976cb92f305cca65ab11254589ea28bb8b59d3161506350adaa237d22f", make_leaves(4));
test("d26ba4eb398747bdd39255b1fadb99b803ce39696021b3b0bff7301ac146ee4e", make_leaves(10));
}
#[test]
#[ignore]
fn should_generate_and_verify_proof() {
// given
let _ = env_logger::try_init();
let data: Vec<H256> = make_leaves(3);
// when
let proof0 = merkle_proof::<Keccak256, _>(data.clone().into_iter(), 0);
assert!(verify_proof::<Keccak256, _, _>(
&proof0.root,
proof0.proof.clone(),
data.len() as u64,
proof0.leaf_index,
&data[0],
));
let proof1 = merkle_proof::<Keccak256, _>(data.clone().into_iter(), 1);
assert!(verify_proof::<Keccak256, _, _>(
&proof1.root,
proof1.proof,
data.len() as u64,
proof1.leaf_index,
&proof1.leaf,
));
let proof2 = merkle_proof::<Keccak256, _>(data.clone().into_iter(), 2);
assert!(verify_proof::<Keccak256, _, _>(
&proof2.root,
proof2.proof,
data.len() as u64,
proof2.leaf_index,
&proof2.leaf
));
// then
assert_eq!(hex::encode(proof0.root), hex::encode(proof1.root));
assert_eq!(hex::encode(proof2.root), hex::encode(proof1.root));
assert!(!verify_proof::<Keccak256, _, _>(
&H256::from_slice(&hex!(
"fb3b3be94be9e983ba5e094c9c51a7d96a4fa2e5d8e891df00ca89ba05bb1239"
)),
proof0.proof,
data.len() as u64,
proof0.leaf_index,
&proof0.leaf
));
assert!(!verify_proof::<Keccak256, _, _>(
&proof0.root,
vec![],
data.len() as u64,
proof0.leaf_index,
&proof0.leaf
));
}
#[test]
#[should_panic]
fn should_panic_on_invalid_leaf_index() {
let _ = env_logger::try_init();
merkle_proof::<Keccak256, _>(make_leaves(1).into_iter(), 5);
}
}
@@ -0,0 +1,34 @@
[package]
name = "snowbridge-outbound-queue-runtime-api"
description = "Snowbridge Outbound Queue Runtime API"
version = "0.1.0"
edition = "2021"
authors = ["Snowfork <contact@snowfork.com>"]
repository = "https://github.com/Snowfork/snowbridge"
license = "Apache-2.0"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { version = "3.1.5", package = "parity-scale-codec", features = ["derive"], default-features = false }
sp-core = { path = "../../../../../../substrate/primitives/core", default-features = false }
sp-std = { path = "../../../../../../substrate/primitives/std", default-features = false }
sp-api = { path = "../../../../../../substrate/primitives/api", default-features = false }
frame-support = { path = "../../../../../../substrate/frame/support", default-features = false }
xcm = { package = "staging-xcm", path = "../../../../../../polkadot/xcm", default-features = false }
snowbridge-outbound-queue-merkle-tree = { path = "../merkle-tree", default-features = false }
snowbridge-core = { path = "../../../primitives/core", default-features = false }
[features]
default = ["std"]
std = [
"codec/std",
"frame-support/std",
"snowbridge-core/std",
"snowbridge-outbound-queue-merkle-tree/std",
"sp-api/std",
"sp-core/std",
"sp-std/std",
"xcm/std",
]
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::traits::tokens::Balance as BalanceT;
use snowbridge_core::outbound::Message;
use snowbridge_outbound_queue_merkle_tree::MerkleProof;
sp_api::decl_runtime_apis! {
pub trait OutboundQueueApi<Balance> where Balance: BalanceT
{
/// Generate a merkle proof for a committed message identified by `leaf_index`.
/// The merkle root is stored in the block header as a
/// `\[`sp_runtime::generic::DigestItem::Other`\]`
fn prove_message(leaf_index: u64) -> Option<MerkleProof>;
/// Calculate the delivery fee for `message`
fn calculate_fee(message: Message) -> Option<Balance>;
}
}
@@ -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,413 @@
// 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-system` pallet:
//!
//! 1. `snowbridge_router_primitives::outbound::EthereumBlobExporter::deliver`
//! 2. `snowbridge_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, FeeConfigRecord, 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,
},
FeeConfigChanged {
fee_config: FeeConfigRecord,
},
}
#[pallet::error]
pub enum Error<T> {
/// The message is too large
MessageTooLarge,
/// The pallet is halted
Halted,
// Invalid fee config
InvalidFeeConfig,
/// 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,23 @@
//! 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,98 @@
//! 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,268 @@
// 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,
};
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
]
);
});
}
@@ -0,0 +1,99 @@
use codec::{Decode, Encode, MaxEncodedLen};
use ethabi::Token;
use frame_support::traits::ProcessMessage;
use scale_info::TypeInfo;
use serde::{Deserialize, Serialize};
use sp_arithmetic::FixedU128;
use sp_core::H256;
use sp_runtime::{traits::Zero, 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())),
])
}
}
/// Configuration for fee calculations
#[derive(
Encode,
Decode,
Copy,
Clone,
PartialEq,
RuntimeDebug,
MaxEncodedLen,
TypeInfo,
Serialize,
Deserialize,
)]
pub struct FeeConfigRecord {
/// ETH/DOT exchange rate
pub exchange_rate: FixedU128,
/// Ether fee per unit of gas
pub fee_per_gas: u128,
/// Ether reward for delivering message
pub reward: u128,
}
#[derive(RuntimeDebug)]
pub struct InvalidFeeConfig;
impl FeeConfigRecord {
pub fn validate(&self) -> Result<(), InvalidFeeConfig> {
if self.exchange_rate == FixedU128::zero() {
return Err(InvalidFeeConfig)
}
if self.fee_per_gas == 0 {
return Err(InvalidFeeConfig)
}
if self.reward == 0 {
return Err(InvalidFeeConfig)
}
Ok(())
}
}
@@ -0,0 +1,81 @@
//! Autogenerated weights for `snowbridge_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_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_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))
}
}