diff --git a/Cargo.lock b/Cargo.lock index 8e4735e8..c9cc5d3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,6 +340,33 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "bridge-channel" +version = "0.1.1" +dependencies = [ + "bridge-types", + "ethabi", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "once_cell", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "rlp", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", +] + [[package]] name = "bridge-common" version = "0.1.0" diff --git a/pallets/channel/Cargo.toml b/pallets/channel/Cargo.toml new file mode 100644 index 00000000..b85d3c4a --- /dev/null +++ b/pallets/channel/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "bridge-channel" +description = "Bridge Channel" +version = "0.1.1" +edition = "2021" +authors = ['Polka Biome Ltd. '] +repository = "https://github.com/sora-xor/sora2-common" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.101", optional = true } +codec = { version = "3", package = "parity-scale-codec", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2", default-features = false, features = ["derive"] } +hex-literal = { version = "0.4.1", optional = true } +rlp = { version = "0.5", default-features = false, optional = true } + +frame-benchmarking = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false, optional = true } +frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38", default-features = false } + +bridge-types = { path = "../types", default-features = false } +ethabi = { git = "https://github.com/sora-xor/ethabi.git", branch = "sora-v1.6.0", default-features = false } +once_cell = { version = "1.5.2", default-features = false, features = [ + 'alloc', + 'unstable', +] } + +[dev-dependencies] +pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38" } +tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library.git", branch = "polkadot-v0.9.38", package = "orml-tokens" } +traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library.git", branch = "polkadot-v0.9.38", package = "orml-traits" } +currencies = { git = "https://github.com/open-web3-stack/open-runtime-module-library.git", branch = "polkadot-v0.9.38", package = "orml-currencies" } +sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38" } +rlp = { version = "0.5" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "sp-io/std", + "bridge-types/std", + "ethabi/std", +] +runtime-benchmarks = [ + "bridge-types/runtime-benchmarks", + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "hex-literal", + "rlp", +] + +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/channel/src/inbound/benchmarking.rs b/pallets/channel/src/inbound/benchmarking.rs new file mode 100644 index 00000000..668a0a31 --- /dev/null +++ b/pallets/channel/src/inbound/benchmarking.rs @@ -0,0 +1,74 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! BridgeInboundChannel pallet benchmarking + +use super::*; +use bridge_types::GenericNetworkId; +use frame_benchmarking::{benchmarks, impl_benchmark_test_suite}; +use frame_system::{self, RawOrigin}; +use sp_std::prelude::*; + +const BASE_NETWORK_ID: GenericNetworkId = GenericNetworkId::Sub(SubNetworkId::Mainnet); + +#[allow(unused_imports)] +use crate::inbound::Pallet as BridgeInboundChannel; + +// This collection of benchmarks should include a benchmark for each +// call dispatched by the channel, i.e. each "app" pallet function +// that can be invoked by MessageDispatch. The most expensive call +// should be used in the `submit` benchmark. +// +// We rely on configuration via chain spec of the app pallets because +// we don't have access to their storage here. +benchmarks! { + // Benchmark `submit` extrinsic under worst case conditions: + // * `submit` dispatches the DotApp::unlock call + // * `unlock` call successfully unlocks DOT + submit { + let messages = vec![]; + let commitment = bridge_types::GenericCommitment::Sub( + bridge_types::substrate::Commitment { + messages: messages.try_into().unwrap(), + nonce: 1u64, + } + ); + let proof = T::Verifier::valid_proof().unwrap(); + }: _(RawOrigin::None, BASE_NETWORK_ID, commitment, proof) + verify { + assert_eq!(1, >::get(BASE_NETWORK_ID)); + } +} + +impl_benchmark_test_suite!( + BridgeInboundChannel, + crate::inbound::test::new_tester(), + crate::inbound::test::Test, +); diff --git a/pallets/channel/src/inbound/mod.rs b/pallets/channel/src/inbound/mod.rs new file mode 100644 index 00000000..9c0622fa --- /dev/null +++ b/pallets/channel/src/inbound/mod.rs @@ -0,0 +1,520 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Channel for passing messages from ethereum to substrate. + +use bridge_types::evm::AdditionalEVMOutboundData; +use bridge_types::traits::{ + AppRegistry, EVMFeeHandler, MessageDispatch, MessageStatusNotifier, OutboundChannel, Verifier, +}; +use bridge_types::types::MessageId; +use bridge_types::SubNetworkId; +use bridge_types::{EVMChainId, H160}; +use frame_support::dispatch::DispatchResult; +use frame_support::traits::Get; +use frame_system::RawOrigin; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; +pub use weights::WeightInfo; + +pub const EVM_GAS_OVERHEAD: u64 = 20000; + +#[cfg(test)] +mod test; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use bridge_types::evm::AdditionalEVMInboundData; + use bridge_types::types::MessageStatus; + use bridge_types::{EVMChainId, GenericNetworkId, GenericTimepoint}; + use frame_support::log::warn; + use frame_support::pallet_prelude::{InvalidTransaction, *}; + use frame_support::traits::StorageVersion; + use frame_support::weights::Weight; + use frame_system::{ensure_root, pallet_prelude::*}; + use sp_core::H160; + use sp_std::prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_timestamp::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Verifier module for message verification. + type Verifier: Verifier; + + /// Verifier module for message verification. + type SubstrateMessageDispatch: MessageDispatch; + + /// Verifier module for message verification. + type EVMMessageDispatch: MessageDispatch< + Self, + EVMChainId, + MessageId, + AdditionalEVMInboundData, + >; + + type OutboundChannel: OutboundChannel< + EVMChainId, + Self::AccountId, + AdditionalEVMOutboundData, + >; + + type AssetId; + + type Balance; + + type MessageStatusNotifier: MessageStatusNotifier< + Self::AssetId, + Self::AccountId, + Self::Balance, + >; + + type EVMFeeHandler: EVMFeeHandler; + + /// A configuration for base priority of unsigned transactions. + #[pallet::constant] + type UnsignedPriority: Get; + + /// A configuration for longevity of unsigned transactions. + #[pallet::constant] + type UnsignedLongevity: Get; + + #[pallet::constant] + type ThisNetworkId: Get; + + /// Max bytes in a message payload + #[pallet::constant] + type MaxMessagePayloadSize: Get; + + /// Max number of messages that can be queued and committed in one go for a given channel. + #[pallet::constant] + type MaxMessagesPerCommit: Get; + + #[pallet::constant] + type EVMPriorityFee: Get; + + /// Weight information for extrinsics in this pallet + type WeightInfo: WeightInfo; + } + + #[pallet::storage] + pub type ChannelNonces = StorageMap<_, Identity, GenericNetworkId, u64, ValueQuery>; + + #[pallet::storage] + pub type ReportedChannelNonces = + StorageMap<_, Identity, GenericNetworkId, u64, ValueQuery>; + + #[pallet::storage] + pub type EVMChannelAddresses = + StorageMap<_, Identity, EVMChainId, H160, OptionQuery>; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::event] + // #[pallet::generate_deposit(pub(super) fn deposit_event)] + // This pallet don't have events + pub enum Event {} + + #[pallet::error] + pub enum Error { + /// Message came from an invalid network. + InvalidNetwork, + /// Message came from an invalid outbound channel on the Ethereum side. + InvalidSourceChannel, + /// Submitted invalid commitment type. + InvalidCommitment, + /// Message has an unexpected nonce. + InvalidNonce, + /// Incorrect reward fraction + InvalidRewardFraction, + /// This contract already exists + ContractExists, + /// Call encoding failed. + CallEncodeFailed, + } + + impl Pallet { + fn submit_weight( + commitment: &bridge_types::GenericCommitment< + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + proof: &::Proof, + ) -> Weight { + let commitment_weight = match commitment { + bridge_types::GenericCommitment::EVM(commitment) => match commitment { + bridge_types::evm::Commitment::Outbound(_) => { + ::BlockWeights::get().max_block + } + bridge_types::evm::Commitment::Inbound(commitment) => { + T::EVMMessageDispatch::dispatch_weight(&commitment.payload) + } + bridge_types::evm::Commitment::StatusReport(_) => Default::default(), + bridge_types::evm::Commitment::BaseFeeUpdate(_) => Default::default(), + }, + bridge_types::GenericCommitment::Sub(commitment) => commitment + .messages + .iter() + .map(|m| T::SubstrateMessageDispatch::dispatch_weight(&m.payload)) + .fold(Weight::zero(), |acc, w| acc.saturating_add(w)), + }; + + let proof_weight = T::Verifier::verify_weight(proof); + + ::WeightInfo::submit() + .saturating_add(commitment_weight) + .saturating_add(proof_weight) + } + + fn ensure_evm_channel(chain_id: EVMChainId, channel: H160) -> DispatchResult { + let channel_address = + EVMChannelAddresses::::get(chain_id).ok_or(Error::::InvalidNetwork)?; + ensure!(channel_address == channel, Error::::InvalidSourceChannel); + Ok(()) + } + + fn ensure_channel_nonce(network_id: GenericNetworkId, new_nonce: u64) -> DispatchResult { + let nonce = ChannelNonces::::get(network_id); + ensure!(nonce + 1 == new_nonce, Error::::InvalidNonce); + Ok(()) + } + + fn ensure_reported_nonce(network_id: GenericNetworkId, new_nonce: u64) -> DispatchResult { + let nonce = ReportedChannelNonces::::get(network_id); + ensure!(nonce + 1 == new_nonce, Error::::InvalidNonce); + Ok(()) + } + + fn update_channel_nonce(network_id: GenericNetworkId, new_nonce: u64) -> DispatchResult { + >::try_mutate(network_id, |nonce| -> DispatchResult { + if new_nonce != *nonce + 1 { + Err(Error::::InvalidNonce.into()) + } else { + *nonce += 1; + Ok(()) + } + })?; + Ok(()) + } + + fn update_reported_nonce(network_id: GenericNetworkId, new_nonce: u64) -> DispatchResult { + >::try_mutate(network_id, |nonce| -> DispatchResult { + if new_nonce != *nonce + 1 { + Err(Error::::InvalidNonce.into()) + } else { + *nonce += 1; + Ok(()) + } + })?; + Ok(()) + } + + fn handle_evm_commitment( + chain_id: EVMChainId, + commitment: bridge_types::evm::Commitment< + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + ) -> DispatchResult { + Self::verify_evm_commitment(chain_id, &commitment)?; + let network_id = GenericNetworkId::EVM(chain_id); + match commitment { + bridge_types::evm::Commitment::Inbound(inbound_commitment) => { + Self::update_channel_nonce(network_id, inbound_commitment.nonce)?; + let message_id = MessageId::basic( + network_id, + T::ThisNetworkId::get(), + inbound_commitment.nonce, + ); + T::EVMMessageDispatch::dispatch( + chain_id, + message_id, + GenericTimepoint::EVM(inbound_commitment.block_number), + &inbound_commitment.payload, + AdditionalEVMInboundData { + source: inbound_commitment.source, + }, + ); + } + bridge_types::evm::Commitment::StatusReport(status_report) => { + Self::update_reported_nonce(network_id, status_report.nonce)?; + for (i, result) in status_report.results.into_iter().enumerate() { + let status = if result { + MessageStatus::Done + } else { + MessageStatus::Failed + }; + T::MessageStatusNotifier::update_status( + network_id, + MessageId::batched( + T::ThisNetworkId::get(), + network_id, + status_report.nonce, + i as u64, + ) + .hash(), + status, + GenericTimepoint::EVM(status_report.block_number), + ) + } + // Add some overhead + let gas_used = status_report + .gas_spent + .saturating_add(EVM_GAS_OVERHEAD.into()); + // Priority fee and some additional reward + let gas_price = status_report + .base_fee + .saturating_add(T::EVMPriorityFee::get().into()); + let fee_paid = gas_used.saturating_mul(gas_price); + T::EVMFeeHandler::on_fee_paid(chain_id, status_report.relayer, fee_paid) + } + bridge_types::evm::Commitment::BaseFeeUpdate(update) => { + T::EVMFeeHandler::update_base_fee(chain_id, update.new_base_fee) + } + bridge_types::evm::Commitment::Outbound(_) => { + frame_support::fail!(Error::::InvalidCommitment); + } + } + Ok(()) + } + + fn verify_evm_commitment( + chain_id: EVMChainId, + commitment: &bridge_types::evm::Commitment< + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + ) -> DispatchResult { + let network_id = GenericNetworkId::EVM(chain_id); + match commitment { + bridge_types::evm::Commitment::Inbound(inbound_commitment) => { + Self::ensure_evm_channel(chain_id, inbound_commitment.source)?; + Self::ensure_channel_nonce(network_id, inbound_commitment.nonce)?; + } + bridge_types::evm::Commitment::StatusReport(status_report) => { + Self::ensure_evm_channel(chain_id, status_report.source)?; + Self::ensure_reported_nonce(network_id, status_report.nonce)?; + } + bridge_types::evm::Commitment::BaseFeeUpdate(_) => {} + bridge_types::evm::Commitment::Outbound(_) => { + frame_support::fail!(Error::::InvalidCommitment); + } + } + Ok(()) + } + + fn handle_sub_commitment( + sub_network_id: SubNetworkId, + commitment: bridge_types::substrate::Commitment< + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + ) -> DispatchResult { + Self::verify_sub_commitment(sub_network_id, &commitment)?; + let network_id = GenericNetworkId::Sub(sub_network_id); + Self::update_channel_nonce(network_id, commitment.nonce)?; + for (idx, message) in commitment.messages.into_iter().enumerate() { + let message_id = MessageId::batched( + network_id, + T::ThisNetworkId::get(), + commitment.nonce, + idx as u64, + ); + T::SubstrateMessageDispatch::dispatch( + sub_network_id, + message_id, + message.timepoint, + &message.payload, + (), + ); + } + Ok(()) + } + + fn verify_sub_commitment( + sub_network_id: SubNetworkId, + commitment: &bridge_types::substrate::Commitment< + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + ) -> DispatchResult { + let network_id = GenericNetworkId::Sub(sub_network_id); + Self::ensure_channel_nonce(network_id, commitment.nonce)?; + Ok(()) + } + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Pallet::::submit_weight(commitment, proof))] + pub fn submit( + origin: OriginFor, + network_id: GenericNetworkId, + commitment: bridge_types::GenericCommitment< + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + proof: ::Proof, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + let commitment_hash = commitment.hash(); + T::Verifier::verify(network_id, commitment_hash, &proof)?; + match (network_id, commitment) { + ( + GenericNetworkId::EVM(evm_network_id), + bridge_types::GenericCommitment::EVM(evm_commitment), + ) => Self::handle_evm_commitment(evm_network_id, evm_commitment)?, + ( + GenericNetworkId::Sub(sub_network_id), + bridge_types::GenericCommitment::Sub(sub_commitment), + ) => Self::handle_sub_commitment(sub_network_id, sub_commitment)?, + _ => { + frame_support::fail!(Error::::InvalidCommitment); + } + } + Ok(().into()) + } + + #[pallet::call_index(1)] + #[pallet::weight(0)] + pub fn register_evm_channel( + origin: OriginFor, + network_id: EVMChainId, + channel_address: H160, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + EVMChannelAddresses::::insert(network_id, channel_address); + Ok(().into()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + // mb add prefetch with validate_ancestors=true to not include unneccessary stuff + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + if let Call::submit { + network_id, + commitment, + proof, + } = call + { + match (network_id, &commitment) { + ( + GenericNetworkId::EVM(evm_network_id), + bridge_types::GenericCommitment::EVM(evm_commitment), + ) => Self::verify_evm_commitment(*evm_network_id, evm_commitment) + .map_err(|_| InvalidTransaction::BadProof)?, + ( + GenericNetworkId::Sub(sub_network_id), + bridge_types::GenericCommitment::Sub(sub_commitment), + ) => Self::verify_sub_commitment(*sub_network_id, sub_commitment) + .map_err(|_| InvalidTransaction::BadProof)?, + _ => { + return Err(InvalidTransaction::BadProof.into()); + } + } + let commitment_hash = commitment.hash(); + T::Verifier::verify(*network_id, commitment_hash, proof).map_err(|e| { + warn!("Bad submit proof received: {:?}", e); + InvalidTransaction::BadProof + })?; + ValidTransaction::with_tag_prefix("SubstrateBridgeChannelSubmit") + .priority(T::UnsignedPriority::get()) + .longevity(T::UnsignedLongevity::get()) + .and_provides((network_id, commitment_hash)) + .propagate(true) + .build() + } else { + warn!("Unknown unsigned call, can't validate"); + InvalidTransaction::Call.into() + } + } + } +} + +impl AppRegistry for Pallet { + fn register_app(network_id: EVMChainId, app: H160) -> DispatchResult { + let target = EVMChannelAddresses::::get(network_id).ok_or(Error::::InvalidNetwork)?; + + let message = bridge_types::channel_abi::RegisterAppPayload { app }; + + T::OutboundChannel::submit( + network_id, + &RawOrigin::Root, + message + .encode() + .map_err(|_| Error::::CallEncodeFailed)? + .as_ref(), + AdditionalEVMOutboundData { + target, + max_gas: 100000u64.into(), + }, + )?; + Ok(()) + } + + fn deregister_app(network_id: EVMChainId, app: H160) -> DispatchResult { + let target = EVMChannelAddresses::::get(network_id).ok_or(Error::::InvalidNetwork)?; + + let message = bridge_types::channel_abi::RemoveAppPayload { app }; + + T::OutboundChannel::submit( + network_id, + &RawOrigin::Root, + message + .encode() + .map_err(|_| Error::::CallEncodeFailed)? + .as_ref(), + AdditionalEVMOutboundData { + target, + max_gas: 100000u64.into(), + }, + )?; + Ok(()) + } +} diff --git a/pallets/channel/src/inbound/test.rs b/pallets/channel/src/inbound/test.rs new file mode 100644 index 00000000..9b83860b --- /dev/null +++ b/pallets/channel/src/inbound/test.rs @@ -0,0 +1,429 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::*; +use bridge_types::evm::AdditionalEVMInboundData; +use bridge_types::substrate::BridgeMessage; +use codec::{Decode, Encode, MaxEncodedLen}; + +use frame_support::traits::{Everything, UnfilteredDispatchable}; +use frame_support::{ + assert_err, assert_noop, assert_ok, parameter_types, Deserialize, RuntimeDebug, Serialize, +}; +use scale_info::TypeInfo; +use sp_core::{ConstU128, ConstU64, H256}; +use sp_keyring::AccountKeyring as Keyring; +use sp_runtime::testing::Header; +use sp_runtime::traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, ValidateUnsigned, Verify}; +use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidityError, +}; +use sp_runtime::{DispatchError, MultiSignature}; +use sp_std::convert::From; + +use bridge_types::traits::MessageDispatch; +use bridge_types::{GenericNetworkId, GenericTimepoint}; + +use crate::inbound::Error; + +use crate::inbound as bridge_inbound_channel; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +const BASE_NETWORK_ID: GenericNetworkId = GenericNetworkId::Sub(SubNetworkId::Mainnet); + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage}, + Balances: pallet_balances::{Pallet, Call, Storage, Event}, + BridgeInboundChannel: bridge_inbound_channel::{Pallet, Call, Storage, Event}, + } +); + +pub type Signature = MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +#[derive( + Encode, + Decode, + PartialEq, + Eq, + RuntimeDebug, + Clone, + Copy, + MaxEncodedLen, + TypeInfo, + PartialOrd, + Ord, + Serialize, + Deserialize, +)] +pub enum AssetId { + Xor, + Eth, + Dai, +} + +pub type Balance = u128; + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<65536>; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + /// The ubiquitous event type. + type RuntimeEvent = RuntimeEvent; + type MaxLocks = MaxLocks; + /// The type for recording an account's balance. + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = (); +} + +// Mock verifier +pub struct MockVerifier; + +impl Verifier for MockVerifier { + type Proof = Vec; + + fn verify(network_id: GenericNetworkId, _hash: H256, _proof: &Vec) -> DispatchResult { + let network_id = match network_id { + bridge_types::GenericNetworkId::EVM(_) + | bridge_types::GenericNetworkId::EVMLegacy(_) => { + return Err(Error::::InvalidNetwork.into()) + } + bridge_types::GenericNetworkId::Sub(ni) => ni, + }; + if GenericNetworkId::from(network_id) == BASE_NETWORK_ID { + Ok(()) + } else { + Err(Error::::InvalidNetwork.into()) + } + } + + fn verify_weight(_proof: &Self::Proof) -> frame_support::weights::Weight { + Default::default() + } + + #[cfg(feature = "runtime-benchmarks")] + fn valid_proof() -> Option { + Some(Default::default()) + } +} + +// Mock Dispatch +pub struct MockMessageDispatch; + +impl MessageDispatch for MockMessageDispatch { + fn dispatch(_: SubNetworkId, _: MessageId, _: GenericTimepoint, _: &[u8], _: ()) {} + + fn dispatch_weight(_: &[u8]) -> frame_support::weights::Weight { + Default::default() + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_dispatch_event( + _: MessageId, + ) -> Option<::RuntimeEvent> { + None + } +} + +impl MessageDispatch + for MockMessageDispatch +{ + fn dispatch( + _: EVMChainId, + _: MessageId, + _: GenericTimepoint, + _: &[u8], + _: AdditionalEVMInboundData, + ) { + } + + fn dispatch_weight(_: &[u8]) -> frame_support::weights::Weight { + Default::default() + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_dispatch_event( + _: MessageId, + ) -> Option<::RuntimeEvent> { + None + } +} + +parameter_types! { + pub SourceAccount: AccountId = Keyring::Eve.into(); +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); +} + +parameter_types! { + pub const MaxMessagePayloadSize: u32 = 128; + pub const MaxMessagesPerCommit: u32 = 5; + pub const ThisNetworkId: GenericNetworkId = GenericNetworkId::Sub(SubNetworkId::Mainnet); +} + +pub struct OutboundChannelImpl; + +impl OutboundChannel for OutboundChannelImpl { + fn submit( + _network_id: EVMChainId, + _who: &frame_system::RawOrigin, + _payload: &[u8], + _additional: AdditionalEVMOutboundData, + ) -> Result { + Ok(H256::random()) + } + + fn submit_weight() -> frame_support::weights::Weight { + frame_support::weights::Weight::from_all(1) + } +} + +impl bridge_inbound_channel::Config for Test { + type MessageStatusNotifier = (); + type AssetId = H256; + type Balance = Balance; + type EVMFeeHandler = (); + type OutboundChannel = OutboundChannelImpl; + type RuntimeEvent = RuntimeEvent; + type Verifier = MockVerifier; + type EVMMessageDispatch = MockMessageDispatch; + type SubstrateMessageDispatch = MockMessageDispatch; + type UnsignedLongevity = ConstU64<100>; + type UnsignedPriority = ConstU64<100>; + type MaxMessagePayloadSize = MaxMessagePayloadSize; + type MaxMessagesPerCommit = MaxMessagesPerCommit; + type ThisNetworkId = ThisNetworkId; + type EVMPriorityFee = ConstU128<5_000_000_000>; + type WeightInfo = (); +} + +pub fn new_tester() -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let bob: AccountId = Keyring::Bob.into(); + pallet_balances::GenesisConfig:: { + balances: vec![(bob, 1_000_000_000_000_000_000)], + } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext: sp_io::TestExternalities = storage.into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[test] +fn test_submit() { + new_tester().execute_with(|| { + let origin = RuntimeOrigin::none(); + + // Submit message 1 + let message_1 = BridgeMessage { + timepoint: Default::default(), + payload: Default::default(), + }; + let commitment = + bridge_types::GenericCommitment::Sub(bridge_types::substrate::Commitment { + nonce: 1, + messages: vec![message_1].try_into().unwrap(), + }); + + let call = Call::::submit { + network_id: BASE_NETWORK_ID, + commitment, + proof: vec![], + }; + + assert_ok!(Pallet::::validate_unsigned( + TransactionSource::External, + &call + )); + assert_ok!(call.dispatch_bypass_filter(origin.clone())); + + let nonce: u64 = >::get(BASE_NETWORK_ID); + assert_eq!(nonce, 1); + + // Submit message 2 + let message_2 = BridgeMessage { + timepoint: Default::default(), + payload: Default::default(), + }; + let commitment = + bridge_types::GenericCommitment::Sub(bridge_types::substrate::Commitment { + nonce: 2, + messages: vec![message_2].try_into().unwrap(), + }); + + let call = Call::::submit { + network_id: BASE_NETWORK_ID, + commitment, + proof: vec![], + }; + + assert_ok!(Pallet::::validate_unsigned( + TransactionSource::External, + &call + )); + assert_ok!(call.dispatch_bypass_filter(origin)); + + let nonce: u64 = >::get(BASE_NETWORK_ID); + assert_eq!(nonce, 2); + }); +} + +#[test] +fn test_submit_with_invalid_nonce() { + new_tester().execute_with(|| { + let origin = RuntimeOrigin::none(); + + // Submit message + let message = BridgeMessage { + timepoint: Default::default(), + payload: Default::default(), + }; + let commitment = + bridge_types::GenericCommitment::Sub(bridge_types::substrate::Commitment { + nonce: 1, + messages: vec![message].try_into().unwrap(), + }); + + let call = Call::::submit { + network_id: BASE_NETWORK_ID, + commitment, + proof: vec![], + }; + + assert_ok!(Pallet::::validate_unsigned( + TransactionSource::External, + &call + )); + assert_ok!(call.clone().dispatch_bypass_filter(origin.clone())); + + let nonce: u64 = >::get(BASE_NETWORK_ID); + assert_eq!(nonce, 1); + + // Submit the same again + assert_err!( + Pallet::::validate_unsigned(TransactionSource::External, &call), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + assert_noop!( + call.dispatch_bypass_filter(origin), + Error::::InvalidNonce + ); + }); +} + +#[test] +fn test_submit_with_invalid_network_id() { + new_tester().execute_with(|| { + let origin = RuntimeOrigin::none(); + + // Submit message + let message = BridgeMessage { + timepoint: Default::default(), + payload: Default::default(), + }; + let commitment = + bridge_types::GenericCommitment::Sub(bridge_types::substrate::Commitment { + nonce: 1, + messages: vec![message].try_into().unwrap(), + }); + + let call = Call::::submit { + network_id: SubNetworkId::Kusama.into(), + commitment, + proof: vec![], + }; + + assert_err!( + Pallet::::validate_unsigned(TransactionSource::External, &call), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + assert_noop!( + call.dispatch_bypass_filter(origin), + Error::::InvalidNetwork + ); + }); +} diff --git a/pallets/channel/src/inbound/weights.rs b/pallets/channel/src/inbound/weights.rs new file mode 100644 index 00000000..f9cdec60 --- /dev/null +++ b/pallets/channel/src/inbound/weights.rs @@ -0,0 +1,93 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Autogenerated weights for substrate_bridge_channel::inbound +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `TRX40`, CPU: `AMD Ryzen Threadripper 3960X 24-Core Processor` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("local"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/framenode +// benchmark +// pallet +// --chain=local +// --steps=50 +// --repeat=20 +// --pallet=substrate_bridge_channel::inbound +// --extrinsic=* +// --header=./misc/file_header.txt +// --template=./misc/pallet-weight-template.hbs +// --output=./inbound-channel.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for substrate_bridge_channel::inbound. +pub trait WeightInfo { + fn submit() -> Weight; +} + +/// Weights for substrate_bridge_channel::inbound using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: SubstrateBridgeInboundChannel ChannelNonces (r:1 w:1) + /// Proof Skipped: SubstrateBridgeInboundChannel ChannelNonces (max_values: None, max_size: None, mode: Measured) + fn submit() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `2517` + // Minimum execution time: 4_800_000 picoseconds. + Weight::from_parts(5_100_000, 2517) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: SubstrateBridgeInboundChannel ChannelNonces (r:1 w:1) + /// Proof Skipped: SubstrateBridgeInboundChannel ChannelNonces (max_values: None, max_size: None, mode: Measured) + fn submit() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `2517` + // Minimum execution time: 4_800_000 picoseconds. + Weight::from_parts(5_100_000, 2517) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/pallets/channel/src/lib.rs b/pallets/channel/src/lib.rs new file mode 100644 index 00000000..3e81ff84 --- /dev/null +++ b/pallets/channel/src/lib.rs @@ -0,0 +1,34 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod inbound; +pub mod outbound; diff --git a/pallets/channel/src/outbound/benchmarking.rs b/pallets/channel/src/outbound/benchmarking.rs new file mode 100644 index 00000000..2433940f --- /dev/null +++ b/pallets/channel/src/outbound/benchmarking.rs @@ -0,0 +1,129 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! BridgeOutboundChannel pallet benchmarking +use super::*; + +use bridge_types::substrate::BridgeMessage; +use bridge_types::traits::OutboundChannel; +use bridge_types::GenericBridgeMessage; +use bridge_types::GenericNetworkId; +use frame_benchmarking::benchmarks; +use frame_system::EventRecord; +use frame_system::RawOrigin; +use sp_std::prelude::*; + +const BASE_NETWORK_ID: GenericNetworkId = GenericNetworkId::Sub(SubNetworkId::Mainnet); + +#[allow(unused_imports)] +use crate::outbound::Pallet as BridgeOutboundChannel; + +fn assert_last_event(system_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + // compare to the last event record + let EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +benchmarks! { + where_clause { + where crate::outbound::Event::: Into<::RuntimeEvent> + } + + // Benchmark `on_initialize` under worst case conditions, i.e. messages + // in queue are committed. + on_initialize { + let m in 1 .. T::MaxMessagesPerCommit::get(); + let p in 0 .. T::MaxMessagePayloadSize::get(); + + for _ in 0 .. m { + let payload: Vec = (0..).take(p as usize).collect(); + MessageQueues::::try_append( + BASE_NETWORK_ID, GenericBridgeMessage::Sub(BridgeMessage { + payload: payload.try_into().unwrap(), + timepoint: Default::default(), + })).unwrap(); + } + + let block_number = 0u32.into(); + + }: { BridgeOutboundChannel::::on_initialize(block_number) } + verify { + assert_eq!(>::get(BASE_NETWORK_ID).len(), 0); + } + + // Benchmark 'on_initialize` for the best case, i.e. nothing is done + // because it's not a commitment interval. + on_initialize_non_interval { + MessageQueues::::take(BASE_NETWORK_ID); + let payload: Vec = (0..).take(10).collect(); + MessageQueues::::try_append( + BASE_NETWORK_ID, GenericBridgeMessage::Sub(BridgeMessage { + payload: payload.try_into().unwrap(), + timepoint: Default::default(), + })).unwrap(); + + let interval: T::BlockNumber = 10u32.into(); + Interval::::put(interval); + let block_number: T::BlockNumber = 12u32.into(); + + }: { BridgeOutboundChannel::::on_initialize(block_number) } + verify { + assert_eq!(>::get(BASE_NETWORK_ID).len(), 1); + } + + // Benchmark 'on_initialize` for the case where it is a commitment interval + // but there are no messages in the queue. + on_initialize_no_messages { + MessageQueues::::take(BASE_NETWORK_ID); + + let block_number = Interval::::get(); + + }: { BridgeOutboundChannel::::on_initialize(block_number) } + + submit { + + }: { + BridgeOutboundChannel::::submit(SubNetworkId::Rococo, &RawOrigin::Root, &[0u8; 128], ()).unwrap() + } + verify { + assert_last_event::(crate::outbound::Event::::MessageAccepted { + network_id: GenericNetworkId::Sub(SubNetworkId::Rococo), + batch_nonce: 1, + message_nonce: 0 + }.into()); + } + + impl_benchmark_test_suite!( + BridgeOutboundChannel, + crate::outbound::test::new_tester(), + crate::outbound::test::Test, + ); +} diff --git a/pallets/channel/src/outbound/mod.rs b/pallets/channel/src/outbound/mod.rs new file mode 100644 index 00000000..6152dda0 --- /dev/null +++ b/pallets/channel/src/outbound/mod.rs @@ -0,0 +1,459 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Channel for passing messages from substrate to ethereum. + +use bridge_types::evm::AdditionalEVMOutboundData; +use bridge_types::substrate::BridgeMessage; +use bridge_types::traits::EVMOutboundChannel; +use bridge_types::traits::OutboundChannel; +use bridge_types::traits::TimepointProvider; +use bridge_types::EVMChainId; +use bridge_types::GenericNetworkId; +use bridge_types::SubNetworkId; +use frame_support::ensure; +use frame_support::log::error; +use frame_support::pallet_prelude::*; +use frame_support::traits::Get; +use frame_support::weights::Weight; +use frame_system::pallet_prelude::*; +use frame_system::RawOrigin; +use sp_core::H256; +use sp_runtime::DispatchError; + +use bridge_types::types::MessageNonce; + +pub mod weights; +pub use weights::WeightInfo; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod test; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use bridge_types::traits::AuxiliaryDigestHandler; + use bridge_types::traits::MessageStatusNotifier; + use bridge_types::traits::TimepointProvider; + use bridge_types::types::AuxiliaryDigestItem; + use bridge_types::types::GenericCommitmentWithBlock; + use bridge_types::types::MessageId; + use bridge_types::types::MessageStatus; + use bridge_types::GenericBridgeMessage; + use bridge_types::GenericCommitment; + use bridge_types::GenericNetworkId; + use bridge_types::GenericTimepoint; + use frame_support::log::debug; + use frame_support::traits::StorageVersion; + use sp_core::U256; + use sp_runtime::traits::Zero; + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_timestamp::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Max bytes in a message payload + type MaxMessagePayloadSize: Get; + + /// Max number of messages that can be queued and committed in one go for a given channel. + type MaxMessagesPerCommit: Get; + + type AssetId; + + type Balance; + + type MaxGasPerCommit: Get; + + type MaxGasPerMessage: Get; + + type MessageStatusNotifier: MessageStatusNotifier< + Self::AssetId, + Self::AccountId, + Self::Balance, + >; + + type AuxiliaryDigestHandler: AuxiliaryDigestHandler; + + type TimepointProvider: TimepointProvider; + + #[pallet::constant] + type ThisNetworkId: Get; + + /// Weight information for extrinsics in this pallet + type WeightInfo: WeightInfo; + } + + /// Interval between committing messages. + #[pallet::storage] + #[pallet::getter(fn interval)] + pub(crate) type Interval = + StorageValue<_, T::BlockNumber, ValueQuery, DefaultInterval>; + + #[pallet::type_value] + pub(crate) fn DefaultInterval() -> T::BlockNumber { + // TODO: Select interval + 10u32.into() + } + + /// Messages waiting to be committed. To update the queue, use `append_message_queue` and `take_message_queue` methods + /// (to keep correct value in [QueuesTotalGas]). + #[pallet::storage] + pub(crate) type MessageQueues = StorageMap< + _, + Identity, + GenericNetworkId, + BoundedVec, T::MaxMessagesPerCommit>, + ValueQuery, + >; + + #[pallet::storage] + pub type QueueTotalGas = StorageMap<_, Identity, GenericNetworkId, U256, ValueQuery>; + + #[pallet::storage] + pub type ChannelNonces = StorageMap<_, Identity, GenericNetworkId, u64, ValueQuery>; + + #[pallet::storage] + pub type LatestCommitment = StorageMap< + _, + Identity, + GenericNetworkId, + GenericCommitmentWithBlock< + BlockNumberFor, + T::MaxMessagesPerCommit, + T::MaxMessagePayloadSize, + >, + OptionQuery, + >; + + #[pallet::storage] + pub type EVMSubmitGas = + StorageMap<_, Identity, EVMChainId, U256, ValueQuery, DefaultEVMSubmitGas>; + + #[pallet::type_value] + pub fn DefaultEVMSubmitGas() -> U256 { + 200_000u32.into() + } + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::generate_store(trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::hooks] + impl Hooks> for Pallet { + // Generate a message commitment every [`Interval`] blocks. + // + // The commitment hash is included in an [`AuxiliaryDigestItem`] in the block header, + // with the corresponding commitment is persisted offchain. + fn on_initialize(now: T::BlockNumber) -> Weight { + let interval = Self::interval(); + let mut weight = Default::default(); + if now % interval == Zero::zero() { + for chain_id in MessageQueues::::iter_keys() { + weight += Self::commit(chain_id); + } + } + weight + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + MessageAccepted { + network_id: GenericNetworkId, + batch_nonce: u64, + message_nonce: MessageNonce, + }, + } + + #[pallet::error] + pub enum Error { + /// The message payload exceeds byte limit. + PayloadTooLarge, + /// No more messages can be queued for the channel during this commit cycle. + QueueSizeLimitReached, + /// Maximum gas for queued batch exceeds limit. + MaxGasTooBig, + /// Cannot pay the fee to submit a message. + NoFunds, + /// Cannot increment nonce + Overflow, + /// This channel already exists + ChannelExists, + /// Network does not support this kind of message (it's a developer's mistake) + MessageTypeIsNotSupported, + /// Message consume too much gas + MessageGasLimitExceeded, + /// Commitment consume too much gas + CommitmentGasLimitExceeded, + } + + impl Pallet { + pub(crate) fn commit(network_id: GenericNetworkId) -> Weight { + debug!("Commit substrate messages"); + let messages = MessageQueues::::take(network_id); + if messages.is_empty() { + return ::WeightInfo::on_initialize_no_messages(); + } + + let batch_nonce = ChannelNonces::::mutate(network_id, |nonce| { + *nonce += 1; + *nonce + }); + + for idx in 0..messages.len() as u64 { + T::MessageStatusNotifier::update_status( + network_id, + MessageId::batched(T::ThisNetworkId::get(), network_id, batch_nonce, idx) + .hash(), + MessageStatus::Committed, + GenericTimepoint::Pending, + ); + } + + let average_payload_size = Self::average_payload_size(&messages); + let messages_count = messages.len(); + + let commitment = match network_id { + GenericNetworkId::EVM(_) => { + let (messages, total_gas) = messages.iter().fold( + (BoundedVec::default(), U256::zero()), + |(mut messages, total_gas), message| match message { + GenericBridgeMessage::EVM(message) => { + let total_gas = total_gas.saturating_add(message.max_gas); + if messages.try_push(message.clone()).is_err() { + error!("Messages limit exceeded, ignoring (if you noticed this message, please report it)"); + } + (messages, total_gas) + } + _ => { + error!("Message is not an EVM message, ignoring (if you noticed this message, please report it)"); + (messages, total_gas) + }, + }, + ); + GenericCommitment::EVM(bridge_types::evm::Commitment::Outbound( + bridge_types::evm::OutboundCommitment { + messages, + total_max_gas: total_gas, + nonce: batch_nonce, + }, + )) + } + GenericNetworkId::Sub(_) => { + let messages = messages.iter().fold( + BoundedVec::default(), + |mut messages, message| match message { + GenericBridgeMessage::Sub(message) => { + if messages.try_push(message.clone()).is_err() { + error!("Messages limit exceeded, ignoring (if you noticed this message, please report it)"); + } + messages + } + _ => { + error!("Message is not an Substrate bridge message, ignoring (if you noticed this message, please report it)"); + messages + }, + }, + ); + bridge_types::GenericCommitment::Sub(bridge_types::substrate::Commitment { + messages, + nonce: batch_nonce, + }) + } + GenericNetworkId::EVMLegacy(_) => { + error!("EVMLegacy messages are not supported by this channel (if you noticed this message, please report it)"); + return ::WeightInfo::on_initialize_no_messages(); + } + }; + + let commitment_hash = commitment.hash(); + let digest_item = AuxiliaryDigestItem::Commitment(network_id, commitment_hash); + T::AuxiliaryDigestHandler::add_item(digest_item); + + let commitment = bridge_types::types::GenericCommitmentWithBlock { + commitment, + block_number: >::block_number(), + }; + LatestCommitment::::insert(network_id, commitment); + + ::WeightInfo::on_initialize( + messages_count as u32, + average_payload_size as u32, + ) + } + + fn average_payload_size( + messages: &[GenericBridgeMessage], + ) -> usize { + let sum: usize = messages.iter().fold(0, |acc, x| acc + x.payload().len()); + // We overestimate message payload size rather than underestimate. + // So add 1 here to account for integer division truncation. + (sum / messages.len()).saturating_add(1) + } + } + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub interval: T::BlockNumber, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { + interval: 10u32.into(), + } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + Interval::::set(self.interval); + } + } + + impl Pallet { + pub fn submit_message( + network_id: GenericNetworkId, + who: &RawOrigin, + message: GenericBridgeMessage, + ) -> Result { + debug!("Send message from {:?} to network {:?}", who, network_id); + let messages_count = MessageQueues::::decode_len(network_id).unwrap_or(0) as u64; + ensure!( + messages_count < T::MaxMessagesPerCommit::get() as u64, + Error::::QueueSizeLimitReached, + ); + let batch_nonce = ChannelNonces::::get(network_id) + .checked_add(1) + .ok_or(Error::::Overflow)?; + + MessageQueues::::try_append(network_id, message) + .map_err(|_| Error::::QueueSizeLimitReached)?; + Self::deposit_event(Event::MessageAccepted { + network_id, + batch_nonce, + message_nonce: messages_count, + }); + Ok(MessageId::batched( + T::ThisNetworkId::get(), + network_id, + batch_nonce, + messages_count, + ) + .hash()) + } + } +} + +impl OutboundChannel for Pallet { + /// Submit message on the outbound channel + fn submit( + network_id: SubNetworkId, + who: &RawOrigin, + payload: &[u8], + _: (), + ) -> Result { + let message = BridgeMessage { + payload: payload + .to_vec() + .try_into() + .map_err(|_| Error::::PayloadTooLarge)?, + timepoint: T::TimepointProvider::get_timepoint(), + }; + Self::submit_message( + network_id.into(), + who, + bridge_types::GenericBridgeMessage::Sub(message), + ) + } + + fn submit_weight() -> Weight { + ::WeightInfo::submit() + } +} + +impl OutboundChannel for Pallet { + /// Submit message on the outbound channel + fn submit( + network_id: EVMChainId, + who: &RawOrigin, + payload: &[u8], + additional_data: AdditionalEVMOutboundData, + ) -> Result { + ensure!( + additional_data.max_gas < T::MaxGasPerMessage::get(), + Error::::MessageGasLimitExceeded + ); + QueueTotalGas::::try_mutate(GenericNetworkId::EVM(network_id), |total_gas| { + *total_gas = total_gas.saturating_add(additional_data.max_gas); + ensure!( + *total_gas < T::MaxGasPerCommit::get(), + Error::::CommitmentGasLimitExceeded + ); + Ok::<(), DispatchError>(()) + })?; + let message = bridge_types::evm::Message { + payload: payload + .to_vec() + .try_into() + .map_err(|_| Error::::PayloadTooLarge)?, + target: additional_data.target, + max_gas: additional_data.max_gas, + }; + Self::submit_message( + network_id.into(), + who, + bridge_types::GenericBridgeMessage::EVM(message), + ) + } + + fn submit_weight() -> Weight { + ::WeightInfo::submit() + } +} + +impl EVMOutboundChannel for Pallet { + fn submit_gas(network_id: EVMChainId) -> Result { + Ok(EVMSubmitGas::::get(network_id)) + } +} diff --git a/pallets/channel/src/outbound/test.rs b/pallets/channel/src/outbound/test.rs new file mode 100644 index 00000000..9258b0c9 --- /dev/null +++ b/pallets/channel/src/outbound/test.rs @@ -0,0 +1,333 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::*; +use bridge_types::GenericNetworkId; +use codec::{Decode, Encode, MaxEncodedLen}; +use currencies::BasicCurrencyAdapter; + +use bridge_types::traits::{OutboundChannel, TimepointProvider}; +use frame_support::traits::{Everything, GenesisBuild}; +use frame_support::{assert_noop, assert_ok, parameter_types, Deserialize, Serialize}; +use frame_system::RawOrigin; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_keyring::AccountKeyring as Keyring; +use sp_runtime::testing::Header; +use sp_runtime::traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}; +use sp_runtime::{AccountId32, MultiSignature}; +use sp_std::convert::From; +use traits::parameter_type_with_key; + +use crate::outbound as bridge_outbound_channel; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +const BASE_NETWORK_ID: GenericNetworkId = GenericNetworkId::Sub(SubNetworkId::Mainnet); + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage}, + Tokens: tokens::{Pallet, Call, Config, Storage, Event}, + Currencies: currencies::{Pallet, Call, Storage}, + Balances: pallet_balances::{Pallet, Call, Storage, Event}, + BridgeOutboundChannel: bridge_outbound_channel::{Pallet, Config, Storage, Event}, + } +); + +pub type Signature = MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +#[derive( + Encode, + Decode, + PartialEq, + Eq, + Debug, + Clone, + Copy, + MaxEncodedLen, + TypeInfo, + PartialOrd, + Ord, + Serialize, + Deserialize, +)] +pub enum AssetId { + Xor, + Eth, + Dai, +} + +pub type Balance = u128; +pub type Amount = i128; + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<65536>; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: AssetId| -> Balance { + 0 + }; +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = (); +} + +impl tokens::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = AssetId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = (); + type DustRemovalWhitelist = Everything; +} + +impl currencies::Config for Test { + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type GetNativeCurrencyId = GetBaseAssetId; + type WeightInfo = (); +} +parameter_types! { + pub const GetBaseAssetId: AssetId = AssetId::Xor; + pub GetTeamReservesAccountId: AccountId = AccountId32::from([0; 32]); + pub GetFeeAccountId: AccountId = AccountId32::from([1; 32]); + pub GetTreasuryAccountId: AccountId = AccountId32::from([2; 32]); +} + +parameter_types! { + pub const MaxMessagePayloadSize: u32 = 128; + pub const MaxMessagesPerCommit: u32 = 5; + pub const ThisNetworkId: GenericNetworkId = GenericNetworkId::Sub(SubNetworkId::Mainnet); +} + +pub struct GenericTimepointProvider; + +impl TimepointProvider for GenericTimepointProvider { + fn get_timepoint() -> bridge_types::GenericTimepoint { + bridge_types::GenericTimepoint::Sora(System::block_number() as u32) + } +} + +parameter_types! { + pub const BridgeMaxTotalGasLimit: u64 = 5_000_000; + pub const BridgeMaxGasPerMessage: u64 = 5_000_000; +} + +impl bridge_outbound_channel::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MaxMessagePayloadSize = MaxMessagePayloadSize; + type MaxMessagesPerCommit = MaxMessagesPerCommit; + type MessageStatusNotifier = (); + type AuxiliaryDigestHandler = (); + type AssetId = (); + type Balance = u128; + type WeightInfo = (); + type TimepointProvider = GenericTimepointProvider; + type ThisNetworkId = ThisNetworkId; + type MaxGasPerCommit = BridgeMaxTotalGasLimit; + type MaxGasPerMessage = BridgeMaxGasPerMessage; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = (); + type WeightInfo = (); +} + +pub fn new_tester() -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let config: bridge_outbound_channel::GenesisConfig = + bridge_outbound_channel::GenesisConfig { + interval: 10u32.into(), + }; + config.assimilate_storage(&mut storage).unwrap(); + + let bob: AccountId = Keyring::Bob.into(); + + pallet_balances::GenesisConfig:: { + balances: vec![(bob, 1u32.into())], + } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext: sp_io::TestExternalities = storage.into(); + + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[test] +fn test_submit() { + new_tester().execute_with(|| { + let who: AccountId = Keyring::Bob.into(); + + assert_ok!(BridgeOutboundChannel::submit( + BASE_NETWORK_ID.sub().unwrap(), + &RawOrigin::Signed(who.clone()), + &[0, 1, 2], + () + )); + BridgeOutboundChannel::commit(BASE_NETWORK_ID); + assert_eq!(>::get(BASE_NETWORK_ID), 1); + + assert_ok!(BridgeOutboundChannel::submit( + BASE_NETWORK_ID.sub().unwrap(), + &RawOrigin::Signed(who), + &[0, 1, 2], + () + )); + BridgeOutboundChannel::commit(BASE_NETWORK_ID); + assert_eq!(>::get(BASE_NETWORK_ID), 2); + }); +} + +#[test] +fn test_submit_exceeds_queue_limit() { + new_tester().execute_with(|| { + let who: AccountId = Keyring::Bob.into(); + + let max_messages = MaxMessagesPerCommit::get(); + (0..max_messages).for_each(|_| { + BridgeOutboundChannel::submit( + BASE_NETWORK_ID.sub().unwrap(), + &RawOrigin::Signed(who.clone()), + &[0, 1, 2], + (), + ) + .unwrap(); + }); + + assert_noop!( + BridgeOutboundChannel::submit( + BASE_NETWORK_ID.sub().unwrap(), + &RawOrigin::Signed(who), + &[0, 1, 2], + () + ), + Error::::QueueSizeLimitReached, + ); + }) +} + +#[test] +fn test_submit_exceeds_payload_limit() { + new_tester().execute_with(|| { + let who: AccountId = Keyring::Bob.into(); + + let max_payload_bytes = MaxMessagePayloadSize::get(); + let payload: Vec = (0..).take(max_payload_bytes as usize + 1).collect(); + + assert_noop!( + BridgeOutboundChannel::submit( + BASE_NETWORK_ID.sub().unwrap(), + &RawOrigin::Signed(who), + payload.as_slice(), + () + ), + Error::::PayloadTooLarge, + ); + }) +} + +#[test] +fn test_submit_fails_on_nonce_overflow() { + new_tester().execute_with(|| { + let who: AccountId = Keyring::Bob.into(); + + >::insert(BASE_NETWORK_ID, u64::MAX); + assert_noop!( + BridgeOutboundChannel::submit( + BASE_NETWORK_ID.sub().unwrap(), + &RawOrigin::Signed(who), + &[0, 1, 2], + () + ), + Error::::Overflow, + ); + }); +} diff --git a/pallets/channel/src/outbound/weights.rs b/pallets/channel/src/outbound/weights.rs new file mode 100644 index 00000000..0727c211 --- /dev/null +++ b/pallets/channel/src/outbound/weights.rs @@ -0,0 +1,200 @@ +// This file is part of the SORA network and Polkaswap app. + +// Copyright (c) 2020, 2021, Polka Biome Ltd. All rights reserved. +// SPDX-License-Identifier: BSD-4-Clause + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// All advertising materials mentioning features or use of this software must display +// the following acknowledgement: This product includes software developed by Polka Biome +// Ltd., SORA, and Polkaswap. +// +// Neither the name of the Polka Biome Ltd. nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY Polka Biome Ltd. AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Polka Biome Ltd. BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Autogenerated weights for substrate_bridge_channel::outbound +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `TRX40`, CPU: `AMD Ryzen Threadripper 3960X 24-Core Processor` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("local"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/framenode +// benchmark +// pallet +// --chain=local +// --steps=50 +// --repeat=20 +// --pallet=substrate_bridge_channel::outbound +// --extrinsic=* +// --header=./misc/file_header.txt +// --template=./misc/pallet-weight-template.hbs +// --output=./outbound-channel.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for substrate_bridge_channel::outbound. +pub trait WeightInfo { + fn on_initialize(m: u32, p: u32, ) -> Weight; + fn on_initialize_non_interval() -> Weight; + fn on_initialize_no_messages() -> Weight; + fn submit() -> Weight; +} + +/// Weights for substrate_bridge_channel::outbound using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: SubstrateBridgeOutboundChannel Interval (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel Interval (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel MessageQueues (r:2 w:1) + /// Proof Skipped: SubstrateBridgeOutboundChannel MessageQueues (max_values: None, max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel ChannelNonces (r:1 w:1) + /// Proof Skipped: SubstrateBridgeOutboundChannel ChannelNonces (max_values: None, max_size: None, mode: Measured) + /// Storage: BridgeProxy Senders (r:20 w:0) + /// Proof Skipped: BridgeProxy Senders (max_values: None, max_size: None, mode: Measured) + /// Storage: LeafProvider LatestDigest (r:1 w:1) + /// Proof Skipped: LeafProvider LatestDigest (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 20]`. + /// The range of component `p` is `[0, 256]`. + fn on_initialize(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + m * (259 ±0) + p * (20 ±0)` + // Estimated: `7321 + m * (3006 ±2) + p * (42 ±0)` + // Minimum execution time: 24_211_000 picoseconds. + Weight::from_parts(7_859_569, 7321) + // Standard Error: 31_278 + .saturating_add(Weight::from_parts(4_421_066, 0).saturating_mul(m.into())) + // Standard Error: 2_388 + .saturating_add(Weight::from_parts(69_998, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(m.into()))) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 3006).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 42).saturating_mul(p.into())) + } + /// Storage: SubstrateBridgeOutboundChannel Interval (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel Interval (max_values: Some(1), max_size: None, mode: Measured) + fn on_initialize_non_interval() -> Weight { + // Proof Size summary in bytes: + // Measured: `65` + // Estimated: `560` + // Minimum execution time: 2_340_000 picoseconds. + Weight::from_parts(2_400_000, 560) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: SubstrateBridgeOutboundChannel Interval (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel Interval (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel MessageQueues (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel MessageQueues (max_values: None, max_size: None, mode: Measured) + fn on_initialize_no_messages() -> Weight { + // Proof Size summary in bytes: + // Measured: `31` + // Estimated: `3032` + // Minimum execution time: 4_850_000 picoseconds. + Weight::from_parts(5_030_000, 3032) + .saturating_add(T::DbWeight::get().reads(2_u64)) + } + /// Storage: SubstrateBridgeOutboundChannel MessageQueues (r:1 w:1) + /// Proof Skipped: SubstrateBridgeOutboundChannel MessageQueues (max_values: None, max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel ChannelNonces (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel ChannelNonces (max_values: None, max_size: None, mode: Measured) + fn submit() -> Weight { + // Proof Size summary in bytes: + // Measured: `31` + // Estimated: `5012` + // Minimum execution time: 7_380_000 picoseconds. + Weight::from_parts(7_460_000, 5012) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: SubstrateBridgeOutboundChannel Interval (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel Interval (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel MessageQueues (r:2 w:1) + /// Proof Skipped: SubstrateBridgeOutboundChannel MessageQueues (max_values: None, max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel ChannelNonces (r:1 w:1) + /// Proof Skipped: SubstrateBridgeOutboundChannel ChannelNonces (max_values: None, max_size: None, mode: Measured) + /// Storage: BridgeProxy Senders (r:20 w:0) + /// Proof Skipped: BridgeProxy Senders (max_values: None, max_size: None, mode: Measured) + /// Storage: LeafProvider LatestDigest (r:1 w:1) + /// Proof Skipped: LeafProvider LatestDigest (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `m` is `[1, 20]`. + /// The range of component `p` is `[0, 256]`. + fn on_initialize(m: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + m * (259 ±0) + p * (20 ±0)` + // Estimated: `7321 + m * (3006 ±2) + p * (42 ±0)` + // Minimum execution time: 24_211_000 picoseconds. + Weight::from_parts(7_859_569, 7321) + // Standard Error: 31_278 + .saturating_add(Weight::from_parts(4_421_066, 0).saturating_mul(m.into())) + // Standard Error: 2_388 + .saturating_add(Weight::from_parts(69_998, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(m.into()))) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 3006).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 42).saturating_mul(p.into())) + } + /// Storage: SubstrateBridgeOutboundChannel Interval (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel Interval (max_values: Some(1), max_size: None, mode: Measured) + fn on_initialize_non_interval() -> Weight { + // Proof Size summary in bytes: + // Measured: `65` + // Estimated: `560` + // Minimum execution time: 2_340_000 picoseconds. + Weight::from_parts(2_400_000, 560) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: SubstrateBridgeOutboundChannel Interval (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel Interval (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel MessageQueues (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel MessageQueues (max_values: None, max_size: None, mode: Measured) + fn on_initialize_no_messages() -> Weight { + // Proof Size summary in bytes: + // Measured: `31` + // Estimated: `3032` + // Minimum execution time: 4_850_000 picoseconds. + Weight::from_parts(5_030_000, 3032) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + } + /// Storage: SubstrateBridgeOutboundChannel MessageQueues (r:1 w:1) + /// Proof Skipped: SubstrateBridgeOutboundChannel MessageQueues (max_values: None, max_size: None, mode: Measured) + /// Storage: SubstrateBridgeOutboundChannel ChannelNonces (r:1 w:0) + /// Proof Skipped: SubstrateBridgeOutboundChannel ChannelNonces (max_values: None, max_size: None, mode: Measured) + fn submit() -> Weight { + // Proof Size summary in bytes: + // Measured: `31` + // Estimated: `5012` + // Minimum execution time: 7_380_000 picoseconds. + Weight::from_parts(7_460_000, 5012) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +}