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,95 @@
[package]
name = "snowbridge-ethereum-beacon-client"
description = "Snowbridge Beacon Client Pallet"
version = "0.0.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", optional = true }
serde_json = { version = "1.0.96", optional = true }
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"] }
ssz_rs = { version = "0.9.0", default-features = false }
ssz_rs_derive = { version = "0.9.0", default-features = false }
byte-slice-cast = { version = "1.2.1", default-features = false }
rlp = { version = "0.5.2", default-features = false }
hex-literal = { version = "0.4.1", optional = true }
log = { version = "0.4.20", default-features = false }
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, optional = true }
snowbridge-core = { path = "../../primitives/core", default-features = false }
snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false }
primitives = { package = "snowbridge-beacon-primitives", path = "../../primitives/beacon", default-features = false }
static_assertions = { version = "1.1.0", default-features = false }
bp-runtime = { path = "../../../../../bridges/primitives/runtime", default-features = false }
pallet-timestamp = { path = "../../../../../substrate/frame/timestamp", default-features = false, optional = true }
[dev-dependencies]
rand = "0.8.5"
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
serde_json = "1.0.96"
hex-literal = "0.4.1"
pallet-timestamp = { path = "../../../../../substrate/frame/timestamp" }
sp-io = { path = "../../../../../substrate/primitives/io" }
serde = "1.0.188"
[features]
default = ["std"]
fuzzing = [
"hex-literal",
"pallet-timestamp",
"serde",
"serde_json",
"sp-io",
]
std = [
"bp-runtime/std",
"byte-slice-cast/std",
"codec/std",
"frame-support/std",
"frame-system/std",
"log/std",
"pallet-timestamp/std",
"primitives/std",
"rlp/std",
"scale-info/std",
"serde",
"snowbridge-core/std",
"snowbridge-ethereum/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"ssz_rs/std",
'frame-benchmarking/std',
]
runtime-benchmarks = [
"beacon-spec-mainnet",
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"hex-literal",
"pallet-timestamp?/runtime-benchmarks",
"snowbridge-core/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-timestamp?/try-runtime",
"sp-runtime/try-runtime",
]
beacon-spec-mainnet = []
@@ -0,0 +1,88 @@
# Motivation
Demonstrate that
[FastAggregateVerify](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.3.4) is the most
expensive call in ethereum beacon light client, though in [#13031](https://github.com/paritytech/substrate/pull/13031)
Parity team has wrapped some low level host functions for `bls-12381` but adding a high level host function specific
for it is super helpful.
# Benchmark
We add several benchmarks
[here](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/benchmarking/mod.rs#L98-L124)
as following to demonstrate
[bls_fast_aggregate_verify](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/lib.rs#L764)
is the main bottleneck. Test data
[here](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/benchmarking/data_mainnet.rs#L553-L1120)
is real from goerli network which contains 512 public keys from sync committee.
## sync_committee_period_update
Base line benchmark for extrinsic [sync_committee_period_update](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/lib.rs#L233)
## bls_fast_aggregate_verify
Subfunction of extrinsic `sync_committee_period_update` which does what
[FastAggregateVerify](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.3.4) requires.
## bls_aggregate_pubkey
Subfunction of `bls_fast_aggregate_verify` which decompress and instantiate G1 pubkeys only.
## bls_verify_message
Subfunction of `bls_fast_aggregate_verify` which verify the prepared signature only.
# Result
## hardware spec
Run benchmark in a EC2 instance
```
cargo run --release --bin polkadot-parachain --features runtime-benchmarks -- benchmark machine --base-path /mnt/scratch/benchmark
+----------+----------------+-------------+-------------+-------------------+
| Category | Function | Score | Minimum | Result |
+===========================================================================+
| CPU | BLAKE2-256 | 1.08 GiBs | 1.00 GiBs | ✅ Pass (107.5 %) |
|----------+----------------+-------------+-------------+-------------------|
| CPU | SR25519-Verify | 568.87 KiBs | 666.00 KiBs | ❌ Fail ( 85.4 %) |
|----------+----------------+-------------+-------------+-------------------|
| Memory | Copy | 13.67 GiBs | 14.32 GiBs | ✅ Pass ( 95.4 %) |
|----------+----------------+-------------+-------------+-------------------|
| Disk | Seq Write | 334.35 MiBs | 450.00 MiBs | ❌ Fail ( 74.3 %) |
|----------+----------------+-------------+-------------+-------------------|
| Disk | Rnd Write | 143.59 MiBs | 200.00 MiBs | ❌ Fail ( 71.8 %) |
+----------+----------------+-------------+-------------+-------------------+
```
## benchmark
```
cargo run --release --bin polkadot-parachain \
--features runtime-benchmarks \
-- \
benchmark pallet \
--base-path /mnt/scratch/benchmark \
--chain=bridge-hub-rococo-dev \
--pallet=snowbridge_ethereum_beacon_client \
--extrinsic="*" \
--execution=wasm --wasm-execution=compiled \
--steps 50 --repeat 20 \
--output ./parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_ethereum_beacon_client.rs
```
### [Weights](https://github.com/Snowfork/cumulus/blob/ron/benchmark-beacon-bridge/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_ethereum_beacon_client.rs)
|extrinsic | minimum execution time benchmarked(us) |
| --------------------------------------- |----------------------------------------|
|sync_committee_period_update | 123_126 |
|bls_fast_aggregate_verify| 121_083 |
|bls_aggregate_pubkey | 90_306 |
|bls_verify_message | 28_000 |
- [bls_fast_aggregate_verify](#bls_fast_aggregate_verify) consumes 98% execution time of [sync_committee_period_update](#sync_committee_period_update)
- [bls_aggregate_pubkey](#bls_aggregate_pubkey) consumes 75% execution time of [bls_fast_aggregate_verify](#bls_fast_aggregate_verify)
- [bls_verify_message](#bls_verify_message) consumes 23% execution time of [bls_fast_aggregate_verify](#bls_fast_aggregate_verify)
# Conclusion
A high level host function specific for
[bls_fast_aggregate_verify](https://github.com/Snowfork/snowbridge/blob/8891ca3cdcf2e04d8118c206588c956541ae4710/parachain/pallets/ethereum-beacon-client/src/lib.rs#L764)
is super helpful.
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
mod fixtures;
mod util;
use crate::Pallet as EthereumBeaconClient;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use fixtures::{
make_checkpoint, make_execution_header_update, make_finalized_header_update,
make_sync_committee_update,
};
use primitives::{
fast_aggregate_verify, prepare_aggregate_pubkey, prepare_aggregate_signature,
verify_merkle_branch,
};
use util::*;
#[benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn force_checkpoint() -> Result<(), BenchmarkError> {
let checkpoint_update = make_checkpoint();
let block_root: H256 = checkpoint_update.header.hash_tree_root().unwrap();
#[extrinsic_call]
_(RawOrigin::Root, Box::new(*checkpoint_update));
assert!(<LatestFinalizedBlockRoot<T>>::get() == block_root);
assert!(<FinalizedBeaconState<T>>::get(block_root).is_some());
Ok(())
}
#[benchmark]
fn submit() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let checkpoint_update = make_checkpoint();
let finalized_header_update = make_finalized_header_update();
let block_root: H256 = finalized_header_update.finalized_header.hash_tree_root().unwrap();
EthereumBeaconClient::<T>::process_checkpoint_update(&checkpoint_update)?;
#[extrinsic_call]
submit(RawOrigin::Signed(caller.clone()), Box::new(*finalized_header_update));
assert!(<LatestFinalizedBlockRoot<T>>::get() == block_root);
assert!(<FinalizedBeaconState<T>>::get(block_root).is_some());
Ok(())
}
#[benchmark]
fn submit_with_sync_committee() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let checkpoint_update = make_checkpoint();
let sync_committee_update = make_sync_committee_update();
EthereumBeaconClient::<T>::process_checkpoint_update(&checkpoint_update)?;
#[extrinsic_call]
submit(RawOrigin::Signed(caller.clone()), Box::new(*sync_committee_update));
assert!(<NextSyncCommittee<T>>::exists());
Ok(())
}
#[benchmark]
fn submit_execution_header() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let checkpoint_update = make_checkpoint();
let finalized_header_update = make_finalized_header_update();
let execution_header_update = make_execution_header_update();
let execution_header_hash = execution_header_update.execution_header.block_hash;
EthereumBeaconClient::<T>::process_checkpoint_update(&checkpoint_update)?;
EthereumBeaconClient::<T>::process_update(&finalized_header_update)?;
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), Box::new(*execution_header_update));
assert!(<ExecutionHeaders<T>>::contains_key(execution_header_hash));
Ok(())
}
#[benchmark(extra)]
fn bls_fast_aggregate_verify_pre_aggregated() -> Result<(), BenchmarkError> {
EthereumBeaconClient::<T>::process_checkpoint_update(&make_checkpoint())?;
let update = make_sync_committee_update();
let participant_pubkeys = participant_pubkeys::<T>(&update)?;
let signing_root = signing_root::<T>(&update)?;
let agg_sig =
prepare_aggregate_signature(&update.sync_aggregate.sync_committee_signature).unwrap();
let agg_pub_key = prepare_aggregate_pubkey(&participant_pubkeys).unwrap();
#[block]
{
agg_sig.fast_aggregate_verify_pre_aggregated(signing_root.as_bytes(), &agg_pub_key);
}
Ok(())
}
#[benchmark(extra)]
fn bls_fast_aggregate_verify() -> Result<(), BenchmarkError> {
EthereumBeaconClient::<T>::process_checkpoint_update(&make_checkpoint())?;
let update = make_sync_committee_update();
let current_sync_committee = <CurrentSyncCommittee<T>>::get();
let absent_pubkeys = absent_pubkeys::<T>(&update)?;
let signing_root = signing_root::<T>(&update)?;
#[block]
{
fast_aggregate_verify(
&current_sync_committee.aggregate_pubkey,
&absent_pubkeys,
signing_root,
&update.sync_aggregate.sync_committee_signature,
)
.unwrap();
}
Ok(())
}
#[benchmark(extra)]
fn verify_merkle_proof() -> Result<(), BenchmarkError> {
EthereumBeaconClient::<T>::process_checkpoint_update(&make_checkpoint())?;
let update = make_sync_committee_update();
let block_root: H256 = update.finalized_header.hash_tree_root().unwrap();
#[block]
{
verify_merkle_branch(
block_root,
&update.finality_branch,
config::FINALIZED_ROOT_SUBTREE_INDEX,
config::FINALIZED_ROOT_DEPTH,
update.attested_header.state_root,
);
}
Ok(())
}
impl_benchmark_test_suite!(
EthereumBeaconClient,
crate::mock::mainnet::new_tester(),
crate::mock::mainnet::Test
);
}
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate::{
decompress_sync_committee_bits, Config, CurrentSyncCommittee, Pallet as EthereumBeaconClient,
Update, ValidatorsRoot, Vec,
};
use primitives::PublicKeyPrepared;
use sp_core::H256;
pub fn participant_pubkeys<T: Config>(
update: &Update,
) -> Result<Vec<PublicKeyPrepared>, &'static str> {
let sync_committee_bits =
decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
let current_sync_committee = <CurrentSyncCommittee<T>>::get();
let pubkeys = EthereumBeaconClient::<T>::find_pubkeys(
&sync_committee_bits,
(*current_sync_committee.pubkeys).as_ref(),
true,
);
Ok(pubkeys)
}
pub fn absent_pubkeys<T: Config>(update: &Update) -> Result<Vec<PublicKeyPrepared>, &'static str> {
let sync_committee_bits =
decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
let current_sync_committee = <CurrentSyncCommittee<T>>::get();
let pubkeys = EthereumBeaconClient::<T>::find_pubkeys(
&sync_committee_bits,
(*current_sync_committee.pubkeys).as_ref(),
false,
);
Ok(pubkeys)
}
pub fn signing_root<T: Config>(update: &Update) -> Result<H256, &'static str> {
let validators_root = <ValidatorsRoot<T>>::get();
let signing_root = EthereumBeaconClient::<T>::signing_root(
&update.attested_header,
validators_root,
update.signature_slot,
)?;
Ok(signing_root)
}
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pub const SLOTS_PER_EPOCH: usize = 32;
pub const SECONDS_PER_SLOT: usize = 12;
pub const EPOCHS_PER_SYNC_COMMITTEE_PERIOD: usize = 256;
pub const SYNC_COMMITTEE_SIZE: usize = 512;
pub const SYNC_COMMITTEE_BITS_SIZE: usize = SYNC_COMMITTEE_SIZE / 8;
pub const SLOTS_PER_HISTORICAL_ROOT: usize = 8192;
pub const IS_MINIMAL: bool = false;
pub const BLOCK_ROOT_AT_INDEX_DEPTH: usize = 13;
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pub const SLOTS_PER_EPOCH: usize = 8;
pub const SECONDS_PER_SLOT: usize = 6;
pub const EPOCHS_PER_SYNC_COMMITTEE_PERIOD: usize = 8;
pub const SYNC_COMMITTEE_SIZE: usize = 32;
pub const SYNC_COMMITTEE_BITS_SIZE: usize = SYNC_COMMITTEE_SIZE / 8;
pub const SLOTS_PER_HISTORICAL_ROOT: usize = 64;
pub const IS_MINIMAL: bool = true;
pub const BLOCK_ROOT_AT_INDEX_DEPTH: usize = 6;
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use primitives::merkle_proof::{generalized_index_length, subtree_index};
use static_assertions::const_assert;
pub mod mainnet;
pub mod minimal;
#[cfg(not(feature = "beacon-spec-mainnet"))]
pub use minimal::*;
#[cfg(feature = "beacon-spec-mainnet")]
pub use mainnet::*;
// Generalized Indices
// get_generalized_index(BeaconState, 'block_roots')
pub const BLOCK_ROOTS_INDEX: usize = 37;
pub const BLOCK_ROOTS_SUBTREE_INDEX: usize = subtree_index(BLOCK_ROOTS_INDEX);
pub const BLOCK_ROOTS_DEPTH: usize = generalized_index_length(BLOCK_ROOTS_INDEX);
// get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')
pub const FINALIZED_ROOT_INDEX: usize = 105;
pub const FINALIZED_ROOT_SUBTREE_INDEX: usize = subtree_index(FINALIZED_ROOT_INDEX);
pub const FINALIZED_ROOT_DEPTH: usize = generalized_index_length(FINALIZED_ROOT_INDEX);
// get_generalized_index(BeaconState, 'current_sync_committee')
pub const CURRENT_SYNC_COMMITTEE_INDEX: usize = 54;
pub const CURRENT_SYNC_COMMITTEE_SUBTREE_INDEX: usize = subtree_index(CURRENT_SYNC_COMMITTEE_INDEX);
pub const CURRENT_SYNC_COMMITTEE_DEPTH: usize =
generalized_index_length(CURRENT_SYNC_COMMITTEE_INDEX);
// get_generalized_index(BeaconState, 'next_sync_committee')
pub const NEXT_SYNC_COMMITTEE_INDEX: usize = 55;
pub const NEXT_SYNC_COMMITTEE_SUBTREE_INDEX: usize = subtree_index(NEXT_SYNC_COMMITTEE_INDEX);
pub const NEXT_SYNC_COMMITTEE_DEPTH: usize = generalized_index_length(NEXT_SYNC_COMMITTEE_INDEX);
// get_generalized_index(BeaconBlockBody, 'execution_payload')
pub const EXECUTION_HEADER_INDEX: usize = 25;
pub const EXECUTION_HEADER_SUBTREE_INDEX: usize = subtree_index(EXECUTION_HEADER_INDEX);
pub const EXECUTION_HEADER_DEPTH: usize = generalized_index_length(EXECUTION_HEADER_INDEX);
pub const MAX_EXTRA_DATA_BYTES: usize = 32;
pub const MAX_LOGS_BLOOM_SIZE: usize = 256;
pub const MAX_FEE_RECIPIENT_SIZE: usize = 20;
pub const MAX_BRANCH_PROOF_SIZE: usize = 20;
/// DomainType('0x07000000')
/// <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#domain-types>
pub const DOMAIN_SYNC_COMMITTEE: [u8; 4] = [7, 0, 0, 0];
pub const PUBKEY_SIZE: usize = 48;
pub const SIGNATURE_SIZE: usize = 96;
const_assert!(SYNC_COMMITTEE_BITS_SIZE == SYNC_COMMITTEE_SIZE / 8);
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate::config::{
EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, SYNC_COMMITTEE_BITS_SIZE,
SYNC_COMMITTEE_SIZE,
};
/// Decompress packed bitvector into byte vector according to SSZ deserialization rules. Each byte
/// in the decompressed vector is either 0 or 1.
pub fn decompress_sync_committee_bits(
input: [u8; SYNC_COMMITTEE_BITS_SIZE],
) -> [u8; SYNC_COMMITTEE_SIZE] {
primitives::decompress_sync_committee_bits::<SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_BITS_SIZE>(
input,
)
}
/// Compute the sync committee period in which a slot is contained.
pub fn compute_period(slot: u64) -> u64 {
slot / SLOTS_PER_EPOCH as u64 / EPOCHS_PER_SYNC_COMMITTEE_PERIOD as u64
}
/// Compute epoch in which a slot is contained.
pub fn compute_epoch(slot: u64, slots_per_epoch: u64) -> u64 {
slot / slots_per_epoch
}
/// Sums the bit vector of sync committee participation.
pub fn sync_committee_sum(sync_committee_bits: &[u8]) -> u32 {
sync_committee_bits.iter().fold(0, |acc: u32, x| acc + *x as u32)
}
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use snowbridge_core::inbound::{
VerificationError::{self, *},
*,
};
use snowbridge_ethereum::Receipt;
impl<T: Config> Verifier for Pallet<T> {
/// Verify a message by verifying the existence of the corresponding
/// Ethereum log in a block. Returns the log if successful. The execution header containing
/// the log should be in the beacon client storage, meaning it has been verified and is an
/// ancestor of a finalized beacon block.
fn verify(event_log: &Log, proof: &Proof) -> Result<(), VerificationError> {
log::info!(
target: "ethereum-beacon-client",
"💫 Verifying message with block hash {}",
proof.block_hash,
);
let header = <ExecutionHeaderBuffer<T>>::get(proof.block_hash).ok_or(HeaderNotFound)?;
let receipt = match Self::verify_receipt_inclusion(header.receipts_root, proof) {
Ok(receipt) => receipt,
Err(err) => {
log::error!(
target: "ethereum-beacon-client",
"💫 Verification of receipt inclusion failed for block {}: {:?}",
proof.block_hash,
err
);
return Err(err)
},
};
log::trace!(
target: "ethereum-beacon-client",
"💫 Verified receipt inclusion for transaction at index {} in block {}",
proof.tx_index, proof.block_hash,
);
event_log.validate().map_err(|_| InvalidLog)?;
// Convert snowbridge_core::inbound::Log to snowbridge_ethereum::Log.
let event_log = snowbridge_ethereum::Log {
address: event_log.address,
topics: event_log.topics.clone(),
data: event_log.data.clone(),
};
if !receipt.contains_log(&event_log) {
log::error!(
target: "ethereum-beacon-client",
"💫 Event log not found in receipt for transaction at index {} in block {}",
proof.tx_index, proof.block_hash,
);
return Err(LogNotFound)
}
log::info!(
target: "ethereum-beacon-client",
"💫 Receipt verification successful for {}",
proof.block_hash,
);
Ok(())
}
}
impl<T: Config> Pallet<T> {
/// Verifies that the receipt encoded in `proof.data` is included in the block given by
/// `proof.block_hash`.
pub fn verify_receipt_inclusion(
receipts_root: H256,
proof: &Proof,
) -> Result<Receipt, VerificationError> {
let result = verify_receipt_proof(receipts_root, &proof.data.1).ok_or(InvalidProof)?;
match result {
Ok(receipt) => Ok(receipt),
Err(err) => {
log::trace!(
target: "ethereum-beacon-client",
"💫 Failed to decode transaction receipt: {}",
err
);
Err(InvalidProof)
},
}
}
}
@@ -0,0 +1,841 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Ethereum Beacon Client
//!
//! A light client that verifies consensus updates signed by the sync committee of the beacon chain.
//!
//! # Extrinsics
//!
//! ## Governance
//!
//! * [`Call::force_checkpoint`]: Set the initial trusted consensus checkpoint.
//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
//! processing of conensus updates.
//!
//! ## Consensus Updates
//!
//! * [`Call::submit`]: Submit a finalized beacon header with an optional sync committee update
//! * [`Call::submit_execution_header`]: Submit an execution header together with an ancestry proof
//! that can be verified against an already imported finalized beacon header.
#![cfg_attr(not(feature = "std"), no_std)]
pub mod config;
pub mod functions;
pub mod impls;
pub mod types;
pub mod weights;
#[cfg(any(test, feature = "fuzzing"))]
pub mod mock;
#[cfg(all(test, not(feature = "beacon-spec-mainnet")))]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
use frame_support::{
dispatch::DispatchResult, pallet_prelude::OptionQuery, traits::Get, transactional,
};
use frame_system::ensure_signed;
use primitives::{
fast_aggregate_verify, verify_merkle_branch, verify_receipt_proof, BeaconHeader, BlsError,
CompactBeaconState, CompactExecutionHeader, ExecutionHeaderState, ForkData, ForkVersion,
ForkVersions, PublicKeyPrepared, SigningData,
};
use snowbridge_core::{BasicOperatingMode, RingBufferMap};
use sp_core::H256;
use sp_std::prelude::*;
pub use weights::WeightInfo;
use functions::{
compute_epoch, compute_period, decompress_sync_committee_bits, sync_committee_sum,
};
pub use types::ExecutionHeaderBuffer;
use types::{
CheckpointUpdate, ExecutionHeaderUpdate, FinalizedBeaconStateBuffer, SyncCommitteePrepared,
Update,
};
pub use pallet::*;
pub use config::SLOTS_PER_HISTORICAL_ROOT;
pub const LOG_TARGET: &str = "ethereum-beacon-client";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct MaxFinalizedHeadersToKeep<T: Config>(PhantomData<T>);
impl<T: Config> Get<u32> for MaxFinalizedHeadersToKeep<T> {
fn get() -> u32 {
// Consider max latency allowed between LatestFinalizedState and LatestExecutionState is
// the total slots in one sync_committee_period so 1 should be fine we keep 2 periods
// here for redundancy.
const MAX_REDUNDANCY: u32 = 2;
config::EPOCHS_PER_SYNC_COMMITTEE_PERIOD as u32 * MAX_REDUNDANCY
}
}
#[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>;
#[pallet::constant]
type ForkVersions: Get<ForkVersions>;
/// Maximum number of execution headers to keep
#[pallet::constant]
type MaxExecutionHeadersToKeep: Get<u32>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
BeaconHeaderImported {
block_hash: H256,
slot: u64,
},
ExecutionHeaderImported {
block_hash: H256,
block_number: u64,
},
SyncCommitteeUpdated {
period: u64,
},
/// Set OperatingMode
OperatingModeChanged {
mode: BasicOperatingMode,
},
}
#[pallet::error]
pub enum Error<T> {
SkippedSyncCommitteePeriod,
/// Attested header is older than latest finalized header.
IrrelevantUpdate,
NotBootstrapped,
SyncCommitteeParticipantsNotSupermajority,
InvalidHeaderMerkleProof,
InvalidSyncCommitteeMerkleProof,
InvalidExecutionHeaderProof,
InvalidAncestryMerkleProof,
InvalidBlockRootsRootMerkleProof,
HeaderNotFinalized,
BlockBodyHashTreeRootFailed,
HeaderHashTreeRootFailed,
SyncCommitteeHashTreeRootFailed,
SigningRootHashTreeRootFailed,
ForkDataHashTreeRootFailed,
ExpectedFinalizedHeaderNotStored,
BLSPreparePublicKeysFailed,
BLSVerificationFailed(BlsError),
InvalidUpdateSlot,
/// The given update is not in the expected period, or the given next sync committee does
/// not match the next sync committee in storage.
InvalidSyncCommitteeUpdate,
ExecutionHeaderTooFarBehind,
ExecutionHeaderSkippedBlock,
Halted,
}
/// Latest imported checkpoint root
#[pallet::storage]
#[pallet::getter(fn initial_checkpoint_root)]
pub(super) type InitialCheckpointRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
/// Latest imported finalized block root
#[pallet::storage]
#[pallet::getter(fn latest_finalized_block_root)]
pub(super) type LatestFinalizedBlockRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
/// Beacon state by finalized block root
#[pallet::storage]
#[pallet::getter(fn finalized_beacon_state)]
pub(super) type FinalizedBeaconState<T: Config> =
StorageMap<_, Identity, H256, CompactBeaconState, OptionQuery>;
/// Finalized Headers: Current position in ring buffer
#[pallet::storage]
pub(crate) type FinalizedBeaconStateIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Finalized Headers: Mapping of ring buffer index to a pruning candidate
#[pallet::storage]
pub(crate) type FinalizedBeaconStateMapping<T: Config> =
StorageMap<_, Identity, u32, H256, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn validators_root)]
pub(super) type ValidatorsRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
/// Sync committee for current period
#[pallet::storage]
pub(super) type CurrentSyncCommittee<T: Config> =
StorageValue<_, SyncCommitteePrepared, ValueQuery>;
/// Sync committee for next period
#[pallet::storage]
pub(super) type NextSyncCommittee<T: Config> =
StorageValue<_, SyncCommitteePrepared, ValueQuery>;
/// Latest imported execution header
#[pallet::storage]
#[pallet::getter(fn latest_execution_state)]
pub(super) type LatestExecutionState<T: Config> =
StorageValue<_, ExecutionHeaderState, ValueQuery>;
/// Execution Headers
#[pallet::storage]
pub type ExecutionHeaders<T: Config> =
StorageMap<_, Identity, H256, CompactExecutionHeader, OptionQuery>;
/// Execution Headers: Current position in ring buffer
#[pallet::storage]
pub type ExecutionHeaderIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Execution Headers: Mapping of ring buffer index to a pruning candidate
#[pallet::storage]
pub type ExecutionHeaderMapping<T: Config> = StorageMap<_, Identity, u32, H256, ValueQuery>;
/// The current operating mode of the pallet.
#[pallet::storage]
#[pallet::getter(fn operating_mode)]
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::force_checkpoint())]
#[transactional]
/// Used for pallet initialization and light client resetting. Needs to be called by
/// the root origin.
pub fn force_checkpoint(
origin: OriginFor<T>,
update: Box<CheckpointUpdate>,
) -> DispatchResult {
ensure_root(origin)?;
Self::process_checkpoint_update(&update)?;
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight({
match update.next_sync_committee_update {
None => T::WeightInfo::submit(),
Some(_) => T::WeightInfo::submit_with_sync_committee(),
}
})]
#[transactional]
/// Submits a new finalized beacon header update. The update may contain the next
/// sync committee.
pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResult {
ensure_signed(origin)?;
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
Self::process_update(&update)?;
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::submit_execution_header())]
#[transactional]
/// Submits a new execution header update. The relevant related beacon header
/// is also included to prove the execution header, as well as ancestry proof data.
pub fn submit_execution_header(
origin: OriginFor<T>,
update: Box<ExecutionHeaderUpdate>,
) -> DispatchResult {
ensure_signed(origin)?;
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
Self::process_execution_header_update(&update)?;
Ok(())
}
/// Halt or resume all pallet operations. May only be called by root.
#[pallet::call_index(3)]
#[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>::set(mode);
Self::deposit_event(Event::OperatingModeChanged { mode });
Ok(())
}
}
impl<T: Config> Pallet<T> {
/// Forces a finalized beacon header checkpoint update. The current sync committee,
/// with a header attesting to the current sync committee, should be provided.
/// An `block_roots` proof should also be provided. This is used for ancestry proofs
/// for execution header updates.
pub(crate) fn process_checkpoint_update(update: &CheckpointUpdate) -> DispatchResult {
let sync_committee_root = update
.current_sync_committee
.hash_tree_root()
.map_err(|_| Error::<T>::SyncCommitteeHashTreeRootFailed)?;
// Verifies the sync committee in the Beacon state.
ensure!(
verify_merkle_branch(
sync_committee_root,
&update.current_sync_committee_branch,
config::CURRENT_SYNC_COMMITTEE_SUBTREE_INDEX,
config::CURRENT_SYNC_COMMITTEE_DEPTH,
update.header.state_root
),
Error::<T>::InvalidSyncCommitteeMerkleProof
);
let header_root: H256 = update
.header
.hash_tree_root()
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
// This is used for ancestry proofs in ExecutionHeader updates. This verifies the
// BeaconState: the beacon state root is the tree root; the `block_roots` hash is the
// tree leaf.
ensure!(
verify_merkle_branch(
update.block_roots_root,
&update.block_roots_branch,
config::BLOCK_ROOTS_SUBTREE_INDEX,
config::BLOCK_ROOTS_DEPTH,
update.header.state_root
),
Error::<T>::InvalidBlockRootsRootMerkleProof
);
let sync_committee_prepared: SyncCommitteePrepared = (&update.current_sync_committee)
.try_into()
.map_err(|_| <Error<T>>::BLSPreparePublicKeysFailed)?;
<CurrentSyncCommittee<T>>::set(sync_committee_prepared);
<NextSyncCommittee<T>>::kill();
InitialCheckpointRoot::<T>::set(header_root);
<LatestExecutionState<T>>::kill();
Self::store_validators_root(update.validators_root);
Self::store_finalized_header(header_root, update.header, update.block_roots_root)?;
Ok(())
}
pub(crate) fn process_update(update: &Update) -> DispatchResult {
Self::cross_check_execution_state()?;
Self::verify_update(update)?;
Self::apply_update(update)?;
Ok(())
}
/// Cross check to make sure that execution header import does not fall too far behind
/// finalised beacon header import. If that happens just return an error and pause
/// processing until execution header processing has caught up.
pub(crate) fn cross_check_execution_state() -> DispatchResult {
let latest_finalized_state =
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
.ok_or(Error::<T>::NotBootstrapped)?;
let latest_execution_state = Self::latest_execution_state();
// The execution header import should be at least within the slot range of a sync
// committee period.
let max_latency = config::EPOCHS_PER_SYNC_COMMITTEE_PERIOD * config::SLOTS_PER_EPOCH;
ensure!(
latest_execution_state.beacon_slot == 0 ||
latest_finalized_state.slot <
latest_execution_state.beacon_slot + max_latency as u64,
Error::<T>::ExecutionHeaderTooFarBehind
);
Ok(())
}
/// References and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#validate_light_client_update>
/// Verifies that provided next sync committee is valid through a series of checks
/// (including checking that a sync committee period isn't skipped and that the header is
/// signed by the current sync committee.
fn verify_update(update: &Update) -> DispatchResult {
// Verify sync committee has sufficient participants.
let participation =
decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
Self::sync_committee_participation_is_supermajority(&participation)?;
// Verify update does not skip a sync committee period.
ensure!(
update.signature_slot > update.attested_header.slot &&
update.attested_header.slot >= update.finalized_header.slot,
Error::<T>::InvalidUpdateSlot
);
// Retrieve latest finalized state.
let latest_finalized_state =
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
.ok_or(Error::<T>::NotBootstrapped)?;
let store_period = compute_period(latest_finalized_state.slot);
let signature_period = compute_period(update.signature_slot);
if <NextSyncCommittee<T>>::exists() {
ensure!(
(store_period..=store_period + 1).contains(&signature_period),
Error::<T>::SkippedSyncCommitteePeriod
)
} else {
ensure!(signature_period == store_period, Error::<T>::SkippedSyncCommitteePeriod)
}
// Verify update is relevant.
let update_attested_period = compute_period(update.attested_header.slot);
let update_has_next_sync_committee = !<NextSyncCommittee<T>>::exists() &&
(update.next_sync_committee_update.is_some() &&
update_attested_period == store_period);
ensure!(
update.attested_header.slot > latest_finalized_state.slot ||
update_has_next_sync_committee,
Error::<T>::IrrelevantUpdate
);
// Verify that the `finality_branch`, if present, confirms `finalized_header` to match
// the finalized checkpoint root saved in the state of `attested_header`.
let finalized_block_root: H256 = update
.finalized_header
.hash_tree_root()
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
ensure!(
verify_merkle_branch(
finalized_block_root,
&update.finality_branch,
config::FINALIZED_ROOT_SUBTREE_INDEX,
config::FINALIZED_ROOT_DEPTH,
update.attested_header.state_root
),
Error::<T>::InvalidHeaderMerkleProof
);
// Though following check does not belong to ALC spec we verify block_roots_root to
// match the finalized checkpoint root saved in the state of `finalized_header` so to
// cache it for later use in `verify_ancestry_proof`.
ensure!(
verify_merkle_branch(
update.block_roots_root,
&update.block_roots_branch,
config::BLOCK_ROOTS_SUBTREE_INDEX,
config::BLOCK_ROOTS_DEPTH,
update.finalized_header.state_root
),
Error::<T>::InvalidBlockRootsRootMerkleProof
);
// Verify that the `next_sync_committee`, if present, actually is the next sync
// committee saved in the state of the `attested_header`.
if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
let sync_committee_root = next_sync_committee_update
.next_sync_committee
.hash_tree_root()
.map_err(|_| Error::<T>::SyncCommitteeHashTreeRootFailed)?;
if update_attested_period == store_period && <NextSyncCommittee<T>>::exists() {
let next_committee_root = <NextSyncCommittee<T>>::get().root;
ensure!(
sync_committee_root == next_committee_root,
Error::<T>::InvalidSyncCommitteeUpdate
);
}
ensure!(
verify_merkle_branch(
sync_committee_root,
&next_sync_committee_update.next_sync_committee_branch,
config::NEXT_SYNC_COMMITTEE_SUBTREE_INDEX,
config::NEXT_SYNC_COMMITTEE_DEPTH,
update.attested_header.state_root
),
Error::<T>::InvalidSyncCommitteeMerkleProof
);
}
// Verify sync committee aggregate signature.
let sync_committee = if signature_period == store_period {
<CurrentSyncCommittee<T>>::get()
} else {
<NextSyncCommittee<T>>::get()
};
let absent_pubkeys =
Self::find_pubkeys(&participation, (*sync_committee.pubkeys).as_ref(), false);
let signing_root = Self::signing_root(
&update.attested_header,
Self::validators_root(),
update.signature_slot,
)?;
// Improvement here per <https://eth2book.info/capella/part2/building_blocks/signatures/#sync-aggregates>
// suggested start from the full set aggregate_pubkey then subtracting the absolute
// minority that did not participate.
fast_aggregate_verify(
&sync_committee.aggregate_pubkey,
&absent_pubkeys,
signing_root,
&update.sync_aggregate.sync_committee_signature,
)
.map_err(|e| Error::<T>::BLSVerificationFailed(e))?;
Ok(())
}
/// Reference and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#apply_light_client_update
/// Applies a finalized beacon header update to the beacon client. If a next sync committee
/// is present in the update, verify the sync committee by converting it to a
/// SyncCommitteePrepared type. Stores the provided finalized header.
fn apply_update(update: &Update) -> DispatchResult {
let latest_finalized_state =
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
.ok_or(Error::<T>::NotBootstrapped)?;
if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
let store_period = compute_period(latest_finalized_state.slot);
let update_finalized_period = compute_period(update.finalized_header.slot);
let sync_committee_prepared: SyncCommitteePrepared = (&next_sync_committee_update
.next_sync_committee)
.try_into()
.map_err(|_| <Error<T>>::BLSPreparePublicKeysFailed)?;
if !<NextSyncCommittee<T>>::exists() {
ensure!(
update_finalized_period == store_period,
<Error<T>>::InvalidSyncCommitteeUpdate
);
<NextSyncCommittee<T>>::set(sync_committee_prepared);
} else if update_finalized_period == store_period + 1 {
<CurrentSyncCommittee<T>>::set(<NextSyncCommittee<T>>::get());
<NextSyncCommittee<T>>::set(sync_committee_prepared);
}
log::info!(
target: LOG_TARGET,
"💫 SyncCommitteeUpdated at period {}.",
update_finalized_period
);
Self::deposit_event(Event::SyncCommitteeUpdated {
period: update_finalized_period,
});
};
if update.finalized_header.slot > latest_finalized_state.slot {
let finalized_block_root: H256 = update
.finalized_header
.hash_tree_root()
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
Self::store_finalized_header(
finalized_block_root,
update.finalized_header,
update.block_roots_root,
)?;
}
Ok(())
}
/// Validates an execution header for import. The beacon header containing the execution
/// header is sent, plus the execution header, along with a proof that the execution header
/// is rooted in the beacon header body.
pub(crate) fn process_execution_header_update(
update: &ExecutionHeaderUpdate,
) -> DispatchResult {
let latest_finalized_state =
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
.ok_or(Error::<T>::NotBootstrapped)?;
// Checks that the header is an ancestor of a finalized header, using slot number.
ensure!(
update.header.slot <= latest_finalized_state.slot,
Error::<T>::HeaderNotFinalized
);
// Checks that we don't skip execution headers, they need to be imported sequentially.
let latest_execution_state: ExecutionHeaderState = Self::latest_execution_state();
ensure!(
latest_execution_state.block_number == 0 ||
update.execution_header.block_number ==
latest_execution_state.block_number + 1,
Error::<T>::ExecutionHeaderSkippedBlock
);
// Gets the hash tree root of the execution header, in preparation for the execution
// header proof (used to check that the execution header is rooted in the beacon
// header body.
let execution_header_root: H256 = update
.execution_header
.hash_tree_root()
.map_err(|_| Error::<T>::BlockBodyHashTreeRootFailed)?;
ensure!(
verify_merkle_branch(
execution_header_root,
&update.execution_branch,
config::EXECUTION_HEADER_SUBTREE_INDEX,
config::EXECUTION_HEADER_DEPTH,
update.header.body_root
),
Error::<T>::InvalidExecutionHeaderProof
);
let block_root: H256 = update
.header
.hash_tree_root()
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
match &update.ancestry_proof {
Some(proof) => {
Self::verify_ancestry_proof(
block_root,
update.header.slot,
&proof.header_branch,
proof.finalized_block_root,
)?;
},
None => {
// If the ancestry proof is not provided, we expect this header to be a
// finalized header. We need to check that the header hash matches the finalized
// header root at the expected slot.
let state = <FinalizedBeaconState<T>>::get(block_root)
.ok_or(Error::<T>::ExpectedFinalizedHeaderNotStored)?;
if update.header.slot != state.slot {
return Err(Error::<T>::ExpectedFinalizedHeaderNotStored.into())
}
},
}
Self::store_execution_header(
update.execution_header.block_hash,
update.execution_header.clone().into(),
update.header.slot,
block_root,
);
Ok(())
}
/// Verify that `block_root` is an ancestor of `finalized_block_root` Used to prove that
/// an execution header is an ancestor of a finalized header (i.e. the blocks are
/// on the same chain).
fn verify_ancestry_proof(
block_root: H256,
block_slot: u64,
block_root_proof: &[H256],
finalized_block_root: H256,
) -> DispatchResult {
let state = <FinalizedBeaconState<T>>::get(finalized_block_root)
.ok_or(Error::<T>::ExpectedFinalizedHeaderNotStored)?;
ensure!(block_slot < state.slot, Error::<T>::HeaderNotFinalized);
let index_in_array = block_slot % (SLOTS_PER_HISTORICAL_ROOT as u64);
let leaf_index = (SLOTS_PER_HISTORICAL_ROOT as u64) + index_in_array;
ensure!(
verify_merkle_branch(
block_root,
block_root_proof,
leaf_index as usize,
config::BLOCK_ROOT_AT_INDEX_DEPTH,
state.block_roots_root
),
Error::<T>::InvalidAncestryMerkleProof
);
Ok(())
}
/// Computes the signing root for a given beacon header and domain. The hash tree root
/// of the beacon header is computed, and then the combination of the beacon header hash
/// and the domain makes up the signing root.
pub(super) fn compute_signing_root(
beacon_header: &BeaconHeader,
domain: H256,
) -> Result<H256, DispatchError> {
let beacon_header_root = beacon_header
.hash_tree_root()
.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
let hash_root = SigningData { object_root: beacon_header_root, domain }
.hash_tree_root()
.map_err(|_| Error::<T>::SigningRootHashTreeRootFailed)?;
Ok(hash_root)
}
/// Stores a compacted (slot and block roots root (hash of the `block_roots` beacon state
/// field, used for ancestry proof)) beacon state in a ring buffer map, with the header root
/// as map key.
fn store_finalized_header(
header_root: H256,
header: BeaconHeader,
block_roots_root: H256,
) -> DispatchResult {
let slot = header.slot;
<FinalizedBeaconStateBuffer<T>>::insert(
header_root,
CompactBeaconState { slot: header.slot, block_roots_root },
);
<LatestFinalizedBlockRoot<T>>::set(header_root);
log::info!(
target: LOG_TARGET,
"💫 Updated latest finalized block root {} at slot {}.",
header_root,
slot
);
Self::deposit_event(Event::BeaconHeaderImported { block_hash: header_root, slot });
Ok(())
}
/// Stores the provided execution header in pallet storage. The header is stored
/// in a ring buffer map, with the block hash as map key. The last imported execution
/// header is also kept in storage, for the relayer to check import progress.
pub(crate) fn store_execution_header(
block_hash: H256,
header: CompactExecutionHeader,
beacon_slot: u64,
beacon_block_root: H256,
) {
let block_number = header.block_number;
<ExecutionHeaderBuffer<T>>::insert(block_hash, header);
log::trace!(
target: LOG_TARGET,
"💫 Updated latest execution block at {} to number {}.",
block_hash,
block_number
);
LatestExecutionState::<T>::mutate(|s| {
s.beacon_block_root = beacon_block_root;
s.beacon_slot = beacon_slot;
s.block_hash = block_hash;
s.block_number = block_number;
});
Self::deposit_event(Event::ExecutionHeaderImported { block_hash, block_number });
}
/// Stores the validators root in storage. Validators root is the hash tree root of all the
/// validators at genesis and is used to used to identify the chain that we are on
/// (used in conjunction with the fork version).
/// <https://eth2book.info/capella/part3/containers/state/#genesis_validators_root>
fn store_validators_root(validators_root: H256) {
<ValidatorsRoot<T>>::set(validators_root);
}
/// Returns the domain for the domain_type and fork_version. The domain is used to
/// distinguish between the different players in the chain (see DomainTypes
/// <https://eth2book.info/capella/part3/config/constants/#domain-types>) and to ensure we are
/// addressing the correct chain.
/// <https://eth2book.info/capella/part3/helper/misc/#compute_domain>
pub(super) fn compute_domain(
domain_type: Vec<u8>,
fork_version: ForkVersion,
genesis_validators_root: H256,
) -> Result<H256, DispatchError> {
let fork_data_root =
Self::compute_fork_data_root(fork_version, genesis_validators_root)?;
let mut domain = [0u8; 32];
domain[0..4].copy_from_slice(&(domain_type));
domain[4..32].copy_from_slice(&(fork_data_root.0[..28]));
Ok(domain.into())
}
/// Computes the fork data root. The fork data root is a merkleization of the current
/// fork version and the genesis validators root.
fn compute_fork_data_root(
current_version: ForkVersion,
genesis_validators_root: H256,
) -> Result<H256, DispatchError> {
let hash_root = ForkData {
current_version,
genesis_validators_root: genesis_validators_root.into(),
}
.hash_tree_root()
.map_err(|_| Error::<T>::ForkDataHashTreeRootFailed)?;
Ok(hash_root)
}
/// Checks that the sync committee bits (the votes of the sync committee members,
/// represented by bits 0 and 1) is more than a supermajority (2/3 of the votes are
/// positive).
pub(super) fn sync_committee_participation_is_supermajority(
sync_committee_bits: &[u8],
) -> DispatchResult {
let sync_committee_sum = sync_committee_sum(sync_committee_bits);
ensure!(
((sync_committee_sum * 3) as usize) >= sync_committee_bits.len() * 2,
Error::<T>::SyncCommitteeParticipantsNotSupermajority
);
Ok(())
}
/// Returns the fork version based on the current epoch. The hard fork versions
/// are defined in pallet config.
pub(super) fn compute_fork_version(epoch: u64) -> ForkVersion {
Self::select_fork_version(&T::ForkVersions::get(), epoch)
}
/// Returns the fork version based on the current epoch.
pub(super) fn select_fork_version(fork_versions: &ForkVersions, epoch: u64) -> ForkVersion {
if epoch >= fork_versions.capella.epoch {
return fork_versions.capella.version
}
if epoch >= fork_versions.bellatrix.epoch {
return fork_versions.bellatrix.version
}
if epoch >= fork_versions.altair.epoch {
return fork_versions.altair.version
}
fork_versions.genesis.version
}
/// Returns a vector of public keys that participated in the sync committee block signage.
/// Sync committee bits is an array of 0s and 1s, 0 meaning the corresponding sync committee
/// member did not participate in the vote, 1 meaning they participated.
/// This method can find the absent or participating members, based on the participant
/// parameter. participant = false will return absent participants, participant = true will
/// return participating members.
pub fn find_pubkeys(
sync_committee_bits: &[u8],
sync_committee_pubkeys: &[PublicKeyPrepared],
participant: bool,
) -> Vec<PublicKeyPrepared> {
let mut pubkeys: Vec<PublicKeyPrepared> = Vec::new();
for (bit, pubkey) in sync_committee_bits.iter().zip(sync_committee_pubkeys.iter()) {
if *bit == u8::from(participant) {
pubkeys.push(*pubkey);
}
}
pubkeys
}
/// Calculates signing root for BeaconHeader. The signing root is used for the message
/// value in BLS signature verification.
pub fn signing_root(
header: &BeaconHeader,
validators_root: H256,
signature_slot: u64,
) -> Result<H256, DispatchError> {
let fork_version = Self::compute_fork_version(compute_epoch(
signature_slot,
config::SLOTS_PER_EPOCH as u64,
));
let domain_type = config::DOMAIN_SYNC_COMMITTEE.to_vec();
// Domains are used for for seeds, for signatures, and for selecting aggregators.
let domain = Self::compute_domain(domain_type, fork_version, validators_root)?;
// Hash tree root of SigningData - object root + domain
let signing_root = Self::compute_signing_root(header, domain)?;
Ok(signing_root)
}
}
}
@@ -0,0 +1,275 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate as ethereum_beacon_client;
use frame_support::parameter_types;
use pallet_timestamp;
use primitives::{Fork, ForkVersions};
use sp_core::H256;
use sp_runtime::traits::{BlakeTwo256, IdentityLookup};
#[cfg(not(feature = "beacon-spec-mainnet"))]
pub mod minimal {
use super::*;
use crate::config;
use hex_literal::hex;
use primitives::CompactExecutionHeader;
use snowbridge_core::inbound::{Log, Proof};
use sp_runtime::BuildStorage;
use std::{fs::File, path::PathBuf};
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test {
System: frame_system::{Pallet, Call, Storage, Event<T>},
Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
EthereumBeaconClient: ethereum_beacon_client::{Pallet, Call, Storage, Event<T>},
}
);
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const SS58Prefix: u8 = 42;
}
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type OnSetCode = ();
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type RuntimeTask = RuntimeTask;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix;
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type Block = Block;
}
impl pallet_timestamp::Config for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ();
type WeightInfo = ();
}
parameter_types! {
pub const ExecutionHeadersPruneThreshold: u32 = 10;
pub const ChainForkVersions: ForkVersions = ForkVersions{
genesis: Fork {
version: [0, 0, 0, 1], // 0x00000001
epoch: 0,
},
altair: Fork {
version: [1, 0, 0, 1], // 0x01000001
epoch: 0,
},
bellatrix: Fork {
version: [2, 0, 0, 1], // 0x02000001
epoch: 0,
},
capella: Fork {
version: [3, 0, 0, 1], // 0x03000001
epoch: 0,
},
};
}
impl ethereum_beacon_client::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ForkVersions = ChainForkVersions;
type MaxExecutionHeadersToKeep = ExecutionHeadersPruneThreshold;
type WeightInfo = ();
}
// Build genesis storage according to the mock runtime.
pub fn new_tester() -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
let mut ext = sp_io::TestExternalities::new(t);
let _ = ext.execute_with(|| Timestamp::set(RuntimeOrigin::signed(1), 30_000));
ext
}
fn load_fixture<T>(basename: &str) -> Result<T, serde_json::Error>
where
T: for<'de> serde::Deserialize<'de>,
{
let filepath: PathBuf =
[env!("CARGO_MANIFEST_DIR"), "tests", "fixtures", basename].iter().collect();
serde_json::from_reader(File::open(filepath).unwrap())
}
pub fn load_execution_header_update_fixture() -> primitives::ExecutionHeaderUpdate {
load_fixture("execution-header-update.minimal.json").unwrap()
}
pub fn load_checkpoint_update_fixture(
) -> primitives::CheckpointUpdate<{ config::SYNC_COMMITTEE_SIZE }> {
load_fixture("initial-checkpoint.minimal.json").unwrap()
}
pub fn load_sync_committee_update_fixture(
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
load_fixture("sync-committee-update.minimal.json").unwrap()
}
pub fn load_finalized_header_update_fixture(
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
load_fixture("finalized-header-update.minimal.json").unwrap()
}
pub fn load_next_sync_committee_update_fixture(
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
load_fixture("next-sync-committee-update.minimal.json").unwrap()
}
pub fn load_next_finalized_header_update_fixture(
) -> primitives::Update<{ config::SYNC_COMMITTEE_SIZE }, { config::SYNC_COMMITTEE_BITS_SIZE }> {
load_fixture("next-finalized-header-update.minimal.json").unwrap()
}
pub fn get_message_verification_payload() -> (Log, Proof) {
(
Log {
address: hex!("ee9170abfbf9421ad6dd07f6bdec9d89f2b581e0").into(),
topics: vec![
hex!("1b11dcf133cc240f682dab2d3a8e4cd35c5da8c9cf99adac4336f8512584c5ad").into(),
hex!("00000000000000000000000000000000000000000000000000000000000003e8").into(),
hex!("0000000000000000000000000000000000000000000000000000000000000001").into(),
],
data: hex!("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004b000f000000000000000100d184c103f7acc340847eee82a0b909e3358bc28d440edffa1352b13227e8ee646f3ea37456dec701345772617070656420457468657210574554481235003511000000000000000000000000000000000000000000").into(),
},
Proof {
block_hash: hex!("05aaa60b0f27cce9e71909508527264b77ee14da7b5bf915fcc4e32715333213").into(),
tx_index: 0,
data: (vec![
hex!("cf0d1c1ba57d1e0edfb59786c7e30c2b7e12bd54612b00cd21c4eaeecedf44fb").to_vec(),
hex!("d21fc4f68ab05bc4dcb23c67008e92c4d466437cdd6ed7aad0c008944c185510").to_vec(),
hex!("b9890f91ca0d77aa2a4adfaf9b9e40c94cac9e638b6d9797923865872944b646").to_vec(),
], vec![
hex!("f90131a0b601337b3aa10a671caa724eba641e759399979856141d3aea6b6b4ac59b889ba00c7d5dd48be9060221a02fb8fa213860b4c50d47046c8fa65ffaba5737d569e0a094601b62a1086cd9c9cb71a7ebff9e718f3217fd6e837efe4246733c0a196f63a06a4b0dd0aefc37b3c77828c8f07d1b7a2455ceb5dbfd3c77d7d6aeeddc2f7e8ca0d6e8e23142cdd8ec219e1f5d8b56aa18e456702b195deeaa210327284d42ade4a08a313d4c87023005d1ab631bbfe3f5de1e405d0e66d0bef3e033f1e5711b5521a0bf09a5d9a48b10ade82b8d6a5362a15921c8b5228a3487479b467db97411d82fa0f95cccae2a7c572ef3c566503e30bac2b2feb2d2f26eebf6d870dcf7f8cf59cea0d21fc4f68ab05bc4dcb23c67008e92c4d466437cdd6ed7aad0c008944c1855108080808080808080").to_vec(),
hex!("f851a0b9890f91ca0d77aa2a4adfaf9b9e40c94cac9e638b6d9797923865872944b646a060a634b9280e3a23fb63375e7bbdd9ab07fd379ab6a67e2312bbc112195fa358808080808080808080808080808080").to_vec(),
hex!("f9030820b9030402f90300018301d6e2b9010000000000000800000000000020040008000000000000000000000000400000008000000000000000000000000000000000000000000000000000000000042010000000001000000000000000000000000000000000040000000000000000000000000000000000000000000000008000000000000000002000000000000000000000000200000000000000200000000000100000000040000001000200008000000000000200000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000f901f5f87a942ffa5ecdbe006d30397c7636d3e015eee251369ff842a0c965575a00553e094ca7c5d14f02e107c258dda06867cbf9e0e69f80e71bbcc1a000000000000000000000000000000000000000000000000000000000000003e8a000000000000000000000000000000000000000000000000000000000000003e8f9011c94ee9170abfbf9421ad6dd07f6bdec9d89f2b581e0f863a01b11dcf133cc240f682dab2d3a8e4cd35c5da8c9cf99adac4336f8512584c5ada000000000000000000000000000000000000000000000000000000000000003e8a00000000000000000000000000000000000000000000000000000000000000001b8a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004b000f000000000000000100d184c103f7acc340847eee82a0b909e3358bc28d440edffa1352b13227e8ee646f3ea37456dec701345772617070656420457468657210574554481235003511000000000000000000000000000000000000000000f858948cf6147918a5cbb672703f879f385036f8793a24e1a01449abf21e49fd025f33495e77f7b1461caefdd3d4bb646424a3f445c4576a5ba0000000000000000000000000440edffa1352b13227e8ee646f3ea37456dec701").to_vec(),
]),
}
)
}
pub fn get_message_verification_header() -> CompactExecutionHeader {
CompactExecutionHeader {
parent_hash: hex!("04a7f6ab8282203562c62f38b0ab41d32aaebe2c7ea687702b463148a6429e04")
.into(),
block_number: 55,
state_root: hex!("894d968712976d613519f973a317cb0781c7b039c89f27ea2b7ca193f7befdb3")
.into(),
receipts_root: hex!("cf0d1c1ba57d1e0edfb59786c7e30c2b7e12bd54612b00cd21c4eaeecedf44fb")
.into(),
}
}
}
#[cfg(feature = "beacon-spec-mainnet")]
pub mod mainnet {
use super::*;
type Block = frame_system::mocking::MockBlock<Test>;
use sp_runtime::BuildStorage;
frame_support::construct_runtime!(
pub enum Test {
System: frame_system::{Pallet, Call, Storage, Event<T>},
Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
EthereumBeaconClient: ethereum_beacon_client::{Pallet, Call, Storage, Event<T>},
}
);
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const SS58Prefix: u8 = 42;
}
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type OnSetCode = ();
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type RuntimeTask = RuntimeTask;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix;
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type Block = Block;
}
impl pallet_timestamp::Config for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ();
type WeightInfo = ();
}
parameter_types! {
pub const ChainForkVersions: ForkVersions = ForkVersions{
genesis: Fork {
version: [0, 0, 16, 32], // 0x00001020
epoch: 0,
},
altair: Fork {
version: [1, 0, 16, 32], // 0x01001020
epoch: 36660,
},
bellatrix: Fork {
version: [2, 0, 16, 32], // 0x02001020
epoch: 112260,
},
capella: Fork {
version: [3, 0, 16, 32], // 0x03001020
epoch: 162304,
},
};
pub const ExecutionHeadersPruneThreshold: u32 = 10;
}
impl ethereum_beacon_client::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ForkVersions = ChainForkVersions;
type MaxExecutionHeadersToKeep = ExecutionHeadersPruneThreshold;
type WeightInfo = ();
}
// Build genesis storage according to the mock runtime.
pub fn new_tester() -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
let mut ext = sp_io::TestExternalities::new(t);
let _ = ext.execute_with(|| Timestamp::set(RuntimeOrigin::signed(1), 30_000));
ext
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
pub use crate::config::{
SLOTS_PER_HISTORICAL_ROOT, SYNC_COMMITTEE_BITS_SIZE as SC_BITS_SIZE,
SYNC_COMMITTEE_SIZE as SC_SIZE,
};
use frame_support::storage::types::OptionQuery;
use snowbridge_core::RingBufferMapImpl;
// Specialize types based on configured sync committee size
pub type SyncCommittee = primitives::SyncCommittee<SC_SIZE>;
pub type SyncCommitteePrepared = primitives::SyncCommitteePrepared<SC_SIZE>;
pub type SyncAggregate = primitives::SyncAggregate<SC_SIZE, SC_BITS_SIZE>;
pub type CheckpointUpdate = primitives::CheckpointUpdate<SC_SIZE>;
pub type Update = primitives::Update<SC_SIZE, SC_BITS_SIZE>;
pub type NextSyncCommitteeUpdate = primitives::NextSyncCommitteeUpdate<SC_SIZE>;
pub use primitives::ExecutionHeaderUpdate;
/// ExecutionHeader ring buffer implementation
pub type ExecutionHeaderBuffer<T> = RingBufferMapImpl<
u32,
<T as crate::Config>::MaxExecutionHeadersToKeep,
crate::ExecutionHeaderIndex<T>,
crate::ExecutionHeaderMapping<T>,
crate::ExecutionHeaders<T>,
OptionQuery,
>;
/// FinalizedState ring buffer implementation
pub(crate) type FinalizedBeaconStateBuffer<T> = RingBufferMapImpl<
u32,
crate::MaxFinalizedHeadersToKeep<T>,
crate::FinalizedBeaconStateIndex<T>,
crate::FinalizedBeaconStateMapping<T>,
crate::FinalizedBeaconState<T>,
OptionQuery,
>;
@@ -0,0 +1,68 @@
//! Autogenerated weights for ethereum_beacon_client
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2022-09-27, STEPS: `10`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("/tmp/snowbridge/spec.json"), DB CACHE: 1024
// Executed Command:
// ./target/release/snowbridge
// benchmark
// pallet
// --chain
// /tmp/snowbridge/spec.json
// --execution=wasm
// --pallet
// ethereum_beacon_client
// --extrinsic
// *
// --steps
// 10
// --repeat
// 10
// --output
// pallets/ethereum-beacon-client/src/weights.rs
// --template
// templates/module-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use sp_std::marker::PhantomData;
/// Weight functions needed for ethereum_beacon_client.
pub trait WeightInfo {
fn force_checkpoint() -> Weight;
fn submit() -> Weight;
fn submit_with_sync_committee() -> Weight;
fn submit_execution_header() -> Weight;
}
// For backwards compatibility and tests
impl WeightInfo for () {
fn force_checkpoint() -> Weight {
Weight::from_parts(97_263_571_000_u64, 0)
.saturating_add(Weight::from_parts(0, 3501))
.saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(9))
}
fn submit() -> Weight {
Weight::from_parts(26_051_019_000_u64, 0)
.saturating_add(Weight::from_parts(0, 93857))
.saturating_add(RocksDbWeight::get().reads(8))
.saturating_add(RocksDbWeight::get().writes(4))
}
fn submit_with_sync_committee() -> Weight {
Weight::from_parts(122_461_312_000_u64, 0)
.saturating_add(Weight::from_parts(0, 93857))
.saturating_add(RocksDbWeight::get().reads(6))
.saturating_add(RocksDbWeight::get().writes(1))
}
fn submit_execution_header() -> Weight {
Weight::from_parts(113_158_000_u64, 0)
.saturating_add(Weight::from_parts(0, 3537))
.saturating_add(RocksDbWeight::get().reads(5))
.saturating_add(RocksDbWeight::get().writes(4))
}
}
@@ -0,0 +1,43 @@
{
"header": {
"slot": 3622,
"proposer_index": 7,
"parent_root": "0x254c9215f6cce83e21b9776afb482181639602d3cb58cf99452a6a4a4f603930",
"state_root": "0xea98df6d30817d63f3e54ea118e2b1ba8675753c72dec1661c503d4eb43f9bdd",
"body_root": "0x765a0616a31d38e0ca2d10f6e8b234dd3d07e16aa929bcbc4de775c93f1972fd"
},
"ancestry_proof": {
"header_branch": [
"0x7690506882ac8c5f01d00f3ade06439259a3a0261ef5d61ec44920678b4104e6",
"0xf01aa0fdd7c9ef7b1affb7854fe8cbcc5c70643ee5b83e032faa702a0675a8cb",
"0x273a7b300b75ffa2c765af50680aa836299264f2107f38010278822313181801",
"0x30fe73a3bae6a31af32656ab759a4b67d27a213e01012b96cc4fedd0f2e77c75",
"0x7246cb3a35f13a1f0bbf907887985bb5382c45f2aa1699dbca48a0a82d5330af",
"0x5e7270e88a22dd4a905b2e76da2c8c358baeddd34de6c64a71bb1c80070ab717"
],
"finalized_block_root": "0xa6fdc5df11c1759d11c9f0353a666715e5677e9ffd7d414e44cff0970553f1c9"
},
"execution_header": {
"parent_hash": "0x6c9657f1267ad6040ea017ff6d02b55c4ba25cb092b8326d321dd98d01d1ee64",
"fee_recipient": "0x0000000000000000000000000000000000000000",
"state_root": "0x01f975f7cdff9b0a8844304aa59062fe18af0fef4636539312dfe20d238600ba",
"receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"prev_randao": "0xcdfcab74bc26b3f4311afdc72d2d21d33a4b045187a01fa208a9d687a6d1d25c",
"block_number": 3622,
"gas_limit": 30000000,
"gas_used": 0,
"timestamp": 1685722543,
"extra_data": "0xd983010b02846765746888676f312e31392e358664617277696e",
"base_fee_per_gas": 7,
"block_hash": "0x38c80e0e26cb80730df627d32f50266bd0fe32fb12b7606300ad81aa2b4033db",
"transactions_root": "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1",
"withdrawals_root": "0x28ba1834a3a7b657460ce79fa3a1d909ab8828fd557659d4d0554a9bdbc0ec30"
},
"execution_branch": [
"0x005b8d55b34b4323bfd4773c28b09eb53bc87959e65411ccd23728c7e42d5ff2",
"0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x7061330dada1ba1c602ba98f647a441885460ed0db00483fea1282385dfab84b"
]
}
@@ -0,0 +1,38 @@
{
"attested_header": {
"slot": 3640,
"proposer_index": 5,
"parent_root": "0xf062fcec9c3379a08e6add37a834b1e39af395fc343973e44957ecebbf2ecddd",
"state_root": "0xb1581cb62fe376e305e02f26463153f5dfb804d8df97ef40fc315c1bc30731ba",
"body_root": "0x98461abcc6d130b7bcb9430292c8a269ea9f01082685347e2968d892f716067c"
},
"sync_aggregate": {
"sync_committee_bits": "0xffffffff",
"sync_committee_signature": "0x925c6e4b67890a7e28a7ca19853f88247e92014b9d233ac9058efd4f3827f0055db308debe17596e635b93727b5a851e1366ca801f30b03fdec722f45011504702a27646488b5ab5e3428fe7b4d4a50132f374612f66e45d68db27c568f96f08"
},
"signature_slot": 3641,
"next_sync_committee_update": null,
"finalized_header": {
"slot": 3624,
"proposer_index": 7,
"parent_root": "0x7690506882ac8c5f01d00f3ade06439259a3a0261ef5d61ec44920678b4104e6",
"state_root": "0x3726ebb8d9973977a71a8389caf5fc5830eeb8cd4fdfbbc7b0c4e6ca3e6a4090",
"body_root": "0x0f9a3f0fa5a4ffaf7c10504c86f23e7d554366ffd069fe958a160b253c3fd409"
},
"finality_branch": [
"0xc501000000000000000000000000000000000000000000000000000000000000",
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
"0x83c3d5360d254f4a44be712c1f433e88e810b6d1e0e789e90bada9e36126b857",
"0x97245fa01a89a6d7b4542cd731fef699f58b2bbaabdd6f641334c9e9eeae3a20",
"0xc3d19c773f66ab94bc2106d5e75a3205398dd6e94b6f8a5716f347741eb9fc5a",
"0x9e5040e56d765c1add56779a716be7497be27cba37f866cd8d34418d55e48715"
],
"block_roots_root": "0x29a54625749fa25f9e36df14a3baa335c58246bba2f8c7eb8b1ec2e4908e2fd0",
"block_roots_branch": [
"0x53616f9298818a8423c98adc47c92aaf82f0c5c911dc4ee5f88ba6d3022341c1",
"0x5d2f1c4bce6f63f26cbe3fbf480281c04a6b14bea74350a88ee945354ecbd79d",
"0x8333eefc7eaa4d10091e2014b3aae2bf6bd2d10c22c67100e189f8ab6caab261",
"0x3edfa69130bc193dec47c27a5903f03d5262b75899b69c0e95ac1816a664a3e7",
"0x5e046000f85aede8d4c28140b27778488d4ad21b1e16e345055d07ee53f2711b"
]
}
@@ -0,0 +1,62 @@
{
"header": {
"slot": 3616,
"proposer_index": 7,
"parent_root": "0x6c5e8c7b32b7bfbb250fa8fd7bc348d7325fb2bfc869e4c506af6802fcad87f4",
"state_root": "0x3e467e3429a1ae36572fe3fe1c953381242e950254cf97c7527a8cea8aa6c9de",
"body_root": "0x7da749680d2b0b4f779047fcfe7d0c13d247f6d23478817fe9c6fbe07993adb2"
},
"current_sync_committee": {
"pubkeys": [
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"
],
"aggregate_pubkey": "0x8fe11476a05750c52618deb79918e2e674f56dfbf12dbce55ae4386d108e8a1e83c6326f5957e2ef19137582ce270dc6"
},
"current_sync_committee_branch": [
"0x46af3f54acbea439b63aa5bb699c8f25ff584b23912366788f7c8e95011ce324",
"0x41dcb71ec3b3940399118d28e09fdc58a8e33b818b8c5cbb933c59929504ca08",
"0xfa53febb29348e3493a50c0e7c6d35796bf69c54dfc6f42f7600612789d0ed6d",
"0x5e7ea1693066b604fc60d4657b43e7a4aafd3f4f54d9a740d2abe765e92d8385",
"0x16c9bca64a82e80c23817bfec345d088e0adc3865e392965c1244f97979f816a"
],
"validators_root": "0x270d43e74ce340de4bca2b1936beca0f4f5408d9e78aec4850920baf659d5b69",
"block_roots_root": "0x00f6bbdeac1e1a922a9bf0e78720c0bffe558d8195e8ede8cb72bbd295f242f2",
"block_roots_branch": [
"0x7a61086fb9e53ab4dd87243d6288c51793696168a73773277630da5b20bf6091",
"0x60733905cdc5dd65d05161bb3138eecc47d6d6057ab36b0d36cf5a3200484143",
"0x86d7de634ae45de5b3cbbc562dd976de7d06a3d96f83147413536e6b108c7a39",
"0x0ada571c9e0da6fce8dd13e6d9ce173768521ac32e0af456634556176789fa6e",
"0x2341538fd0aafbc1ff0f513545e5dcd4b8905dc9e00d6173480c18a4e8086ebc"
]
}
@@ -0,0 +1,38 @@
{
"attested_header": {
"slot": 3696,
"proposer_index": 1,
"parent_root": "0x04a63c5dfb726c31a32a72c1c426ff89e21363223d7096486b629f1d58abe5d8",
"state_root": "0xbe20e69420cbf9400224ec5edeb0843776a2ccf945e9a3ba9311ae812cad1e30",
"body_root": "0x1d2acd1748f1c58096d1edc8badd3a1d7e1dc3c33bcb9229e4c03f3a84efeadb"
},
"sync_aggregate": {
"sync_committee_bits": "0xffffffff",
"sync_committee_signature": "0xafa79bc0f3c731ab1eb6aeafc582a7dd1c100ea471df3af6ff485b58661b3ef8077264dea0b60df9aec2d3ca8ddab6770fc9d061462e5a6dc718146085425f863d00921c42413805cb5b4c5175f36f2087cfed740bb7d57e8d5b48352643cd5b"
},
"signature_slot": 3697,
"next_sync_committee_update": null,
"finalized_header": {
"slot": 3680,
"proposer_index": 7,
"parent_root": "0x4d8f4fc47ad3eb045bd20cae13af6df02f96a3f8d7c8a285190ba10cfe2b84cf",
"state_root": "0xd498766d77277fe16a6a4609ab3ac3a6e9887d162d8dfffdfc9cc4ae833e4127",
"body_root": "0x9ba73bc9a4907cac0b887550e2b01a63dcc70473753ffcc243d33394cc64b4c0"
},
"finality_branch": [
"0xcc01000000000000000000000000000000000000000000000000000000000000",
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
"0x142061c4bc3673bf774cb8c7b6085057bd0ca85672b43afa2d9581b0b6a44e54",
"0x48b8cd8ca9d9563e30c1cca2a854cd7f75eb4cb013d10809b3138a72d94ea0c5",
"0x9b39523d05013ac7cbb9f43e5d6f9dc033b12aa1d6d6edd994ddc4f5efe7be9d",
"0x066c9aa26107bc8cb28bc73e518da6cc865ec1d67516b6ca24663b6b7ae3cb21"
],
"block_roots_root": "0xb15aa2483811d8c5616cb93710f4fcb809d97443caac9de163f943a30f385db6",
"block_roots_branch": [
"0xf7a43ad317417daa4c2a1e93c54895895a824ef1e43320eb44eab16673da5a61",
"0xe4b8d640660f765c2ef4dc886025dc8e54c6e70b66192582f42837ed5e9d8d41",
"0x841f113dc81e76419b6cdec8b0cf2fc20f9381492ed3c79e9b49179b4d3eacbc",
"0xeb5fdc4d8b5282b653ecbc9caa93bcfe482f6d6a32cbb0d9eb011bef947579bb",
"0x1f328cc5640efb191ae6aa86223b1aa9d083b26ac3e1fa3c071327bb09dc5727"
]
}
@@ -0,0 +1,83 @@
{
"attested_header": {
"slot": 3664,
"proposer_index": 4,
"parent_root": "0x15ac23a0c16bfa81e8595621118040c3e6cbddd4b09bae6fb39ba5fefd0258e8",
"state_root": "0x6fb81aa3827e7d580bb05b4df2686c9a49508bde2f8342fd75be609a23dd8362",
"body_root": "0x9906a1ae8065d268f8acb7f1b3119408d2f7f8e6e0764370c16ea3d15134981f"
},
"sync_aggregate": {
"sync_committee_bits": "0xffffffff",
"sync_committee_signature": "0xa9b5584ec9290a4ac6c5616639d031f9ab1064d63b4889f1da52f6f4d66b645fca48bbe2fe8484adb0c05c647edd694d0340cf684b8ccf8e34c6d8cf447cfcfdcb856f5abdcfd85ada5a4a04d4c8f6f40c6e99308893c3941485a436d6c8e5f7"
},
"signature_slot": 3665,
"next_sync_committee_update": {
"next_sync_committee": {
"pubkeys": [
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373"
],
"aggregate_pubkey": "0x8fe11476a05750c52618deb79918e2e674f56dfbf12dbce55ae4386d108e8a1e83c6326f5957e2ef19137582ce270dc6"
},
"next_sync_committee_branch": [
"0x46af3f54acbea439b63aa5bb699c8f25ff584b23912366788f7c8e95011ce324",
"0x5b118fe110ee4a1b0cf9823bc189fb38eb55a7b49adbdafcf466ec7cd4b7fd68",
"0xc2f12fb91a61abedb47f62a98258960edca21f31494cdf59b47a1c721e3e98f8",
"0x16fdfd5e6b591b3140a76efa4593a9c4d105b9e5c62d6f44edbd24790657be50",
"0xc8175ab66690cc94c0a24452754addd62a06948de5db9814e813437a130de452"
]
},
"finalized_header": {
"slot": 3648,
"proposer_index": 1,
"parent_root": "0x991ee98a70e8f90bdd61d0f5554e53d37473e75e16af171f6d88f27d20223dae",
"state_root": "0x59b04d660ac772005a13a7dc1d5f99bb0d0292f3c422f04f7365198d70dd30de",
"body_root": "0x5151f035e146258e7327ad9cf1df13f8ddec7a7842c19993cf739358717b5565"
},
"finality_branch": [
"0xc801000000000000000000000000000000000000000000000000000000000000",
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
"0x142061c4bc3673bf774cb8c7b6085057bd0ca85672b43afa2d9581b0b6a44e54",
"0xc2f12fb91a61abedb47f62a98258960edca21f31494cdf59b47a1c721e3e98f8",
"0x16fdfd5e6b591b3140a76efa4593a9c4d105b9e5c62d6f44edbd24790657be50",
"0xc8175ab66690cc94c0a24452754addd62a06948de5db9814e813437a130de452"
],
"block_roots_root": "0xe6e2adaaad45363d7112945ef670e21c66bcb3276dc450962ade1e8950230380",
"block_roots_branch": [
"0x386ede102258966d4c23031c5a02de2af8180d475c4c1716b07fb5b9f142a817",
"0x35e6c89bc38d993a1957f8a9fb1fbeab7420688091ba2cd7ee7b19b7e187f7d6",
"0x99249309825cafef7e694c09c4fdf95eb4b1e8743d3b23f6959d9980ad2d69b0",
"0x5e028d1d905db6430f0ce4aafbc78f442047ec3a132b4e69557fdf804a4cfbf3",
"0xd34afeab37851937920243683a1c926c41c626aacb145718fce755782d4996dd"
]
}
@@ -0,0 +1,83 @@
{
"attested_header": {
"slot": 3600,
"proposer_index": 7,
"parent_root": "0xdf60c2d58beccd89678b9267c689e9ba1cf1d58ce5114ad5c16e8341459cfd75",
"state_root": "0x023f14c7a38ef4d6ec19b522edfb427c6b70c6ffbd8610ca802dd1491c92c852",
"body_root": "0x0f78a1c45e42711efc5fb7b7f6238be1bee9273f7c44ff6892d815858bb77e25"
},
"sync_aggregate": {
"sync_committee_bits": "0xffffffff",
"sync_committee_signature": "0xa4dd8f0991de88ca6f81476f72f48cdb67b9414ad7bf6bba37f627c5ec84dd2c2ebc12cddd5d2e7c927276cee2d3d144158b4c067db3e9911fe52fe1875b14c93f90e4eb57bf5e8f0e6e6effe22f9ba076f30207e0ec683354961ae8e9779556"
},
"signature_slot": 3601,
"next_sync_committee_update": {
"next_sync_committee": {
"pubkeys": [
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34",
"0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac",
"0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b",
"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e",
"0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e",
"0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373",
"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"
],
"aggregate_pubkey": "0x8fe11476a05750c52618deb79918e2e674f56dfbf12dbce55ae4386d108e8a1e83c6326f5957e2ef19137582ce270dc6"
},
"next_sync_committee_branch": [
"0x1446606d0129c324a4ea374bd29a625175e0659512cd8650097e0a9c38ce6379",
"0xd92466c7e9a53b7b55f4fdb151746a3058931d7559b7e84e7b15384ddc903ca0",
"0x9fd10c3f68b75cfd3ebd2af0d4e2cbbfbe120e0b5423dde89ff0f743c7a4f937",
"0x1ed6aac0ab29a883de2bb2e3579ad4d6807ddcf3db8afcaf0ae25a076ac9a5f4",
"0xf17a840df410a15f0e4e48abf521c29ad0d296d3fb4e8b847ea37f2cc8236f1f"
]
},
"finalized_header": {
"slot": 3584,
"proposer_index": 1,
"parent_root": "0x91c285af2ec25d485310391afe667108b787ec570cdbb0e3fd87b1e0e2c47bd7",
"state_root": "0xccc4baf90024e035f1252520d2f2ef1e50f840ff0ecc8e6e365721e083871a32",
"body_root": "0x91df5e0077434aad609aaa7e030005cee77cca83868ffc2724e5befe9a3f6a02"
},
"finality_branch": [
"0xc001000000000000000000000000000000000000000000000000000000000000",
"0x10c726fac935bf9657cc7476d3cfa7bedec5983dcfb59e8a7df6d0a619e108d7",
"0x83c3d5360d254f4a44be712c1f433e88e810b6d1e0e789e90bada9e36126b857",
"0x9fd10c3f68b75cfd3ebd2af0d4e2cbbfbe120e0b5423dde89ff0f743c7a4f937",
"0x1ed6aac0ab29a883de2bb2e3579ad4d6807ddcf3db8afcaf0ae25a076ac9a5f4",
"0xf17a840df410a15f0e4e48abf521c29ad0d296d3fb4e8b847ea37f2cc8236f1f"
],
"block_roots_root": "0x9eab8a05c396a29c32f4f8ac9654fc0fb7cd97ec659236392ede48951a794505",
"block_roots_branch": [
"0x5c175efdbafacdfdab21c93a318b0e8e2291a5a86c40b1fc564f91ad33c106d4",
"0x5c1e0b76176ab033858b2835f90d5e25d708b563f77efd7d9938f0faa1c20878",
"0x7aea32464adee801e2a05c3af227f24231d3c088e3b7265a5fada9ac850549fe",
"0x9d9fca29e23c5d4ae433adf17e7fd9a0e4d1b09b68f5c45e7ca1b13ebe4a9e98",
"0x6b35238f188021c859d6b317457ebb6fe4cf362cab35c988010cb1343eabbfc5"
]
}
@@ -0,0 +1,93 @@
[package]
name = "snowbridge-inbound-queue"
description = "Snowbridge Inbound 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", optional = true }
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 }
log = { version = "0.4.20", default-features = false }
alloy-primitives = { version = "0.4.2", default-features = false, features = ["rlp"] }
alloy-sol-types = { version = "0.4.2", default-features = false }
alloy-rlp = { version = "0.3.3", default-features = false, features = ["derive"] }
num-traits = { version = "0.2.16", default-features = false }
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 }
pallet-balances = { path = "../../../../../substrate/frame/balances", default-features = false }
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
snowbridge-core = { path = "../../primitives/core", default-features = false }
snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false }
snowbridge-router-primitives = { path = "../../primitives/router", default-features = false }
snowbridge-beacon-primitives = { path = "../../primitives/beacon", default-features = false, optional = true }
[dev-dependencies]
frame-benchmarking = { path = "../../../../../substrate/frame/benchmarking" }
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
snowbridge-beacon-primitives = { path = "../../primitives/beacon" }
snowbridge-ethereum-beacon-client = { path = "../../pallets/ethereum-beacon-client" }
hex-literal = { version = "0.4.1" }
[features]
default = ["std"]
std = [
"alloy-primitives/std",
"alloy-rlp/std",
"alloy-sol-types/std",
"codec/std",
"frame-benchmarking/std",
"frame-support/std",
"frame-system/std",
"log/std",
"num-traits/std",
"pallet-balances/std",
"scale-info/std",
"serde",
"snowbridge-core/std",
"snowbridge-ethereum/std",
"snowbridge-router-primitives/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"xcm-builder/std",
"xcm/std",
]
runtime-benchmarks = [
"frame-benchmarking",
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"hex-literal",
"pallet-balances/runtime-benchmarks",
"snowbridge-beacon-primitives",
"snowbridge-core/runtime-benchmarks",
"snowbridge-ethereum-beacon-client/runtime-benchmarks",
"snowbridge-router-primitives/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/try-runtime",
"snowbridge-ethereum-beacon-client/try-runtime",
"sp-runtime/try-runtime",
]
@@ -0,0 +1,40 @@
use hex_literal::hex;
use snowbridge_beacon_primitives::CompactExecutionHeader;
use snowbridge_core::inbound::{Log, Message, Proof};
use sp_std::vec;
pub struct InboundQueueTest {
pub execution_header: CompactExecutionHeader,
pub message: Message,
}
pub fn make_create_message() -> InboundQueueTest {
InboundQueueTest{
execution_header: CompactExecutionHeader{
parent_hash: hex!("b5608f0af7c3b6fe5c593772fc25436b8d6549eb236adb0855c6ad33e0004e04").into(),
block_number: 115,
state_root: hex!("47ed174789836c622499d9659a4ac32c3b91a7b15642d39b0a11b82ff23995c1").into(),
receipts_root: hex!("42c08b5303fcdf9e49c833fe5f1182cdbc8206bf8aec581125fc34aba11e1f1a").into(),
},
message: Message {
event_log: Log {
address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(),
topics: vec![
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(),
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
],
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(),
},
proof: Proof {
block_hash: hex!("add15f439c8a57fe375d0a679870b1359921d70cb0e3e44f0dd3e272849f4097").into(),
tx_index: 0,
data: (vec![
hex!("42c08b5303fcdf9e49c833fe5f1182cdbc8206bf8aec581125fc34aba11e1f1a").to_vec(),
], vec![
hex!("f9028e822080b9028802f90284018301ed20b9010000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000080000000000000000000000000000004000000000080000000000000000000000000000000000010100000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000040004000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000200000000000010f90179f85894eda338e4dc46038493b885327842fd3e301cab39e1a0f78bb28d4b1d7da699e5c0bc2be29c2b04b5aab6aacf6298fe5304f9db9c6d7ea000000000000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7df9011c94eda338e4dc46038493b885327842fd3e301cab39f863a07153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84fa0c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539a05f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0b8a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").to_vec(),
]),
},
},
}
}
@@ -0,0 +1,55 @@
mod fixtures;
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use crate::Pallet as InboundQueue;
use frame_benchmarking::v2::*;
use frame_support::assert_ok;
use frame_system::RawOrigin;
#[benchmarks]
mod benchmarks {
use super::*;
use crate::benchmarking::fixtures::make_create_message;
#[benchmark]
fn submit() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();
let create_message = make_create_message();
T::Helper::initialize_storage(
create_message.message.proof.block_hash,
create_message.execution_header,
);
let sovereign_account = sibling_sovereign_account::<T>(1000u32.into());
let minimum_balance = T::Token::minimum_balance();
// So that the receiving account exists
assert_ok!(T::Token::mint_into(&caller, minimum_balance));
// Fund the sovereign account (parachain sovereign account) so it can transfer a reward
// fee to the caller account
assert_ok!(T::Token::mint_into(
&sovereign_account,
3_000_000_000_000u128
.try_into()
.unwrap_or_else(|_| panic!("unable to cast sovereign account balance")),
));
#[block]
{
assert_ok!(InboundQueue::<T>::submit(
RawOrigin::Signed(caller.clone()).into(),
create_message.message,
));
}
Ok(())
}
impl_benchmark_test_suite!(InboundQueue, crate::mock::new_tester(), crate::mock::Test);
}
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use snowbridge_core::{inbound::Log, ChannelId};
use sp_core::{RuntimeDebug, H160, H256};
use sp_std::{convert::TryFrom, prelude::*};
use alloy_primitives::B256;
use alloy_sol_types::{sol, SolEvent};
sol! {
event OutboundMessageAccepted(bytes32 indexed channel_id, uint64 nonce, bytes32 indexed message_id, bytes payload);
}
/// An inbound message that has had its outer envelope decoded.
#[derive(Clone, RuntimeDebug)]
pub struct Envelope {
/// The address of the outbound queue on Ethereum that emitted this message as an event log
pub gateway: H160,
/// The message Channel
pub channel_id: ChannelId,
/// A nonce for enforcing replay protection and ordering.
pub nonce: u64,
/// An id for tracing the message on its route (has no role in bridge consensus)
pub message_id: H256,
/// The inner payload generated from the source application.
pub payload: Vec<u8>,
}
#[derive(Copy, Clone, RuntimeDebug)]
pub struct EnvelopeDecodeError;
impl TryFrom<&Log> for Envelope {
type Error = EnvelopeDecodeError;
fn try_from(log: &Log) -> Result<Self, Self::Error> {
let topics: Vec<B256> = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect();
let event = OutboundMessageAccepted::decode_log(topics, &log.data, true)
.map_err(|_| EnvelopeDecodeError)?;
Ok(Self {
gateway: log.address,
channel_id: ChannelId::from(event.channel_id.as_ref()),
nonce: event.nonce,
message_id: H256::from(event.message_id.as_ref()),
payload: event.payload,
})
}
}
@@ -0,0 +1,342 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Inbound Queue
//!
//! # Overview
//!
//! Receives messages emitted by the Gateway contract on Ethereum, whereupon they are verified,
//! translated to XCM, and finally sent to their final destination parachain.
//!
//! The message relayers are rewarded using native currency from the sovereign account of the
//! destination parachain.
//!
//! # Extrinsics
//!
//! ## Governance
//!
//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
//! processing of inbound messages.
//!
//! ## Message Submission
//!
//! * [`Call::submit`]: Submit a message for verification and dispatch the final destination
//! parachain.
#![cfg_attr(not(feature = "std"), no_std)]
mod envelope;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(feature = "runtime-benchmarks")]
use snowbridge_beacon_primitives::CompactExecutionHeader;
pub mod weights;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod test;
use codec::{Decode, DecodeAll, Encode};
use envelope::Envelope;
use frame_support::{
traits::{
fungible::{Inspect, Mutate},
tokens::{Fortitude, Precision, Preservation},
},
weights::WeightToFee,
PalletError,
};
use frame_system::ensure_signed;
use scale_info::TypeInfo;
use sp_core::{H160, H256};
use sp_std::{convert::TryFrom, vec};
use xcm::prelude::{
send_xcm, Instruction::SetTopic, Junction::*, Junctions::*, MultiLocation,
SendError as XcmpSendError, SendXcm, Xcm, XcmHash,
};
use snowbridge_core::{
inbound::{Message, VerificationError, Verifier},
sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, StaticLookup,
};
use snowbridge_router_primitives::{
inbound,
inbound::{ConvertMessage, ConvertMessageError},
};
use sp_runtime::traits::Saturating;
pub use weights::WeightInfo;
type BalanceOf<T> =
<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
pub use pallet::*;
pub const LOG_TARGET: &str = "snowbridge-inbound-queue";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use snowbridge_core::PricingParameters;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<T> {
fn initialize_storage(block_hash: H256, header: CompactExecutionHeader);
}
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The verifier for inbound messages from Ethereum
type Verifier: Verifier;
/// Message relayers are rewarded with this asset
type Token: Mutate<Self::AccountId> + Inspect<Self::AccountId>;
/// XCM message sender
type XcmSender: SendXcm;
// Address of the Gateway contract
#[pallet::constant]
type GatewayAddress: Get<H160>;
/// Convert inbound message to XCM
type MessageConverter: ConvertMessage<
AccountId = Self::AccountId,
Balance = BalanceOf<Self>,
>;
/// Lookup a channel descriptor
type ChannelLookup: StaticLookup<Source = ChannelId, Target = Channel>;
/// Lookup pricing parameters
type PricingParameters: Get<PricingParameters<BalanceOf<Self>>>;
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type Helper: BenchmarkHelper<Self>;
/// Convert a weight value into deductible balance type.
type WeightToFee: WeightToFee<Balance = BalanceOf<Self>>;
/// Convert a length value into deductible balance type
type LengthToFee: WeightToFee<Balance = BalanceOf<Self>>;
/// The upper limit here only used to estimate delivery cost
type MaxMessageSize: Get<u32>;
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T> {
/// A message was received from Ethereum
MessageReceived {
/// The message channel
channel_id: ChannelId,
/// The message nonce
nonce: u64,
/// ID of the XCM message which was forwarded to the final destination parachain
message_id: [u8; 32],
},
/// Set OperatingMode
OperatingModeChanged { mode: BasicOperatingMode },
}
#[pallet::error]
pub enum Error<T> {
/// Message came from an invalid outbound channel on the Ethereum side.
InvalidGateway,
/// Message has an invalid envelope.
InvalidEnvelope,
/// Message has an unexpected nonce.
InvalidNonce,
/// Message has an invalid payload.
InvalidPayload,
/// Message channel is invalid
InvalidChannel,
/// The max nonce for the type has been reached
MaxNonceReached,
/// Cannot convert location
InvalidAccountConversion,
/// Pallet is halted
Halted,
/// Message verification error,
Verification(VerificationError),
/// XCMP send failure
Send(SendError),
/// Message conversion error
ConvertMessage(ConvertMessageError),
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo, PalletError)]
pub enum SendError {
NotApplicable,
NotRoutable,
Transport,
DestinationUnsupported,
ExceedsMaxMessageSize,
MissingArgument,
Fees,
}
impl<T: Config> From<XcmpSendError> for Error<T> {
fn from(e: XcmpSendError) -> Self {
match e {
XcmpSendError::NotApplicable => Error::<T>::Send(SendError::NotApplicable),
XcmpSendError::Unroutable => Error::<T>::Send(SendError::NotRoutable),
XcmpSendError::Transport(_) => Error::<T>::Send(SendError::Transport),
XcmpSendError::DestinationUnsupported =>
Error::<T>::Send(SendError::DestinationUnsupported),
XcmpSendError::ExceedsMaxMessageSize =>
Error::<T>::Send(SendError::ExceedsMaxMessageSize),
XcmpSendError::MissingArgument => Error::<T>::Send(SendError::MissingArgument),
XcmpSendError::Fees => Error::<T>::Send(SendError::Fees),
}
}
}
/// The current nonce for each channel
#[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::call]
impl<T: Config> Pallet<T> {
/// Submit an inbound message originating from the Gateway contract on Ethereum
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::submit())]
pub fn submit(origin: OriginFor<T>, message: Message) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
// submit message to verifier for verification
T::Verifier::verify(&message.event_log, &message.proof)
.map_err(|e| Error::<T>::Verification(e))?;
// Decode event log into an Envelope
let envelope =
Envelope::try_from(&message.event_log).map_err(|_| Error::<T>::InvalidEnvelope)?;
// Verify that the message was submitted from the known Gateway contract
ensure!(T::GatewayAddress::get() == envelope.gateway, Error::<T>::InvalidGateway);
// Retrieve the registered channel for this message
let channel =
T::ChannelLookup::lookup(envelope.channel_id).ok_or(Error::<T>::InvalidChannel)?;
// Verify message nonce
<Nonce<T>>::try_mutate(envelope.channel_id, |nonce| -> DispatchResult {
if *nonce == u64::MAX {
return Err(Error::<T>::MaxNonceReached.into())
}
if envelope.nonce != nonce.saturating_add(1) {
Err(Error::<T>::InvalidNonce.into())
} else {
*nonce = nonce.saturating_add(1);
Ok(())
}
})?;
// Reward relayer from the sovereign account of the destination parachain
// Expected to fail if sovereign account has no funds
let sovereign_account = sibling_sovereign_account::<T>(channel.para_id);
let delivery_cost = Self::calculate_delivery_cost(message.encode().len() as u32);
T::Token::transfer(&sovereign_account, &who, delivery_cost, Preservation::Preserve)?;
// Decode message into XCM
let (xcm, fee) =
match inbound::VersionedMessage::decode_all(&mut envelope.payload.as_ref()) {
Ok(message) => Self::do_convert(envelope.message_id, message)?,
Err(_) => return Err(Error::<T>::InvalidPayload.into()),
};
// We embed fees for xcm execution inside the xcm program using teleports
// so we must burn the amount of the fee embedded into the XCM script.
T::Token::burn_from(&sovereign_account, fee, Precision::Exact, Fortitude::Polite)?;
log::info!(
target: LOG_TARGET,
"💫 xcm {:?} sent with fee {:?}",
xcm,
fee
);
// Attempt to send XCM to a dest parachain
let message_id = Self::send_xcm(xcm, channel.para_id)?;
Self::deposit_event(Event::MessageReceived {
channel_id: envelope.channel_id,
nonce: envelope.nonce,
message_id,
});
Ok(())
}
/// Halt or resume all pallet operations. May only be called by root.
#[pallet::call_index(1)]
#[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>::set(mode);
Self::deposit_event(Event::OperatingModeChanged { mode });
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn do_convert(
message_id: H256,
message: inbound::VersionedMessage,
) -> Result<(Xcm<()>, BalanceOf<T>), Error<T>> {
let (mut xcm, fee) =
T::MessageConverter::convert(message).map_err(|e| Error::<T>::ConvertMessage(e))?;
// Append the message id as an XCM topic
xcm.inner_mut().extend(vec![SetTopic(message_id.into())]);
Ok((xcm, fee))
}
pub fn send_xcm(xcm: Xcm<()>, dest: ParaId) -> Result<XcmHash, Error<T>> {
let dest = MultiLocation { parents: 1, interior: X1(Parachain(dest.into())) };
let (xcm_hash, _) = send_xcm::<T::XcmSender>(dest, xcm).map_err(Error::<T>::from)?;
Ok(xcm_hash)
}
pub fn calculate_delivery_cost(length: u32) -> BalanceOf<T> {
let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit());
let len_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
weight_fee
.saturating_add(len_fee)
.saturating_add(T::PricingParameters::get().rewards.local)
}
}
/// API for accessing the delivery cost of a message
impl<T: Config> Get<BalanceOf<T>> for Pallet<T> {
fn get() -> BalanceOf<T> {
// Cost here based on MaxMessagePayloadSize(the worst case)
Self::calculate_delivery_cost(T::MaxMessageSize::get())
}
}
}
@@ -0,0 +1,311 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use frame_support::{
parameter_types,
traits::{ConstU128, ConstU32, Everything},
weights::IdentityFee,
};
use hex_literal::hex;
use snowbridge_beacon_primitives::{Fork, ForkVersions};
use snowbridge_core::{
gwei,
inbound::{Log, Proof, VerificationError},
meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup,
};
use snowbridge_router_primitives::inbound::MessageToXcm;
use sp_core::{H160, H256};
use sp_runtime::{
traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify},
BuildStorage, FixedU128, MultiSignature,
};
use sp_std::convert::From;
use xcm::v3::{prelude::*, MultiAssets, SendXcm};
use crate::{self as inbound_queue};
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system::{Pallet, Call, Storage, Event<T>},
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
EthereumBeaconClient: snowbridge_ethereum_beacon_client::{Pallet, Call, Storage, Event<T>},
InboundQueue: inbound_queue::{Pallet, Call, Storage, Event<T>},
}
);
pub type Signature = MultiSignature;
pub type AccountId = <<Signature as Verify>::Signer as IdentifyAccount>::AccountId;
parameter_types! {
pub const BlockHashCount: u64 = 250;
}
type Balance = u128;
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 = pallet_balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type Block = Block;
}
impl pallet_balances::Config for Test {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU128<1>;
type AccountStore = System;
type WeightInfo = ();
type FreezeIdentifier = ();
type MaxFreezes = ();
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
type MaxHolds = ();
}
parameter_types! {
pub const ExecutionHeadersPruneThreshold: u32 = 10;
pub const ChainForkVersions: ForkVersions = ForkVersions{
genesis: Fork {
version: [0, 0, 0, 1], // 0x00000001
epoch: 0,
},
altair: Fork {
version: [1, 0, 0, 1], // 0x01000001
epoch: 0,
},
bellatrix: Fork {
version: [2, 0, 0, 1], // 0x02000001
epoch: 0,
},
capella: Fork {
version: [3, 0, 0, 1], // 0x03000001
epoch: 0,
},
};
}
impl snowbridge_ethereum_beacon_client::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ForkVersions = ChainForkVersions;
type MaxExecutionHeadersToKeep = ExecutionHeadersPruneThreshold;
type WeightInfo = ();
}
// Mock verifier
pub struct MockVerifier;
impl Verifier for MockVerifier {
fn verify(_: &Log, _: &Proof) -> Result<(), VerificationError> {
Ok(())
}
}
const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"];
parameter_types! {
pub const EthereumNetwork: xcm::v3::NetworkId = xcm::v3::NetworkId::Ethereum { chain_id: 11155111 };
pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS);
pub const CreateAssetCall: [u8;2] = [53, 0];
pub const CreateAssetExecutionFee: u128 = 2_000_000_000;
pub const CreateAssetDeposit: u128 = 100_000_000_000;
pub const SendTokenExecutionFee: u128 = 1_000_000_000;
pub const InitialFund: u128 = 1_000_000_000_000;
pub const InboundQueuePalletInstance: u8 = 80;
}
#[cfg(feature = "runtime-benchmarks")]
impl<T: snowbridge_ethereum_beacon_client::Config> BenchmarkHelper<T> for Test {
// not implemented since the MockVerifier is used for tests
fn initialize_storage(_: H256, _: CompactExecutionHeader) {}
}
// Mock XCM sender that always succeeds
pub struct MockXcmSender;
impl SendXcm for MockXcmSender {
type Ticket = Xcm<()>;
fn validate(
dest: &mut Option<MultiLocation>,
xcm: &mut Option<xcm::v3::Xcm<()>>,
) -> SendResult<Self::Ticket> {
match dest {
Some(MultiLocation { interior, .. }) => {
if let X1(Parachain(1001)) = interior {
return Err(XcmpSendError::NotApplicable)
}
Ok((xcm.clone().unwrap(), MultiAssets::default()))
},
_ => Ok((xcm.clone().unwrap(), MultiAssets::default())),
}
}
fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, XcmpSendError> {
let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
Ok(hash)
}
}
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;
pub struct MockChannelLookup;
impl StaticLookup for MockChannelLookup {
type Source = ChannelId;
type Target = Channel;
fn lookup(channel_id: Self::Source) -> Option<Self::Target> {
if channel_id !=
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into()
{
return None
}
Some(Channel { agent_id: H256::zero(), para_id: ASSET_HUB_PARAID.into() })
}
}
impl inbound_queue::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Verifier = MockVerifier;
type Token = Balances;
type XcmSender = MockXcmSender;
type WeightInfo = ();
type GatewayAddress = GatewayAddress;
type MessageConverter = MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
>;
type PricingParameters = Parameters;
type ChannelLookup = MockChannelLookup;
#[cfg(feature = "runtime-benchmarks")]
type Helper = Test;
type WeightToFee = IdentityFee<u128>;
type LengthToFee = IdentityFee<u128>;
type MaxMessageSize = ConstU32<1024>;
}
pub fn last_events(n: usize) -> Vec<RuntimeEvent> {
frame_system::Pallet::<Test>::events()
.into_iter()
.rev()
.take(n)
.rev()
.map(|e| e.event)
.collect()
}
pub fn expect_events(e: Vec<RuntimeEvent>) {
assert_eq!(last_events(e.len()), e);
}
pub fn setup() {
System::set_block_number(1);
Balances::mint_into(
&sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into()),
InitialFund::get(),
)
.unwrap();
Balances::mint_into(
&sibling_sovereign_account::<Test>(TEMPLATE_PARAID.into()),
InitialFund::get(),
)
.unwrap();
}
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
}
// Generated from smoketests:
// cd smoketests
// ./make-bindings
// cargo test --test register_token -- --nocapture
pub fn mock_event_log() -> Log {
Log {
// gateway address
address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(),
topics: vec![
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
// channel id
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(),
// message id
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
],
// Nonce + Payload
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(),
}
}
pub fn mock_event_log_invalid_channel() -> Log {
Log {
address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(),
topics: vec![
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
// invalid channel id
hex!("0000000000000000000000000000000000000000000000000000000000000000").into(),
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
],
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(),
}
}
pub fn mock_event_log_invalid_gateway() -> Log {
Log {
// gateway address
address: H160::zero(),
topics: vec![
hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(),
// channel id
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(),
// message id
hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(),
],
// Nonce + Payload
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(),
}
}
pub const ASSET_HUB_PARAID: u32 = 1000u32;
pub const TEMPLATE_PARAID: u32 = 1001u32;
@@ -0,0 +1,211 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use super::*;
use frame_support::{assert_noop, assert_ok};
use hex_literal::hex;
use snowbridge_core::{inbound::Proof, ChannelId};
use sp_keyring::AccountKeyring as Keyring;
use sp_runtime::{DispatchError, TokenError};
use sp_std::convert::From;
use crate::{Error, Event as InboundQueueEvent};
use crate::mock::*;
#[test]
fn test_submit_happy_path() {
new_tester().execute_with(|| {
let relayer: AccountId = Keyring::Bob.into();
let channel_sovereign = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
let origin = RuntimeOrigin::signed(relayer.clone());
// Submit message
let message = Message {
event_log: mock_event_log(),
proof: Proof {
block_hash: Default::default(),
tx_index: Default::default(),
data: Default::default(),
},
};
let initial_fund = InitialFund::get();
assert_eq!(Balances::balance(&relayer), 0);
assert_eq!(Balances::balance(&channel_sovereign), initial_fund);
assert_ok!(InboundQueue::submit(origin.clone(), message.clone()));
expect_events(vec![InboundQueueEvent::MessageReceived {
channel_id: hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539")
.into(),
nonce: 1,
message_id: [
27, 217, 88, 127, 46, 143, 199, 70, 236, 66, 212, 244, 85, 221, 153, 104, 175, 37,
224, 20, 140, 95, 140, 7, 27, 74, 182, 199, 77, 12, 194, 236,
],
}
.into()]);
let delivery_cost = InboundQueue::calculate_delivery_cost(message.encode().len() as u32);
assert!(
Parameters::get().rewards.local < delivery_cost,
"delivery cost exceeds pure reward"
);
assert_eq!(Balances::balance(&relayer), delivery_cost, "relayer was rewarded");
assert!(
Balances::balance(&channel_sovereign) <= initial_fund - delivery_cost,
"sovereign account paid reward"
);
});
}
#[test]
fn test_submit_xcm_invalid_channel() {
new_tester().execute_with(|| {
let relayer: AccountId = Keyring::Bob.into();
let origin = RuntimeOrigin::signed(relayer);
// Deposit funds into sovereign account of parachain 1001
let sovereign_account = sibling_sovereign_account::<Test>(TEMPLATE_PARAID.into());
println!("account: {}", sovereign_account);
let _ = Balances::mint_into(&sovereign_account, 10000);
// Submit message
let message = Message {
event_log: mock_event_log_invalid_channel(),
proof: Proof {
block_hash: Default::default(),
tx_index: Default::default(),
data: Default::default(),
},
};
assert_noop!(
InboundQueue::submit(origin.clone(), message.clone()),
Error::<Test>::InvalidChannel,
);
});
}
#[test]
fn test_submit_with_invalid_gateway() {
new_tester().execute_with(|| {
let relayer: AccountId = Keyring::Bob.into();
let origin = RuntimeOrigin::signed(relayer);
// Deposit funds into sovereign account of Asset Hub (Statemint)
let sovereign_account = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
let _ = Balances::mint_into(&sovereign_account, 10000);
// Submit message
let message = Message {
event_log: mock_event_log_invalid_gateway(),
proof: Proof {
block_hash: Default::default(),
tx_index: Default::default(),
data: Default::default(),
},
};
assert_noop!(
InboundQueue::submit(origin.clone(), message.clone()),
Error::<Test>::InvalidGateway
);
});
}
#[test]
fn test_submit_with_invalid_nonce() {
new_tester().execute_with(|| {
let relayer: AccountId = Keyring::Bob.into();
let origin = RuntimeOrigin::signed(relayer);
// Deposit funds into sovereign account of Asset Hub (Statemint)
let sovereign_account = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
let _ = Balances::mint_into(&sovereign_account, 10000);
// Submit message
let message = Message {
event_log: mock_event_log(),
proof: Proof {
block_hash: Default::default(),
tx_index: Default::default(),
data: Default::default(),
},
};
assert_ok!(InboundQueue::submit(origin.clone(), message.clone()));
let nonce: u64 = <Nonce<Test>>::get(ChannelId::from(hex!(
"c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539"
)));
assert_eq!(nonce, 1);
// Submit the same again
assert_noop!(
InboundQueue::submit(origin.clone(), message.clone()),
Error::<Test>::InvalidNonce
);
});
}
#[test]
fn test_submit_no_funds_to_reward_relayers() {
new_tester().execute_with(|| {
let relayer: AccountId = Keyring::Bob.into();
let origin = RuntimeOrigin::signed(relayer);
// Reset balance of sovereign_account to zero so to trigger the FundsUnavailable error
let sovereign_account = sibling_sovereign_account::<Test>(ASSET_HUB_PARAID.into());
Balances::set_balance(&sovereign_account, 0);
// Submit message
let message = Message {
event_log: mock_event_log(),
proof: Proof {
block_hash: Default::default(),
tx_index: Default::default(),
data: Default::default(),
},
};
assert_noop!(
InboundQueue::submit(origin.clone(), message.clone()),
TokenError::FundsUnavailable
);
});
}
#[test]
fn test_set_operating_mode() {
new_tester().execute_with(|| {
let relayer: AccountId = Keyring::Bob.into();
let origin = RuntimeOrigin::signed(relayer);
let message = Message {
event_log: mock_event_log(),
proof: Proof {
block_hash: Default::default(),
tx_index: Default::default(),
data: Default::default(),
},
};
assert_ok!(InboundQueue::set_operating_mode(
RuntimeOrigin::root(),
snowbridge_core::BasicOperatingMode::Halted
));
assert_noop!(InboundQueue::submit(origin, message), Error::<Test>::Halted);
});
}
#[test]
fn test_set_operating_mode_root_only() {
new_tester().execute_with(|| {
assert_noop!(
InboundQueue::set_operating_mode(
RuntimeOrigin::signed(Keyring::Bob.into()),
snowbridge_core::BasicOperatingMode::Halted
),
DispatchError::BadOrigin
);
});
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Autogenerated weights for `snowbridge_inbound_queue`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-07-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `macbook pro 14 m2`, CPU: `m2-arm64`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-rococo-dev"), DB CACHE: 1024
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use sp_std::marker::PhantomData;
/// Weight functions needed for ethereum_beacon_client.
pub trait WeightInfo {
fn submit() -> Weight;
}
// For backwards compatibility and tests
impl WeightInfo for () {
fn submit() -> Weight {
Weight::from_parts(70_000_000, 0)
.saturating_add(Weight::from_parts(0, 3601))
.saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(2))
}
}
@@ -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))
}
}
@@ -0,0 +1,83 @@
[package]
name = "snowbridge-system"
description = "Snowbridge System"
version = "0.1.1"
authors = ["Snowfork <contact@snowfork.com>"]
edition = "2021"
repository = "https://github.com/Snowfork/snowbridge"
license = "Apache-2.0"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
"derive",
] }
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
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 }
log = { version = "0.4.20", default-features = false }
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
snowbridge-core = { path = "../../primitives/core", default-features = false }
[dev-dependencies]
hex = "0.4.1"
hex-literal = { version = "0.4.1" }
pallet-balances = { path = "../../../../../substrate/frame/balances" }
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
polkadot-primitives = { path = "../../../../../polkadot/primitives" }
pallet-message-queue = { path = "../../../../../substrate/frame/message-queue" }
snowbridge-outbound-queue = { path = "../outbound-queue" }
[features]
default = ["std"]
std = [
"codec/std",
"ethabi/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"log/std",
"scale-info/std",
"snowbridge-core/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-message-queue/runtime-benchmarks",
"polkadot-primitives/runtime-benchmarks",
"snowbridge-core/runtime-benchmarks",
"snowbridge-outbound-queue/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/try-runtime",
"pallet-message-queue/try-runtime",
"snowbridge-outbound-queue/try-runtime",
"sp-runtime/try-runtime",
]
@@ -0,0 +1 @@
License: MIT-0
@@ -0,0 +1,32 @@
[package]
name = "snowbridge-system-runtime-api"
description = "Snowbridge System 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 = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
"derive",
] }
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 }
xcm = { package = "staging-xcm", path = "../../../../../../polkadot/xcm", default-features = false }
snowbridge-core = { path = "../../../primitives/core", default-features = false }
[features]
default = ["std"]
std = [
"codec/std",
"snowbridge-core/std",
"sp-api/std",
"sp-core/std",
"sp-std/std",
"xcm/std",
]
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
#![cfg_attr(not(feature = "std"), no_std)]
use snowbridge_core::AgentId;
use xcm::VersionedMultiLocation;
sp_api::decl_runtime_apis! {
pub trait ControlApi
{
fn agent_id(location: VersionedMultiLocation) -> Option<AgentId>;
}
}
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Helpers for implementing runtime api
use snowbridge_core::AgentId;
use xcm::{prelude::*, VersionedMultiLocation};
use crate::{agent_id_of, Config};
pub fn agent_id<Runtime>(location: VersionedMultiLocation) -> Option<AgentId>
where
Runtime: Config,
{
let location: MultiLocation = location.try_into().ok()?;
agent_id_of::<Runtime>(&location).ok()
}
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Benchmarking setup for pallet-template
use super::*;
#[allow(unused)]
use crate::Pallet as SnowbridgeControl;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use snowbridge_core::{eth, outbound::OperatingMode};
use sp_runtime::SaturatedConversion;
use xcm::prelude::*;
#[allow(clippy::result_large_err)]
fn fund_sovereign_account<T: Config>(para_id: ParaId) -> Result<(), BenchmarkError> {
let amount: BalanceOf<T> = (10_000_000_000_000_u64).saturated_into::<u128>().saturated_into();
let sovereign_account = sibling_sovereign_account::<T>(para_id);
T::Token::mint_into(&sovereign_account, amount)?;
Ok(())
}
#[benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn upgrade() -> Result<(), BenchmarkError> {
let impl_address = H160::repeat_byte(1);
let impl_code_hash = H256::repeat_byte(1);
// Assume 256 bytes passed to initializer
let params: Vec<u8> = (0..256).map(|_| 1u8).collect();
#[extrinsic_call]
_(
RawOrigin::Root,
impl_address,
impl_code_hash,
Some(Initializer { params, maximum_required_gas: 100000 }),
);
Ok(())
}
#[benchmark]
fn set_operating_mode() -> Result<(), BenchmarkError> {
#[extrinsic_call]
_(RawOrigin::Root, OperatingMode::RejectingOutboundMessages);
Ok(())
}
#[benchmark]
fn set_pricing_parameters() -> Result<(), BenchmarkError> {
let params = T::DefaultPricingParameters::get();
#[extrinsic_call]
_(RawOrigin::Root, params);
Ok(())
}
#[benchmark]
fn create_agent() -> Result<(), BenchmarkError> {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let origin = T::Helper::make_xcm_origin(origin_location);
fund_sovereign_account::<T>(origin_para_id.into())?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin);
Ok(())
}
#[benchmark]
fn create_channel() -> Result<(), BenchmarkError> {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let origin = T::Helper::make_xcm_origin(origin_location);
fund_sovereign_account::<T>(origin_para_id.into())?;
SnowbridgeControl::<T>::create_agent(origin.clone())?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, OperatingMode::Normal);
Ok(())
}
#[benchmark]
fn update_channel() -> Result<(), BenchmarkError> {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let origin = T::Helper::make_xcm_origin(origin_location);
fund_sovereign_account::<T>(origin_para_id.into())?;
SnowbridgeControl::<T>::create_agent(origin.clone())?;
SnowbridgeControl::<T>::create_channel(origin.clone(), OperatingMode::Normal)?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, OperatingMode::RejectingOutboundMessages);
Ok(())
}
#[benchmark]
fn force_update_channel() -> Result<(), BenchmarkError> {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let origin = T::Helper::make_xcm_origin(origin_location);
let channel_id: ChannelId = ParaId::from(origin_para_id).into();
fund_sovereign_account::<T>(origin_para_id.into())?;
SnowbridgeControl::<T>::create_agent(origin.clone())?;
SnowbridgeControl::<T>::create_channel(origin.clone(), OperatingMode::Normal)?;
#[extrinsic_call]
_(RawOrigin::Root, channel_id, OperatingMode::RejectingOutboundMessages);
Ok(())
}
#[benchmark]
fn transfer_native_from_agent() -> Result<(), BenchmarkError> {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let origin = T::Helper::make_xcm_origin(origin_location);
fund_sovereign_account::<T>(origin_para_id.into())?;
SnowbridgeControl::<T>::create_agent(origin.clone())?;
SnowbridgeControl::<T>::create_channel(origin.clone(), OperatingMode::Normal)?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, H160::default(), 1);
Ok(())
}
#[benchmark]
fn force_transfer_native_from_agent() -> Result<(), BenchmarkError> {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let origin = T::Helper::make_xcm_origin(origin_location);
fund_sovereign_account::<T>(origin_para_id.into())?;
SnowbridgeControl::<T>::create_agent(origin.clone())?;
let versioned_location: VersionedMultiLocation = origin_location.into();
#[extrinsic_call]
_(RawOrigin::Root, Box::new(versioned_location), H160::default(), 1);
Ok(())
}
#[benchmark]
fn set_token_transfer_fees() -> Result<(), BenchmarkError> {
#[extrinsic_call]
_(RawOrigin::Root, 1, 1, eth(1));
Ok(())
}
impl_benchmark_test_suite!(
SnowbridgeControl,
crate::mock::new_test_ext(true),
crate::mock::Test
);
}
@@ -0,0 +1,681 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Governance API for controlling the Ethereum side of the bridge
//!
//! # Extrinsics
//!
//! ## Agents
//!
//! Agents are smart contracts on Ethereum that act as proxies for consensus systems on Polkadot
//! networks.
//!
//! * [`Call::create_agent`]: Create agent for a sibling parachain
//! * [`Call::transfer_native_from_agent`]: Withdraw ether from an agent
//!
//! The `create_agent` extrinsic should be called via an XCM `Transact` instruction from the sibling
//! parachain.
//!
//! ## Channels
//!
//! Each sibling parachain has its own dedicated messaging channel for sending and receiving
//! messages. As a prerequisite to creating a channel, the sibling should have already created
//! an agent using the `create_agent` extrinsic.
//!
//! * [`Call::create_channel`]: Create channel for a sibling
//! * [`Call::update_channel`]: Update a channel for a sibling
//!
//! ## Governance
//!
//! Only Polkadot governance itself can call these extrinsics. Delivery fees are waived.
//!
//! * [`Call::upgrade`]`: Upgrade the gateway contract
//! * [`Call::set_operating_mode`]: Update the operating mode of the gateway contract
//! * [`Call::force_update_channel`]: Allow root to update a channel for a sibling
//! * [`Call::force_transfer_native_from_agent`]: Allow root to withdraw ether from an agent
//!
//! Typically, Polkadot governance will use the `force_transfer_native_from_agent` and
//! `force_update_channel` and extrinsics to manage agents and channels for system parachains.
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod migration;
pub mod api;
pub mod weights;
pub use weights::*;
use frame_support::{
pallet_prelude::*,
traits::{
fungible::{Inspect, Mutate},
tokens::Preservation,
Contains, EnsureOrigin,
},
};
use frame_system::pallet_prelude::*;
use snowbridge_core::{
meth,
outbound::{Command, Initializer, Message, OperatingMode, SendError, SendMessage},
sibling_sovereign_account, AgentId, Channel, ChannelId, ParaId,
PricingParameters as PricingParametersRecord, PRIMARY_GOVERNANCE_CHANNEL,
SECONDARY_GOVERNANCE_CHANNEL,
};
use sp_core::{RuntimeDebug, H160, H256};
use sp_io::hashing::blake2_256;
use sp_runtime::{traits::BadOrigin, DispatchError, SaturatedConversion};
use sp_std::prelude::*;
use xcm::prelude::*;
use xcm_executor::traits::ConvertLocation;
#[cfg(feature = "runtime-benchmarks")]
use frame_support::traits::OriginTrait;
pub use pallet::*;
pub type BalanceOf<T> =
<<T as pallet::Config>::Token as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub type PricingParametersOf<T> = PricingParametersRecord<BalanceOf<T>>;
/// Ensure origin location is a sibling
fn ensure_sibling<T>(location: &MultiLocation) -> Result<(ParaId, H256), DispatchError>
where
T: Config,
{
match location {
MultiLocation { parents: 1, interior: X1(Parachain(para_id)) } => {
let agent_id = agent_id_of::<T>(location)?;
Ok(((*para_id).into(), agent_id))
},
_ => Err(BadOrigin.into()),
}
}
/// Hash the location to produce an agent id
fn agent_id_of<T: Config>(location: &MultiLocation) -> Result<H256, DispatchError> {
T::AgentIdOf::convert_location(location).ok_or(Error::<T>::LocationConversionFailed.into())
}
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<O>
where
O: OriginTrait,
{
fn make_xcm_origin(location: MultiLocation) -> O;
}
/// Whether a fee should be withdrawn to an account for sending an outbound message
#[derive(Clone, PartialEq, RuntimeDebug)]
pub enum PaysFee<T>
where
T: Config,
{
/// Fully charge includes (local + remote fee)
Yes(AccountIdOf<T>),
/// Partially charge includes local fee only
Partial(AccountIdOf<T>),
/// No charge
No,
}
#[frame_support::pallet]
pub mod pallet {
use snowbridge_core::StaticLookup;
use sp_core::U256;
use super::*;
#[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>;
/// Send messages to Ethereum
type OutboundQueue: SendMessage<Balance = BalanceOf<Self>>;
/// Origin check for XCM locations that can create agents
type SiblingOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = MultiLocation>;
/// Converts MultiLocation to AgentId
type AgentIdOf: ConvertLocation<AgentId>;
/// Token reserved for control operations
type Token: Mutate<Self::AccountId>;
/// TreasuryAccount to collect fees
#[pallet::constant]
type TreasuryAccount: Get<Self::AccountId>;
/// Number of decimal places of local currency
type DefaultPricingParameters: Get<PricingParametersOf<Self>>;
/// Cost of delivering a message from Ethereum
type InboundDeliveryCost: Get<BalanceOf<Self>>;
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type Helper: BenchmarkHelper<Self::RuntimeOrigin>;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// An Upgrade message was sent to the Gateway
Upgrade {
impl_address: H160,
impl_code_hash: H256,
initializer_params_hash: Option<H256>,
},
/// An CreateAgent message was sent to the Gateway
CreateAgent {
location: Box<MultiLocation>,
agent_id: AgentId,
},
/// An CreateChannel message was sent to the Gateway
CreateChannel {
channel_id: ChannelId,
agent_id: AgentId,
},
/// An UpdateChannel message was sent to the Gateway
UpdateChannel {
channel_id: ChannelId,
mode: OperatingMode,
},
/// An SetOperatingMode message was sent to the Gateway
SetOperatingMode {
mode: OperatingMode,
},
/// An TransferNativeFromAgent message was sent to the Gateway
TransferNativeFromAgent {
agent_id: AgentId,
recipient: H160,
amount: u128,
},
/// A SetTokenTransferFees message was sent to the Gateway
SetTokenTransferFees {
create_asset_xcm: u128,
transfer_asset_xcm: u128,
register_token: U256,
},
PricingParametersChanged {
params: PricingParametersOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
LocationConversionFailed,
AgentAlreadyCreated,
NoAgent,
ChannelAlreadyCreated,
NoChannel,
UnsupportedLocationVersion,
InvalidLocation,
Send(SendError),
InvalidTokenTransferFees,
InvalidPricingParameters,
}
/// The set of registered agents
#[pallet::storage]
#[pallet::getter(fn agents)]
pub type Agents<T: Config> = StorageMap<_, Twox64Concat, AgentId, (), OptionQuery>;
/// The set of registered channels
#[pallet::storage]
#[pallet::getter(fn channels)]
pub type Channels<T: Config> = StorageMap<_, Twox64Concat, ChannelId, Channel, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn parameters)]
pub type PricingParameters<T: Config> =
StorageValue<_, PricingParametersOf<T>, ValueQuery, T::DefaultPricingParameters>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
// Own parachain id
pub para_id: ParaId,
// AssetHub's parachain id
pub asset_hub_para_id: ParaId,
#[serde(skip)]
pub _config: PhantomData<T>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
Pallet::<T>::initialize(self.para_id, self.asset_hub_para_id).expect("infallible; qed");
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Sends command to the Gateway contract to upgrade itself with a new implementation
/// contract
///
/// Fee required: No
///
/// - `origin`: Must be `Root`.
/// - `impl_address`: The address of the implementation contract.
/// - `impl_code_hash`: The codehash of the implementation contract.
/// - `initializer`: Optionally call an initializer on the implementation contract.
#[pallet::call_index(0)]
#[pallet::weight((T::WeightInfo::upgrade(), DispatchClass::Operational))]
pub fn upgrade(
origin: OriginFor<T>,
impl_address: H160,
impl_code_hash: H256,
initializer: Option<Initializer>,
) -> DispatchResult {
ensure_root(origin)?;
let initializer_params_hash: Option<H256> =
initializer.as_ref().map(|i| H256::from(blake2_256(i.params.as_ref())));
let command = Command::Upgrade { impl_address, impl_code_hash, initializer };
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
Self::deposit_event(Event::<T>::Upgrade {
impl_address,
impl_code_hash,
initializer_params_hash,
});
Ok(())
}
/// Sends a message to the Gateway contract to change its operating mode
///
/// Fee required: No
///
/// - `origin`: Must be `MultiLocation`
#[pallet::call_index(1)]
#[pallet::weight((T::WeightInfo::set_operating_mode(), DispatchClass::Operational))]
pub fn set_operating_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
ensure_root(origin)?;
let command = Command::SetOperatingMode { mode };
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
Self::deposit_event(Event::<T>::SetOperatingMode { mode });
Ok(())
}
/// Set pricing parameters on both sides of the bridge
///
/// Fee required: No
///
/// - `origin`: Must be root
#[pallet::call_index(2)]
#[pallet::weight((T::WeightInfo::set_pricing_parameters(), DispatchClass::Operational))]
pub fn set_pricing_parameters(
origin: OriginFor<T>,
params: PricingParametersOf<T>,
) -> DispatchResult {
ensure_root(origin)?;
params.validate().map_err(|_| Error::<T>::InvalidPricingParameters)?;
PricingParameters::<T>::put(params.clone());
let command = Command::SetPricingParameters {
exchange_rate: params.exchange_rate.into(),
delivery_cost: T::InboundDeliveryCost::get().saturated_into::<u128>(),
};
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
Self::deposit_event(Event::PricingParametersChanged { params });
Ok(())
}
/// Sends a command to the Gateway contract to instantiate a new agent contract representing
/// `origin`.
///
/// Fee required: Yes
///
/// - `origin`: Must be `MultiLocation` of a sibling parachain
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::create_agent())]
pub fn create_agent(origin: OriginFor<T>) -> DispatchResult {
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
// Ensure that origin location is some consensus system on a sibling parachain
let (para_id, agent_id) = ensure_sibling::<T>(&origin_location)?;
// Record the agent id or fail if it has already been created
ensure!(!Agents::<T>::contains_key(agent_id), Error::<T>::AgentAlreadyCreated);
Agents::<T>::insert(agent_id, ());
let command = Command::CreateAgent { agent_id };
let pays_fee = PaysFee::<T>::Yes(sibling_sovereign_account::<T>(para_id));
Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;
Self::deposit_event(Event::<T>::CreateAgent {
location: Box::new(origin_location),
agent_id,
});
Ok(())
}
/// Sends a message to the Gateway contract to create a new channel representing `origin`
///
/// Fee required: Yes
///
/// This extrinsic is permissionless, so a fee is charged to prevent spamming and pay
/// for execution costs on the remote side.
///
/// The message is sent over the bridge on BridgeHub's own channel to the Gateway.
///
/// - `origin`: Must be `MultiLocation`
/// - `mode`: Initial operating mode of the channel
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::create_channel())]
pub fn create_channel(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
// Ensure that origin location is a sibling parachain
let (para_id, agent_id) = ensure_sibling::<T>(&origin_location)?;
let channel_id: ChannelId = para_id.into();
ensure!(Agents::<T>::contains_key(agent_id), Error::<T>::NoAgent);
ensure!(!Channels::<T>::contains_key(channel_id), Error::<T>::ChannelAlreadyCreated);
let channel = Channel { agent_id, para_id };
Channels::<T>::insert(channel_id, channel);
let command = Command::CreateChannel { channel_id, agent_id, mode };
let pays_fee = PaysFee::<T>::Yes(sibling_sovereign_account::<T>(para_id));
Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;
Self::deposit_event(Event::<T>::CreateChannel { channel_id, agent_id });
Ok(())
}
/// Sends a message to the Gateway contract to update a channel configuration
///
/// The origin must already have a channel initialized, as this message is sent over it.
///
/// A partial fee will be charged for local processing only.
///
/// - `origin`: Must be `MultiLocation`
/// - `mode`: Initial operating mode of the channel
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::update_channel())]
pub fn update_channel(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
// Ensure that origin location is a sibling parachain
let (para_id, _) = ensure_sibling::<T>(&origin_location)?;
let channel_id: ChannelId = para_id.into();
ensure!(Channels::<T>::contains_key(channel_id), Error::<T>::NoChannel);
let command = Command::UpdateChannel { channel_id, mode };
let pays_fee = PaysFee::<T>::Partial(sibling_sovereign_account::<T>(para_id));
// Parachains send the update message on their own channel
Self::send(channel_id, command, pays_fee)?;
Self::deposit_event(Event::<T>::UpdateChannel { channel_id, mode });
Ok(())
}
/// Sends a message to the Gateway contract to update an arbitrary channel
///
/// Fee required: No
///
/// - `origin`: Must be root
/// - `channel_id`: ID of channel
/// - `mode`: Initial operating mode of the channel
/// - `outbound_fee`: Fee charged to users for sending outbound messages to Polkadot
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::force_update_channel())]
pub fn force_update_channel(
origin: OriginFor<T>,
channel_id: ChannelId,
mode: OperatingMode,
) -> DispatchResult {
ensure_root(origin)?;
ensure!(Channels::<T>::contains_key(channel_id), Error::<T>::NoChannel);
let command = Command::UpdateChannel { channel_id, mode };
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
Self::deposit_event(Event::<T>::UpdateChannel { channel_id, mode });
Ok(())
}
/// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`.
///
/// A partial fee will be charged for local processing only.
///
/// - `origin`: Must be `MultiLocation`
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::transfer_native_from_agent())]
pub fn transfer_native_from_agent(
origin: OriginFor<T>,
recipient: H160,
amount: u128,
) -> DispatchResult {
let origin_location: MultiLocation = T::SiblingOrigin::ensure_origin(origin)?;
// Ensure that origin location is some consensus system on a sibling parachain
let (para_id, agent_id) = ensure_sibling::<T>(&origin_location)?;
// Since the origin is also the owner of the channel, they only need to pay
// the local processing fee.
let pays_fee = PaysFee::<T>::Partial(sibling_sovereign_account::<T>(para_id));
Self::do_transfer_native_from_agent(
agent_id,
para_id.into(),
recipient,
amount,
pays_fee,
)
}
/// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`.
///
/// Privileged. Can only be called by root.
///
/// Fee required: No
///
/// - `origin`: Must be root
/// - `location`: Location used to resolve the agent
/// - `recipient`: Recipient of funds
/// - `amount`: Amount to transfer
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::force_transfer_native_from_agent())]
pub fn force_transfer_native_from_agent(
origin: OriginFor<T>,
location: Box<VersionedMultiLocation>,
recipient: H160,
amount: u128,
) -> DispatchResult {
ensure_root(origin)?;
// Ensure that location is some consensus system on a sibling parachain
let location: MultiLocation =
(*location).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
let (_, agent_id) =
ensure_sibling::<T>(&location).map_err(|_| Error::<T>::InvalidLocation)?;
let pays_fee = PaysFee::<T>::No;
Self::do_transfer_native_from_agent(
agent_id,
PRIMARY_GOVERNANCE_CHANNEL,
recipient,
amount,
pays_fee,
)
}
/// Sends a message to the Gateway contract to update fee related parameters for
/// token transfers.
///
/// Privileged. Can only be called by root.
///
/// Fee required: No
///
/// - `origin`: Must be root
/// - `create_asset_xcm`: The XCM execution cost for creating a new asset class on AssetHub,
/// in DOT
/// - `transfer_asset_xcm`: The XCM execution cost for performing a reserve transfer on
/// AssetHub, in DOT
/// - `register_token`: The Ether fee for registering a new token, to discourage spamming
#[pallet::call_index(9)]
#[pallet::weight((T::WeightInfo::set_token_transfer_fees(), DispatchClass::Operational))]
pub fn set_token_transfer_fees(
origin: OriginFor<T>,
create_asset_xcm: u128,
transfer_asset_xcm: u128,
register_token: U256,
) -> DispatchResult {
ensure_root(origin)?;
// Basic validation of new costs. Particularly for token registration, we want to ensure
// its relatively expensive to discourage spamming. Like at least 100 USD.
ensure!(
create_asset_xcm > 0 && transfer_asset_xcm > 0 && register_token > meth(100),
Error::<T>::InvalidTokenTransferFees
);
let command = Command::SetTokenTransferFees {
create_asset_xcm,
transfer_asset_xcm,
register_token,
};
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
Self::deposit_event(Event::<T>::SetTokenTransferFees {
create_asset_xcm,
transfer_asset_xcm,
register_token,
});
Ok(())
}
}
impl<T: Config> Pallet<T> {
/// Send `command` to the Gateway on the Channel identified by `channel_id`
fn send(channel_id: ChannelId, command: Command, pays_fee: PaysFee<T>) -> DispatchResult {
let message = Message { id: None, channel_id, command };
let (ticket, fee) =
T::OutboundQueue::validate(&message).map_err(|err| Error::<T>::Send(err))?;
let payment = match pays_fee {
PaysFee::Yes(account) => Some((account, fee.total())),
PaysFee::Partial(account) => Some((account, fee.local)),
PaysFee::No => None,
};
if let Some((payer, fee)) = payment {
T::Token::transfer(
&payer,
&T::TreasuryAccount::get(),
fee,
Preservation::Preserve,
)?;
}
T::OutboundQueue::deliver(ticket).map_err(|err| Error::<T>::Send(err))?;
Ok(())
}
/// Issue a `Command::TransferNativeFromAgent` command. The command will be sent on the
/// channel `channel_id`
pub fn do_transfer_native_from_agent(
agent_id: H256,
channel_id: ChannelId,
recipient: H160,
amount: u128,
pays_fee: PaysFee<T>,
) -> DispatchResult {
ensure!(Agents::<T>::contains_key(agent_id), Error::<T>::NoAgent);
let command = Command::TransferNativeFromAgent { agent_id, recipient, amount };
Self::send(channel_id, command, pays_fee)?;
Self::deposit_event(Event::<T>::TransferNativeFromAgent {
agent_id,
recipient,
amount,
});
Ok(())
}
/// Initializes agents and channels.
pub fn initialize(para_id: ParaId, asset_hub_para_id: ParaId) -> Result<(), DispatchError> {
// Asset Hub
let asset_hub_location: MultiLocation =
ParentThen(X1(Parachain(asset_hub_para_id.into()))).into();
let asset_hub_agent_id = agent_id_of::<T>(&asset_hub_location)?;
let asset_hub_channel_id: ChannelId = asset_hub_para_id.into();
Agents::<T>::insert(asset_hub_agent_id, ());
Channels::<T>::insert(
asset_hub_channel_id,
Channel { agent_id: asset_hub_agent_id, para_id: asset_hub_para_id },
);
// Governance channels
let bridge_hub_agent_id = agent_id_of::<T>(&MultiLocation::here())?;
// Agent for BridgeHub
Agents::<T>::insert(bridge_hub_agent_id, ());
// Primary governance channel
Channels::<T>::insert(
PRIMARY_GOVERNANCE_CHANNEL,
Channel { agent_id: bridge_hub_agent_id, para_id },
);
// Secondary governance channel
Channels::<T>::insert(
SECONDARY_GOVERNANCE_CHANNEL,
Channel { agent_id: bridge_hub_agent_id, para_id },
);
Ok(())
}
/// Checks if the pallet has been initialized.
pub(crate) fn is_initialized() -> bool {
let primary_exists = Channels::<T>::contains_key(PRIMARY_GOVERNANCE_CHANNEL);
let secondary_exists = Channels::<T>::contains_key(SECONDARY_GOVERNANCE_CHANNEL);
primary_exists && secondary_exists
}
}
impl<T: Config> StaticLookup for Pallet<T> {
type Source = ChannelId;
type Target = Channel;
fn lookup(channel_id: Self::Source) -> Option<Self::Target> {
Channels::<T>::get(channel_id)
}
}
impl<T: Config> Contains<ChannelId> for Pallet<T> {
fn contains(channel_id: &ChannelId) -> bool {
Channels::<T>::get(channel_id).is_some()
}
}
impl<T: Config> Get<PricingParametersOf<T>> for Pallet<T> {
fn get() -> PricingParametersOf<T> {
PricingParameters::<T>::get()
}
}
}
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Governance API for controlling the Ethereum side of the bridge
use super::*;
use frame_support::traits::OnRuntimeUpgrade;
use log;
#[cfg(feature = "try-runtime")]
use sp_runtime::TryRuntimeError;
pub mod v0 {
use frame_support::{pallet_prelude::*, weights::Weight};
use super::*;
const LOG_TARGET: &str = "ethereum_system::migration";
pub struct InitializeOnUpgrade<T, BridgeHubParaId, AssetHubParaId>(
sp_std::marker::PhantomData<(T, BridgeHubParaId, AssetHubParaId)>,
);
impl<T, BridgeHubParaId, AssetHubParaId> OnRuntimeUpgrade
for InitializeOnUpgrade<T, BridgeHubParaId, AssetHubParaId>
where
T: Config,
BridgeHubParaId: Get<u32>,
AssetHubParaId: Get<u32>,
{
fn on_runtime_upgrade() -> Weight {
if !Pallet::<T>::is_initialized() {
Pallet::<T>::initialize(
BridgeHubParaId::get().into(),
AssetHubParaId::get().into(),
)
.expect("infallible; qed");
log::info!(
target: LOG_TARGET,
"Ethereum system initialized."
);
T::DbWeight::get().reads_writes(2, 5)
} else {
log::info!(
target: LOG_TARGET,
"Ethereum system already initialized. Skipping."
);
T::DbWeight::get().reads(2)
}
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
if !Pallet::<T>::is_initialized() {
log::info!(
target: LOG_TARGET,
"Agents and channels not initialized. Initialization will run."
);
} else {
log::info!(
target: LOG_TARGET,
"Agents and channels are initialized. Initialization will not run."
);
}
Ok(vec![])
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
frame_support::ensure!(
Pallet::<T>::is_initialized(),
"Agents and channels were not initialized."
);
Ok(())
}
}
}
@@ -0,0 +1,270 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate as snowbridge_system;
use frame_support::{
parameter_types,
traits::{tokens::fungible::Mutate, ConstU128, ConstU16, ConstU64, ConstU8},
weights::IdentityFee,
PalletId,
};
use sp_core::H256;
use xcm_executor::traits::ConvertLocation;
use snowbridge_core::{
gwei, meth, outbound::ConstantGasMeter, sibling_sovereign_account, AgentId, AllowSiblingsOnly,
ParaId, PricingParameters, Rewards,
};
use sp_runtime::{
traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, Keccak256},
AccountId32, BuildStorage, FixedU128,
};
use xcm::prelude::*;
#[cfg(feature = "runtime-benchmarks")]
use crate::BenchmarkHelper;
type Block = frame_system::mocking::MockBlock<Test>;
type Balance = u128;
pub type AccountId = AccountId32;
// A stripped-down version of pallet-xcm that only inserts an XCM origin into the runtime
#[allow(dead_code)]
#[frame_support::pallet]
mod pallet_xcm_origin {
use frame_support::{
pallet_prelude::*,
traits::{Contains, OriginTrait},
};
use xcm::latest::prelude::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeOrigin: From<Origin> + From<<Self as frame_system::Config>::RuntimeOrigin>;
}
// Insert this custom Origin into the aggregate RuntimeOrigin
#[pallet::origin]
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct Origin(pub MultiLocation);
impl From<MultiLocation> for Origin {
fn from(location: MultiLocation) -> Origin {
Origin(location)
}
}
/// `EnsureOrigin` implementation succeeding with a `MultiLocation` value to recognize and
/// filter the contained location
pub struct EnsureXcm<F>(PhantomData<F>);
impl<O: OriginTrait + From<Origin>, F: Contains<MultiLocation>> EnsureOrigin<O> for EnsureXcm<F>
where
O::PalletsOrigin: From<Origin> + TryInto<Origin, Error = O::PalletsOrigin>,
{
type Success = MultiLocation;
fn try_origin(outer: O) -> Result<Self::Success, O> {
outer.try_with_caller(|caller| {
caller.try_into().and_then(|o| match o {
Origin(location) if F::contains(&location) => Ok(location),
o => Err(o.into()),
})
})
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<O, ()> {
Ok(O::from(Origin(MultiLocation { parents: 1, interior: X1(Parachain(2000)) })))
}
}
}
// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
XcmOrigin: pallet_xcm_origin::{Pallet, Origin},
OutboundQueue: snowbridge_outbound_queue::{Pallet, Call, Storage, Event<T>},
EthereumSystem: snowbridge_system,
MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event<T>}
}
);
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
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 = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ConstU16<42>;
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type Block = Block;
}
impl pallet_balances::Config for Test {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU128<1>;
type AccountStore = System;
type WeightInfo = ();
type FreezeIdentifier = ();
type MaxFreezes = ();
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
type MaxHolds = ();
}
impl pallet_xcm_origin::Config for Test {
type RuntimeOrigin = RuntimeOrigin;
}
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 MaxMessagePayloadSize: u32 = 1024;
pub const MaxMessagesPerBlock: u32 = 20;
pub const OwnParaId: ParaId = ParaId::new(1013);
}
impl snowbridge_outbound_queue::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Hashing = Keccak256;
type MessageQueue = MessageQueue;
type Decimals = ConstU8<10>;
type MaxMessagePayloadSize = MaxMessagePayloadSize;
type MaxMessagesPerBlock = MaxMessagesPerBlock;
type GasMeter = ConstantGasMeter;
type Balance = u128;
type PricingParameters = EthereumSystem;
type Channels = EthereumSystem;
type WeightToFee = IdentityFee<u128>;
type WeightInfo = ();
}
parameter_types! {
pub const SS58Prefix: u8 = 42;
pub const AnyNetwork: Option<NetworkId> = None;
pub const RelayNetwork: Option<NetworkId> = Some(NetworkId::Kusama);
pub const RelayLocation: MultiLocation = MultiLocation::parent();
pub UniversalLocation: InteriorMultiLocation =
X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(1013));
}
pub const DOT: u128 = 10_000_000_000;
parameter_types! {
pub TreasuryAccount: AccountId = PalletId(*b"py/trsry").into_account_truncating();
pub Fee: u64 = 1000;
pub const RococoNetwork: NetworkId = NetworkId::Rococo;
pub const InitialFunding: u128 = 1_000_000_000_000;
pub AssetHubParaId: ParaId = ParaId::new(1000);
pub TestParaId: u32 = 2000;
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 InboundDeliveryCost: u128 = 1_000_000_000;
}
#[cfg(feature = "runtime-benchmarks")]
impl BenchmarkHelper<RuntimeOrigin> for () {
fn make_xcm_origin(location: MultiLocation) -> RuntimeOrigin {
RuntimeOrigin::from(pallet_xcm_origin::Origin(location))
}
}
impl crate::Config for Test {
type RuntimeEvent = RuntimeEvent;
type OutboundQueue = OutboundQueue;
type SiblingOrigin = pallet_xcm_origin::EnsureXcm<AllowSiblingsOnly>;
type AgentIdOf = snowbridge_core::AgentIdOf;
type TreasuryAccount = TreasuryAccount;
type Token = Balances;
type DefaultPricingParameters = Parameters;
type WeightInfo = ();
type InboundDeliveryCost = InboundDeliveryCost;
#[cfg(feature = "runtime-benchmarks")]
type Helper = ();
}
// Build genesis storage according to the mock runtime.
pub fn new_test_ext(genesis_build: bool) -> sp_io::TestExternalities {
let mut storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
if genesis_build {
crate::GenesisConfig::<Test> {
para_id: OwnParaId::get(),
asset_hub_para_id: AssetHubParaId::get(),
_config: Default::default(),
}
.assimilate_storage(&mut storage)
.unwrap();
}
let mut ext: sp_io::TestExternalities = storage.into();
let initial_amount = InitialFunding::get();
let test_para_id = TestParaId::get();
let sovereign_account = sibling_sovereign_account::<Test>(test_para_id.into());
let treasury_account = TreasuryAccount::get();
ext.execute_with(|| {
System::set_block_number(1);
Balances::mint_into(&AccountId32::from([0; 32]), initial_amount).unwrap();
Balances::mint_into(&sovereign_account, initial_amount).unwrap();
Balances::mint_into(&treasury_account, initial_amount).unwrap();
});
ext
}
// Test helpers
pub fn make_xcm_origin(location: MultiLocation) -> RuntimeOrigin {
pallet_xcm_origin::Origin(location).into()
}
pub fn make_agent_id(location: MultiLocation) -> AgentId {
<Test as snowbridge_system::Config>::AgentIdOf::convert_location(&location)
.expect("convert location")
}
@@ -0,0 +1,664 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use crate::{mock::*, *};
use frame_support::{assert_noop, assert_ok};
use hex_literal::hex;
use snowbridge_core::{eth, sibling_sovereign_account_raw};
use sp_core::H256;
use sp_runtime::{AccountId32, DispatchError::BadOrigin, TokenError};
#[test]
fn create_agent() {
new_test_ext(true).execute_with(|| {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let agent_id = make_agent_id(origin_location);
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
// fund sovereign account of origin
let _ = Balances::mint_into(&sovereign_account, 10000);
assert!(!Agents::<Test>::contains_key(agent_id));
let origin = make_xcm_origin(origin_location);
assert_ok!(EthereumSystem::create_agent(origin));
assert!(Agents::<Test>::contains_key(agent_id));
});
}
#[test]
fn test_agent_for_here() {
new_test_ext(true).execute_with(|| {
let origin_location = MultiLocation::here();
let agent_id = make_agent_id(origin_location);
assert_eq!(
agent_id,
hex!("03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314").into(),
)
});
}
#[test]
fn create_agent_fails_on_funds_unavailable() {
new_test_ext(true).execute_with(|| {
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
let origin = make_xcm_origin(origin_location);
// Reset balance of sovereign_account to zero so to trigger the FundsUnavailable error
let sovereign_account = sibling_sovereign_account::<Test>(2000.into());
Balances::set_balance(&sovereign_account, 0);
assert_noop!(EthereumSystem::create_agent(origin), TokenError::FundsUnavailable);
});
}
#[test]
fn create_agent_bad_origin() {
new_test_ext(true).execute_with(|| {
// relay chain location not allowed
assert_noop!(
EthereumSystem::create_agent(make_xcm_origin(MultiLocation {
parents: 1,
interior: Here,
})),
BadOrigin,
);
// local account location not allowed
assert_noop!(
EthereumSystem::create_agent(make_xcm_origin(MultiLocation {
parents: 0,
interior: X1(Junction::AccountId32 { network: None, id: [67u8; 32] }),
})),
BadOrigin,
);
// Signed origin not allowed
assert_noop!(
EthereumSystem::create_agent(RuntimeOrigin::signed([14; 32].into())),
BadOrigin
);
// None origin not allowed
assert_noop!(EthereumSystem::create_agent(RuntimeOrigin::none()), BadOrigin);
});
}
#[test]
fn upgrade_as_root() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, None));
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::Upgrade {
impl_address: address,
impl_code_hash: code_hash,
initializer_params_hash: None,
}));
});
}
#[test]
fn upgrade_as_signed_fails() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::signed(AccountId32::new([0; 32]));
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
assert_noop!(EthereumSystem::upgrade(origin, address, code_hash, None), BadOrigin);
});
}
#[test]
fn upgrade_with_params() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
let initializer: Option<Initializer> =
Some(Initializer { params: [0; 256].into(), maximum_required_gas: 10000 });
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, initializer));
});
}
#[test]
fn set_operating_mode() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let mode = OperatingMode::RejectingOutboundMessages;
assert_ok!(EthereumSystem::set_operating_mode(origin, mode));
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::SetOperatingMode {
mode,
}));
});
}
#[test]
fn set_operating_mode_as_signed_fails() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::signed([14; 32].into());
let mode = OperatingMode::RejectingOutboundMessages;
assert_noop!(EthereumSystem::set_operating_mode(origin, mode), BadOrigin);
});
}
#[test]
fn set_pricing_parameters() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let mut params = Parameters::get();
params.rewards.local = 7;
assert_ok!(EthereumSystem::set_pricing_parameters(origin, params));
assert_eq!(PricingParameters::<Test>::get().rewards.local, 7);
});
}
#[test]
fn set_pricing_parameters_as_signed_fails() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::signed([14; 32].into());
let params = Parameters::get();
assert_noop!(EthereumSystem::set_pricing_parameters(origin, params), BadOrigin);
});
}
#[test]
fn set_pricing_parameters_invalid() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let mut params = Parameters::get();
params.rewards.local = 0;
assert_noop!(
EthereumSystem::set_pricing_parameters(origin.clone(), params),
Error::<Test>::InvalidPricingParameters
);
let mut params = Parameters::get();
params.exchange_rate = 0u128.into();
assert_noop!(
EthereumSystem::set_pricing_parameters(origin.clone(), params),
Error::<Test>::InvalidPricingParameters
);
params = Parameters::get();
params.fee_per_gas = sp_core::U256::zero();
assert_noop!(
EthereumSystem::set_pricing_parameters(origin.clone(), params),
Error::<Test>::InvalidPricingParameters
);
params = Parameters::get();
params.rewards.local = 0;
assert_noop!(
EthereumSystem::set_pricing_parameters(origin.clone(), params),
Error::<Test>::InvalidPricingParameters
);
params = Parameters::get();
params.rewards.remote = sp_core::U256::zero();
assert_noop!(
EthereumSystem::set_pricing_parameters(origin, params),
Error::<Test>::InvalidPricingParameters
);
});
}
#[test]
fn set_token_transfer_fees() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
assert_ok!(EthereumSystem::set_token_transfer_fees(origin, 1, 1, eth(1)));
});
}
#[test]
fn set_token_transfer_fees_root_only() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::signed([14; 32].into());
assert_noop!(EthereumSystem::set_token_transfer_fees(origin, 1, 1, 1.into()), BadOrigin);
});
}
#[test]
fn set_token_transfer_fees_invalid() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
assert_noop!(
EthereumSystem::set_token_transfer_fees(origin, 0, 0, 0.into()),
Error::<Test>::InvalidTokenTransferFees
);
});
}
#[test]
fn create_channel() {
new_test_ext(true).execute_with(|| {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
let origin = make_xcm_origin(origin_location);
// fund sovereign account of origin
let _ = Balances::mint_into(&sovereign_account, 10000);
assert_ok!(EthereumSystem::create_agent(origin.clone()));
assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
});
}
#[test]
fn create_channel_fail_already_exists() {
new_test_ext(true).execute_with(|| {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
let origin = make_xcm_origin(origin_location);
// fund sovereign account of origin
let _ = Balances::mint_into(&sovereign_account, 10000);
assert_ok!(EthereumSystem::create_agent(origin.clone()));
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
assert_noop!(
EthereumSystem::create_channel(origin, OperatingMode::Normal),
Error::<Test>::ChannelAlreadyCreated
);
});
}
#[test]
fn create_channel_bad_origin() {
new_test_ext(true).execute_with(|| {
// relay chain location not allowed
assert_noop!(
EthereumSystem::create_channel(
make_xcm_origin(MultiLocation { parents: 1, interior: Here }),
OperatingMode::Normal,
),
BadOrigin,
);
// child of sibling location not allowed
assert_noop!(
EthereumSystem::create_channel(
make_xcm_origin(MultiLocation {
parents: 1,
interior: X2(
Parachain(2000),
Junction::AccountId32 { network: None, id: [67u8; 32] }
),
}),
OperatingMode::Normal,
),
BadOrigin,
);
// local account location not allowed
assert_noop!(
EthereumSystem::create_channel(
make_xcm_origin(MultiLocation {
parents: 0,
interior: X1(Junction::AccountId32 { network: None, id: [67u8; 32] }),
}),
OperatingMode::Normal,
),
BadOrigin,
);
// Signed origin not allowed
assert_noop!(
EthereumSystem::create_channel(
RuntimeOrigin::signed([14; 32].into()),
OperatingMode::Normal,
),
BadOrigin
);
// None origin not allowed
assert_noop!(EthereumSystem::create_agent(RuntimeOrigin::none()), BadOrigin);
});
}
#[test]
fn update_channel() {
new_test_ext(true).execute_with(|| {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
let origin = make_xcm_origin(origin_location);
// First create the channel
let _ = Balances::mint_into(&sovereign_account, 10000);
assert_ok!(EthereumSystem::create_agent(origin.clone()));
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
// Now try to update it
assert_ok!(EthereumSystem::update_channel(origin, OperatingMode::Normal));
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::UpdateChannel {
channel_id: ParaId::from(2000).into(),
mode: OperatingMode::Normal,
}));
});
}
#[test]
fn update_channel_bad_origin() {
new_test_ext(true).execute_with(|| {
let mode = OperatingMode::Normal;
// relay chain location not allowed
assert_noop!(
EthereumSystem::update_channel(
make_xcm_origin(MultiLocation { parents: 1, interior: Here }),
mode,
),
BadOrigin,
);
// child of sibling location not allowed
assert_noop!(
EthereumSystem::update_channel(
make_xcm_origin(MultiLocation {
parents: 1,
interior: X2(
Parachain(2000),
Junction::AccountId32 { network: None, id: [67u8; 32] }
),
}),
mode,
),
BadOrigin,
);
// local account location not allowed
assert_noop!(
EthereumSystem::update_channel(
make_xcm_origin(MultiLocation {
parents: 0,
interior: X1(Junction::AccountId32 { network: None, id: [67u8; 32] }),
}),
mode,
),
BadOrigin,
);
// Signed origin not allowed
assert_noop!(
EthereumSystem::update_channel(RuntimeOrigin::signed([14; 32].into()), mode),
BadOrigin
);
// None origin not allowed
assert_noop!(EthereumSystem::update_channel(RuntimeOrigin::none(), mode), BadOrigin);
});
}
#[test]
fn update_channel_fails_not_exist() {
new_test_ext(true).execute_with(|| {
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
let origin = make_xcm_origin(origin_location);
// Now try to update it
assert_noop!(
EthereumSystem::update_channel(origin, OperatingMode::Normal),
Error::<Test>::NoChannel
);
});
}
#[test]
fn force_update_channel() {
new_test_ext(true).execute_with(|| {
let origin_para_id = 2000;
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(origin_para_id)) };
let sovereign_account = sibling_sovereign_account::<Test>(origin_para_id.into());
let origin = make_xcm_origin(origin_location);
let channel_id: ChannelId = ParaId::from(origin_para_id).into();
// First create the channel
let _ = Balances::mint_into(&sovereign_account, 10000);
assert_ok!(EthereumSystem::create_agent(origin.clone()));
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
// Now try to force update it
let force_origin = RuntimeOrigin::root();
assert_ok!(EthereumSystem::force_update_channel(
force_origin,
channel_id,
OperatingMode::Normal,
));
System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::UpdateChannel {
channel_id: ParaId::from(2000).into(),
mode: OperatingMode::Normal,
}));
});
}
#[test]
fn force_update_channel_bad_origin() {
new_test_ext(true).execute_with(|| {
let mode = OperatingMode::Normal;
// signed origin not allowed
assert_noop!(
EthereumSystem::force_update_channel(
RuntimeOrigin::signed([14; 32].into()),
ParaId::from(1000).into(),
mode,
),
BadOrigin,
);
});
}
#[test]
fn transfer_native_from_agent() {
new_test_ext(true).execute_with(|| {
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
let origin = make_xcm_origin(origin_location);
let recipient: H160 = [27u8; 20].into();
let amount = 103435;
// First create the agent and channel
assert_ok!(EthereumSystem::create_agent(origin.clone()));
assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
let origin = make_xcm_origin(origin_location);
assert_ok!(EthereumSystem::transfer_native_from_agent(origin, recipient, amount),);
System::assert_last_event(RuntimeEvent::EthereumSystem(
crate::Event::TransferNativeFromAgent {
agent_id: make_agent_id(origin_location),
recipient,
amount,
},
));
});
}
#[test]
fn force_transfer_native_from_agent() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let location = MultiLocation { parents: 1, interior: X1(Parachain(2000)) };
let versioned_location: Box<VersionedMultiLocation> = Box::new(location.into());
let recipient: H160 = [27u8; 20].into();
let amount = 103435;
// First create the agent
Agents::<Test>::insert(make_agent_id(location), ());
assert_ok!(EthereumSystem::force_transfer_native_from_agent(
origin,
versioned_location,
recipient,
amount
),);
System::assert_last_event(RuntimeEvent::EthereumSystem(
crate::Event::TransferNativeFromAgent {
agent_id: make_agent_id(location),
recipient,
amount,
},
));
});
}
#[test]
fn force_transfer_native_from_agent_bad_origin() {
new_test_ext(true).execute_with(|| {
let recipient: H160 = [27u8; 20].into();
let amount = 103435;
// signed origin not allowed
assert_noop!(
EthereumSystem::force_transfer_native_from_agent(
RuntimeOrigin::signed([14; 32].into()),
Box::new(
MultiLocation {
parents: 1,
interior: X2(
Parachain(2000),
Junction::AccountId32 { network: None, id: [67u8; 32] }
),
}
.into()
),
recipient,
amount,
),
BadOrigin,
);
});
}
// NOTE: The following tests are not actually tests and are more about obtaining location
// conversions for devops purposes. They need to be removed here and incorporated into a command
// line utility.
#[ignore]
#[test]
fn check_sibling_sovereign_account() {
new_test_ext(true).execute_with(|| {
let para_id = 1001;
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
let sovereign_account_raw = sibling_sovereign_account_raw(para_id.into());
println!(
"Sovereign account for parachain {}: {:#?}",
para_id,
hex::encode(sovereign_account.clone())
);
assert_eq!(sovereign_account, sovereign_account_raw.into());
});
}
#[test]
fn charge_fee_for_create_agent() {
new_test_ext(true).execute_with(|| {
let para_id: u32 = TestParaId::get();
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(para_id)) };
let origin = make_xcm_origin(origin_location);
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
let (_, agent_id) = ensure_sibling::<Test>(&origin_location).unwrap();
let initial_sovereign_balance = Balances::balance(&sovereign_account);
assert_ok!(EthereumSystem::create_agent(origin.clone()));
let fee_charged = initial_sovereign_balance - Balances::balance(&sovereign_account);
assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
// assert sovereign_balance decreased by (fee.base_fee + fee.delivery_fee)
let message = Message {
id: None,
channel_id: ParaId::from(para_id).into(),
command: Command::CreateAgent { agent_id },
};
let (_, fee) = OutboundQueue::validate(&message).unwrap();
assert_eq!(fee.local + fee.remote, fee_charged);
// and treasury_balance increased
let treasury_balance = Balances::balance(&TreasuryAccount::get());
assert!(treasury_balance > InitialFunding::get());
let final_sovereign_balance = Balances::balance(&sovereign_account);
// (sovereign_balance + treasury_balance) keeps the same
assert_eq!(final_sovereign_balance + treasury_balance, { InitialFunding::get() * 2 });
});
}
#[test]
fn charge_fee_for_transfer_native_from_agent() {
new_test_ext(true).execute_with(|| {
let para_id: u32 = TestParaId::get();
let origin_location = MultiLocation { parents: 1, interior: X1(Parachain(para_id)) };
let recipient: H160 = [27u8; 20].into();
let amount = 103435;
let origin = make_xcm_origin(origin_location);
let (_, agent_id) = ensure_sibling::<Test>(&origin_location).unwrap();
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
// create_agent & create_channel first
assert_ok!(EthereumSystem::create_agent(origin.clone()));
assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
// assert sovereign_balance decreased by only the base_fee
let sovereign_balance_before = Balances::balance(&sovereign_account);
assert_ok!(EthereumSystem::transfer_native_from_agent(origin.clone(), recipient, amount));
let message = Message {
id: None,
channel_id: ParaId::from(para_id).into(),
command: Command::TransferNativeFromAgent { agent_id, recipient, amount },
};
let (_, fee) = OutboundQueue::validate(&message).unwrap();
let sovereign_balance_after = Balances::balance(&sovereign_account);
assert_eq!(sovereign_balance_after + fee.local, sovereign_balance_before);
});
}
#[test]
fn charge_fee_for_upgrade() {
new_test_ext(true).execute_with(|| {
let para_id: u32 = TestParaId::get();
let origin = RuntimeOrigin::root();
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
let initializer: Option<Initializer> =
Some(Initializer { params: [0; 256].into(), maximum_required_gas: 10000 });
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, initializer.clone()));
// assert sovereign_balance does not change as we do not charge for sudo operations
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
let sovereign_balance = Balances::balance(&sovereign_account);
assert_eq!(sovereign_balance, InitialFunding::get());
});
}
#[test]
fn genesis_build_initializes_correctly() {
new_test_ext(true).execute_with(|| {
assert!(EthereumSystem::is_initialized(), "Ethereum uninitialized.");
});
}
#[test]
fn no_genesis_build_is_uninitialized() {
new_test_ext(false).execute_with(|| {
assert!(!EthereumSystem::is_initialized(), "Ethereum initialized.");
});
}
@@ -0,0 +1,249 @@
//! Autogenerated weights for `snowbridge_system`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-10-09, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `crake.local`, 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_system
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --template
// ../parachain/templates/module-weight-template.hbs
// --output
// ../parachain/pallets/control/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_system`.
pub trait WeightInfo {
fn upgrade() -> Weight;
fn create_agent() -> Weight;
fn create_channel() -> Weight;
fn update_channel() -> Weight;
fn force_update_channel() -> Weight;
fn set_operating_mode() -> Weight;
fn transfer_native_from_agent() -> Weight;
fn force_transfer_native_from_agent() -> Weight;
fn set_token_transfer_fees() -> Weight;
fn set_pricing_parameters() -> Weight;
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: ParachainInfo ParachainId (r:1 w:0)
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:1 w:1)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:1)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn upgrade() -> Weight {
// Proof Size summary in bytes:
// Measured: `80`
// Estimated: `3517`
// Minimum execution time: 44_000_000 picoseconds.
Weight::from_parts(44_000_000, 3517)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: EthereumSystem Agents (r:1 w:1)
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
/// Storage: System Account (r:2 w:2)
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
/// Storage: ParachainInfo ParachainId (r:1 w:0)
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:1 w:1)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:1)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn create_agent() -> Weight {
// Proof Size summary in bytes:
// Measured: `187`
// Estimated: `6196`
// Minimum execution time: 85_000_000 picoseconds.
Weight::from_parts(85_000_000, 6196)
.saturating_add(RocksDbWeight::get().reads(7_u64))
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
/// Storage: System Account (r:2 w:2)
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
/// Storage: EthereumSystem Agents (r:1 w:0)
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
/// Storage: EthereumSystem Channels (r:1 w:1)
/// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen)
/// Storage: ParachainInfo ParachainId (r:1 w:0)
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:1 w:1)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:1 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn create_channel() -> Weight {
// Proof Size summary in bytes:
// Measured: `602`
// Estimated: `69050`
// Minimum execution time: 83_000_000 picoseconds.
Weight::from_parts(83_000_000, 69050)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
/// Storage: EthereumSystem Channels (r:1 w:0)
/// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:2 w:2)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:0)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn update_channel() -> Weight {
// Proof Size summary in bytes:
// Measured: `256`
// Estimated: `6044`
// Minimum execution time: 40_000_000 picoseconds.
Weight::from_parts(40_000_000, 6044)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: EthereumSystem Channels (r:1 w:0)
/// Proof: EthereumSystem Channels (max_values: None, max_size: Some(12), added: 2487, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:2 w:2)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:0)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn force_update_channel() -> Weight {
// Proof Size summary in bytes:
// Measured: `256`
// Estimated: `6044`
// Minimum execution time: 41_000_000 picoseconds.
Weight::from_parts(41_000_000, 6044)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: ParachainInfo ParachainId (r:1 w:0)
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:1 w:1)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:1)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn set_operating_mode() -> Weight {
// Proof Size summary in bytes:
// Measured: `80`
// Estimated: `3517`
// Minimum execution time: 31_000_000 picoseconds.
Weight::from_parts(31_000_000, 3517)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: EthereumSystem Agents (r:1 w:0)
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:2 w:2)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:0)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn transfer_native_from_agent() -> Weight {
// Proof Size summary in bytes:
// Measured: `252`
// Estimated: `6044`
// Minimum execution time: 45_000_000 picoseconds.
Weight::from_parts(45_000_000, 6044)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: EthereumSystem Agents (r:1 w:0)
/// Proof: EthereumSystem Agents (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:2 w:2)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:0)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn force_transfer_native_from_agent() -> Weight {
// Proof Size summary in bytes:
// Measured: `252`
// Estimated: `6044`
// Minimum execution time: 42_000_000 picoseconds.
Weight::from_parts(42_000_000, 6044)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: ParachainInfo ParachainId (r:1 w:0)
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:1 w:1)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:1)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn set_token_transfer_fees() -> Weight {
// Proof Size summary in bytes:
// Measured: `80`
// Estimated: `3517`
// Minimum execution time: 31_000_000 picoseconds.
Weight::from_parts(42_000_000, 3517)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: ParachainInfo ParachainId (r:1 w:0)
/// Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
/// Storage: EthereumOutboundQueue PalletOperatingMode (r:1 w:0)
/// Proof: EthereumOutboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
/// Storage: MessageQueue BookStateFor (r:1 w:1)
/// Proof: MessageQueue BookStateFor (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
/// Storage: MessageQueue ServiceHead (r:1 w:1)
/// Proof: MessageQueue ServiceHead (max_values: Some(1), max_size: Some(5), added: 500, mode: MaxEncodedLen)
/// Storage: MessageQueue Pages (r:0 w:1)
/// Proof: MessageQueue Pages (max_values: None, max_size: Some(65585), added: 68060, mode: MaxEncodedLen)
fn set_pricing_parameters() -> Weight {
// Proof Size summary in bytes:
// Measured: `80`
// Estimated: `3517`
// Minimum execution time: 31_000_000 picoseconds.
Weight::from_parts(42_000_000, 3517)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
}