From e31c4099f7e4050579384a3d8319c6d33963e2bd Mon Sep 17 00:00:00 2001 From: Dmitrii Novikov Date: Tue, 6 Aug 2024 15:30:09 +0400 Subject: [PATCH] feat: `pallet-gear-eth-bridge` (#4015) --- .maintain/frame-weight-template.hbs | 1 + Cargo.lock | 84 +++ Cargo.toml | 8 +- gbuiltins/eth-bridge/Cargo.toml | 12 + gbuiltins/eth-bridge/src/lib.rs | 38 + gsdk/src/metadata/generated.rs | 143 ++++ node/service/Cargo.toml | 2 + node/service/src/client.rs | 2 + node/service/src/rpc/mod.rs | 6 +- pallets/gear-builtin/rpc/Cargo.toml | 2 +- pallets/gear-eth-bridge/Cargo.toml | 102 +++ pallets/gear-eth-bridge/rpc/Cargo.toml | 20 + .../rpc/runtime-api/Cargo.toml | 21 + .../rpc/runtime-api/src/lib.rs | 30 + pallets/gear-eth-bridge/rpc/src/lib.rs | 80 +++ pallets/gear-eth-bridge/src/benchmarking.rs | 71 ++ pallets/gear-eth-bridge/src/builtin.rs | 105 +++ pallets/gear-eth-bridge/src/internal.rs | 138 ++++ pallets/gear-eth-bridge/src/lib.rs | 564 +++++++++++++++ pallets/gear-eth-bridge/src/mock.rs | 430 +++++++++++ pallets/gear-eth-bridge/src/tests.rs | 678 ++++++++++++++++++ pallets/gear-eth-bridge/src/weights.rs | 107 +++ runtime/vara/Cargo.toml | 7 + runtime/vara/src/lib.rs | 132 +++- runtime/vara/src/tests.rs | 46 ++ runtime/vara/src/weights/mod.rs | 4 + .../vara/src/weights/pallet_gear_builtin.rs | 42 ++ .../src/weights/pallet_gear_eth_bridge.rs | 106 +++ utils/wasm-proc/src/main.rs | 3 +- 29 files changed, 2975 insertions(+), 9 deletions(-) create mode 100644 gbuiltins/eth-bridge/Cargo.toml create mode 100644 gbuiltins/eth-bridge/src/lib.rs create mode 100644 pallets/gear-eth-bridge/Cargo.toml create mode 100644 pallets/gear-eth-bridge/rpc/Cargo.toml create mode 100644 pallets/gear-eth-bridge/rpc/runtime-api/Cargo.toml create mode 100644 pallets/gear-eth-bridge/rpc/runtime-api/src/lib.rs create mode 100644 pallets/gear-eth-bridge/rpc/src/lib.rs create mode 100644 pallets/gear-eth-bridge/src/benchmarking.rs create mode 100644 pallets/gear-eth-bridge/src/builtin.rs create mode 100644 pallets/gear-eth-bridge/src/internal.rs create mode 100644 pallets/gear-eth-bridge/src/lib.rs create mode 100644 pallets/gear-eth-bridge/src/mock.rs create mode 100644 pallets/gear-eth-bridge/src/tests.rs create mode 100644 pallets/gear-eth-bridge/src/weights.rs create mode 100644 runtime/vara/src/weights/pallet_gear_eth_bridge.rs diff --git a/.maintain/frame-weight-template.hbs b/.maintain/frame-weight-template.hbs index 765cf477abc..c6613fbc240 100644 --- a/.maintain/frame-weight-template.hbs +++ b/.maintain/frame-weight-template.hbs @@ -31,6 +31,7 @@ #![allow(unused_parens)] #![allow(unused_imports)] #![allow(clippy::unnecessary_cast)] +#![allow(missing_docs)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use sp_std::marker::PhantomData; diff --git a/Cargo.lock b/Cargo.lock index 2ed23bdb753..a7a0fc64748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1673,6 +1673,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[package]] +name = "binary-merkle-tree" +version = "4.0.0-dev" +source = "git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-v1.4.0#09bdd2a6953d057ae360ec3ef6ec735f9306cc04" +dependencies = [ + "hash-db", + "log", +] + [[package]] name = "bincode" version = "1.3.3" @@ -6028,6 +6037,15 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "gbuiltin-eth-bridge" +version = "1.5.0" +dependencies = [ + "gprimitives", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "gbuiltin-staking" version = "1.5.0" @@ -6674,6 +6692,8 @@ dependencies = [ "log", "pallet-gear-builtin-rpc", "pallet-gear-builtin-rpc-runtime-api", + "pallet-gear-eth-bridge-rpc", + "pallet-gear-eth-bridge-rpc-runtime-api", "pallet-gear-rpc", "pallet-gear-rpc-runtime-api", "pallet-gear-staking-rewards-rpc", @@ -11150,6 +11170,68 @@ dependencies = [ "wabt", ] +[[package]] +name = "pallet-gear-eth-bridge" +version = "1.5.0" +dependencies = [ + "binary-merkle-tree", + "env_logger", + "frame-benchmarking", + "frame-election-provider-support", + "frame-support", + "frame-support-test", + "frame-system", + "gbuiltin-eth-bridge", + "gear-common", + "gear-core", + "gear-core-errors", + "gprimitives", + "log", + "pallet-authorship", + "pallet-balances", + "pallet-gear", + "pallet-gear-bank", + "pallet-gear-builtin", + "pallet-gear-gas", + "pallet-gear-messenger", + "pallet-gear-program", + "pallet-gear-scheduler", + "pallet-grandpa", + "pallet-session", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "serde", + "sp-consensus-grandpa", + "sp-core", + "sp-io", + "sp-runtime", + "sp-session", + "sp-staking", + "sp-std 8.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-v1.4.0)", +] + +[[package]] +name = "pallet-gear-eth-bridge-rpc" +version = "1.5.0" +dependencies = [ + "jsonrpsee 0.16.3", + "pallet-gear-eth-bridge-rpc-runtime-api", + "primitive-types", + "sp-api", + "sp-blockchain", + "sp-runtime", +] + +[[package]] +name = "pallet-gear-eth-bridge-rpc-runtime-api" +version = "1.5.0" +dependencies = [ + "pallet-gear-eth-bridge", + "sp-api", + "sp-core", +] + [[package]] name = "pallet-gear-gas" version = "1.5.0" @@ -18162,6 +18244,8 @@ dependencies = [ "pallet-gear-builtin", "pallet-gear-builtin-rpc-runtime-api", "pallet-gear-debug", + "pallet-gear-eth-bridge", + "pallet-gear-eth-bridge-rpc-runtime-api", "pallet-gear-gas", "pallet-gear-messenger", "pallet-gear-payment", diff --git a/Cargo.toml b/Cargo.toml index a5b37e003b1..c471c1267aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,8 +82,7 @@ members = [ "examples/waiting-proxy", "examples/wat", "galloc", - "gbuiltins/bls381", - "gbuiltins/staking", + "gbuiltins/*", "gcli", "gclient", "gcore", @@ -211,6 +210,7 @@ common = { package = "gear-common", path = "common", default-features = false } core-processor = { package = "gear-core-processor", path = "core-processor", default-features = false } galloc = { path = "galloc" } gbuiltin-bls381 = { path = "gbuiltins/bls381", default-features = false } +gbuiltin-eth-bridge = { path = "gbuiltins/eth-bridge", default-features = false } gbuiltin-staking = { path = "gbuiltins/staking" } gcore = { path = "gcore" } gcli = { path = "gcli" } @@ -258,6 +258,9 @@ actor-system-error = { path = "utils/actor-system-error" } calc-stack-height = { path = "utils/calc-stack-height" } pallet-gear = { path = "pallets/gear", default-features = false } pallet-gear-debug = { path = "pallets/gear-debug", default-features = false } +pallet-gear-eth-bridge = { path = "pallets/gear-eth-bridge", default-features = false } +pallet-gear-eth-bridge-rpc = { path = "pallets/gear-eth-bridge/rpc", default-features = false } +pallet-gear-eth-bridge-rpc-runtime-api = { path = "pallets/gear-eth-bridge/rpc/runtime-api", default-features = false } pallet-gear-gas = { path = "pallets/gas", default-features = false } pallet-gear-messenger = { path = "pallets/gear-messenger", default-features = false } pallet-gear-payment = { path = "pallets/payment", default-features = false } @@ -303,6 +306,7 @@ sandbox-wasmer-types = { package = "wasmer-types", version = "4.3.4" } sandbox-wasmi = { package = "wasmi", git = "https://github.com/gear-tech/wasmi", branch = "v0.13.2-sign-ext", features = ["virtual_memory"] } # Substrate deps +binary-merkle-tree = { version = "4.0.0-dev", git = "https://github.com/gear-tech/polkadot-sdk.git", branch = "gear-v1.4.0", default-features = false } frame-benchmarking = { version = "4.0.0-dev", git = "https://github.com/gear-tech/polkadot-sdk.git", branch = "gear-v1.4.0", default-features = false } frame-benchmarking-cli = { version = "4.0.0-dev", git = "https://github.com/gear-tech/polkadot-sdk.git", branch = "gear-v1.4.0" } frame-election-provider-support = { version = "4.0.0-dev", git = "https://github.com/gear-tech/polkadot-sdk.git", branch = "gear-v1.4.0", default-features = false } diff --git a/gbuiltins/eth-bridge/Cargo.toml b/gbuiltins/eth-bridge/Cargo.toml new file mode 100644 index 00000000000..f5b83ce16e4 --- /dev/null +++ b/gbuiltins/eth-bridge/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gbuiltin-eth-bridge" +description = "Entities for working with Gear builtin actor providing `pallet-gear-eth-bridge` interface" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +parity-scale-codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +gprimitives.workspace = true diff --git a/gbuiltins/eth-bridge/src/lib.rs b/gbuiltins/eth-bridge/src/lib.rs new file mode 100644 index 00000000000..a42ae7f6b5e --- /dev/null +++ b/gbuiltins/eth-bridge/src/lib.rs @@ -0,0 +1,38 @@ +// 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 . + +#![no_std] + +extern crate alloc; + +use alloc::vec::Vec; +use gprimitives::{H160, H256, U256}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] +pub enum Request { + #[codec(index = 0)] + SendEthMessage { destination: H160, payload: Vec }, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] +pub enum Response { + #[codec(index = 0)] + EthMessageQueued { nonce: U256, hash: H256 }, +} diff --git a/gsdk/src/metadata/generated.rs b/gsdk/src/metadata/generated.rs index 4edc4c06d65..e32efdd40d5 100644 --- a/gsdk/src/metadata/generated.rs +++ b/gsdk/src/metadata/generated.rs @@ -2674,6 +2674,91 @@ pub mod runtime_types { } } } + pub mod pallet_gear_eth_bridge { + use super::runtime_types; + pub mod internal { + use super::runtime_types; + #[derive(Debug, crate::gp::Decode, crate::gp::DecodeAsType, crate::gp::Encode)] + pub struct EthMessage { + pub nonce: runtime_types::primitive_types::U256, + pub source: ::subxt::utils::H256, + pub destination: ::subxt::utils::H160, + pub payload: ::std::vec::Vec<::core::primitive::u8>, + } + } + pub mod pallet { + use super::runtime_types; + #[derive(Debug, crate::gp::Decode, crate::gp::DecodeAsType, crate::gp::Encode)] + #[doc = "Contains a variant per dispatchable extrinsic that this pallet has."] + pub enum Call { + #[codec(index = 0)] + #[doc = "See [`Pallet::pause`]."] + pause, + #[codec(index = 1)] + #[doc = "See [`Pallet::unpause`]."] + unpause, + #[codec(index = 2)] + #[doc = "See [`Pallet::send_eth_message`]."] + send_eth_message { + destination: ::subxt::utils::H160, + payload: ::std::vec::Vec<::core::primitive::u8>, + }, + } + #[derive(Debug, crate::gp::Decode, crate::gp::DecodeAsType, crate::gp::Encode)] + #[doc = "Pallet Gear Eth Bridge's error."] + pub enum Error { + #[codec(index = 0)] + #[doc = "The error happens when bridge got called before"] + #[doc = "proper initialization after deployment."] + BridgeIsNotYetInitialized, + #[codec(index = 1)] + #[doc = "The error happens when bridge got called when paused."] + BridgeIsPaused, + #[codec(index = 2)] + #[doc = "The error happens when bridging message sent with too big payload."] + MaxPayloadSizeExceeded, + #[codec(index = 3)] + #[doc = "The error happens when bridging queue capacity exceeded,"] + #[doc = "so message couldn't be sent."] + QueueCapacityExceeded, + #[codec(index = 4)] + #[doc = "The error happens when bridging thorough builtin and message value"] + #[doc = "is inapplicable to operation or insufficient."] + IncorrectValueApplied, + } + #[derive(Debug, crate::gp::Decode, crate::gp::DecodeAsType, crate::gp::Encode)] + #[doc = "Pallet Gear Eth Bridge's event."] + pub enum Event { + #[codec(index = 0)] + #[doc = "Grandpa validator's keys set was hashed and set in storage at"] + #[doc = "first block of the last session in the era."] + AuthoritySetHashChanged(::subxt::utils::H256), + #[codec(index = 1)] + #[doc = "Bridge got cleared on initialization of the second block in a new era."] + BridgeCleared, + #[codec(index = 2)] + #[doc = "Optimistically, single-time called event defining that pallet"] + #[doc = "got initialized and started processing session changes,"] + #[doc = "as well as putting initial zeroed queue merkle root."] + BridgeInitialized, + #[codec(index = 3)] + #[doc = "Bridge was paused and temporary doesn't process any incoming requests."] + BridgePaused, + #[codec(index = 4)] + #[doc = "Bridge was unpaused and from now on processes any incoming requests."] + BridgeUnpaused, + #[codec(index = 5)] + #[doc = "A new message was queued for bridging."] + MessageQueued { + message: runtime_types::pallet_gear_eth_bridge::internal::EthMessage, + hash: ::subxt::utils::H256, + }, + #[codec(index = 6)] + #[doc = "Merkle root of the queue changed: new messages queued within the block."] + QueueMerkleRootChanged(::subxt::utils::H256), + } + } + } pub mod pallet_gear_gas { use super::runtime_types; pub mod pallet { @@ -6425,6 +6510,11 @@ pub mod runtime_types { } } } + pub mod primitive_types { + use super::runtime_types; + #[derive(Debug, crate::gp::Decode, crate::gp::DecodeAsType, crate::gp::Encode)] + pub struct U256(pub [::core::primitive::u64; 4usize]); + } pub mod sp_arithmetic { use super::runtime_types; pub mod fixed_point { @@ -7725,6 +7815,8 @@ pub mod runtime_types { StakingRewards(runtime_types::pallet_gear_staking_rewards::pallet::Call), #[codec(index = 107)] GearVoucher(runtime_types::pallet_gear_voucher::pallet::Call), + #[codec(index = 110)] + GearEthBridge(runtime_types::pallet_gear_eth_bridge::pallet::Call), #[codec(index = 99)] Sudo(runtime_types::pallet_sudo::pallet::Call), #[codec(index = 199)] @@ -7800,6 +7892,8 @@ pub mod runtime_types { GearVoucher(runtime_types::pallet_gear_voucher::pallet::Error), #[codec(index = 108)] GearBank(runtime_types::pallet_gear_bank::pallet::Error), + #[codec(index = 110)] + GearEthBridge(runtime_types::pallet_gear_eth_bridge::pallet::Error), #[codec(index = 99)] Sudo(runtime_types::pallet_sudo::pallet::Error), #[codec(index = 199)] @@ -7867,6 +7961,8 @@ pub mod runtime_types { StakingRewards(runtime_types::pallet_gear_staking_rewards::pallet::Event), #[codec(index = 107)] GearVoucher(runtime_types::pallet_gear_voucher::pallet::Event), + #[codec(index = 110)] + GearEthBridge(runtime_types::pallet_gear_eth_bridge::pallet::Event), #[codec(index = 99)] Sudo(runtime_types::pallet_sudo::pallet::Event), #[codec(index = 199)] @@ -8141,6 +8237,22 @@ pub mod calls { } } } + #[doc = "Calls of pallet `GearEthBridge`."] + pub enum GearEthBridgeCall { + Pause, + Unpause, + SendEthMessage, + } + impl CallInfo for GearEthBridgeCall { + const PALLET: &'static str = "GearEthBridge"; + fn call_name(&self) -> &'static str { + match self { + Self::Pause => "pause", + Self::Unpause => "unpause", + Self::SendEthMessage => "send_eth_message", + } + } + } #[doc = "Calls of pallet `GearVoucher`."] pub enum GearVoucherCall { Issue, @@ -8941,6 +9053,34 @@ pub mod storage { } } } + #[doc = "Storage of pallet `GearEthBridge`."] + pub enum GearEthBridgeStorage { + Initialized, + Paused, + AuthoritySetHash, + QueueMerkleRoot, + Queue, + SessionsTimer, + ClearTimer, + MessageNonce, + QueueChanged, + } + impl StorageInfo for GearEthBridgeStorage { + const PALLET: &'static str = "GearEthBridge"; + fn storage_name(&self) -> &'static str { + match self { + Self::Initialized => "Initialized", + Self::Paused => "Paused", + Self::AuthoritySetHash => "AuthoritySetHash", + Self::QueueMerkleRoot => "QueueMerkleRoot", + Self::Queue => "Queue", + Self::SessionsTimer => "SessionsTimer", + Self::ClearTimer => "ClearTimer", + Self::MessageNonce => "MessageNonce", + Self::QueueChanged => "QueueChanged", + } + } + } #[doc = "Storage of pallet `GearGas`."] pub enum GearGasStorage { TotalIssuance, @@ -9609,6 +9749,9 @@ pub mod exports { pub mod gear_voucher { pub use super::runtime_types::pallet_gear_voucher::pallet::Event; } + pub mod gear_eth_bridge { + pub use super::runtime_types::pallet_gear_eth_bridge::pallet::Event; + } pub mod sudo { pub use super::runtime_types::pallet_sudo::pallet::Event; } diff --git a/node/service/Cargo.toml b/node/service/Cargo.toml index 86774116ad4..efd3c51908e 100644 --- a/node/service/Cargo.toml +++ b/node/service/Cargo.toml @@ -28,6 +28,8 @@ pallet-gear-staking-rewards-rpc.workspace = true pallet-gear-staking-rewards-rpc-runtime-api = { workspace = true, features = ["std"] } pallet-gear-builtin-rpc.workspace = true pallet-gear-builtin-rpc-runtime-api = { workspace = true, features = ["std"] } +pallet-gear-eth-bridge-rpc.workspace = true +pallet-gear-eth-bridge-rpc-runtime-api = { workspace = true, features = ["std"] } runtime-primitives = { workspace = true, features = ["std"] } gear-runtime-interface = { workspace = true, features = ["std"] } authorship.workspace = true diff --git a/node/service/src/client.rs b/node/service/src/client.rs index 831a55e61a9..ba556f096a4 100644 --- a/node/service/src/client.rs +++ b/node/service/src/client.rs @@ -95,6 +95,7 @@ pub trait RuntimeApiCollection: + pallet_gear_rpc_runtime_api::GearApi + pallet_gear_staking_rewards_rpc_runtime_api::GearStakingRewardsApi + pallet_gear_builtin_rpc_runtime_api::GearBuiltinApi + + pallet_gear_eth_bridge_rpc_runtime_api::GearEthBridgeApi { } @@ -112,6 +113,7 @@ impl RuntimeApiCollection for Api where + pallet_gear_rpc_runtime_api::GearApi + pallet_gear_staking_rewards_rpc_runtime_api::GearStakingRewardsApi + pallet_gear_builtin_rpc_runtime_api::GearBuiltinApi + + pallet_gear_eth_bridge_rpc_runtime_api::GearEthBridgeApi { } diff --git a/node/service/src/rpc/mod.rs b/node/service/src/rpc/mod.rs index 2eb27388eb1..82baa36a2ab 100644 --- a/node/service/src/rpc/mod.rs +++ b/node/service/src/rpc/mod.rs @@ -109,6 +109,7 @@ where C::Api: substrate_frame_rpc_system::AccountNonceApi, C::Api: pallet_gear_rpc::GearRuntimeApi, C::Api: pallet_gear_builtin_rpc::GearBuiltinRuntimeApi, + C::Api: pallet_gear_eth_bridge_rpc::GearEthBridgeRuntimeApi, C::Api: pallet_gear_staking_rewards_rpc::GearStakingRewardsRuntimeApi, C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, C::Api: BabeApi, @@ -119,6 +120,7 @@ where B::State: StateBackend>, { use pallet_gear_builtin_rpc::{GearBuiltin, GearBuiltinApiServer}; + use pallet_gear_eth_bridge_rpc::{GearEthBridge, GearEthBridgeApiServer}; use pallet_gear_rpc::{Gear, GearApiServer}; use pallet_gear_staking_rewards_rpc::{GearStakingRewards, GearStakingRewardsApiServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; @@ -211,7 +213,9 @@ where io.merge(GearStakingRewards::new(client.clone()).into_rpc())?; - io.merge(GearBuiltin::new(client).into_rpc())?; + io.merge(GearBuiltin::new(client.clone()).into_rpc())?; + + io.merge(GearEthBridge::new(client).into_rpc())?; Ok(io) } diff --git a/pallets/gear-builtin/rpc/Cargo.toml b/pallets/gear-builtin/rpc/Cargo.toml index a7573d16472..513a88c4a52 100644 --- a/pallets/gear-builtin/rpc/Cargo.toml +++ b/pallets/gear-builtin/rpc/Cargo.toml @@ -17,4 +17,4 @@ sp-core.workspace = true sp-runtime.workspace = true # Local packages -pallet-gear-builtin-rpc-runtime-api.workspace = true +pallet-gear-builtin-rpc-runtime-api = { workspace = true, features = ["std"] } diff --git a/pallets/gear-eth-bridge/Cargo.toml b/pallets/gear-eth-bridge/Cargo.toml new file mode 100644 index 00000000000..ce05edba94e --- /dev/null +++ b/pallets/gear-eth-bridge/Cargo.toml @@ -0,0 +1,102 @@ +[package] +name = "pallet-gear-eth-bridge" +description = "Pallet containing interface for storing and mutating data for Ethereum bridging" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# Operational deps. +log.workspace = true +parity-scale-codec.workspace = true +scale-info.workspace = true +serde = { workspace = true, features = ["derive"], optional = true } + +# Gear deps. +common.workspace = true +gbuiltin-eth-bridge.workspace = true +gear-core.workspace = true +gprimitives.workspace = true +pallet-gear-builtin.workspace = true + +# Substrate deps. +binary-merkle-tree.workspace = true +frame-benchmarking = { workspace = true, optional = true } +frame-support.workspace = true +frame-system.workspace = true +sp-consensus-grandpa.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true + +[dev-dependencies] +gprimitives.workspace = true +gear-core-errors.workspace = true +sp-core = { workspace = true, features = ["std"] } +sp-io = { workspace = true, features = ["std"] } +sp-staking = { workspace = true, features = ["std"] } +frame-support-test = { workspace = true, features = ["std"] } +frame-election-provider-support = { workspace = true, features = ["std"] } +pallet-authorship = { workspace = true, features = ["std"] } +pallet-balances = { workspace = true, features = ["std"] } +pallet-grandpa = { workspace = true, features = ["std"] } +pallet-session = { workspace = true, features = ["std"] } +sp-session = { workspace = true, features = ["std"] } +pallet-gear = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true, features = ["std"] } +pallet-gear-bank = { workspace = true, features = ["std"] } +pallet-gear-builtin = { workspace = true, features = ["std"] } +pallet-gear-gas = { workspace = true, features = ["std"] } +pallet-gear-messenger = { workspace = true, features = ["std"] } +pallet-gear-program = { workspace = true, features = ["std"] } +pallet-gear-scheduler = { workspace = true, features = ["std"] } +env_logger.workspace = true + +[features] +default = ['std'] +std = [ + # Operational deps. + "log/std", + "parity-scale-codec/std", + "scale-info/std", + "dep:serde", + + # Gear deps. + "common/std", + "gear-core/std", + "pallet-gear-builtin/std", + + # Substrate deps. + "binary-merkle-tree/std", + "frame-support/std", + "frame-system/std", + "sp-consensus-grandpa/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + # Gear deps. + "common/runtime-benchmarks", + "pallet-gear-builtin/runtime-benchmarks", + + # Substrate deps. + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + # Gear deps. + "common/runtime-benchmarks", + "pallet-gear-builtin/runtime-benchmarks", + + # Substrate deps. + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/gear-eth-bridge/rpc/Cargo.toml b/pallets/gear-eth-bridge/rpc/Cargo.toml new file mode 100644 index 00000000000..1f53ff6ba58 --- /dev/null +++ b/pallets/gear-eth-bridge/rpc/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pallet-gear-eth-bridge-rpc" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +jsonrpsee = { workspace = true, features = ["server", "macros"] } +primitive-types.workspace = true + +# Substrate packages +sp-api.workspace = true +sp-blockchain.workspace = true +sp-runtime.workspace = true + +# Local packages +pallet-gear-eth-bridge-rpc-runtime-api = { workspace = true, features = ["std"] } diff --git a/pallets/gear-eth-bridge/rpc/runtime-api/Cargo.toml b/pallets/gear-eth-bridge/rpc/runtime-api/Cargo.toml new file mode 100644 index 00000000000..44d9a351d8e --- /dev/null +++ b/pallets/gear-eth-bridge/rpc/runtime-api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pallet-gear-eth-bridge-rpc-runtime-api" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +sp-api.workspace = true +sp-core.workspace = true +pallet-gear-eth-bridge.workspace = true + +[features] +default = ["std"] +std = [ + "sp-api/std", + "sp-core/std", + "pallet-gear-eth-bridge/std", +] diff --git a/pallets/gear-eth-bridge/rpc/runtime-api/src/lib.rs b/pallets/gear-eth-bridge/rpc/runtime-api/src/lib.rs new file mode 100644 index 00000000000..25169ac6737 --- /dev/null +++ b/pallets/gear-eth-bridge/rpc/runtime-api/src/lib.rs @@ -0,0 +1,30 @@ +// 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 . + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet_gear_eth_bridge::Proof; + +use sp_core::H256; + +sp_api::decl_runtime_apis! { + pub trait GearEthBridgeApi { + /// Generate merkle inclusion proof of the message hash in bridge queue. + fn merkle_proof(hash: H256) -> Option; + } +} diff --git a/pallets/gear-eth-bridge/rpc/src/lib.rs b/pallets/gear-eth-bridge/rpc/src/lib.rs new file mode 100644 index 00000000000..c53e555a613 --- /dev/null +++ b/pallets/gear-eth-bridge/rpc/src/lib.rs @@ -0,0 +1,80 @@ +// 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 . + +//! RPC interface for the gear module. + +use jsonrpsee::{ + core::RpcResult, + proc_macros::rpc, + types::error::{CallError, ErrorObject}, +}; +use pallet_gear_eth_bridge_rpc_runtime_api::Proof; +use primitive_types::H256; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_runtime::traits::Block; +use std::sync::Arc; + +pub use pallet_gear_eth_bridge_rpc_runtime_api::GearEthBridgeApi as GearEthBridgeRuntimeApi; + +#[rpc(server)] +pub trait GearEthBridgeApi { + #[method(name = "gearEthBridge_merkleProof")] + fn merkle_proof(&self, hash: H256, at: Option) -> RpcResult; +} + +/// Provides RPC methods to query token economics related data. +pub struct GearEthBridge { + /// Shared reference to the client. + client: Arc, + _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>, + ) { + assert_eq!(Session::current_index(), session); + assert_eq!(System::block_number(), block_number); + + if let Some(events) = events.into() { + let system_events = System::events() + .into_iter() + .map(|v| v.event) + .collect::>(); + + assert_eq!( + system_events, + events.to_vec(), + "System events count: {}, expected: {N}", + system_events.len() + ); + } + + System::reset_events(); + } +} diff --git a/pallets/gear-eth-bridge/src/weights.rs b/pallets/gear-eth-bridge/src/weights.rs new file mode 100644 index 00000000000..f597e66e719 --- /dev/null +++ b/pallets/gear-eth-bridge/src/weights.rs @@ -0,0 +1,107 @@ +// 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 . + +//! Autogenerated weights for pallet_gear_eth_bridge +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2024-08-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! CPU: `` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("vara-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/gear benchmark pallet --chain=vara-dev --steps=50 --repeat=20 --pallet=pallet_gear_eth_bridge --extrinsic=* --heap-pages=4096 --output=./pallets/gear-eth-bridge/weights.rs --template=.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_gear_eth_bridge. +pub trait WeightInfo { + fn pause() -> Weight; + fn unpause() -> Weight; + fn send_eth_message() -> Weight; +} + +/// Weights for pallet_gear_eth_bridge using the Gear node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn pause() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn unpause() -> Weight { + // Proof Size summary in bytes: + // Measured: `100` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn send_eth_message() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `34255` + // Minimum execution time: 49_000_000 picoseconds. + Weight::from_parts(50_000_000, 34255) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn pause() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn unpause() -> Weight { + // Proof Size summary in bytes: + // Measured: `100` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn send_eth_message() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `34255` + // Minimum execution time: 49_000_000 picoseconds. + Weight::from_parts(50_000_000, 34255) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } +} diff --git a/runtime/vara/Cargo.toml b/runtime/vara/Cargo.toml index 0b96fb98971..522648a09fe 100644 --- a/runtime/vara/Cargo.toml +++ b/runtime/vara/Cargo.toml @@ -97,11 +97,13 @@ pallet-gear-debug = { workspace = true, optional = true } pallet-gear-gas.workspace = true pallet-gear-payment.workspace = true pallet-gear-builtin.workspace = true +pallet-gear-eth-bridge = { workspace = true, optional = true } pallet-gear-staking-rewards.workspace = true pallet-gear-voucher.workspace = true pallet-gear-rpc-runtime-api.workspace = true pallet-gear-staking-rewards-rpc-runtime-api.workspace = true pallet-gear-builtin-rpc-runtime-api.workspace = true +pallet-gear-eth-bridge-rpc-runtime-api.workspace = true runtime-primitives.workspace = true gbuiltin-staking.workspace = true @@ -148,10 +150,12 @@ std = [ "pallet-gear-payment/std", "pallet-gear-program/std", "pallet-gear-builtin/std", + "pallet-gear-eth-bridge?/std", "pallet-gear-staking-rewards/std", "pallet-gear-rpc-runtime-api/std", "pallet-gear-staking-rewards-rpc-runtime-api/std", "pallet-gear-builtin-rpc-runtime-api/std", + "pallet-gear-eth-bridge-rpc-runtime-api/std", "pallet-grandpa/std", "pallet-identity/std", "pallet-im-online/std", @@ -209,6 +213,7 @@ runtime-benchmarks = [ "pallet-gear/runtime-benchmarks", "pallet-gear-voucher/runtime-benchmarks", "pallet-gear-builtin/runtime-benchmarks", + "pallet-gear-eth-bridge?/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", "pallet-staking/runtime-benchmarks", @@ -233,6 +238,7 @@ try-runtime = [ "pallet-gear-bank/try-runtime", "pallet-gear-scheduler/try-runtime", "pallet-gear-builtin/try-runtime", + "pallet-gear-eth-bridge?/try-runtime", "pallet-gear-voucher/try-runtime", "pallet-gear-debug?/try-runtime", "pallet-authority-discovery/try-runtime", @@ -275,5 +281,6 @@ fuzz = [ ] dev = [ "pallet-gear-debug", + "pallet-gear-eth-bridge", "pallet-sudo", ] diff --git a/runtime/vara/src/lib.rs b/runtime/vara/src/lib.rs index e0301352e9e..c8f51a1d27f 100644 --- a/runtime/vara/src/lib.rs +++ b/runtime/vara/src/lib.rs @@ -97,12 +97,14 @@ use sp_api::{CallApiAt, CallContext, Extensions, OverlayedChanges, ProofRecorder use sp_core::{crypto::KeyTypeId, ConstBool, ConstU64, ConstU8, OpaqueMetadata, H256}; #[cfg(any(feature = "std", test))] use sp_runtime::traits::HashingFor; +#[cfg(not(feature = "dev"))] +use sp_runtime::traits::OpaqueKeys; use sp_runtime::{ codec::{Decode, Encode, MaxEncodedLen}, create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdLookup, BlakeTwo256, Block as BlockT, ConvertInto, DispatchInfoOf, Dispatchable, - IdentityLookup, NumberFor, One, OpaqueKeys, SignedExtension, + IdentityLookup, NumberFor, One, SignedExtension, }, transaction_validity::{TransactionPriority, TransactionSource, TransactionValidity}, ApplyExtrinsicResult, FixedU128, Perbill, Percent, Permill, Perquintill, RuntimeDebug, @@ -398,6 +400,7 @@ impl pallet_transaction_payment::Config for Runtime { type FeeMultiplierUpdate = pallet_gear_payment::GearFeeMultiplier; } +// **IMPORTANT**: update this value with care, GearEthBridge is sensitive to this. impl_opaque_keys! { pub struct SessionKeys { pub babe: Babe, @@ -407,14 +410,82 @@ impl_opaque_keys! { } } +#[cfg(feature = "dev")] +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(); + 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(), + ); + } + } +} + +#[cfg(feature = "dev")] +pub type VaraSessionHandler = ( + Babe, + grandpa_keys_handler::GrandpaAndGearEthBridge, + ImOnline, + AuthorityDiscovery, +); + +#[cfg(not(feature = "dev"))] +pub type VaraSessionHandler = ::KeyTypeIdProviders; + impl pallet_session::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorId = ::AccountId; type ValidatorIdOf = pallet_staking::StashOf; type ShouldEndSession = Babe; type NextSessionRotation = Babe; + // **IMPORTANT**: update this value with care, GearEthBridge is sensitive to this. type SessionManager = pallet_session_historical::NoteHistoricalRoot; - type SessionHandler = ::KeyTypeIdProviders; + type SessionHandler = VaraSessionHandler; type Keys = SessionKeys; type WeightInfo = pallet_session::weights::SubstrateWeight; } @@ -620,6 +691,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime { parameter_types! { // Six sessions in an era (12 hours) + // **IMPORTANT**: update this value with care, GearEthBridge is sensitive to this. pub const SessionsPerEra: sp_staking::SessionIndex = 6; // 42 eras for unbonding (7 days) pub const BondingDuration: sp_staking::EraIndex = 14; @@ -658,6 +730,7 @@ impl pallet_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Slash = Treasury; type Reward = StakingRewards; + // **IMPORTANT**: update this value with care, GearEthBridge is sensitive to this. type SessionsPerEra = SessionsPerEra; type BondingDuration = BondingDuration; type SlashDeferDuration = SlashDeferDuration; @@ -1097,15 +1170,33 @@ impl pallet_gear_messenger::Config for Runtime { } /// Builtin actors arranged in a tuple. +#[cfg(not(feature = "dev"))] +pub type BuiltinActors = ( + pallet_gear_builtin::bls12_381::Actor, + pallet_gear_builtin::staking::Actor, +); + +/// Builtin actors arranged in a tuple. +#[cfg(feature = "dev")] pub type BuiltinActors = ( pallet_gear_builtin::bls12_381::Actor, pallet_gear_builtin::staking::Actor, + pallet_gear_eth_bridge::Actor, ); impl pallet_gear_builtin::Config for Runtime { type RuntimeCall = RuntimeCall; type Builtins = BuiltinActors; - type WeightInfo = pallet_gear_builtin::weights::SubstrateWeight; + type WeightInfo = weights::pallet_gear_builtin::SubstrateWeight; +} + +#[cfg(feature = "dev")] +impl pallet_gear_eth_bridge::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MaxPayloadSize = ConstU32<16_384>; // 16 KiB + type QueueCapacity = ConstU32<2048>; + type SessionsPerEra = SessionsPerEra; + type WeightInfo = weights::pallet_gear_eth_bridge::SubstrateWeight; } pub struct ExtraFeeFilter; @@ -1234,6 +1325,7 @@ construct_runtime!( GearVoucher: pallet_gear_voucher = 107, GearBank: pallet_gear_bank = 108, GearBuiltin: pallet_gear_builtin = 109, + GearEthBridge: pallet_gear_eth_bridge = 110, Sudo: pallet_sudo = 99, @@ -1295,6 +1387,8 @@ construct_runtime!( GearVoucher: pallet_gear_voucher = 107, GearBank: pallet_gear_bank = 108, GearBuiltin: pallet_gear_builtin = 109, + // Uncomment me, once ready for prod runtime. + // GearEthBridge: pallet_gear_eth_bridge = 110, // NOTE (!): `pallet_sudo` used to be idx(99). // NOTE (!): `pallet_airdrop` used to be idx(198). @@ -1350,7 +1444,23 @@ type DebugInfo = (); #[macro_use] extern crate frame_benchmarking; -#[cfg(feature = "runtime-benchmarks")] +#[cfg(all(feature = "runtime-benchmarks", feature = "dev"))] +mod benches { + define_benchmarks!( + // Substrate pallets + [frame_system, SystemBench::] + [pallet_balances, Balances] + [pallet_timestamp, Timestamp] + [pallet_utility, Utility] + // Gear pallets + [pallet_gear, Gear] + [pallet_gear_voucher, GearVoucher] + [pallet_gear_builtin, GearBuiltin] + [pallet_gear_eth_bridge, GearEthBridge] + ); +} + +#[cfg(all(feature = "runtime-benchmarks", not(feature = "dev")))] mod benches { define_benchmarks!( // Substrate pallets @@ -1476,6 +1586,20 @@ impl_runtime_apis_plus_common! { } } + impl pallet_gear_eth_bridge_rpc_runtime_api::GearEthBridgeApi for Runtime { + fn merkle_proof(hash: H256) -> Option { + match () { + #[cfg(not(feature = "dev"))] + () => { + let _ = hash; + None + }, + #[cfg(feature = "dev")] + () => GearEthBridge::merkle_proof(hash), + } + } + } + impl sp_genesis_builder::GenesisBuilder for Runtime { fn create_default_config() -> Vec { create_default_config::() diff --git a/runtime/vara/src/tests.rs b/runtime/vara/src/tests.rs index baee35aa5ea..19f890c424a 100644 --- a/runtime/vara/src/tests.rs +++ b/runtime/vara/src/tests.rs @@ -18,6 +18,7 @@ use super::*; use crate::Runtime; +use frame_support::traits::StorageInstance; use gear_lazy_pages_common::LazyPagesCosts; use pallet_gear::{InstructionWeights, MemoryWeights, SyscallWeights}; use runtime_common::weights::{ @@ -25,6 +26,51 @@ use runtime_common::weights::{ PagesCosts, }; +#[cfg(feature = "dev")] +#[test] +fn bridge_storages_have_correct_prefixes() { + // # SAFETY: Do not change storage prefixes without total bridge re-deploy. + const PALLET_PREFIX: &str = "GearEthBridge"; + + assert_eq!( + pallet_gear_eth_bridge::AuthoritySetHashPrefix::::pallet_prefix(), + PALLET_PREFIX + ); + assert_eq!( + pallet_gear_eth_bridge::QueueMerkleRootPrefix::::pallet_prefix(), + PALLET_PREFIX + ); + + assert_eq!( + pallet_gear_eth_bridge::AuthoritySetHashPrefix::::STORAGE_PREFIX, + "AuthoritySetHash" + ); + assert_eq!( + pallet_gear_eth_bridge::QueueMerkleRootPrefix::::STORAGE_PREFIX, + "QueueMerkleRoot" + ); +} + +#[cfg(feature = "dev")] +#[test] +fn bridge_session_timer_is_correct() { + assert_eq!( + ::SessionsPerEra::get(), + ::SessionsPerEra::get() + ); + + // # SAFETY: Do not change staking's SessionsPerEra parameter without + // making sure of correct integration with already running network. + // + // Change of the param will require migrating `pallet-gear-eth-bridge`'s + // `ClearTimer` (or any actual time- or epoch- dependent entity) and + // corresponding constant or even total bridge re-deploy. + assert_eq!( + ::SessionsPerEra::get(), + 6 + ); +} + #[test] fn instruction_weights_heuristics_test() { let weights = InstructionWeights::::default(); diff --git a/runtime/vara/src/weights/mod.rs b/runtime/vara/src/weights/mod.rs index 96bcf44fb45..0ce10f0fe5d 100644 --- a/runtime/vara/src/weights/mod.rs +++ b/runtime/vara/src/weights/mod.rs @@ -23,6 +23,10 @@ pub mod frame_system; pub mod pallet_balances; pub mod pallet_gear; +pub mod pallet_gear_builtin; pub mod pallet_gear_voucher; pub mod pallet_timestamp; pub mod pallet_utility; + +#[cfg(feature = "dev")] +pub mod pallet_gear_eth_bridge; diff --git a/runtime/vara/src/weights/pallet_gear_builtin.rs b/runtime/vara/src/weights/pallet_gear_builtin.rs index d2d5dd10978..93d3345ec97 100644 --- a/runtime/vara/src/weights/pallet_gear_builtin.rs +++ b/runtime/vara/src/weights/pallet_gear_builtin.rs @@ -46,6 +46,8 @@ pub trait WeightInfo { fn bls12_381_msm_g2(c: u32, ) -> Weight; fn bls12_381_mul_projective_g1(c: u32, ) -> Weight; fn bls12_381_mul_projective_g2(c: u32, ) -> Weight; + fn bls12_381_aggregate_g1(c: u32, ) -> Weight; + fn bls12_381_map_to_g2affine(c: u32, ) -> Weight; } /// Weights for pallet_gear_builtin using the Gear node and recommended hardware. @@ -132,6 +134,26 @@ impl pallet_gear_builtin::WeightInfo for SubstrateWeigh // Standard Error: 31_685 .saturating_add(Weight::from_parts(172_755_175, 0).saturating_mul(c.into())) } + /// The range of component `c` is `[1, 1000]`. + fn bls12_381_aggregate_g1(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_500_000 picoseconds. + Weight::from_parts(15_805_112, 0) + // Standard Error: 962 + .saturating_add(Weight::from_parts(962_975, 0).saturating_mul(c.into())) + } + /// The range of component `c` is `[0, 8388608]`. + fn bls12_381_map_to_g2affine(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 558_009_000 picoseconds. + Weight::from_parts(481_741_677, 0) + // Standard Error: 11 + .saturating_add(Weight::from_parts(565, 0).saturating_mul(c.into())) + } } // For backwards compatibility and tests @@ -217,4 +239,24 @@ impl WeightInfo for () { // Standard Error: 31_685 .saturating_add(Weight::from_parts(172_755_175, 0).saturating_mul(c.into())) } + /// The range of component `c` is `[1, 1000]`. + fn bls12_381_aggregate_g1(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_500_000 picoseconds. + Weight::from_parts(15_805_112, 0) + // Standard Error: 962 + .saturating_add(Weight::from_parts(962_975, 0).saturating_mul(c.into())) + } + /// The range of component `c` is `[0, 8388608]`. + fn bls12_381_map_to_g2affine(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 558_009_000 picoseconds. + Weight::from_parts(481_741_677, 0) + // Standard Error: 11 + .saturating_add(Weight::from_parts(565, 0).saturating_mul(c.into())) + } } diff --git a/runtime/vara/src/weights/pallet_gear_eth_bridge.rs b/runtime/vara/src/weights/pallet_gear_eth_bridge.rs new file mode 100644 index 00000000000..7e41ca32f3c --- /dev/null +++ b/runtime/vara/src/weights/pallet_gear_eth_bridge.rs @@ -0,0 +1,106 @@ +// 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 . + +//! Autogenerated weights for pallet_gear_eth_bridge +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2024-08-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! CPU: `` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("vara-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/gear benchmark pallet --chain=vara-dev --steps=50 --repeat=20 --pallet=pallet_gear_eth_bridge --extrinsic=* --heap-pages=4096 --output=./pallets/gear-eth-bridge/weights.rs --template=.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_gear_eth_bridge. +pub trait WeightInfo { + fn pause() -> Weight; + fn unpause() -> Weight; + fn send_eth_message() -> Weight; +} + +/// Weights for pallet_gear_eth_bridge using the Gear node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl pallet_gear_eth_bridge::WeightInfo for SubstrateWeight { + fn pause() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn unpause() -> Weight { + // Proof Size summary in bytes: + // Measured: `100` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn send_eth_message() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `34255` + // Minimum execution time: 49_000_000 picoseconds. + Weight::from_parts(50_000_000, 34255) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn pause() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn unpause() -> Weight { + // Proof Size summary in bytes: + // Measured: `100` + // Estimated: `1486` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 1486) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn send_eth_message() -> Weight { + // Proof Size summary in bytes: + // Measured: `121` + // Estimated: `34255` + // Minimum execution time: 49_000_000 picoseconds. + Weight::from_parts(50_000_000, 34255) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } +} diff --git a/utils/wasm-proc/src/main.rs b/utils/wasm-proc/src/main.rs index 0cb28647689..9e47f7973d2 100644 --- a/utils/wasm-proc/src/main.rs +++ b/utils/wasm-proc/src/main.rs @@ -24,7 +24,7 @@ use gear_wasm_builder::{ use parity_wasm::elements::External; use std::{collections::HashSet, fs, path::PathBuf}; -const RT_ALLOWED_IMPORTS: [&str; 75] = [ +const RT_ALLOWED_IMPORTS: [&str; 76] = [ // From `Allocator` (substrate/primitives/io/src/lib.rs) "ext_allocator_free_version_1", "ext_allocator_malloc_version_1", @@ -52,6 +52,7 @@ const RT_ALLOWED_IMPORTS: [&str; 75] = [ // From `Hashing` (substrate/primitives/io/src/lib.rs) "ext_hashing_blake2_128_version_1", "ext_hashing_blake2_256_version_1", + "ext_hashing_keccak_256_version_1", "ext_hashing_twox_128_version_1", "ext_hashing_twox_64_version_1", // From `Logging` (substrate/primitives/io/src/lib.rs)