diff --git a/Cargo.lock b/Cargo.lock
index 0a1cdf691..2c8ac5511 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3288,12 +3288,16 @@ dependencies = [
"polkadot-primitives",
"polkadot-runtime-common",
"polkadot-runtime-parachains",
+ "rand",
"scale-info",
"separator",
"serde",
"serde_derive",
"serde_json",
"smallvec",
+ "snowbridge-beacon-primitives",
+ "snowbridge-milagro-bls",
+ "snowbridge-pallet-ethereum-client",
"sp-api",
"sp-arithmetic",
"sp-authority-discovery",
@@ -4028,6 +4032,16 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "ethabi-decode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d398648d65820a727d6a81e58b962f874473396a047e4c30bafe3240953417"
+dependencies = [
+ "ethereum-types",
+ "tiny-keccak",
+]
+
[[package]]
name = "ethbloom"
version = "0.13.0"
@@ -10507,6 +10521,12 @@ dependencies = [
"unicode-normalization",
]
+[[package]]
+name = "parity-bytes"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b56e3a2420138bdb970f84dfb9c774aea80fa0e7371549eedec0d80c209c67"
+
[[package]]
name = "parity-db"
version = "0.4.13"
@@ -15119,6 +15139,15 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "serde-big-array"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd31f59f6fe2b0c055371bb2f16d7f0aa7d8881676c04a55b1596d1a17cd10a4"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "serde_bytes"
version = "0.11.14"
@@ -15569,6 +15598,134 @@ dependencies = [
"subtle 2.5.0",
]
+[[package]]
+name = "snowbridge-amcl"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460a9ed63cdf03c1b9847e8a12a5f5ba19c4efd5869e4a737e05be25d7c427e5"
+dependencies = [
+ "parity-scale-codec",
+ "scale-info",
+]
+
+[[package]]
+name = "snowbridge-beacon-primitives"
+version = "0.9.0"
+source = "git+https://github.com/moondance-labs/polkadot-sdk?branch=tanssi-polkadot-stable2407#28cdd1c8b6c59b1f72a934d04608bf8af82caa72"
+dependencies = [
+ "byte-slice-cast",
+ "frame-support",
+ "hex",
+ "parity-scale-codec",
+ "rlp",
+ "scale-info",
+ "serde",
+ "snowbridge-ethereum",
+ "snowbridge-milagro-bls",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+ "ssz_rs",
+ "ssz_rs_derive",
+]
+
+[[package]]
+name = "snowbridge-core"
+version = "0.9.0"
+source = "git+https://github.com/moondance-labs/polkadot-sdk?branch=tanssi-polkadot-stable2407#28cdd1c8b6c59b1f72a934d04608bf8af82caa72"
+dependencies = [
+ "ethabi-decode",
+ "frame-support",
+ "frame-system",
+ "hex-literal 0.4.1",
+ "parity-scale-codec",
+ "polkadot-parachain-primitives",
+ "scale-info",
+ "serde",
+ "snowbridge-beacon-primitives",
+ "sp-arithmetic",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+ "staging-xcm",
+ "staging-xcm-builder",
+]
+
+[[package]]
+name = "snowbridge-ethereum"
+version = "0.9.0"
+source = "git+https://github.com/moondance-labs/polkadot-sdk?branch=tanssi-polkadot-stable2407#28cdd1c8b6c59b1f72a934d04608bf8af82caa72"
+dependencies = [
+ "ethabi-decode",
+ "ethbloom",
+ "ethereum-types",
+ "hex-literal 0.4.1",
+ "parity-bytes",
+ "parity-scale-codec",
+ "rlp",
+ "scale-info",
+ "serde",
+ "serde-big-array",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+]
+
+[[package]]
+name = "snowbridge-milagro-bls"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "026aa8638f690a53e3f7676024b9e913b1cab0111d1b7b92669d40a188f9d7e6"
+dependencies = [
+ "hex",
+ "lazy_static",
+ "parity-scale-codec",
+ "rand",
+ "scale-info",
+ "snowbridge-amcl",
+ "zeroize",
+]
+
+[[package]]
+name = "snowbridge-pallet-ethereum-client"
+version = "0.9.0"
+source = "git+https://github.com/moondance-labs/polkadot-sdk?branch=tanssi-polkadot-stable2407#28cdd1c8b6c59b1f72a934d04608bf8af82caa72"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "hex-literal 0.4.1",
+ "log",
+ "pallet-timestamp",
+ "parity-scale-codec",
+ "scale-info",
+ "serde",
+ "serde_json",
+ "snowbridge-beacon-primitives",
+ "snowbridge-core",
+ "snowbridge-ethereum",
+ "snowbridge-pallet-ethereum-client-fixtures",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+ "static_assertions",
+]
+
+[[package]]
+name = "snowbridge-pallet-ethereum-client-fixtures"
+version = "0.17.0"
+source = "git+https://github.com/moondance-labs/polkadot-sdk?branch=tanssi-polkadot-stable2407#28cdd1c8b6c59b1f72a934d04608bf8af82caa72"
+dependencies = [
+ "hex-literal 0.4.1",
+ "snowbridge-beacon-primitives",
+ "snowbridge-core",
+ "sp-core",
+ "sp-std",
+]
+
[[package]]
name = "socket2"
version = "0.4.10"
@@ -16530,6 +16687,29 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "ssz_rs"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057291e5631f280978fa9c8009390663ca4613359fc1318e36a8c24c392f6d1f"
+dependencies = [
+ "bitvec",
+ "num-bigint",
+ "sha2 0.9.9",
+ "ssz_rs_derive",
+]
+
+[[package]]
+name = "ssz_rs_derive"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f07d54c4d01a1713eb363b55ba51595da15f6f1211435b71466460da022aa140"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
diff --git a/Cargo.toml b/Cargo.toml
index b15f706a7..7f295cd92 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -252,6 +252,12 @@ westend-runtime = { git = "https://github.com/moondance-labs/polkadot-sdk", bran
westend-runtime-constants = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407", default-features = false }
xcm-runtime-apis = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407", default-features = false }
+
+# Bridges (wasm)
+milagro-bls = { package = "snowbridge-milagro-bls", version = "1.5.4", default-features = false }
+snowbridge-beacon-primitives = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407", default-features = false }
+snowbridge-pallet-ethereum-client = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407", default-features = false }
+
# Polkadot (client)
polkadot-cli = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407" }
polkadot-node-subsystem = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407" }
diff --git a/solo-chains/runtime/dancelight/Cargo.toml b/solo-chains/runtime/dancelight/Cargo.toml
index 18a018646..edff6fa7c 100644
--- a/solo-chains/runtime/dancelight/Cargo.toml
+++ b/solo-chains/runtime/dancelight/Cargo.toml
@@ -138,8 +138,14 @@ tanssi-runtime-common = { workspace = true }
# Moonkit
pallet-migrations = { workspace = true }
+# Bridges
+snowbridge-beacon-primitives = { workspace = true }
+snowbridge-pallet-ethereum-client = { workspace = true }
+
[dev-dependencies]
keyring = { workspace = true }
+milagro-bls = { workspace = true, features = [ "std" ] }
+rand = { workspace = true, features = [ "std", "std_rng" ] }
remote-externalities = { workspace = true }
separator = { workspace = true }
serde_json = { workspace = true }
@@ -177,6 +183,7 @@ std = [
"inherents/std",
"keyring/std",
"log/std",
+ "milagro-bls/std",
"nimbus-primitives/std",
"offchain-primitives/std",
"pallet-asset-rate/std",
@@ -235,12 +242,16 @@ std = [
"parity-scale-codec/std",
"polkadot-parachain-primitives/std",
"primitives/std",
+ "rand/std",
"runtime-common/std",
"runtime-parachains/std",
"scale-info/std",
"serde/std",
"serde_derive",
"serde_json/std",
+ "snowbridge-beacon-primitives/std",
+ "snowbridge-pallet-ethereum-client/fuzzing",
+ "snowbridge-pallet-ethereum-client/std",
"sp-api/std",
"sp-arithmetic/std",
"sp-consensus-aura/std",
@@ -270,6 +281,7 @@ std = [
"xcm/std",
]
no_std = []
+
runtime-benchmarks = [
"cumulus-pallet-parachain-system/runtime-benchmarks",
"cumulus-primitives-core/runtime-benchmarks",
@@ -323,6 +335,7 @@ runtime-benchmarks = [
"primitives/runtime-benchmarks",
"runtime-common/runtime-benchmarks",
"runtime-parachains/runtime-benchmarks",
+ "snowbridge-pallet-ethereum-client/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
"tanssi-runtime-common/runtime-benchmarks",
@@ -389,6 +402,7 @@ try-runtime = [
"pallet-xcm/try-runtime",
"runtime-common/try-runtime",
"runtime-parachains/try-runtime",
+ "snowbridge-pallet-ethereum-client/try-runtime",
"sp-runtime/try-runtime",
"tanssi-runtime-common/try-runtime",
]
diff --git a/solo-chains/runtime/dancelight/src/bridge_to_ethereum_config.rs b/solo-chains/runtime/dancelight/src/bridge_to_ethereum_config.rs
new file mode 100644
index 000000000..887aa9df5
--- /dev/null
+++ b/solo-chains/runtime/dancelight/src/bridge_to_ethereum_config.rs
@@ -0,0 +1,91 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+//! The bridge to ethereum config
+
+pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32;
+use crate::{parameter_types, weights, Runtime, RuntimeEvent};
+use snowbridge_beacon_primitives::{Fork, ForkVersions};
+
+// For tests, benchmarks and fast-runtime configurations we use the mocked fork versions
+#[cfg(any(
+ feature = "std",
+ feature = "fast-runtime",
+ feature = "runtime-benchmarks",
+ test
+))]
+parameter_types! {
+ pub const ChainForkVersions: ForkVersions = ForkVersions {
+ genesis: Fork {
+ version: [0, 0, 0, 0], // 0x00000000
+ epoch: 0,
+ },
+ altair: Fork {
+ version: [1, 0, 0, 0], // 0x01000000
+ epoch: 0,
+ },
+ bellatrix: Fork {
+ version: [2, 0, 0, 0], // 0x02000000
+ epoch: 0,
+ },
+ capella: Fork {
+ version: [3, 0, 0, 0], // 0x03000000
+ epoch: 0,
+ },
+ deneb: Fork {
+ version: [4, 0, 0, 0], // 0x04000000
+ epoch: 0,
+ }
+ };
+}
+
+// Holesky: https://github.com/eth-clients/holesky
+#[cfg(not(any(
+ feature = "std",
+ feature = "fast-runtime",
+ feature = "runtime-benchmarks",
+ test
+)))]
+parameter_types! {
+ pub const ChainForkVersions: ForkVersions = ForkVersions {
+ genesis: Fork {
+ version: hex_literal::hex!("01017000"), // 0x01017000
+ epoch: 0,
+ },
+ altair: Fork {
+ version: hex_literal::hex!("01017000"), // 0x01017000
+ epoch: 0,
+ },
+ bellatrix: Fork {
+ version: hex_literal::hex!("01017000"), // 0x01017000
+ epoch: 0,
+ },
+ capella: Fork {
+ version: hex_literal::hex!("01017001"), // 0x01017001
+ epoch: 256,
+ },
+ deneb: Fork {
+ version: hex_literal::hex!("01017002"), // 0x01017002
+ epoch: 29696,
+ },
+ };
+}
+
+impl snowbridge_pallet_ethereum_client::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type ForkVersions = ChainForkVersions;
+ type WeightInfo = weights::snowbridge_pallet_ethereum_client::SubstrateWeight;
+}
diff --git a/solo-chains/runtime/dancelight/src/lib.rs b/solo-chains/runtime/dancelight/src/lib.rs
index 89281b3ea..e31216f5e 100644
--- a/solo-chains/runtime/dancelight/src/lib.rs
+++ b/solo-chains/runtime/dancelight/src/lib.rs
@@ -143,6 +143,8 @@ use dancelight_runtime_constants::{currency::*, fee::*, time::*};
// XCM configurations.
pub mod xcm_config;
+pub mod bridge_to_ethereum_config;
+
// Weights
mod weights;
@@ -1573,9 +1575,10 @@ construct_runtime! {
// BEEFY Bridges support.
Beefy: pallet_beefy = 240,
// MMR leaf construction must be after session in order to have a leaf's next_auth_set
- // refer to block. See issue polkadot-fellows/runtimes#160 for details.
+ // refer to block.
Mmr: pallet_mmr = 241,
MmrLeaf: pallet_beefy_mmr = 242,
+ EthereumBeaconClient: snowbridge_pallet_ethereum_client = 243,
ParasSudoWrapper: paras_sudo_wrapper = 250,
@@ -1903,6 +1906,9 @@ mod benches {
[pallet_xcm, PalletXcmExtrinsicsBenchmark::]
[pallet_xcm_benchmarks::fungible, pallet_xcm_benchmarks::fungible::Pallet::]
[pallet_xcm_benchmarks::generic, pallet_xcm_benchmarks::generic::Pallet::]
+
+ // Bridges
+ [snowbridge_pallet_ethereum_client, EthereumBeaconClient]
);
}
diff --git a/solo-chains/runtime/dancelight/src/tests/common/mod.rs b/solo-chains/runtime/dancelight/src/tests/common/mod.rs
index 85562f84d..fc873e13e 100644
--- a/solo-chains/runtime/dancelight/src/tests/common/mod.rs
+++ b/solo-chains/runtime/dancelight/src/tests/common/mod.rs
@@ -1179,3 +1179,13 @@ pub fn set_dummy_boot_node(para_manager: RuntimeOrigin, para_id: ParaId) {
"profile should be correctly assigned"
);
}
+use milagro_bls::Keypair;
+pub fn generate_ethereum_pub_keys(n: u32) -> Vec {
+ let mut keys = vec![];
+
+ for _i in 0..n {
+ let keypair = Keypair::random(&mut rand::thread_rng());
+ keys.push(keypair);
+ }
+ keys
+}
diff --git a/solo-chains/runtime/dancelight/src/tests/ethereum_client.rs b/solo-chains/runtime/dancelight/src/tests/ethereum_client.rs
new file mode 100644
index 000000000..6545750c2
--- /dev/null
+++ b/solo-chains/runtime/dancelight/src/tests/ethereum_client.rs
@@ -0,0 +1,309 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+#![cfg(test)]
+
+use {
+ crate::tests::common::*,
+ crate::EthereumBeaconClient,
+ frame_support::{assert_noop, assert_ok},
+ snowbridge_pallet_ethereum_client::functions::*,
+ snowbridge_pallet_ethereum_client::mock::*,
+ sp_core::H256,
+ sp_std::vec,
+};
+#[test]
+fn test_ethereum_force_checkpoint() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ // Alice gets 10k extra tokens for her mapping deposit
+ (AccountId::from(ALICE), 210_000 * UNIT),
+ (AccountId::from(BOB), 100_000 * UNIT),
+ (AccountId::from(CHARLIE), 100_000 * UNIT),
+ (AccountId::from(DAVE), 100_000 * UNIT),
+ ])
+ .build()
+ .execute_with(|| {
+ // This tests submits the initial checkpoint that contains the initial sync committee
+ let checkpoint =
+ Box::new(snowbridge_pallet_ethereum_client::mock::load_checkpoint_update_fixture());
+ assert_ok!(EthereumBeaconClient::force_checkpoint(
+ root_origin(),
+ checkpoint.clone()
+ ));
+ // assert checkpoint has updated storages
+ assert_eq!(
+ EthereumBeaconClient::initial_checkpoint_root(),
+ checkpoint.header.hash_tree_root().unwrap()
+ );
+ // sync committee is correct
+ let unwrap_keys: Vec =
+ snowbridge_pallet_ethereum_client::CurrentSyncCommittee::::get()
+ .pubkeys
+ .iter()
+ .map(|key| {
+ let unwrapped = key.as_bytes();
+ let pubkey: snowbridge_beacon_primitives::PublicKey = unwrapped.into();
+ pubkey
+ })
+ .collect();
+ assert_eq!(
+ unwrap_keys,
+ checkpoint.current_sync_committee.pubkeys.to_vec()
+ );
+ })
+}
+
+#[test]
+fn test_invalid_initial_checkpoint() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ // Alice gets 10k extra tokens for her mapping deposit
+ (AccountId::from(ALICE), 210_000 * UNIT),
+ (AccountId::from(BOB), 100_000 * UNIT),
+ (AccountId::from(CHARLIE), 100_000 * UNIT),
+ (AccountId::from(DAVE), 100_000 * UNIT),
+ ])
+ .build()
+ .execute_with(|| {
+ let mut checkpoint_invalid_sync_committee_proof = Box::new(snowbridge_pallet_ethereum_client::mock::load_checkpoint_update_fixture());
+
+ let mut checkpoint_invalid_blocks_root_proof = checkpoint_invalid_sync_committee_proof.clone();
+
+ let mut check_invalid_sync_committee = checkpoint_invalid_sync_committee_proof.clone();
+
+ checkpoint_invalid_sync_committee_proof.current_sync_committee_branch[0] = H256::default();
+ checkpoint_invalid_blocks_root_proof.block_roots_branch[0] = H256::default();
+ let new_random_keys: Vec = generate_ethereum_pub_keys(snowbridge_pallet_ethereum_client::config::SYNC_COMMITTEE_SIZE as u32).iter().map(|key| {
+ let public: snowbridge_beacon_primitives::PublicKey = key.pk.as_bytes().into();
+ public
+ }).collect();
+ check_invalid_sync_committee.current_sync_committee.pubkeys = new_random_keys.try_into().expect("cannot convert keys");
+ assert_noop!(
+ EthereumBeaconClient::force_checkpoint(root_origin(), checkpoint_invalid_sync_committee_proof),
+ snowbridge_pallet_ethereum_client::Error::::InvalidSyncCommitteeMerkleProof
+ );
+
+ assert_noop!(
+ EthereumBeaconClient::force_checkpoint(root_origin(), checkpoint_invalid_blocks_root_proof),
+ snowbridge_pallet_ethereum_client::Error::::InvalidBlockRootsRootMerkleProof
+ );
+
+ assert_noop!(
+ EthereumBeaconClient::force_checkpoint(root_origin(), check_invalid_sync_committee),
+ snowbridge_pallet_ethereum_client::Error::::InvalidSyncCommitteeMerkleProof
+ );
+ });
+}
+
+#[test]
+fn test_submit_update_using_same_committee_same_checkpoint() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ // Alice gets 10k extra tokens for her mapping deposit
+ (AccountId::from(ALICE), 210_000 * UNIT),
+ (AccountId::from(BOB), 100_000 * UNIT),
+ (AccountId::from(CHARLIE), 100_000 * UNIT),
+ (AccountId::from(DAVE), 100_000 * UNIT),
+ ])
+ .build()
+ .execute_with(|| {
+ // This tests submits a new header signed by the sync committee members within the same
+ // period BUT without injecting the next sync committee
+ let initial_checkpoint =
+ Box::new(snowbridge_pallet_ethereum_client::mock::load_checkpoint_update_fixture());
+ let update_header = Box::new(
+ snowbridge_pallet_ethereum_client::mock::load_finalized_header_update_fixture(),
+ );
+
+ let initial_period = compute_period(initial_checkpoint.header.slot);
+ let update_period = compute_period(update_header.finalized_header.slot);
+ assert_eq!(initial_period, update_period);
+ assert_ok!(EthereumBeaconClient::force_checkpoint(
+ root_origin(),
+ initial_checkpoint.clone()
+ ));
+ assert_ok!(EthereumBeaconClient::submit(
+ origin_of(ALICE.into()),
+ update_header.clone()
+ ));
+ let block_root: H256 = update_header.finalized_header.hash_tree_root().unwrap();
+ assert!(snowbridge_pallet_ethereum_client::FinalizedBeaconState::<
+ Runtime,
+ >::contains_key(block_root));
+ });
+}
+
+#[test]
+fn test_submit_update_with_next_sync_committee_in_current_period() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ // Alice gets 10k extra tokens for her mapping deposit
+ (AccountId::from(ALICE), 210_000 * UNIT),
+ (AccountId::from(BOB), 100_000 * UNIT),
+ (AccountId::from(CHARLIE), 100_000 * UNIT),
+ (AccountId::from(DAVE), 100_000 * UNIT),
+ ])
+ .build()
+ .execute_with(|| {
+ // This tests submits a new header signed by the sync committee members within the same
+ // period AND injecting the next sync committee
+ let initial_checkpoint = Box::new(load_checkpoint_update_fixture());
+ let update_header = Box::new(load_sync_committee_update_fixture());
+ let initial_period = compute_period(initial_checkpoint.header.slot);
+ let update_period = compute_period(update_header.finalized_header.slot);
+ assert_eq!(initial_period, update_period);
+ assert_ok!(EthereumBeaconClient::force_checkpoint(
+ root_origin(),
+ initial_checkpoint.clone()
+ ));
+ assert!(!snowbridge_pallet_ethereum_client::NextSyncCommittee::<
+ Runtime,
+ >::exists());
+ assert_ok!(EthereumBeaconClient::submit(
+ origin_of(ALICE.into()),
+ update_header.clone()
+ ));
+ assert!(snowbridge_pallet_ethereum_client::NextSyncCommittee::<
+ Runtime,
+ >::exists());
+ });
+}
+
+#[test]
+fn test_submit_update_with_next_sync_committee_in_current_period_without_majority() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ // Alice gets 10k extra tokens for her mapping deposit
+ (AccountId::from(ALICE), 210_000 * UNIT),
+ (AccountId::from(BOB), 100_000 * UNIT),
+ (AccountId::from(CHARLIE), 100_000 * UNIT),
+ (AccountId::from(DAVE), 100_000 * UNIT),
+ ])
+ .build()
+ .execute_with(|| {
+ // This tests submits a new header signed by the sync committee members within the same
+ // period BUT putting all signed bits to 0
+ let initial_checkpoint = Box::new(load_checkpoint_update_fixture());
+ let mut update_header = Box::new(load_sync_committee_update_fixture());
+ update_header.sync_aggregate.sync_committee_bits = [0u8; snowbridge_pallet_ethereum_client::config::SYNC_COMMITTEE_BITS_SIZE];
+ let initial_period = compute_period(initial_checkpoint.header.slot);
+ let update_period = compute_period(update_header.finalized_header.slot);
+ assert_eq!(initial_period, update_period);
+ assert_ok!(EthereumBeaconClient::force_checkpoint(
+ root_origin(),
+ initial_checkpoint.clone()
+ ));
+ assert_noop!(EthereumBeaconClient::submit(origin_of(ALICE.into()), update_header.clone()), snowbridge_pallet_ethereum_client::Error::::SyncCommitteeParticipantsNotSupermajority);
+ });
+}
+
+#[test]
+fn test_submit_update_in_next_period() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ // Alice gets 10k extra tokens for her mapping deposit
+ (AccountId::from(ALICE), 210_000 * UNIT),
+ (AccountId::from(BOB), 100_000 * UNIT),
+ (AccountId::from(CHARLIE), 100_000 * UNIT),
+ (AccountId::from(DAVE), 100_000 * UNIT),
+ ])
+ .build()
+ .execute_with(|| {
+ // This test submits, assuming current period is n:
+ // 1. A header in the next period n+1 but without having set the next sync committee, which fails
+ // 2. Then submits a proper sync committee update for this period (n), indicating who the next sync committee will be
+ // 3. Then submits an update on the next period (n+1), but without indicating who the next committee is going to be again (in period n+2), which fails
+ // 4. Then submits a header on the next period(n+1), this time indicating who the next sync committee is going to be
+ // 4. Then submits an update on the next period(n+1)
+
+ // Generate fixtures using a separate thread with 50MB stack size because those structs are huge
+ let (
+ initial_checkpoint,
+ sync_committee_update,
+ next_sync_committee_update,
+ next_update,
+ ) = with_increased_stack_size(50, || {
+ let initial_checkpoint = Box::new(load_checkpoint_update_fixture());
+ let sync_committee_update = Box::new(load_sync_committee_update_fixture());
+ let next_sync_committee_update =
+ Box::new(load_next_sync_committee_update_fixture());
+ let next_update = Box::new(load_next_finalized_header_update_fixture());
+
+ (
+ initial_checkpoint,
+ sync_committee_update,
+ next_sync_committee_update,
+ next_update,
+ )
+ });
+
+ let initial_period = compute_period(initial_checkpoint.header.slot);
+
+ assert_ok!(EthereumBeaconClient::force_checkpoint(
+ root_origin(),
+ initial_checkpoint.clone()
+ ));
+
+ // we need an update about the sync committee before we proceed
+ assert_noop!(
+ EthereumBeaconClient::submit(origin_of(ALICE.into()), next_update.clone()),
+ snowbridge_pallet_ethereum_client::Error::::SkippedSyncCommitteePeriod
+ );
+
+ assert_ok!(EthereumBeaconClient::submit(
+ origin_of(ALICE.into()),
+ sync_committee_update.clone()
+ ));
+
+ // we need an update about the next sync committee
+ assert_noop!(
+ EthereumBeaconClient::submit(origin_of(ALICE.into()), next_update.clone()),
+ snowbridge_pallet_ethereum_client::Error::::SyncCommitteeUpdateRequired
+ );
+
+ assert_ok!(EthereumBeaconClient::submit(
+ origin_of(ALICE.into()),
+ next_sync_committee_update.clone()
+ ));
+
+ // check we are now in period +1
+ let latest_finalized_block_root =
+ snowbridge_pallet_ethereum_client::LatestFinalizedBlockRoot::::get();
+ let last_finalized_state = snowbridge_pallet_ethereum_client::FinalizedBeaconState::<
+ Runtime,
+ >::get(latest_finalized_block_root)
+ .unwrap();
+ let last_synced_period = compute_period(last_finalized_state.slot);
+ assert_eq!(last_synced_period, initial_period + 1);
+
+ assert_ok!(EthereumBeaconClient::submit(
+ origin_of(ALICE.into()),
+ next_update.clone()
+ ));
+ });
+}
+
+fn with_increased_stack_size(stack_size_mb: usize, closure: F) -> T
+where
+ F: FnOnce() -> T + Send + 'static,
+ T: Send + 'static,
+{
+ let builder = std::thread::Builder::new().stack_size(stack_size_mb * 1024 * 1024);
+ let handle = builder.spawn(closure).unwrap();
+
+ handle.join().unwrap()
+}
diff --git a/solo-chains/runtime/dancelight/src/tests/mod.rs b/solo-chains/runtime/dancelight/src/tests/mod.rs
index 4bfb9e8b2..48445c337 100644
--- a/solo-chains/runtime/dancelight/src/tests/mod.rs
+++ b/solo-chains/runtime/dancelight/src/tests/mod.rs
@@ -24,6 +24,7 @@ mod author_noting_tests;
mod collator_assignment_tests;
mod common;
mod core_scheduling_tests;
+mod ethereum_client;
mod inflation_rewards;
mod integration_test;
mod relay_configuration;
diff --git a/solo-chains/runtime/dancelight/src/weights/mod.rs b/solo-chains/runtime/dancelight/src/weights/mod.rs
index 99e117472..5e91a36c3 100644
--- a/solo-chains/runtime/dancelight/src/weights/mod.rs
+++ b/solo-chains/runtime/dancelight/src/weights/mod.rs
@@ -45,3 +45,4 @@ pub mod runtime_parachains_inclusion;
pub mod runtime_parachains_initializer;
pub mod runtime_parachains_paras;
pub mod runtime_parachains_paras_inherent;
+pub mod snowbridge_pallet_ethereum_client;
diff --git a/solo-chains/runtime/dancelight/src/weights/snowbridge_pallet_ethereum_client.rs b/solo-chains/runtime/dancelight/src/weights/snowbridge_pallet_ethereum_client.rs
new file mode 100644
index 000000000..7c9bceeaf
--- /dev/null
+++ b/solo-chains/runtime/dancelight/src/weights/snowbridge_pallet_ethereum_client.rs
@@ -0,0 +1,123 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+
+//! Autogenerated weights for snowbridge_pallet_ethereum_client
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 42.0.0
+//! DATE: 2024-10-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `girazoki-XPS-15-9530`, CPU: `13th Gen Intel(R) Core(TM) i9-13900H`
+//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
+
+// Executed Command:
+// target/release/tanssi-relay
+// benchmark
+// pallet
+// --execution=wasm
+// --wasm-execution=compiled
+// --pallet
+// snowbridge_pallet_ethereum_client
+// --extrinsic
+// *
+// --chain=dev
+// --steps
+// 50
+// --repeat
+// 20
+// --template=./benchmarking/frame-weight-runtime-template.hbs
+// --json-file
+// raw.json
+// --output
+// tmp/snowbridge_pallet_ethereum_client.rs
+
+#![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;
+
+/// Weights for snowbridge_pallet_ethereum_client using the Substrate node and recommended hardware.
+pub struct SubstrateWeight(PhantomData);
+impl snowbridge_pallet_ethereum_client::WeightInfo for SubstrateWeight {
+ /// Storage: `EthereumBeaconClient::FinalizedBeaconStateIndex` (r:1 w:1)
+ /// Proof: `EthereumBeaconClient::FinalizedBeaconStateIndex` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::FinalizedBeaconStateMapping` (r:1 w:1)
+ /// Proof: `EthereumBeaconClient::FinalizedBeaconStateMapping` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::NextSyncCommittee` (r:0 w:1)
+ /// Proof: `EthereumBeaconClient::NextSyncCommittee` (`max_values`: Some(1), `max_size`: Some(92372), added: 92867, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::InitialCheckpointRoot` (r:0 w:1)
+ /// Proof: `EthereumBeaconClient::InitialCheckpointRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::ValidatorsRoot` (r:0 w:1)
+ /// Proof: `EthereumBeaconClient::ValidatorsRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:0 w:1)
+ /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::CurrentSyncCommittee` (r:0 w:1)
+ /// Proof: `EthereumBeaconClient::CurrentSyncCommittee` (`max_values`: Some(1), `max_size`: Some(92372), added: 92867, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:0 w:1)
+ /// Proof: `EthereumBeaconClient::FinalizedBeaconState` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
+ fn force_checkpoint() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `109`
+ // Estimated: `3501`
+ // Minimum execution time: 74_948_835_000 picoseconds.
+ Weight::from_parts(75_200_136_000, 3501)
+ .saturating_add(T::DbWeight::get().reads(2_u64))
+ .saturating_add(T::DbWeight::get().writes(8_u64))
+ }
+ /// Storage: `EthereumBeaconClient::OperatingMode` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::FinalizedBeaconState` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::NextSyncCommittee` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::NextSyncCommittee` (`max_values`: Some(1), `max_size`: Some(92372), added: 92867, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::CurrentSyncCommittee` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::CurrentSyncCommittee` (`max_values`: Some(1), `max_size`: Some(92372), added: 92867, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::ValidatorsRoot` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::ValidatorsRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ fn submit() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `92782`
+ // Estimated: `93857`
+ // Minimum execution time: 18_237_384_000 picoseconds.
+ Weight::from_parts(18_313_625_000, 93857)
+ .saturating_add(T::DbWeight::get().reads(6_u64))
+ }
+ /// Storage: `EthereumBeaconClient::OperatingMode` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::FinalizedBeaconState` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::NextSyncCommittee` (r:1 w:1)
+ /// Proof: `EthereumBeaconClient::NextSyncCommittee` (`max_values`: Some(1), `max_size`: Some(92372), added: 92867, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::CurrentSyncCommittee` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::CurrentSyncCommittee` (`max_values`: Some(1), `max_size`: Some(92372), added: 92867, mode: `MaxEncodedLen`)
+ /// Storage: `EthereumBeaconClient::ValidatorsRoot` (r:1 w:0)
+ /// Proof: `EthereumBeaconClient::ValidatorsRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+ fn submit_with_sync_committee() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `92782`
+ // Estimated: `93857`
+ // Minimum execution time: 93_102_798_000 picoseconds.
+ Weight::from_parts(93_339_834_000, 93857)
+ .saturating_add(T::DbWeight::get().reads(6_u64))
+ .saturating_add(T::DbWeight::get().writes(1_u64))
+ }
+}
\ No newline at end of file
diff --git a/test/moonwall.config.json b/test/moonwall.config.json
index 84d97d283..faa710c1d 100644
--- a/test/moonwall.config.json
+++ b/test/moonwall.config.json
@@ -51,6 +51,7 @@
"name": "dev_tanssi_relay",
"envVars": ["DEBUG_COLORS=1"],
"testFileDir": ["suites/dev-tanssi-relay", "suites/common-all", "suites/common-tanssi"],
+ "runScripts": ["download-ethereum-client-test-files.sh"],
"multiThreads": true,
"timeout": 240000,
"reporters": ["basic"],
diff --git a/test/scripts/download-ethereum-client-test-files.sh b/test/scripts/download-ethereum-client-test-files.sh
new file mode 100755
index 000000000..008ede140
--- /dev/null
+++ b/test/scripts/download-ethereum-client-test-files.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# Exit on any error
+set -e
+
+# Always run the commands from the "test" dir
+cd $(dirname $0)/..
+
+# Grab Polkadot version
+branch=$(egrep -o '/polkadot-sdk.*#([^\"]*)' ../Cargo.lock | head -1)
+polkadot_release=$(echo $branch | sed 's/.*branch=//' | sed 's/#.*//')
+if [ -f tmp/ethereum_client_test/latest_version.txt ]; then
+ stored_version=$(< tmp/ethereum_client_test/latest_version.txt)
+ if [[ "$polkadot_release" == "$stored_version" ]]; then
+ echo "Stored version is latest, nothing to do"
+ exit 0;
+ fi
+fi
+mkdir -p tmp
+wget -O - tmp/ethereum_client_test https://github.com/moondance-labs/polkadot-sdk/archive/$polkadot_release.tar.gz | tar -xz --strip=6 "polkadot-sdk-$polkadot_release/bridges/snowbridge/pallets/ethereum-client/tests/fixtures"
+# remove for a clean move
+rm -rf tmp/ethereum_client_test
+mv -u fixtures tmp/ethereum_client_test
+echo $polkadot_release > tmp/ethereum_client_test/latest_version.txt
\ No newline at end of file
diff --git a/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-initial-checkpoint.ts b/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-initial-checkpoint.ts
new file mode 100644
index 000000000..b955ecc32
--- /dev/null
+++ b/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-initial-checkpoint.ts
@@ -0,0 +1,43 @@
+import "@tanssi/api-augment";
+import { describeSuite, expect, beforeAll } from "@moonwall/cli";
+import { ApiPromise } from "@polkadot/api";
+import { readFileSync } from "fs";
+import { KeyringPair } from "@moonwall/util";
+
+describeSuite({
+ id: "DTR1201",
+ title: "Ethereum Beacon Client tests",
+ foundationMethods: "dev",
+
+ testCases: ({ it, context }) => {
+ let polkadotJs: ApiPromise;
+ let alice: KeyringPair;
+
+ beforeAll(async () => {
+ polkadotJs = context.polkadotJs();
+ alice = context.keyring.alice;
+ });
+
+ it({
+ id: "E01",
+ title: "Ethreum client should accept an intiial checkpoint",
+ test: async function () {
+ const initialCheckpoint = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/initial-checkpoint.json").toString()
+ );
+ const tx = polkadotJs.tx.ethereumBeaconClient.forceCheckpoint(initialCheckpoint);
+ const signedTx = await polkadotJs.tx.sudo.sudo(tx).signAsync(alice);
+ await context.createBlock([signedTx]);
+ const checkpointRoot = await polkadotJs.query.ethereumBeaconClient.validatorsRoot();
+ expect(checkpointRoot.toHuman()).to.equal(initialCheckpoint["validators_root"]);
+
+ const latestFinalizedBlockRoot = await polkadotJs.query.ethereumBeaconClient.latestFinalizedBlockRoot();
+ const latestFinalizedSlot = await polkadotJs.query.ethereumBeaconClient.finalizedBeaconState(
+ latestFinalizedBlockRoot
+ );
+
+ expect(latestFinalizedSlot.toHuman().slot).to.equal(initialCheckpoint["header"]["slot"].toString());
+ },
+ });
+ },
+});
diff --git a/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-submit-update-for-next-period-without-committee.ts b/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-submit-update-for-next-period-without-committee.ts
new file mode 100644
index 000000000..ee4c859c5
--- /dev/null
+++ b/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-submit-update-for-next-period-without-committee.ts
@@ -0,0 +1,56 @@
+import "@tanssi/api-augment";
+import { describeSuite, expect, beforeAll } from "@moonwall/cli";
+import { ApiPromise } from "@polkadot/api";
+import { readFileSync } from "fs";
+import { KeyringPair } from "@moonwall/util";
+
+describeSuite({
+ id: "DTR1202",
+ title: "Ethereum Beacon Client tests",
+ foundationMethods: "dev",
+
+ testCases: ({ it, context }) => {
+ let polkadotJs: ApiPromise;
+ let alice: KeyringPair;
+ let initialSlot;
+ String;
+
+ beforeAll(async () => {
+ polkadotJs = context.polkadotJs();
+ alice = context.keyring.alice;
+
+ const initialCheckpoint = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/initial-checkpoint.json").toString()
+ );
+ initialSlot = initialCheckpoint["header"]["slot"].toString();
+ const tx = polkadotJs.tx.ethereumBeaconClient.forceCheckpoint(initialCheckpoint);
+ const signedTx = await polkadotJs.tx.sudo.sudo(tx).signAsync(alice);
+ await context.createBlock([signedTx]);
+ });
+
+ it({
+ id: "E02",
+ title: "Ethreum client should not be able to receive an update for the next period without the next sync committee",
+ test: async function () {
+ const nextPeriodUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/next-finalized-header-update.json").toString()
+ );
+ const tx = polkadotJs.tx.ethereumBeaconClient.submit(nextPeriodUpdate);
+ const signedTx = await tx.signAsync(alice);
+ const { result } = await context.createBlock([signedTx]);
+
+ expect(result[0].successful).to.be.false;
+ expect(result[0].error.section).to.eq("ethereumBeaconClient");
+ expect(result[0].error.name).to.eq("SkippedSyncCommitteePeriod");
+
+ const latestFinalizedBlockRoot = await polkadotJs.query.ethereumBeaconClient.latestFinalizedBlockRoot();
+ const latestFinalizedSlot = await polkadotJs.query.ethereumBeaconClient.finalizedBeaconState(
+ latestFinalizedBlockRoot
+ );
+
+ // The update did not go through, so the slot is the same as the initial one
+ expect(latestFinalizedSlot.toHuman().slot).to.equal(initialSlot);
+ },
+ });
+ },
+});
diff --git a/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-submit-update-same-period.ts b/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-submit-update-same-period.ts
new file mode 100644
index 000000000..c6bdd0c0b
--- /dev/null
+++ b/test/suites/dev-tanssi-relay/eth-client/test-eth-ciient-submit-update-same-period.ts
@@ -0,0 +1,50 @@
+import "@tanssi/api-augment";
+import { describeSuite, expect, beforeAll } from "@moonwall/cli";
+import { ApiPromise } from "@polkadot/api";
+import { readFileSync } from "fs";
+import { KeyringPair } from "@moonwall/util";
+
+describeSuite({
+ id: "DTR1203",
+ title: "Ethereum Beacon Client tests",
+ foundationMethods: "dev",
+
+ testCases: ({ it, context }) => {
+ let polkadotJs: ApiPromise;
+ let alice: KeyringPair;
+
+ beforeAll(async () => {
+ polkadotJs = context.polkadotJs();
+ alice = context.keyring.alice;
+
+ const initialCheckpoint = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/initial-checkpoint.json").toString()
+ );
+ const tx = polkadotJs.tx.ethereumBeaconClient.forceCheckpoint(initialCheckpoint);
+ const signedTx = await polkadotJs.tx.sudo.sudo(tx).signAsync(alice);
+ await context.createBlock([signedTx]);
+ });
+
+ it({
+ id: "E02",
+ title: "Ethreum client should be able to receive an update within the same period by same committee",
+ test: async function () {
+ const samePeriodUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/finalized-header-update.json").toString()
+ );
+ const tx = polkadotJs.tx.ethereumBeaconClient.submit(samePeriodUpdate);
+ const signedTx = await tx.signAsync(alice);
+ await context.createBlock([signedTx]);
+
+ const latestFinalizedBlockRoot = await polkadotJs.query.ethereumBeaconClient.latestFinalizedBlockRoot();
+ const latestFinalizedSlot = await polkadotJs.query.ethereumBeaconClient.finalizedBeaconState(
+ latestFinalizedBlockRoot
+ );
+
+ expect(latestFinalizedSlot.toHuman().slot).to.equal(
+ samePeriodUpdate["finalized_header"]["slot"].toString()
+ );
+ },
+ });
+ },
+});
diff --git a/test/suites/dev-tanssi-relay/eth-client/test-eth-client-submit-update-cannot-be-done-without-injecting-new-committee.ts b/test/suites/dev-tanssi-relay/eth-client/test-eth-client-submit-update-cannot-be-done-without-injecting-new-committee.ts
new file mode 100644
index 000000000..2230da068
--- /dev/null
+++ b/test/suites/dev-tanssi-relay/eth-client/test-eth-client-submit-update-cannot-be-done-without-injecting-new-committee.ts
@@ -0,0 +1,79 @@
+import "@tanssi/api-augment";
+import { describeSuite, expect, beforeAll } from "@moonwall/cli";
+import { ApiPromise } from "@polkadot/api";
+import { readFileSync } from "fs";
+import { KeyringPair } from "@moonwall/util";
+
+describeSuite({
+ id: "DTR1204",
+ title: "Ethereum Beacon Client tests",
+ foundationMethods: "dev",
+
+ testCases: ({ it, context }) => {
+ let polkadotJs: ApiPromise;
+ let alice: KeyringPair;
+ let initialSlot;
+ String;
+
+ beforeAll(async () => {
+ polkadotJs = context.polkadotJs();
+ alice = context.keyring.alice;
+
+ const initialCheckpoint = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/initial-checkpoint.json").toString()
+ );
+ initialSlot = initialCheckpoint["header"]["slot"].toString();
+ const tx = polkadotJs.tx.ethereumBeaconClient.forceCheckpoint(initialCheckpoint);
+ const signedTx = await polkadotJs.tx.sudo.sudo(tx).signAsync(alice);
+ await context.createBlock([signedTx]);
+ });
+
+ it({
+ id: "E01",
+ title: "Ethreum client should not be able to receive an update for the next period without pushing the following sync committee",
+ test: async function () {
+ // Next sync committee shold give us the default values
+ const nextSyncCommitteeBeforeUpdate = await polkadotJs.query.ethereumBeaconClient.nextSyncCommittee();
+ expect(nextSyncCommitteeBeforeUpdate.root.toHuman()).to.be.eq(
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ );
+
+ const thisPeriodNextSyncCommitteeUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/sync-committee-update.json").toString()
+ );
+ await context.createBlock([
+ await polkadotJs.tx.ethereumBeaconClient.submit(thisPeriodNextSyncCommitteeUpdate).signAsync(alice),
+ ]);
+
+ // Now the next sync committee should have been populated
+ const nextSyncCommittee = await polkadotJs.query.ethereumBeaconClient.nextSyncCommittee();
+ expect(nextSyncCommittee.root.toHuman()).to.not.be.eq(
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ );
+
+ // Now we are injecting an update for the next period, but without specifying who the next committee is.
+ // this will fail, if you push an update in a new period, you always need to push the new sync committee
+ const nextPeriodUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/next-finalized-header-update.json").toString()
+ );
+ const tx = polkadotJs.tx.ethereumBeaconClient.submit(nextPeriodUpdate);
+ const signedTx = await tx.signAsync(alice);
+ const { result } = await context.createBlock([signedTx]);
+
+ expect(result[0].successful).to.be.false;
+ expect(result[0].error.section).to.eq("ethereumBeaconClient");
+ expect(result[0].error.name).to.eq("SyncCommitteeUpdateRequired");
+
+ const latestFinalizedBlockRoot = await polkadotJs.query.ethereumBeaconClient.latestFinalizedBlockRoot();
+ const latestFinalizedSlot = await polkadotJs.query.ethereumBeaconClient.finalizedBeaconState(
+ latestFinalizedBlockRoot
+ );
+
+ // The update did not go through, so the slot is the same as the latest one we pushed
+ // The sync committee update has a a finalized slot lower than the initial, so we keep the
+ // initial
+ expect(latestFinalizedSlot.toHuman().slot).to.equal(initialSlot);
+ },
+ });
+ },
+});
diff --git a/test/suites/dev-tanssi-relay/eth-client/test-eth-client-submit-update-next-period-with-proper-committees.ts b/test/suites/dev-tanssi-relay/eth-client/test-eth-client-submit-update-next-period-with-proper-committees.ts
new file mode 100644
index 000000000..09a286f8d
--- /dev/null
+++ b/test/suites/dev-tanssi-relay/eth-client/test-eth-client-submit-update-next-period-with-proper-committees.ts
@@ -0,0 +1,82 @@
+import "@tanssi/api-augment";
+import { describeSuite, expect, beforeAll } from "@moonwall/cli";
+import { ApiPromise } from "@polkadot/api";
+import { readFileSync } from "fs";
+import { KeyringPair } from "@moonwall/util";
+
+describeSuite({
+ id: "DTR1205",
+ title: "Ethereum Beacon Client tests",
+ foundationMethods: "dev",
+
+ testCases: ({ it, context }) => {
+ let polkadotJs: ApiPromise;
+ let alice: KeyringPair;
+
+ beforeAll(async () => {
+ polkadotJs = context.polkadotJs();
+ alice = context.keyring.alice;
+
+ const initialCheckpoint = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/initial-checkpoint.json").toString()
+ );
+ const tx = polkadotJs.tx.ethereumBeaconClient.forceCheckpoint(initialCheckpoint);
+ const signedTx = await polkadotJs.tx.sudo.sudo(tx).signAsync(alice);
+ await context.createBlock([signedTx]);
+ });
+
+ it({
+ id: "E01",
+ title: "Ethreum client should be able to receive an update for the next period when pushing all committee info",
+ test: async function () {
+ // Next sync committee shold give us the default values
+ const nextSyncCommitteeBeforeUpdate = await polkadotJs.query.ethereumBeaconClient.nextSyncCommittee();
+ expect(nextSyncCommitteeBeforeUpdate.root.toHuman()).to.be.eq(
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ );
+
+ const thisPeriodNextSyncCommitteeUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/sync-committee-update.json").toString()
+ );
+ await context.createBlock([
+ await polkadotJs.tx.ethereumBeaconClient.submit(thisPeriodNextSyncCommitteeUpdate).signAsync(alice),
+ ]);
+
+ // Now the next sync committee should have been populated
+ const nextSyncCommittee = await polkadotJs.query.ethereumBeaconClient.nextSyncCommittee();
+ expect(nextSyncCommittee.root.toHuman()).to.not.be.eq(
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ );
+
+ // Now we are injecting the first update of the next period
+ // this should contain the next sync committee
+ const nextPeriodSyncCommitteeUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/next-sync-committee-update.json").toString()
+ );
+ await context.createBlock([
+ await polkadotJs.tx.ethereumBeaconClient.submit(nextPeriodSyncCommitteeUpdate).signAsync(alice),
+ ]);
+
+ // Now we are injecting an update for the period 'intial period +1' for which
+ // we have already pushed the sync committee update. Since this needs to be done
+ // only once per period, we shoudl be good
+ const nextPeriodUpdate = JSON.parse(
+ readFileSync("tmp/ethereum_client_test/next-finalized-header-update.json").toString()
+ );
+
+ await context.createBlock([
+ await polkadotJs.tx.ethereumBeaconClient.submit(nextPeriodUpdate).signAsync(alice),
+ ]);
+
+ const latestFinalizedBlockRoot = await polkadotJs.query.ethereumBeaconClient.latestFinalizedBlockRoot();
+ const latestFinalizedSlot = await polkadotJs.query.ethereumBeaconClient.finalizedBeaconState(
+ latestFinalizedBlockRoot
+ );
+
+ // The update did go through, we should have the latest state
+ const expectedSlot = nextPeriodUpdate["finalized_header"]["slot"];
+ expect(latestFinalizedSlot.toHuman().slot.replace(/,/g, "")).to.equal(expectedSlot.toString());
+ },
+ });
+ },
+});
diff --git a/typescript-api/src/dancelight/interfaces/augment-api-consts.ts b/typescript-api/src/dancelight/interfaces/augment-api-consts.ts
index 4da72612c..6a5c3382f 100644
--- a/typescript-api/src/dancelight/interfaces/augment-api-consts.ts
+++ b/typescript-api/src/dancelight/interfaces/augment-api-consts.ts
@@ -14,6 +14,7 @@ import type {
FrameSystemLimitsBlockLength,
FrameSystemLimitsBlockWeights,
PalletReferendaTrackInfo,
+ SnowbridgeBeaconPrimitivesForkVersions,
SpVersionRuntimeVersion,
SpWeightsRuntimeDbWeight,
SpWeightsWeightV2Weight,
@@ -127,6 +128,11 @@ declare module "@polkadot/api-base/types/consts" {
/** Generic const */
[key: string]: Codec;
};
+ ethereumBeaconClient: {
+ forkVersions: SnowbridgeBeaconPrimitivesForkVersions & AugmentedConst;
+ /** Generic const */
+ [key: string]: Codec;
+ };
fellowshipReferenda: {
/**
* Quantization level for the referendum wakeup scheduler. A higher number will result in fewer storage
diff --git a/typescript-api/src/dancelight/interfaces/augment-api-errors.ts b/typescript-api/src/dancelight/interfaces/augment-api-errors.ts
index 1c5c4bcf5..ddcc575ff 100644
--- a/typescript-api/src/dancelight/interfaces/augment-api-errors.ts
+++ b/typescript-api/src/dancelight/interfaces/augment-api-errors.ts
@@ -191,6 +191,44 @@ declare module "@polkadot/api-base/types/errors" {
/** Generic error */
[key: string]: AugmentedError;
};
+ ethereumBeaconClient: {
+ BlockBodyHashTreeRootFailed: AugmentedError;
+ BLSPreparePublicKeysFailed: AugmentedError;
+ BLSVerificationFailed: AugmentedError;
+ ExecutionHeaderSkippedBlock: AugmentedError;
+ ExecutionHeaderTooFarBehind: AugmentedError;
+ ExpectedFinalizedHeaderNotStored: AugmentedError;
+ ForkDataHashTreeRootFailed: AugmentedError;
+ Halted: AugmentedError;
+ HeaderHashTreeRootFailed: AugmentedError;
+ HeaderNotFinalized: AugmentedError;
+ InvalidAncestryMerkleProof: AugmentedError;
+ InvalidBlockRootsRootMerkleProof: AugmentedError;
+ InvalidExecutionHeaderProof: AugmentedError;
+ /**
+ * The gap between the finalized headers is larger than the sync committee period, rendering execution headers
+ * unprovable using ancestry proofs (blocks root size is the same as the sync committee period slots).
+ */
+ InvalidFinalizedHeaderGap: AugmentedError;
+ InvalidHeaderMerkleProof: AugmentedError;
+ InvalidSyncCommitteeMerkleProof: AugmentedError;
+ /**
+ * 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: AugmentedError;
+ InvalidUpdateSlot: AugmentedError;
+ /** Attested header is older than latest finalized header. */
+ IrrelevantUpdate: AugmentedError;
+ NotBootstrapped: AugmentedError;
+ SigningRootHashTreeRootFailed: AugmentedError;
+ SkippedSyncCommitteePeriod: AugmentedError;
+ SyncCommitteeHashTreeRootFailed: AugmentedError;
+ SyncCommitteeParticipantsNotSupermajority: AugmentedError;
+ SyncCommitteeUpdateRequired: AugmentedError;
+ /** Generic error */
+ [key: string]: AugmentedError;
+ };
fellowshipCollective: {
/** Account is already a member. */
AlreadyMember: AugmentedError;
diff --git a/typescript-api/src/dancelight/interfaces/augment-api-events.ts b/typescript-api/src/dancelight/interfaces/augment-api-events.ts
index 14c2e043d..1d1ff8bf6 100644
--- a/typescript-api/src/dancelight/interfaces/augment-api-events.ts
+++ b/typescript-api/src/dancelight/interfaces/augment-api-events.ts
@@ -27,6 +27,7 @@ import type {
PolkadotRuntimeParachainsDisputesDisputeLocation,
PolkadotRuntimeParachainsDisputesDisputeResult,
PolkadotRuntimeParachainsInclusionAggregateMessageOrigin,
+ SnowbridgeCoreOperatingModeBasicOperatingMode,
SpConsensusGrandpaAppPublic,
SpRuntimeDispatchError,
SpRuntimeDispatchErrorWithPostInfo,
@@ -198,6 +199,18 @@ declare module "@polkadot/api-base/types/events" {
/** Generic event */
[key: string]: AugmentedEvent;
};
+ ethereumBeaconClient: {
+ BeaconHeaderImported: AugmentedEvent;
+ /** Set OperatingMode */
+ OperatingModeChanged: AugmentedEvent<
+ ApiType,
+ [mode: SnowbridgeCoreOperatingModeBasicOperatingMode],
+ { mode: SnowbridgeCoreOperatingModeBasicOperatingMode }
+ >;
+ SyncCommitteeUpdated: AugmentedEvent;
+ /** Generic event */
+ [key: string]: AugmentedEvent;
+ };
fellowshipCollective: {
/** A member `who` has been added. */
MemberAdded: AugmentedEvent;
diff --git a/typescript-api/src/dancelight/interfaces/augment-api-query.ts b/typescript-api/src/dancelight/interfaces/augment-api-query.ts
index 8c2d993d8..c4db62082 100644
--- a/typescript-api/src/dancelight/interfaces/augment-api-query.ts
+++ b/typescript-api/src/dancelight/interfaces/augment-api-query.ts
@@ -103,6 +103,9 @@ import type {
PolkadotRuntimeParachainsSchedulerPalletCoreOccupied,
PolkadotRuntimeParachainsSchedulerPalletParasEntry,
PolkadotRuntimeParachainsSharedAllowedRelayParentsTracker,
+ SnowbridgeBeaconPrimitivesCompactBeaconState,
+ SnowbridgeBeaconPrimitivesSyncCommitteePrepared,
+ SnowbridgeCoreOperatingModeBasicOperatingMode,
SpAuthorityDiscoveryAppPublic,
SpConsensusBabeAppPublic,
SpConsensusBabeBabeEpochConfiguration,
@@ -607,6 +610,55 @@ declare module "@polkadot/api-base/types/storage" {
/** Generic query */
[key: string]: QueryableStorageEntry;
};
+ ethereumBeaconClient: {
+ /** Sync committee for current period */
+ currentSyncCommittee: AugmentedQuery<
+ ApiType,
+ () => Observable,
+ []
+ > &
+ QueryableStorageEntry;
+ /** Beacon state by finalized block root */
+ finalizedBeaconState: AugmentedQuery<
+ ApiType,
+ (arg: H256 | string | Uint8Array) => Observable