,
+ _marker: std::marker::PhantomData,
+}
+
+impl GearEthBridge {
+ /// Creates a new instance of the GearBridge Rpc helper.
+ pub fn new(client: Arc) -> Self {
+ Self {
+ client,
+ _marker: Default::default(),
+ }
+ }
+}
+
+impl GearEthBridgeApiServer for GearEthBridge
+where
+ C: 'static + ProvideRuntimeApi + HeaderBackend,
+ C::Api: GearEthBridgeRuntimeApi,
+{
+ fn merkle_proof(&self, hash: H256, at: Option) -> RpcResult {
+ let api = self.client.runtime_api();
+ let at_hash = at.unwrap_or_else(|| self.client.info().best_hash);
+
+ api.merkle_proof(at_hash, hash)
+ .map_err(|e| ErrorObject::owned(8000, "RPC error", Some(format!("{e:?}"))))
+ .and_then(|opt| {
+ opt.ok_or_else(|| {
+ ErrorObject::owned(
+ 8000,
+ "Runtime error",
+ Some(String::from("Hash wasn't found in a queue")),
+ )
+ })
+ })
+ .map_err(|e| CallError::Custom(e).into())
+ }
+}
diff --git a/pallets/gear-eth-bridge/src/benchmarking.rs b/pallets/gear-eth-bridge/src/benchmarking.rs
new file mode 100644
index 00000000000..7a0cc05daa8
--- /dev/null
+++ b/pallets/gear-eth-bridge/src/benchmarking.rs
@@ -0,0 +1,71 @@
+// This file is part of Gear.
+
+// Copyright (C) 2022-2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program 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.
+
+// This program 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 this program. If not, see .
+
+//! Benchmarks for Pallet Gear Eth Bridge.
+
+use crate::{Call, Config, Pallet};
+use common::{benchmarking, Origin};
+use frame_benchmarking::benchmarks;
+use frame_system::RawOrigin;
+use sp_runtime::traits::Get;
+use sp_std::vec;
+
+#[cfg(test)]
+use crate::mock;
+
+benchmarks! {
+ where_clause { where T::AccountId: Origin }
+
+ pause {
+ // Initially pallet is uninitialized so we hack it for benchmarks.
+ crate::Initialized::::put(true);
+
+ // Initially pallet is paused so we need to unpause it first.
+ assert!(Pallet::::unpause(RawOrigin::Root.into()).is_ok());
+ }: _(RawOrigin::Root)
+ verify {
+ assert!(crate::Paused::::get());
+ }
+
+ unpause {
+ // Initially pallet is uninitialized so we hack it for benchmarks.
+ crate::Initialized::::put(true);
+ }: _(RawOrigin::Root)
+ verify {
+ assert!(!crate::Paused::::get());
+ }
+
+ send_eth_message {
+ // Initially pallet is uninitialized so we hack it for benchmarks.
+ crate::Initialized::::put(true);
+
+ // Initially pallet is paused so we need to unpause it first.
+ assert!(Pallet::::unpause(RawOrigin::Root.into()).is_ok());
+
+ let origin = benchmarking::account::("origin", 0, 0);
+
+ let destination = [42; 20].into();
+
+ let payload = vec![42; T::MaxPayloadSize::get() as usize];
+ }: _(RawOrigin::Signed(origin), destination, payload)
+ verify {
+ assert!(!crate::Queue::::get().is_empty());
+ }
+
+ impl_benchmark_test_suite!(Pallet, mock::new_test_ext(), mock::Test);
+}
diff --git a/pallets/gear-eth-bridge/src/builtin.rs b/pallets/gear-eth-bridge/src/builtin.rs
new file mode 100644
index 00000000000..038a4759532
--- /dev/null
+++ b/pallets/gear-eth-bridge/src/builtin.rs
@@ -0,0 +1,105 @@
+// This file is part of Gear.
+
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program 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.
+
+// This program 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 this program. If not, see .
+
+use crate::{Config, Error, Pallet, WeightInfo};
+use common::Origin;
+use core::marker::PhantomData;
+use gbuiltin_eth_bridge::{Request, Response};
+use gear_core::{
+ message::{Payload, StoredDispatch},
+ str::LimitedStr,
+};
+use gprimitives::{ActorId, H160};
+use pallet_gear_builtin::{BuiltinActor, BuiltinActorError};
+use parity_scale_codec::{Decode, Encode};
+use sp_runtime::traits::Zero;
+use sp_std::vec::Vec;
+
+/// Gear builtin actor providing functionality of `pallet-gear-eth-bridge`.
+///
+/// Check out `gbuiltin-eth-bridge` to observe builtin interface.
+pub struct Actor(PhantomData);
+
+impl BuiltinActor for Actor
+where
+ T::AccountId: Origin,
+{
+ const ID: u64 = 3;
+
+ type Error = BuiltinActorError;
+
+ fn handle(dispatch: &StoredDispatch, gas_limit: u64) -> (Result, u64) {
+ if !dispatch.value().is_zero() {
+ return (
+ Err(BuiltinActorError::Custom(LimitedStr::from_small_str(
+ error_to_str(&Error::::IncorrectValueApplied),
+ ))),
+ 0,
+ );
+ }
+
+ let Ok(request) = Request::decode(&mut dispatch.payload_bytes()) else {
+ return (Err(BuiltinActorError::DecodingError), 0);
+ };
+
+ match request {
+ Request::SendEthMessage {
+ destination,
+ payload,
+ } => send_message_request::(dispatch.source(), destination, payload, gas_limit),
+ }
+ }
+}
+
+fn send_message_request(
+ source: ActorId,
+ destination: H160,
+ payload: Vec,
+ gas_limit: u64,
+) -> (Result, u64)
+where
+ T::AccountId: Origin,
+{
+ let gas_cost = ::WeightInfo::send_eth_message().ref_time();
+
+ if gas_limit < gas_cost {
+ return (Err(BuiltinActorError::InsufficientGas), 0);
+ }
+
+ let res = Pallet::::queue_message(source, destination, payload)
+ .map(|(nonce, hash)| {
+ Response::EthMessageQueued { nonce, hash }
+ .encode()
+ .try_into()
+ .unwrap_or_else(|_| unreachable!("response max encoded len is less than maximum"))
+ })
+ .map_err(|e| BuiltinActorError::Custom(LimitedStr::from_small_str(error_to_str(&e))));
+
+ (res, gas_cost)
+}
+
+pub fn error_to_str(error: &Error) -> &'static str {
+ match error {
+ Error::BridgeIsNotYetInitialized => "Send message: bridge is not yet initialized",
+ Error::BridgeIsPaused => "Send message: bridge is paused",
+ Error::MaxPayloadSizeExceeded => "Send message: message max payload size exceeded",
+ Error::QueueCapacityExceeded => "Send message: queue capacity exceeded",
+ Error::IncorrectValueApplied => "Send message: incorrect value applied",
+ _ => unimplemented!(),
+ }
+}
diff --git a/pallets/gear-eth-bridge/src/internal.rs b/pallets/gear-eth-bridge/src/internal.rs
new file mode 100644
index 00000000000..67724043ba3
--- /dev/null
+++ b/pallets/gear-eth-bridge/src/internal.rs
@@ -0,0 +1,138 @@
+// This file is part of Gear.
+
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program 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.
+
+// This program 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 this program. If not, see .
+
+use crate::{Config, Error, MessageNonce};
+use binary_merkle_tree::MerkleProof;
+use frame_support::{ensure, traits::Get};
+use gprimitives::{ActorId, H160, H256, U256};
+use parity_scale_codec::{Decode, Encode};
+use scale_info::TypeInfo;
+use sp_runtime::traits::{Hash, Keccak256};
+use sp_std::vec::Vec;
+
+/// Type representing merkle proof of message's inclusion into bridging queue.
+#[derive(Clone, Debug, Default, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, TypeInfo)]
+#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
+pub struct Proof {
+ /// Merkle root of the tree this proof associated with.
+ pub root: H256,
+ /// Proof itself: collection of hashes required for verification.
+ pub proof: Vec,
+ /// Number of leaves in the tree.
+ pub number_of_leaves: u64,
+ /// Leaf index we're proving inclusion.
+ pub leaf_index: u64,
+ /// Leaf value for inclusion proving.
+ pub leaf: H256,
+}
+
+impl From> for Proof {
+ fn from(value: MerkleProof) -> Self {
+ Self {
+ root: value.root,
+ proof: value.proof,
+ number_of_leaves: value.number_of_leaves as u64,
+ leaf_index: value.leaf_index as u64,
+ leaf: value.leaf,
+ }
+ }
+}
+
+/// Type representing message being bridged from gear to eth.
+#[derive(Clone, Debug, Default, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, TypeInfo)]
+pub struct EthMessage {
+ nonce: U256,
+ source: H256,
+ destination: H160,
+ payload: Vec,
+}
+
+impl EthMessage {
+ #[cfg(test)]
+ pub fn new(nonce: U256, source: ActorId, destination: H160, payload: Vec) -> Self {
+ Self {
+ nonce,
+ source: source.into_bytes().into(),
+ destination,
+ payload,
+ }
+ }
+
+ pub(crate) fn try_new(
+ source: ActorId,
+ destination: H160,
+ payload: Vec,
+ ) -> Result> {
+ ensure!(
+ payload.len() <= T::MaxPayloadSize::get() as usize,
+ Error::::MaxPayloadSizeExceeded
+ );
+
+ let nonce = MessageNonce::::mutate(|nonce| {
+ let res = *nonce;
+ *nonce = nonce.saturating_add(U256::one());
+ res
+ });
+
+ Ok(Self {
+ nonce,
+ source: source.into_bytes().into(),
+ destination,
+ payload,
+ })
+ }
+
+ /// Message's nonce getter.
+ pub fn nonce(&self) -> U256 {
+ self.nonce
+ }
+
+ /// Message's source getter.
+ pub fn source(&self) -> H256 {
+ self.source
+ }
+
+ /// Message's destination getter.
+ pub fn destination(&self) -> H160 {
+ self.destination
+ }
+
+ /// Message's payload bytes getter.
+ pub fn payload(&self) -> &[u8] {
+ &self.payload
+ }
+
+ /// Returns hash of the message using `Keccak256` hasher.
+ ///
+ /// Has `pub(crate)` visibility due to dependency on substrate
+ /// runtime interface (keccak hashing).
+ pub(crate) fn hash(&self) -> H256 {
+ let mut nonce = [0; 32];
+ self.nonce.to_little_endian(&mut nonce);
+
+ let bytes = [
+ nonce.as_ref(),
+ self.source.as_bytes(),
+ self.destination.as_bytes(),
+ self.payload.as_ref(),
+ ]
+ .concat();
+
+ Keccak256::hash(&bytes)
+ }
+}
diff --git a/pallets/gear-eth-bridge/src/lib.rs b/pallets/gear-eth-bridge/src/lib.rs
new file mode 100644
index 00000000000..364e8592341
--- /dev/null
+++ b/pallets/gear-eth-bridge/src/lib.rs
@@ -0,0 +1,564 @@
+// This file is part of Gear.
+
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program 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.
+
+// This program 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 this program. If not, see .
+
+//! Pallet Gear Eth Bridge.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![doc(html_favicon_url = "https://gear-tech.io/favicons/favicon.ico")]
+#![doc(html_logo_url = "https://docs.gear.rs/logo.svg")]
+#![warn(missing_docs)]
+// TODO: remove on rust update.
+#![allow(unknown_lints)]
+#![allow(clippy::manual_inspect)]
+
+pub use builtin::Actor;
+pub use internal::{EthMessage, Proof};
+pub use pallet::*;
+pub use weights::WeightInfo;
+
+pub mod weights;
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+
+mod builtin;
+mod internal;
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+#[frame_support::pallet]
+pub mod pallet {
+ use super::*;
+ use common::Origin;
+ use frame_support::{
+ pallet_prelude::*,
+ traits::{ConstBool, OneSessionHandler, StorageInstance, StorageVersion},
+ StorageHasher,
+ };
+ use frame_system::{
+ ensure_root, ensure_signed,
+ pallet_prelude::{BlockNumberFor, OriginFor},
+ };
+ use gprimitives::{ActorId, H160, H256, U256};
+ use sp_runtime::{
+ traits::{Keccak256, One, Saturating, Zero},
+ BoundToRuntimeAppPublic, RuntimeAppPublic,
+ };
+ use sp_std::vec::Vec;
+
+ type QueueCapacityOf = ::QueueCapacity;
+ type SessionsPerEraOf = ::SessionsPerEra;
+
+ /// Pallet Gear Eth Bridge's storage version.
+ pub const ETH_BRIDGE_STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
+
+ /// Pallet Gear Eth Bridge's config.
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// Type representing aggregated runtime event.
+ type RuntimeEvent: From>
+ + TryInto>
+ + IsType<::RuntimeEvent>;
+
+ /// Constant defining maximal payload size in bytes of message for bridging.
+ #[pallet::constant]
+ type MaxPayloadSize: Get;
+
+ /// Constant defining maximal amount of messages that are able to be
+ /// bridged within the single staking era.
+ #[pallet::constant]
+ type QueueCapacity: Get;
+
+ /// Constant defining amount of sessions in manager for keys rotation.
+ /// Similar to `pallet_staking::SessionsPerEra`.
+ #[pallet::constant]
+ type SessionsPerEra: Get;
+
+ /// Weight cost incurred by pallet calls.
+ type WeightInfo: WeightInfo;
+ }
+
+ /// Pallet Gear Eth Bridge's event.
+ #[pallet::event]
+ #[pallet::generate_deposit(fn deposit_event)]
+ pub enum Event {
+ /// Grandpa validator's keys set was hashed and set in storage at
+ /// first block of the last session in the era.
+ AuthoritySetHashChanged(H256),
+
+ /// Bridge got cleared on initialization of the second block in a new era.
+ BridgeCleared,
+
+ /// Optimistically, single-time called event defining that pallet
+ /// got initialized and started processing session changes,
+ /// as well as putting initial zeroed queue merkle root.
+ BridgeInitialized,
+
+ /// Bridge was paused and temporary doesn't process any incoming requests.
+ BridgePaused,
+
+ /// Bridge was unpaused and from now on processes any incoming requests.
+ BridgeUnpaused,
+
+ /// A new message was queued for bridging.
+ MessageQueued {
+ /// Enqueued message.
+ message: EthMessage,
+ /// Hash of the enqueued message.
+ hash: H256,
+ },
+
+ /// Merkle root of the queue changed: new messages queued within the block.
+ QueueMerkleRootChanged(H256),
+ }
+
+ /// Pallet Gear Eth Bridge's error.
+ #[pallet::error]
+ #[cfg_attr(test, derive(Clone))]
+ pub enum Error {
+ /// The error happens when bridge got called before
+ /// proper initialization after deployment.
+ BridgeIsNotYetInitialized,
+
+ /// The error happens when bridge got called when paused.
+ BridgeIsPaused,
+
+ /// The error happens when bridging message sent with too big payload.
+ MaxPayloadSizeExceeded,
+
+ /// The error happens when bridging queue capacity exceeded,
+ /// so message couldn't be sent.
+ QueueCapacityExceeded,
+
+ /// The error happens when bridging thorough builtin and message value
+ /// is inapplicable to operation or insufficient.
+ IncorrectValueApplied,
+ }
+
+ /// Lifecycle storage.
+ ///
+ /// Defines if pallet got initialized and focused on common session changes.
+ #[pallet::storage]
+ pub(crate) type Initialized = StorageValue<_, bool, ValueQuery>;
+
+ /// Lifecycle storage.
+ ///
+ /// Defines if pallet is accepting any mutable requests. Governance-ruled.
+ #[pallet::storage]
+ pub(crate) type Paused = StorageValue<_, bool, ValueQuery, ConstBool>;
+
+ /// Primary storage.
+ ///
+ /// Keeps hash of queued validator keys for the next era.
+ ///
+ /// **Invariant**: Key exists in storage since first block of some era's last
+ /// session, until initialization of the second block of the next era.
+ #[pallet::storage]
+ pub(crate) type AuthoritySetHash = StorageValue<_, H256>;
+
+ /// Primary storage.
+ ///
+ /// Keeps merkle root of the bridge's queued messages.
+ ///
+ /// **Invariant**: Key exists since pallet initialization. If queue is empty,
+ /// zeroed hash set in storage.
+ #[pallet::storage]
+ pub(crate) type QueueMerkleRoot = StorageValue<_, H256>;
+
+ /// Primary storage.
+ ///
+ /// Keeps bridge's queued messages keccak hashes.
+ #[pallet::storage]
+ pub(crate) type Queue = StorageValue<_, BoundedVec>, ValueQuery>;
+
+ /// Operational storage.
+ ///
+ /// Declares timer of the session changes (`on_new_session` calls),
+ /// when `queued_validators` must be stored within the pallet.
+ ///
+ /// **Invariant**: reducing each time on new session, it equals 0 only
+ /// since storing grandpa keys hash until next session change,
+ /// when it becomes `SessionPerEra - 1`.
+ #[pallet::storage]
+ pub(crate) type SessionsTimer = StorageValue<_, u32, ValueQuery>;
+
+ /// Operational storage.
+ ///
+ /// Defines in how many on_initialize hooks queue, queue merkle root and
+ /// grandpa keys hash should be cleared.
+ ///
+ /// **Invariant**: set to 2 on_init hooks when new session with authorities
+ /// set change, then decreasing to zero on each new block hook. When equals
+ /// to zero, reset is performed.
+ #[pallet::storage]
+ pub(crate) type ClearTimer = StorageValue<_, u32>;
+
+ /// Operational storage.
+ ///
+ /// Keeps next message's nonce for bridging. Must be increased on each use.
+ #[pallet::storage]
+ pub(crate) type MessageNonce = StorageValue<_, U256, ValueQuery>;
+
+ /// Operational storage.
+ ///
+ /// Defines if queue was changed within the block, it's necessary to
+ /// update queue merkle root by the end of the block.
+ #[pallet::storage]
+ pub(crate) type QueueChanged = StorageValue<_, bool, ValueQuery>;
+
+ /// Pallet Gear Eth Bridge's itself.
+ #[pallet::pallet]
+ #[pallet::storage_version(ETH_BRIDGE_STORAGE_VERSION)]
+ pub struct Pallet(_);
+
+ #[pallet::call]
+ impl Pallet
+ where
+ T::AccountId: Origin,
+ {
+ /// Root extrinsic that pauses pallet.
+ /// When paused, no new messages could be queued.
+ #[pallet::call_index(0)]
+ #[pallet::weight(::WeightInfo::pause())]
+ pub fn pause(origin: OriginFor) -> DispatchResultWithPostInfo {
+ // Ensuring called by root.
+ ensure_root(origin)?;
+
+ // Ensuring that pallet is initialized.
+ ensure!(
+ Initialized::::get(),
+ Error::::BridgeIsNotYetInitialized
+ );
+
+ // Taking value (so pausing it) with checking if it was unpaused.
+ if !Paused::::take() {
+ // Depositing event about bridge being paused.
+ Self::deposit_event(Event::::BridgePaused);
+ }
+
+ // Returning successful result without weight refund.
+ Ok(().into())
+ }
+
+ /// Root extrinsic that unpauses pallet.
+ /// When paused, no new messages could be queued.
+ #[pallet::call_index(1)]
+ #[pallet::weight(::WeightInfo::unpause())]
+ pub fn unpause(origin: OriginFor) -> DispatchResultWithPostInfo {
+ // Ensuring called by root.
+ ensure_root(origin)?;
+
+ // Ensuring that pallet is initialized.
+ ensure!(
+ Initialized::::get(),
+ Error::::BridgeIsNotYetInitialized
+ );
+
+ // Checking if pallet is paused.
+ if Paused::::get() {
+ // Unpausing pallet.
+ Paused::::put(false);
+
+ // Depositing event about bridge being unpaused.
+ Self::deposit_event(Event::::BridgeUnpaused);
+ }
+
+ // Returning successful result without weight refund.
+ Ok(().into())
+ }
+
+ /// Extrinsic that inserts message in a bridging queue,
+ /// updating queue merkle root at the end of the block.
+ #[pallet::call_index(2)]
+ #[pallet::weight(::WeightInfo::send_eth_message())]
+ pub fn send_eth_message(
+ origin: OriginFor,
+ destination: H160,
+ payload: Vec,
+ ) -> DispatchResultWithPostInfo {
+ let source = ensure_signed(origin)?.cast();
+
+ Self::queue_message(source, destination, payload)?;
+
+ Ok(().into())
+ }
+ }
+
+ impl Pallet {
+ pub(crate) fn queue_message(
+ source: ActorId,
+ destination: H160,
+ payload: Vec,
+ ) -> Result<(U256, H256), Error> {
+ // Ensuring that pallet is initialized.
+ ensure!(
+ Initialized::::get(),
+ Error::::BridgeIsNotYetInitialized
+ );
+
+ // Ensuring that pallet isn't paused.
+ ensure!(!Paused::::get(), Error::::BridgeIsPaused);
+
+ // Creating new message from given data.
+ //
+ // Inside goes query and bump of nonce,
+ // as well as checking payload size.
+ let message = EthMessage::try_new(source, destination, payload)?;
+
+ // Appending hash of the message into the queue
+ // if it's capacity wasn't exceeded.
+ let hash = Queue::::mutate(|v| {
+ (v.len() < QueueCapacityOf::::get() as usize)
+ .then(|| {
+ let hash = message.hash();
+
+ // Always `Ok`: check performed above as in inner implementation.
+ v.try_push(hash).map(|()| hash).ok()
+ })
+ .flatten()
+ .ok_or(Error::::QueueCapacityExceeded)
+ })
+ .inspect_err(|_| {
+ // In case of error, reverting increase of `MessageNonce` performed
+ // in message creation to keep builtin interactions transactional.
+ MessageNonce::::mutate_exists(|nonce| {
+ *nonce = nonce.and_then(|inner| {
+ inner.checked_sub(U256::one()).filter(|new| !new.is_zero())
+ });
+ });
+ })?;
+
+ // Marking queue as changed, so root will be updated later.
+ QueueChanged::::put(true);
+
+ // Extracting nonce to return.
+ let nonce = message.nonce();
+
+ // Depositing event about message being queued for bridging.
+ Self::deposit_event(Event::::MessageQueued { message, hash });
+
+ Ok((nonce, hash))
+ }
+
+ /// Returns merkle inclusion proof of the message hash in the queue.
+ pub fn merkle_proof(hash: H256) -> Option {
+ // Querying actual queue.
+ let queue = Queue::::get();
+
+ // Lookup for hash index within the queue.
+ let idx = queue.iter().position(|&v| v == hash)?;
+
+ // Generating proof.
+ let proof = binary_merkle_tree::merkle_proof::(queue, idx);
+
+ // Returning appropriate type.
+ Some(proof.into())
+ }
+ }
+
+ #[pallet::hooks]
+ impl Hooks> for Pallet {
+ fn on_initialize(_bn: BlockNumberFor) -> Weight {
+ // Resulting weight of the hook.
+ //
+ // Initially consists of one read of `ClearTimer` storage.
+ let mut weight = T::DbWeight::get().reads(1);
+
+ // Querying timer and checking its value if some.
+ if let Some(timer) = ClearTimer::::get() {
+ // Asserting invariant that in case of key existence, it's non-zero.
+ debug_assert!(!timer.is_zero());
+
+ // Decreasing timer.
+ let new_timer = timer.saturating_sub(1);
+
+ if new_timer.is_zero() {
+ // Removing timer for the next session hook.
+ ClearTimer::::kill();
+
+ // Removing grandpa set hash from storage.
+ AuthoritySetHash::::kill();
+
+ // Removing queued messages from storage.
+ Queue::::kill();
+
+ // Setting zero queue root, keeping invariant of this key existence.
+ QueueMerkleRoot::::put(H256::zero());
+
+ // Depositing event about clearing the bridge.
+ Self::deposit_event(Event::::BridgeCleared);
+
+ // Increasing resulting weight by 3 writes of above keys removal.
+ weight = weight.saturating_add(T::DbWeight::get().writes(4));
+ } else {
+ // Put back non-zero timer to schedule clearing.
+ ClearTimer::::put(new_timer);
+
+ // Increasing resulting weight by 1 writes of above keys insertion.
+ weight = weight.saturating_add(T::DbWeight::get().writes(1));
+ }
+ }
+
+ // Returning weight.
+ weight
+ }
+
+ fn on_finalize(_bn: BlockNumberFor) {
+ // If queue wasn't changed, than nothing to do here.
+ if !QueueChanged::::take() {
+ return;
+ }
+
+ // Querying actual queue.
+ let queue = Queue::::get();
+
+ // Checking invariant.
+ //
+ // If queue was changed within the block, it couldn't be empty.
+ debug_assert!(!queue.is_empty());
+
+ // Calculating new queue merkle root.
+ let root = binary_merkle_tree::merkle_root::(queue);
+
+ // Updating queue merkle root in storage.
+ QueueMerkleRoot::::put(root);
+
+ // Depositing event about queue root being updated.
+ Self::deposit_event(Event::::QueueMerkleRootChanged(root));
+ }
+ }
+
+ impl BoundToRuntimeAppPublic for Pallet {
+ type Public = sp_consensus_grandpa::AuthorityId;
+ }
+
+ impl OneSessionHandler for Pallet {
+ type Key = ::Public;
+
+ fn on_genesis_session<'a, I: 'a>(_validators: I) {}
+
+ // TODO: consider support of `Stalled` changes of grandpa (#4113).
+ fn on_new_session<'a, I>(changed: bool, _validators: I, queued_validators: I)
+ where
+ I: 'a + Iterator- ,
+ {
+ // If historically pallet hasn't yet faced `changed = true`,
+ // any type of calculations aren't performed.
+ if !Initialized::::get() && !changed {
+ return;
+ }
+
+ // Here starts common processing of properly initialized pallet.
+ if changed {
+ // Checking invariant.
+ //
+ // Reset scheduling must be resolved on the first block
+ // after session changed.
+ debug_assert!(ClearTimer::::get().is_none());
+
+ // First time facing `changed = true`, so from now on, pallet
+ // is starting handling grandpa sets and queue.
+ if !Initialized::::get() {
+ // Setting pallet status initialized.
+ Initialized::::put(true);
+
+ // Depositing event about getting initialized.
+ Self::deposit_event(Event::::BridgeInitialized);
+
+ // Invariant.
+ //
+ // At any single point of pallet existence, when it's active
+ // and queue is empty, queue merkle root must present
+ // in storage and be zeroed.
+ QueueMerkleRoot::::put(H256::zero());
+ } else {
+ // Scheduling reset on next block's init.
+ //
+ // Firstly, it will decrease in the same block, because call of
+ // `on_new_session` hook will be performed earlier in the same
+ // block, because `pallet_session` triggers it in its `on_init`
+ // and has smaller pallet id.
+ ClearTimer::::put(2);
+ }
+
+ // Checking invariant.
+ //
+ // Timer is supposed to be `null` (default zero), if was just
+ // initialized, otherwise zero set in storage.
+ debug_assert!(SessionsTimer::::get().is_zero());
+
+ // Scheduling settlement of grandpa keys in `SessionsPerEra - 1` session changes.
+ SessionsTimer::::put(SessionsPerEraOf::::get().saturating_sub(One::one()));
+ } else {
+ // Reducing timer. If became zero, it means we're at the last
+ // session of the era and queued keys must be kept.
+ let to_set_grandpa_keys = SessionsTimer::::mutate(|timer| {
+ timer.saturating_dec();
+ timer.is_zero()
+ });
+
+ // Setting future keys hash, if needed.
+ if to_set_grandpa_keys {
+ // Collecting all keys into `Vec`.
+ let keys_bytes = queued_validators
+ .flat_map(|(_, key)| key.to_raw_vec())
+ .collect::>();
+
+ // Hashing keys bytes with `Blake2`.
+ let grandpa_set_hash = Blake2_256::hash(&keys_bytes).into();
+
+ // Setting new grandpa set hash into storage.
+ AuthoritySetHash::::put(grandpa_set_hash);
+
+ // Depositing event about update in the set.
+ Self::deposit_event(Event::::AuthoritySetHashChanged(grandpa_set_hash));
+ }
+ }
+ }
+
+ fn on_disabled(_validator_index: u32) {}
+ }
+
+ /// Prefix alias of the `pallet_gear_eth_bridge::AuthoritySetHash` storage.
+ pub struct AuthoritySetHashPrefix(PhantomData);
+
+ impl StorageInstance for AuthoritySetHashPrefix {
+ const STORAGE_PREFIX: &'static str =
+ <_GeneratedPrefixForStorageAuthoritySetHash as StorageInstance>::STORAGE_PREFIX;
+
+ fn pallet_prefix() -> &'static str {
+ <_GeneratedPrefixForStorageAuthoritySetHash as StorageInstance>::pallet_prefix()
+ }
+ }
+
+ /// Prefix alias of the `pallet_gear_eth_bridge::QueueMerkleRoot` storage.
+ pub struct QueueMerkleRootPrefix(PhantomData);
+
+ impl StorageInstance for QueueMerkleRootPrefix {
+ const STORAGE_PREFIX: &'static str =
+ <_GeneratedPrefixForStorageQueueMerkleRoot as StorageInstance>::STORAGE_PREFIX;
+
+ fn pallet_prefix() -> &'static str {
+ <_GeneratedPrefixForStorageQueueMerkleRoot as StorageInstance>::pallet_prefix()
+ }
+ }
+}
diff --git a/pallets/gear-eth-bridge/src/mock.rs b/pallets/gear-eth-bridge/src/mock.rs
new file mode 100644
index 00000000000..9bf83023569
--- /dev/null
+++ b/pallets/gear-eth-bridge/src/mock.rs
@@ -0,0 +1,430 @@
+// This file is part of Gear.
+
+// Copyright (C) 2021-2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program 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.
+
+// This program 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 this program. If not, see .
+
+use crate as pallet_gear_eth_bridge;
+use frame_support::{
+ construct_runtime, parameter_types,
+ traits::{ConstBool, ConstU32, ConstU64, FindAuthor, Hooks},
+};
+use frame_support_test::TestRandomness;
+use frame_system::{self as system, pallet_prelude::BlockNumberFor};
+use gprimitives::ActorId;
+use pallet_session::{SessionManager, ShouldEndSession};
+use sp_core::{ed25519::Public, H256};
+use sp_runtime::{
+ impl_opaque_keys,
+ traits::{BlakeTwo256, IdentityLookup},
+ BuildStorage,
+};
+use sp_std::convert::{TryFrom, TryInto};
+
+pub type AccountId = u64;
+type BlockNumber = u64;
+type Balance = u128;
+type Block = frame_system::mocking::MockBlock;
+pub type Moment = u64;
+
+pub(crate) const SIGNER: AccountId = 1;
+pub(crate) const BLOCK_AUTHOR: AccountId = 10001;
+
+pub(crate) const EXISTENTIAL_DEPOSIT: u128 = UNITS;
+pub(crate) const ENDOWMENT: u128 = 1_000 * UNITS;
+
+pub(crate) const UNITS: u128 = 1_000_000_000_000; // 10^(-12) precision
+pub(crate) const MILLISECS_PER_BLOCK: u64 = 3_000;
+
+// Configure a mock runtime to test the pallet.
+construct_runtime!(
+ pub enum Test
+ {
+ System: system,
+ Timestamp: pallet_timestamp,
+ Authorship: pallet_authorship,
+ Grandpa: pallet_grandpa,
+ Balances: pallet_balances,
+ Session: pallet_session,
+
+ GearProgram: pallet_gear_program,
+ GearMessenger: pallet_gear_messenger,
+ GearScheduler: pallet_gear_scheduler,
+ GearBank: pallet_gear_bank,
+ Gear: pallet_gear,
+ GearGas: pallet_gear_gas,
+ GearBuiltin: pallet_gear_builtin,
+ GearEthBridge: pallet_gear_eth_bridge,
+ }
+);
+
+impl_opaque_keys! {
+ pub struct SessionKeys {
+ pub grandpa: Grandpa,
+ }
+}
+
+mod grandpa_keys_handler {
+ use super::{AccountId, GearEthBridge, Grandpa};
+ use frame_support::traits::OneSessionHandler;
+ use sp_runtime::BoundToRuntimeAppPublic;
+ use sp_std::vec::Vec;
+
+ /// Due to requirement of pallet_session to have one keys handler for each
+ /// type of opaque keys, this implementation is necessary: aggregates
+ /// `Grandpa` and `GearEthBridge` handling of grandpa keys rotations.
+ pub struct GrandpaAndGearEthBridge;
+
+ impl BoundToRuntimeAppPublic for GrandpaAndGearEthBridge {
+ type Public = ::Public;
+ }
+
+ impl OneSessionHandler for GrandpaAndGearEthBridge {
+ type Key = >::Key;
+ fn on_before_session_ending() {
+ Grandpa::on_before_session_ending();
+ GearEthBridge::on_before_session_ending();
+ }
+ fn on_disabled(validator_index: u32) {
+ Grandpa::on_disabled(validator_index);
+ GearEthBridge::on_disabled(validator_index);
+ }
+ fn on_genesis_session<'a, I>(validators: I)
+ where
+ I: 'a + Iterator
- ,
+ AccountId: 'a,
+ {
+ let validators: Vec<_> = validators.collect();
+ Grandpa::on_genesis_session(validators.clone().into_iter());
+ GearEthBridge::on_genesis_session(validators.into_iter());
+ }
+ fn on_new_session<'a, I>(changed: bool, validators: I, queued_validators: I)
+ where
+ I: 'a + Iterator
- ,
+ AccountId: 'a,
+ {
+ let validators: Vec<_> = validators.collect();
+ let queued_validators: Vec<_> = queued_validators.collect();
+
+ log::debug!("on_new_session(changed={changed}, validators={validators:?}, queued_validators={queued_validators:?})");
+
+ Grandpa::on_new_session(
+ changed,
+ validators.clone().into_iter(),
+ queued_validators.clone().into_iter(),
+ );
+ GearEthBridge::on_new_session(
+ changed,
+ validators.into_iter(),
+ queued_validators.into_iter(),
+ );
+ }
+ }
+}
+
+pub type VaraSessionHandler = (grandpa_keys_handler::GrandpaAndGearEthBridge,);
+
+parameter_types! {
+ pub const BlockHashCount: u64 = 250;
+ pub const ExistentialDeposit: Balance = EXISTENTIAL_DEPOSIT;
+}
+
+common::impl_pallet_system!(Test);
+common::impl_pallet_balances!(Test);
+common::impl_pallet_authorship!(Test);
+common::impl_pallet_timestamp!(Test);
+
+parameter_types! {
+ pub const BlockGasLimit: u64 = 100_000_000_000;
+ pub const OutgoingLimit: u32 = 1024;
+ pub const OutgoingBytesLimit: u32 = 64 * 1024 * 1024;
+ pub ReserveThreshold: BlockNumber = 1;
+ pub GearSchedule: pallet_gear::Schedule = >::default();
+ pub RentFreePeriod: BlockNumber = 12_000;
+ pub RentCostPerBlock: Balance = 11;
+ pub ResumeMinimalPeriod: BlockNumber = 100;
+ pub ResumeSessionDuration: BlockNumber = 1_000;
+ pub const PerformanceMultiplier: u32 = 100;
+ pub const BankAddress: AccountId = 15082001;
+ pub const GasMultiplier: common::GasMultiplier = common::GasMultiplier::ValuePerGas(25);
+}
+
+pallet_gear_bank::impl_config!(Test);
+pallet_gear_gas::impl_config!(Test);
+pallet_gear_scheduler::impl_config!(Test);
+pallet_gear_program::impl_config!(Test);
+pallet_gear_messenger::impl_config!(Test, CurrentBlockNumber = Gear);
+pallet_gear::impl_config!(
+ Test,
+ Schedule = GearSchedule,
+ BuiltinDispatcherFactory = GearBuiltin,
+);
+
+impl pallet_gear_builtin::Config for Test {
+ type RuntimeCall = RuntimeCall;
+ type Builtins = (crate::builtin::Actor,);
+ type WeightInfo = ();
+}
+
+pub const EPOCH_DURATION_IN_BLOCKS: BlockNumber = 6;
+
+pub const SLOT_DURATION: Moment = MILLISECS_PER_BLOCK;
+
+pub const EPOCH_DURATION_IN_SLOTS: u64 = {
+ const SLOT_FILL_RATE: f64 = MILLISECS_PER_BLOCK as f64 / SLOT_DURATION as f64;
+
+ (EPOCH_DURATION_IN_BLOCKS as f64 * SLOT_FILL_RATE) as u64
+};
+
+parameter_types! {
+ pub const SessionsPerEra: u32 = 6;
+ pub const EpochDuration: u64 = EPOCH_DURATION_IN_SLOTS;
+ pub const ExpectedBlockTime: Moment = MILLISECS_PER_BLOCK;
+ pub const MaxAuthorities: u32 = 100_000;
+ pub const MaxNominatorRewardedPerValidator: u32 = 256;
+}
+
+impl pallet_grandpa::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+
+ type WeightInfo = ();
+ type MaxAuthorities = MaxAuthorities;
+ type MaxNominators = MaxNominatorRewardedPerValidator;
+ type MaxSetIdSessionEntries = ();
+ type KeyOwnerProof = sp_session::MembershipProof;
+ type EquivocationReportSystem = ();
+}
+
+pub struct TestSessionRotator;
+
+impl ShouldEndSession for TestSessionRotator {
+ fn should_end_session(now: BlockNumber) -> bool {
+ if now > 1 {
+ (now - 1) % EpochDuration::get() == 0
+ } else {
+ false
+ }
+ }
+}
+
+pub fn era_validators(session_idx: u32, do_set_keys: bool) -> Vec {
+ let era = session_idx / SessionsPerEra::get() + 1;
+
+ let first_validator = 1_000 + era as u64;
+ let last_validator = first_validator + 3;
+
+ (first_validator..last_validator)
+ .inspect(|&acc| {
+ if do_set_keys {
+ let grandpa = account_into_grandpa_key(acc);
+ pallet_session::NextKeys::::insert(acc, SessionKeys { grandpa });
+ }
+ })
+ .collect()
+}
+
+pub fn era_validators_authority_set(
+ session_idx: u32,
+) -> Vec<(
+ sp_consensus_grandpa::AuthorityId,
+ sp_consensus_grandpa::AuthorityWeight,
+)> {
+ era_validators(session_idx, false)
+ .into_iter()
+ .map(account_into_grandpa_pair)
+ .collect()
+}
+
+pub fn account_into_grandpa_key(id: AccountId) -> sp_consensus_grandpa::AuthorityId {
+ Public::from_raw(ActorId::from(id).into_bytes()).into()
+}
+
+pub fn account_into_grandpa_pair(
+ id: AccountId,
+) -> (
+ sp_consensus_grandpa::AuthorityId,
+ sp_consensus_grandpa::AuthorityWeight,
+) {
+ (account_into_grandpa_key(id), 1)
+}
+
+pub struct TestSessionManager;
+
+impl SessionManager for TestSessionManager {
+ fn new_session(session_idx: u32) -> Option> {
+ (session_idx % SessionsPerEra::get() == 0).then(|| era_validators(session_idx, true))
+ }
+ fn start_session(_: u32) {}
+ fn end_session(_: u32) {}
+}
+
+impl pallet_session::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type ValidatorId = ::AccountId;
+ type ValidatorIdOf = ();
+ type ShouldEndSession = TestSessionRotator;
+ type NextSessionRotation = ();
+ type SessionManager = TestSessionManager;
+ type SessionHandler = VaraSessionHandler;
+ type Keys = SessionKeys;
+ type WeightInfo = pallet_session::weights::SubstrateWeight;
+}
+
+impl pallet_gear_eth_bridge::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type MaxPayloadSize = ConstU32<1024>;
+ type QueueCapacity = ConstU32<32>;
+ type SessionsPerEra = SessionsPerEra;
+ type WeightInfo = ();
+}
+
+// Build genesis storage according to the mock runtime.
+#[derive(Default)]
+pub struct ExtBuilder {
+ endowed_accounts: Vec,
+ endowment: Balance,
+}
+
+impl ExtBuilder {
+ pub fn endowment(mut self, e: Balance) -> Self {
+ self.endowment = e;
+ self
+ }
+
+ pub fn endowed_accounts(mut self, accounts: Vec) -> Self {
+ self.endowed_accounts = accounts;
+ self
+ }
+
+ pub fn build(self) -> sp_io::TestExternalities {
+ let mut storage = system::GenesisConfig::::default()
+ .build_storage()
+ .unwrap();
+
+ pallet_balances::GenesisConfig:: {
+ balances: self
+ .endowed_accounts
+ .iter()
+ .map(|k| (*k, self.endowment))
+ .collect(),
+ }
+ .assimilate_storage(&mut storage)
+ .unwrap();
+
+ let keys = era_validators(0, false)
+ .into_iter()
+ .map(|i| {
+ let grandpa = account_into_grandpa_key(i);
+
+ (i, i, SessionKeys { grandpa })
+ })
+ .collect();
+
+ pallet_session::GenesisConfig:: { keys }
+ .assimilate_storage(&mut storage)
+ .unwrap();
+
+ let mut ext: sp_io::TestExternalities = storage.into();
+
+ ext.execute_with(|| {
+ on_initialize(1);
+ });
+ ext
+ }
+}
+
+pub(crate) fn run_to_block(n: u64) {
+ while System::block_number() < n {
+ let current_blk = System::block_number();
+
+ Gear::run(RuntimeOrigin::none(), None).unwrap();
+ on_finalize(current_blk);
+
+ let new_block_number = current_blk + 1;
+ on_initialize(new_block_number);
+ }
+}
+
+pub(crate) fn run_to_next_block() {
+ run_for_n_blocks(1)
+}
+
+pub(crate) fn run_for_n_blocks(n: u64) {
+ run_to_block(System::block_number() + n);
+}
+
+// Run on_initialize hooks in order as they appear in AllPalletsWithSystem.
+pub(crate) fn on_initialize(new: BlockNumberFor) {
+ System::set_block_number(new);
+ Timestamp::set_timestamp(new.saturating_mul(MILLISECS_PER_BLOCK));
+ Authorship::on_initialize(new);
+ Grandpa::on_initialize(new);
+ Balances::on_initialize(new);
+ Session::on_initialize(new);
+
+ GearProgram::on_initialize(new);
+ GearMessenger::on_initialize(new);
+ GearScheduler::on_initialize(new);
+ GearBank::on_initialize(new);
+ Gear::on_initialize(new);
+ GearGas::on_initialize(new);
+ GearBuiltin::on_initialize(new);
+ GearEthBridge::on_initialize(new);
+}
+
+// Run on_finalize hooks (in pallets reverse order, as they appear in AllPalletsWithSystem)
+pub(crate) fn on_finalize(bn: BlockNumberFor) {
+ GearEthBridge::on_finalize(bn);
+ GearBuiltin::on_finalize(bn);
+ GearGas::on_finalize(bn);
+ Gear::on_finalize(bn);
+ GearBank::on_finalize(bn);
+ GearScheduler::on_finalize(bn);
+ GearMessenger::on_finalize(bn);
+ GearProgram::on_finalize(bn);
+
+ Session::on_finalize(bn);
+ Balances::on_finalize(bn);
+ Grandpa::on_finalize(bn);
+ Authorship::on_finalize(bn);
+
+ assert!(!System::events().iter().any(|e| {
+ matches!(
+ e.event,
+ RuntimeEvent::Gear(pallet_gear::Event::QueueNotProcessed)
+ )
+ }))
+}
+
+pub(crate) fn on_finalize_gear_block(bn: BlockNumberFor) {
+ Gear::run(frame_support::dispatch::RawOrigin::None.into(), None).unwrap();
+ on_finalize(bn);
+}
+
+pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
+ let bank_address = ::BankAddress::get();
+
+ ExtBuilder::default()
+ .endowment(ENDOWMENT)
+ .endowed_accounts(vec![bank_address, SIGNER, BLOCK_AUTHOR])
+ .build()
+}
+
+pub(crate) fn init_logger() {
+ let _ = env_logger::Builder::from_default_env()
+ .format_module_path(false)
+ .format_level(true)
+ .try_init();
+}
diff --git a/pallets/gear-eth-bridge/src/tests.rs b/pallets/gear-eth-bridge/src/tests.rs
new file mode 100644
index 00000000000..6ee1fea4aab
--- /dev/null
+++ b/pallets/gear-eth-bridge/src/tests.rs
@@ -0,0 +1,678 @@
+use crate::{builtin, mock::*, Config, EthMessage, WeightInfo};
+use common::Origin as _;
+use frame_support::{
+ assert_noop, assert_ok, assert_storage_noop, traits::Get, Blake2_256, StorageHasher,
+};
+use gbuiltin_eth_bridge::{Request, Response};
+use gear_core_errors::{ErrorReplyReason, ReplyCode, SimpleExecutionError};
+use pallet_gear::Event as GearEvent;
+use pallet_grandpa::Event as GrandpaEvent;
+use pallet_session::Event as SessionEvent;
+use parity_scale_codec::{Decode, Encode};
+use sp_core::{H160, H256};
+use sp_runtime::traits::{BadOrigin, Keccak256};
+use utils::*;
+
+const EPOCH_BLOCKS: u64 = EpochDuration::get();
+const ERA_BLOCKS: u64 = EPOCH_BLOCKS * SessionsPerEra::get() as u64;
+const WHEN_INITIALIZED: u64 = 42;
+
+type AuthoritySetHash = crate::AuthoritySetHash;
+type MessageNonce = crate::MessageNonce;
+type Queue = crate::Queue;
+type QueueChanged = crate::QueueChanged;
+type QueueMerkleRoot = crate::QueueMerkleRoot;
+type Initialized = crate::Initialized;
+type Paused = crate::Paused;
+type Event = crate::Event;
+type Error = crate::Error;
+
+#[test]
+fn bridge_got_initialized() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ do_events_assertion(0, 1, []);
+ assert!(!Initialized::get());
+ assert!(!QueueMerkleRoot::exists());
+ assert!(Paused::get());
+
+ run_to_block(EPOCH_BLOCKS);
+ do_events_assertion(0, 6, []);
+
+ run_to_block(EPOCH_BLOCKS + 1);
+ do_events_assertion(1, 7, [SessionEvent::NewSession { session_index: 1 }.into()]);
+
+ run_to_block(EPOCH_BLOCKS * 2);
+ do_events_assertion(1, 12, []);
+
+ run_to_block(EPOCH_BLOCKS * 2 + 1);
+ do_events_assertion(
+ 2,
+ 13,
+ [SessionEvent::NewSession { session_index: 2 }.into()],
+ );
+
+ run_to_block(EPOCH_BLOCKS * 3);
+ do_events_assertion(2, 18, []);
+
+ run_to_block(EPOCH_BLOCKS * 3 + 1);
+ do_events_assertion(
+ 3,
+ 19,
+ [SessionEvent::NewSession { session_index: 3 }.into()],
+ );
+
+ run_to_block(EPOCH_BLOCKS * 4);
+ do_events_assertion(3, 24, []);
+
+ run_to_block(EPOCH_BLOCKS * 4 + 1);
+ do_events_assertion(
+ 4,
+ 25,
+ [SessionEvent::NewSession { session_index: 4 }.into()],
+ );
+
+ run_to_block(EPOCH_BLOCKS * 5);
+ do_events_assertion(4, 30, []);
+
+ run_to_block(EPOCH_BLOCKS * 5 + 1);
+ do_events_assertion(
+ 5,
+ 31,
+ [SessionEvent::NewSession { session_index: 5 }.into()],
+ );
+
+ run_to_block(ERA_BLOCKS);
+ do_events_assertion(5, 36, []);
+
+ run_to_block(ERA_BLOCKS + 1);
+ do_events_assertion(
+ 6,
+ 37,
+ [
+ SessionEvent::NewSession { session_index: 6 }.into(),
+ Event::BridgeInitialized.into(),
+ ],
+ );
+ assert_eq!(QueueMerkleRoot::get(), Some(H256::zero()));
+ assert!(Initialized::get());
+ assert!(Paused::get());
+
+ on_finalize_gear_block(ERA_BLOCKS + 1);
+ do_events_assertion(
+ 6,
+ 37,
+ [GrandpaEvent::NewAuthorities {
+ authority_set: era_validators_authority_set(6),
+ }
+ .into()],
+ );
+ })
+}
+
+#[test]
+fn bridge_unpause_works() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_noop!(
+ GearEthBridge::unpause(RuntimeOrigin::signed(SIGNER)),
+ BadOrigin
+ );
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ System::assert_last_event(Event::BridgeUnpaused.into());
+
+ assert!(!Paused::get());
+
+ assert_storage_noop!(assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root())));
+ })
+}
+
+#[test]
+fn bridge_pause_works() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_noop!(
+ GearEthBridge::pause(RuntimeOrigin::signed(SIGNER)),
+ BadOrigin
+ );
+
+ assert_storage_noop!(assert_ok!(GearEthBridge::pause(RuntimeOrigin::root())));
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ assert_ok!(GearEthBridge::pause(RuntimeOrigin::root()));
+
+ System::assert_last_event(Event::BridgePaused.into());
+
+ assert!(Paused::get());
+
+ assert_storage_noop!(assert_ok!(GearEthBridge::pause(RuntimeOrigin::root())));
+ })
+}
+
+#[test]
+fn bridge_send_eth_message_works() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ assert_noop!(
+ GearEthBridge::send_eth_message(RuntimeOrigin::root(), H160::zero(), vec![]),
+ BadOrigin
+ );
+
+ assert_eq!(MessageNonce::get(), 0.into());
+ assert!(Queue::get().is_empty());
+
+ let destination = H160::random();
+ let payload = H256::random().as_bytes().to_vec();
+
+ let message = EthMessage::new(0.into(), SIGNER.cast(), destination, payload.clone());
+ let hash = message.hash();
+ let mut queue = vec![hash];
+
+ assert_ok!(GearEthBridge::send_eth_message(
+ RuntimeOrigin::signed(SIGNER),
+ destination,
+ payload
+ ));
+
+ System::assert_last_event(Event::MessageQueued { message, hash }.into());
+
+ assert_eq!(MessageNonce::get(), 1.into());
+ assert_eq!(Queue::get(), queue);
+
+ let destination = H160::random();
+ let payload = H256::random().as_bytes().to_vec();
+
+ let message = EthMessage::new(1.into(), SIGNER.cast(), destination, payload.clone());
+ let nonce = message.nonce();
+ let hash = message.hash();
+
+ queue.push(hash);
+
+ let (response, _) = run_block_with_builtin_call(
+ SIGNER,
+ Request::SendEthMessage {
+ destination,
+ payload,
+ },
+ None,
+ 0,
+ );
+
+ let response = Response::decode(&mut response.as_ref()).expect("should be `Response`");
+
+ assert_eq!(response, Response::EthMessageQueued { nonce, hash });
+
+ System::assert_has_event(Event::MessageQueued { message, hash }.into());
+
+ assert_eq!(MessageNonce::get(), 2.into());
+ assert_eq!(Queue::get(), queue);
+ })
+}
+
+#[test]
+fn bridge_queue_root_changes() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ assert!(!QueueChanged::get());
+
+ for _ in 0..4 {
+ assert_ok!(GearEthBridge::send_eth_message(
+ RuntimeOrigin::signed(SIGNER),
+ H160::random(),
+ H256::random().as_bytes().to_vec()
+ ));
+
+ assert!(QueueChanged::get());
+ }
+
+ let expected_root = binary_merkle_tree::merkle_root::(Queue::get());
+
+ on_finalize_gear_block(WHEN_INITIALIZED);
+
+ System::assert_last_event(Event::QueueMerkleRootChanged(expected_root).into());
+ assert!(!QueueChanged::get());
+
+ on_initialize(WHEN_INITIALIZED + 1);
+
+ assert!(!QueueChanged::get());
+ })
+}
+
+#[test]
+fn bridge_updates_authorities_and_clears() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ assert!(!AuthoritySetHash::exists());
+
+ run_to_block(ERA_BLOCKS + 1);
+ do_events_assertion(6, 37, None::<[_; 0]>);
+
+ on_finalize_gear_block(ERA_BLOCKS + 1);
+ do_events_assertion(
+ 6,
+ 37,
+ [GrandpaEvent::NewAuthorities {
+ authority_set: era_validators_authority_set(6),
+ }
+ .into()],
+ );
+
+ on_initialize(ERA_BLOCKS + 2);
+ do_events_assertion(6, 38, []);
+
+ assert!(!AuthoritySetHash::exists());
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ for _ in 0..5 {
+ assert_ok!(GearEthBridge::send_eth_message(
+ RuntimeOrigin::signed(SIGNER),
+ H160::zero(),
+ vec![]
+ ));
+ }
+
+ on_finalize_gear_block(ERA_BLOCKS + 2);
+
+ assert_eq!(System::events().len(), 7);
+ assert!(matches!(
+ System::events().last().expect("infallible").event,
+ RuntimeEvent::GearEthBridge(Event::QueueMerkleRootChanged(_))
+ ));
+ assert!(!QueueMerkleRoot::get().expect("infallible").is_zero());
+
+ on_initialize(ERA_BLOCKS + 3);
+ do_events_assertion(6, 39, None::<[_; 0]>);
+
+ run_to_block(ERA_BLOCKS + EPOCH_BLOCKS * 5);
+ do_events_assertion(
+ 10,
+ 66,
+ [
+ SessionEvent::NewSession { session_index: 7 }.into(),
+ SessionEvent::NewSession { session_index: 8 }.into(),
+ SessionEvent::NewSession { session_index: 9 }.into(),
+ SessionEvent::NewSession { session_index: 10 }.into(),
+ ],
+ );
+
+ let authority_set = era_validators_authority_set(12);
+ let authority_set_ids_concat = authority_set
+ .clone()
+ .into_iter()
+ .flat_map(|(public, _)| public.into_inner().0)
+ .collect::>();
+ let authority_set_hash: H256 = Blake2_256::hash(&authority_set_ids_concat).into();
+
+ run_to_block(ERA_BLOCKS + EPOCH_BLOCKS * 5 + 1);
+ do_events_assertion(
+ 11,
+ 67,
+ [
+ SessionEvent::NewSession { session_index: 11 }.into(),
+ Event::AuthoritySetHashChanged(authority_set_hash).into(),
+ ],
+ );
+
+ assert_eq!(
+ AuthoritySetHash::get().expect("infallible"),
+ authority_set_hash
+ );
+ assert!(!QueueMerkleRoot::get().expect("infallible").is_zero());
+
+ run_to_block(ERA_BLOCKS * 2 + 1);
+ do_events_assertion(
+ 12,
+ 73,
+ [SessionEvent::NewSession { session_index: 12 }.into()],
+ );
+
+ on_finalize_gear_block(ERA_BLOCKS * 2 + 1);
+ System::assert_last_event(GrandpaEvent::NewAuthorities { authority_set }.into());
+
+ System::reset_events();
+
+ on_initialize(ERA_BLOCKS * 2 + 2);
+ do_events_assertion(12, 74, [Event::BridgeCleared.into()]);
+
+ assert!(!AuthoritySetHash::exists());
+ assert!(QueueMerkleRoot::get().expect("infallible").is_zero());
+
+ run_to_block(ERA_BLOCKS * 2 + EPOCH_BLOCKS * 5);
+ do_events_assertion(
+ 16,
+ 102,
+ [
+ SessionEvent::NewSession { session_index: 13 }.into(),
+ SessionEvent::NewSession { session_index: 14 }.into(),
+ SessionEvent::NewSession { session_index: 15 }.into(),
+ SessionEvent::NewSession { session_index: 16 }.into(),
+ ],
+ );
+
+ let authority_set = era_validators_authority_set(18);
+ let authority_set_ids_concat = authority_set
+ .clone()
+ .into_iter()
+ .flat_map(|(public, _)| public.into_inner().0)
+ .collect::>();
+ let authority_set_hash: H256 = Blake2_256::hash(&authority_set_ids_concat).into();
+
+ run_to_block(ERA_BLOCKS * 2 + EPOCH_BLOCKS * 5 + 1);
+ do_events_assertion(
+ 17,
+ 103,
+ [
+ SessionEvent::NewSession { session_index: 17 }.into(),
+ Event::AuthoritySetHashChanged(authority_set_hash).into(),
+ ],
+ );
+
+ run_to_block(ERA_BLOCKS * 3 + 1);
+ on_finalize_gear_block(ERA_BLOCKS * 3 + 1);
+ do_events_assertion(
+ 18,
+ 109,
+ [
+ SessionEvent::NewSession { session_index: 18 }.into(),
+ GrandpaEvent::NewAuthorities { authority_set }.into(),
+ ],
+ );
+
+ on_initialize(ERA_BLOCKS * 3 + 2);
+ do_events_assertion(18, 110, [Event::BridgeCleared.into()]);
+ })
+}
+
+#[test]
+fn bridge_is_not_yet_initialized_err() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ const ERR: Error = Error::BridgeIsNotYetInitialized;
+
+ run_to_block(1);
+ run_block_and_assert_bridge_error(ERR);
+
+ run_to_block(ERA_BLOCKS - 1);
+ run_block_and_assert_bridge_error(ERR);
+ })
+}
+
+#[test]
+fn bridge_is_paused_err() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ const ERR: Error = Error::BridgeIsPaused;
+
+ run_to_block(WHEN_INITIALIZED);
+ run_block_and_assert_messaging_error(
+ Request::SendEthMessage {
+ destination: H160::zero(),
+ payload: vec![],
+ },
+ ERR,
+ );
+ })
+}
+
+#[test]
+fn bridge_max_payload_size_exceeded_err() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ const ERR: Error = Error::MaxPayloadSizeExceeded;
+
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ let max_payload_size: u32 = ::MaxPayloadSize::get();
+
+ run_block_and_assert_messaging_error(
+ Request::SendEthMessage {
+ destination: H160::zero(),
+ payload: vec![0; max_payload_size as usize + 1],
+ },
+ ERR,
+ );
+ })
+}
+
+#[test]
+fn bridge_queue_capacity_exceeded_err() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ const ERR: Error = Error::QueueCapacityExceeded;
+
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ for _ in 0..::QueueCapacity::get() {
+ assert_ok!(GearEthBridge::send_eth_message(
+ RuntimeOrigin::signed(SIGNER),
+ H160::zero(),
+ vec![]
+ ));
+ }
+
+ run_block_and_assert_messaging_error(
+ Request::SendEthMessage {
+ destination: H160::zero(),
+ payload: vec![],
+ },
+ ERR,
+ );
+ })
+}
+
+#[test]
+fn bridge_incorrect_value_applied_err() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ const ERR: Error = Error::IncorrectValueApplied;
+
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ let (response, _) = run_block_with_builtin_call(
+ SIGNER,
+ Request::SendEthMessage {
+ destination: H160::zero(),
+ payload: vec![],
+ },
+ None,
+ 1,
+ );
+
+ assert_eq!(
+ String::from_utf8_lossy(&response),
+ format!("Panic occurred: {}", builtin::error_to_str(&ERR))
+ );
+ })
+}
+
+#[test]
+fn bridge_insufficient_gas_err() {
+ init_logger();
+ new_test_ext().execute_with(|| {
+ const ERR_CODE: ReplyCode = ReplyCode::Error(ErrorReplyReason::Execution(
+ SimpleExecutionError::RanOutOfGas,
+ ));
+
+ run_to_block(WHEN_INITIALIZED);
+
+ assert_ok!(GearEthBridge::unpause(RuntimeOrigin::root()));
+
+ let (_, code) = run_block_with_builtin_call(
+ SIGNER,
+ Request::SendEthMessage {
+ destination: H160::zero(),
+ payload: vec![],
+ },
+ Some(::WeightInfo::send_eth_message().ref_time() - 1),
+ 0,
+ );
+
+ assert_eq!(code, ERR_CODE);
+ })
+}
+
+mod utils {
+ use super::*;
+ use crate::builtin;
+ use gear_core::message::UserMessage;
+ use gprimitives::{ActorId, MessageId};
+ use pallet_gear_builtin::BuiltinActor;
+
+ pub(crate) fn builtin_id() -> ActorId {
+ GearBuiltin::generate_actor_id(builtin::Actor::::ID)
+ }
+
+ #[track_caller]
+ pub(crate) fn run_block_and_assert_bridge_error(error: Error) {
+ assert_noop!(GearEthBridge::pause(RuntimeOrigin::root()), error.clone());
+
+ assert_noop!(GearEthBridge::unpause(RuntimeOrigin::root()), error.clone());
+
+ run_block_and_assert_messaging_error(
+ Request::SendEthMessage {
+ destination: H160::zero(),
+ payload: vec![],
+ },
+ error,
+ );
+ }
+
+ #[track_caller]
+ pub(crate) fn run_block_and_assert_messaging_error(request: Request, error: Error) {
+ let err_str = builtin::error_to_str(&error);
+
+ assert_noop!(
+ match request.clone() {
+ Request::SendEthMessage {
+ destination,
+ payload,
+ } => {
+ GearEthBridge::send_eth_message(
+ RuntimeOrigin::signed(SIGNER),
+ destination,
+ payload,
+ )
+ }
+ },
+ error
+ );
+
+ let (response, _) = run_block_with_builtin_call(SIGNER, request, None, 0);
+
+ assert_eq!(
+ String::from_utf8_lossy(&response),
+ format!("Panic occurred: {err_str}")
+ );
+ }
+
+ #[track_caller]
+ pub(crate) fn run_block_with_builtin_call(
+ source: AccountId,
+ request: Request,
+ gas_limit: Option,
+ value: u128,
+ ) -> (Vec, ReplyCode) {
+ assert_ok!(Gear::send_message(
+ RuntimeOrigin::signed(source),
+ builtin_id(),
+ request.encode(),
+ gas_limit
+ .unwrap_or_else(|| ::WeightInfo::send_eth_message().ref_time()),
+ value,
+ false,
+ ));
+
+ let mid = last_message_queued();
+
+ run_to_next_block();
+
+ let message = last_user_message_sent();
+
+ let reply_details = message.details().expect("must be reply");
+ assert_eq!(reply_details.to_message_id(), mid);
+
+ (
+ message.payload_bytes().to_vec(),
+ reply_details.to_reply_code(),
+ )
+ }
+
+ #[track_caller]
+ pub(crate) fn last_message_queued() -> MessageId {
+ System::events()
+ .into_iter()
+ .rev()
+ .find_map(|e| {
+ if let RuntimeEvent::Gear(GearEvent::MessageQueued { id, .. }) = e.event {
+ Some(id)
+ } else {
+ None
+ }
+ })
+ .expect("message queued not found")
+ }
+
+ #[track_caller]
+ pub(crate) fn last_user_message_sent() -> UserMessage {
+ System::events()
+ .into_iter()
+ .rev()
+ .find_map(|e| {
+ if let RuntimeEvent::Gear(GearEvent::UserMessageSent { message, .. }) = e.event {
+ Some(message)
+ } else {
+ None
+ }
+ })
+ .expect("user message sent not found")
+ }
+
+ #[track_caller]
+ pub(crate) fn do_events_assertion(
+ session: u32,
+ block_number: u64,
+ events: impl Into