mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 05:11:02 +00:00
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:
@@ -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(¶ms.exchange_rate)
|
||||
.expect("exchange rate is not zero; qed")
|
||||
.into_inner();
|
||||
|
||||
// adjust fixed point to match local currency
|
||||
let fee = Self::convert_from_ether_decimals(fee);
|
||||
|
||||
Fee::from((Self::calculate_local_fee(), fee))
|
||||
}
|
||||
|
||||
/// Calculate fee in remote currency for dispatching a message on Ethereum
|
||||
pub(crate) fn calculate_remote_fee(
|
||||
gas_used_at_most: u64,
|
||||
fee_per_gas: U256,
|
||||
reward: U256,
|
||||
) -> U256 {
|
||||
fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward)
|
||||
}
|
||||
|
||||
/// The local component of the message processing fees in native currency
|
||||
pub(crate) fn calculate_local_fee() -> T::Balance {
|
||||
T::WeightToFee::weight_to_fee(
|
||||
&T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()),
|
||||
)
|
||||
}
|
||||
|
||||
// 1 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user