From 48555244b02313446725ac55e04a1f6bafda103d Mon Sep 17 00:00:00 2001 From: Vladimir Stepanenko Date: Tue, 17 Sep 2024 14:54:03 +0700 Subject: [PATCH] Add bridge signer pallet --- Cargo.lock | 61 +++ pallets/bridge-signer/Cargo.toml | 43 ++ pallets/bridge-signer/src/benchmarking.rs | 150 +++++++ pallets/bridge-signer/src/lib.rs | 493 ++++++++++++++++++++++ pallets/bridge-signer/src/mock.rs | 162 +++++++ pallets/bridge-signer/src/tests.rs | 493 ++++++++++++++++++++++ pallets/bridge-signer/src/weights.rs | 236 +++++++++++ pallets/types/src/lib.rs | 1 + pallets/types/src/multisig.rs | 201 +++++++++ pallets/types/src/substrate.rs | 17 + 10 files changed, 1857 insertions(+) create mode 100644 pallets/bridge-signer/Cargo.toml create mode 100644 pallets/bridge-signer/src/benchmarking.rs create mode 100644 pallets/bridge-signer/src/lib.rs create mode 100644 pallets/bridge-signer/src/mock.rs create mode 100644 pallets/bridge-signer/src/tests.rs create mode 100644 pallets/bridge-signer/src/weights.rs create mode 100644 pallets/types/src/multisig.rs diff --git a/Cargo.lock b/Cargo.lock index 1409cc4e..bb14ba4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,6 +407,28 @@ dependencies = [ "sp-std", ] +[[package]] +name = "bridge-signer" +version = "0.1.1" +dependencies = [ + "bridge-common", + "bridge-types", + "derive-where", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-keystore", + "sp-runtime", + "sp-std", +] + [[package]] name = "bridge-types" version = "0.1.0" @@ -688,6 +710,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-where" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -939,6 +972,34 @@ dependencies = [ "uint", ] +[[package]] +name = "evm-bridge" +version = "0.1.1" +dependencies = [ + "bridge-types", + "dispatch", + "ethabi", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "parity-scale-codec", + "rlp", + "rustc-hex", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-keystore", + "sp-runtime", + "sp-std", +] + [[package]] name = "evm-fungible-app" version = "0.1.1" diff --git a/pallets/bridge-signer/Cargo.toml b/pallets/bridge-signer/Cargo.toml new file mode 100644 index 00000000..b7b260fb --- /dev/null +++ b/pallets/bridge-signer/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "bridge-signer" +description = "Bridge signer" +version = "0.1.1" +edition = "2021" +authors = ['Polka Biome Ltd. '] +license = "BSD-4-Clause" +homepage = 'https://sora.org' +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"] } +bridge-common = { path = "../bridge-common", default-features = false } + +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 } + +bridge-types = { path = "../types", default-features = false } +derive-where = "1.2.7" + +[dev-dependencies] +sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38" } +sp-keystore = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.38" } +hex-literal = { version = "0.4.1" } + +[features] +default = ["std"] +std = ["bridge-common/std", "serde", "codec/std", "scale-info/std", "frame-support/std", "frame-system/std", "sp-core/std", "sp-io/std", "sp-runtime/std", "sp-std/std", "bridge-types/std"] +runtime-benchmarks = ["bridge-types/runtime-benchmarks", "frame-benchmarking", "sp-runtime/runtime-benchmarks", "frame-system/runtime-benchmarks"] + +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/bridge-signer/src/benchmarking.rs b/pallets/bridge-signer/src/benchmarking.rs new file mode 100644 index 00000000..19426837 --- /dev/null +++ b/pallets/bridge-signer/src/benchmarking.rs @@ -0,0 +1,150 @@ +// 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(feature = "runtime-benchmarks")] + +use super::*; +use crate::Pallet as BridgeDataSigner; +use bridge_types::GenericNetworkId; +use core::fmt::Write; +use frame_benchmarking::benchmarks; +use frame_support::assert_ok; +use frame_support::traits::EnsureOrigin; +use frame_system::{self, RawOrigin}; +use sp_core::bounded::BoundedBTreeMap; +use sp_core::{bounded::BoundedVec, ecdsa, Get, H256}; +use sp_std::prelude::*; + +fn initial_peers(n: usize) -> BoundedVec::MaxPeers> { + let mut keys = Vec::new(); + for i in 0..n { + let key = generate_key(i); + keys.push(key); + } + + keys.try_into().unwrap() +} + +fn generate_key(i: usize) -> ecdsa::Public { + let mut seed = sp_std::Writer::default(); + core::write!(seed, "//TestPeer//p-{}", i).unwrap(); + sp_io::crypto::ecdsa_generate(sp_core::crypto::key_types::DUMMY, Some(seed.into_inner())) +} + +fn initialize_network( + network_id: GenericNetworkId, + n: usize, +) -> BoundedVec::MaxPeers> { + let keys = initial_peers::(n); + assert_ok!(BridgeDataSigner::::register_network( + RawOrigin::Root.into(), + network_id, + keys.clone() + )); + keys +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +benchmarks! { + register_network { + let n = ::MaxPeers::get(); + let network_id = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + let peers = initial_peers::(n as usize); + }: _(RawOrigin::Root, network_id, peers.clone()) + verify { + assert_last_event::(Event::Initialized{network_id, peers}.into()); + } + + add_peer { + let network_id = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + + initialize_network::(network_id, 3); + let key = generate_key(3); + }: _(RawOrigin::Root, network_id, key) + verify { + assert!(PendingPeerUpdate::::get(network_id)); + } + + remove_peer { + let network_id = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + + let peers = initialize_network::(network_id, 3); + let key = peers[0]; + }: _(RawOrigin::Root, network_id, key) + verify { + assert!(PendingPeerUpdate::::get(network_id)); + } + + finish_add_peer { + let network_id = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + + let peers = initialize_network::(network_id, 3); + let key = generate_key(3); + BridgeDataSigner::::add_peer(RawOrigin::Root.into(), network_id, key).expect("remove_peer: Error adding peer"); + }: { + BridgeDataSigner::::finish_add_peer(T::CallOrigin::try_successful_origin().unwrap(), key)?; + } + verify { + assert!(!PendingPeerUpdate::::get(network_id)); + assert!(BridgeDataSigner::::peers(network_id).expect("add_peer: key found").contains(&key)); + } + + finish_remove_peer { + let network_id = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + + let peers = initialize_network::(network_id, 3); + let key = peers[0]; + BridgeDataSigner::::remove_peer(RawOrigin::Root.into(), network_id, key).expect("remove_peer: Error removing peer"); + }: { + BridgeDataSigner::::finish_remove_peer(T::CallOrigin::try_successful_origin().unwrap(), key)?; + } + verify { + assert!(!PendingPeerUpdate::::get(network_id)); + assert!(!BridgeDataSigner::::peers(network_id).expect("remove_peer: No key found").contains(&key)); + } + + approve { + let network_id = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + let peers = initialize_network::(network_id, 3); + let key = peers[0]; + let data = [3u8; 32]; + let signature = sp_io::crypto::ecdsa_sign_prehashed(sp_core::crypto::key_types::DUMMY, &key, &data).unwrap(); + let mut expected = BoundedBTreeMap::::new(); + expected.try_insert(key, signature.clone()).unwrap(); + }: _(RawOrigin::None, network_id, data.into(), signature) + verify { + assert_eq!(Approvals::::get(network_id, H256::from(data)), expected); + } + + impl_benchmark_test_suite!(BridgeDataSigner, crate::mock::new_test_ext(), mock::Test) +} diff --git a/pallets/bridge-signer/src/lib.rs b/pallets/bridge-signer/src/lib.rs new file mode 100644 index 00000000..523f8835 --- /dev/null +++ b/pallets/bridge-signer/src/lib.rs @@ -0,0 +1,493 @@ +// 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)] + +use bridge_types::{ + multisig::{MultiSignature, MultiSignatures, MultiSigners}, + substrate::BridgeSignerCall, + GenericNetworkId, +}; +use codec::Encode; +use frame_support::{ensure, weights::Weight}; +pub use pallet::*; +use sp_core::Get; +use sp_runtime::DispatchResult; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; +use sp_core::H256; +pub use weights::WeightInfo; + +impl From for Call { + fn from(value: BridgeSignerCall) -> Self { + match value { + BridgeSignerCall::AddPeer { peer } => Call::add_peer_internal { peer }, + BridgeSignerCall::RemovePeer { peer } => Call::remove_peer_internal { peer }, + BridgeSignerCall::FinishAddPeer => Call::finish_add_peer {}, + BridgeSignerCall::FinishRemovePeer => Call::finish_remove_peer {}, + } + } +} + +#[frame_support::pallet] +pub mod pallet { + #![allow(missing_docs)] + + use super::WeightInfo; + use bridge_types::multisig::*; + use bridge_types::types::CallOriginOutput; + use bridge_types::types::GenericAdditionalInboundData; + use bridge_types::{GenericNetworkId, H256}; + use frame_support::dispatch::Pays; + use frame_support::pallet_prelude::*; + use frame_support::weights::Weight; + use frame_system::ensure_root; + use frame_system::pallet_prelude::BlockNumberFor; + use frame_system::pallet_prelude::*; + use sp_core::Get; + use sp_runtime::Saturating; + + /// BEEFY-MMR pallet. + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// The module's configuration trait. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type CallOrigin: EnsureOrigin< + Self::RuntimeOrigin, + Success = CallOriginOutput, + >; + + #[pallet::constant] + type MaxPeers: Get; + + #[pallet::constant] + type ThisNetworkId: Get; + + #[pallet::constant] + type ApprovalCleanUpPeriod: Get>; + + type WeightInfo: WeightInfo; + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let mut removed = 0; + for ((network_id, hash), _) in CleanSchedule::::drain_prefix(now) { + Approvals::::remove(network_id, hash); + removed += 1; + } + T::DbWeight::get().reads_writes(removed * 2, removed * 2) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Initialized { + network_id: GenericNetworkId, + peers: MultiSigners, + }, + AddedPeer { + network_id: GenericNetworkId, + peer: MultiSigner, + }, + RemovedPeer { + network_id: GenericNetworkId, + peer: MultiSigner, + }, + ApprovalAccepted { + network_id: GenericNetworkId, + data: H256, + signature: MultiSignature, + }, + Approved { + network_id: GenericNetworkId, + data: H256, + signatures: MultiSignatures, + }, + } + + #[pallet::error] + pub enum Error { + PalletInitialized, + PalletNotInitialized, + PeerExists, + PeerNotExists, + TooMuchPeers, + FailedToVerifySignature, + PeerNotFound, + TooMuchApprovals, + ApprovalsNotFound, + SignaturesNotFound, + HasPendingPeerUpdate, + DontHavePendingPeerUpdates, + NetworkNotSupported, + AlreadyApproved, + CallerIsNotWhitelisted, + CallerAlreadyWhitelisted, + InvalidProof, + } + + /// Peers + #[pallet::storage] + #[pallet::getter(fn peers)] + pub(super) type Peers = + StorageMap<_, Identity, GenericNetworkId, MultiSigners, OptionQuery>; + + /// Whitelist for approve call + #[pallet::storage] + #[pallet::getter(fn whitelist)] + pub(super) type Whitelist = + StorageDoubleMap<_, Identity, GenericNetworkId, Identity, T::AccountId, bool, ValueQuery>; + + /// Pending peers + #[pallet::storage] + #[pallet::getter(fn pending_peer_update)] + pub(super) type PendingPeerUpdate = + StorageMap<_, Identity, GenericNetworkId, MultiSigner, OptionQuery>; + + /// Approvals + #[pallet::storage] + #[pallet::getter(fn approvals)] + pub(super) type Approvals = StorageDoubleMap< + _, + Identity, + GenericNetworkId, + Identity, + H256, + MultiSignatures, + OptionQuery, + >; + + /// Schedule for approvals cleaning + #[pallet::storage] + #[pallet::getter(fn schedule)] + pub(super) type CleanSchedule = StorageDoubleMap< + _, + Identity, + BlockNumberFor, + Identity, + (GenericNetworkId, H256), + bool, + ValueQuery, + >; + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::register_network())] + pub fn register_network( + origin: OriginFor, + network_id: GenericNetworkId, + peers: MultiSigners, + callers: BoundedVec, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + Peers::::try_mutate(network_id, |storage_peers| { + if storage_peers.is_some() { + return Err(Error::::PalletInitialized); + } else { + *storage_peers = Some(peers.clone()); + } + Ok(()) + })?; + for caller in callers { + Whitelist::::insert(network_id, caller, true); + } + Self::deposit_event(Event::::Initialized { network_id, peers }); + Ok(().into()) + } + + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::approve())] + pub fn approve( + origin: OriginFor, + network_id: GenericNetworkId, + data: H256, + signature: MultiSignature, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!( + Self::whitelist(network_id, who), + Error::::CallerIsNotWhitelisted + ); + let (approvals, is_approved) = Self::verify_approval(&network_id, &data, &signature)?; + Approvals::::insert(network_id, data, &approvals); + if is_approved { + Self::deposit_event(Event::::Approved { + network_id, + data, + signatures: approvals, + }); + } else { + Self::deposit_event(Event::::ApprovalAccepted { + network_id, + data, + signature, + }); + } + let clean_up_block = frame_system::Pallet::::block_number() + .saturating_add(T::ApprovalCleanUpPeriod::get()); + CleanSchedule::::insert(clean_up_block, (network_id, data), true); + Ok(Pays::No.into()) + } + + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::add_peer())] + pub fn add_peer( + origin: OriginFor, + network_id: GenericNetworkId, + peer: MultiSigner, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + ensure!( + PendingPeerUpdate::::get(network_id).is_none(), + Error::::HasPendingPeerUpdate + ); + Peers::::try_mutate(network_id, |peers| { + if let Some(peers) = peers { + if peers.contains(&peer) { + return Err(Error::::PeerExists); + } else { + ensure!(peers.add_peer(peer.clone()), Error::::TooMuchPeers); + } + } else { + return Err(Error::::NetworkNotSupported); + } + Ok(()) + })?; + PendingPeerUpdate::::insert(network_id, peer); + // TODO: Send peer update + Ok(().into()) + } + + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::remove_peer())] + pub fn remove_peer( + origin: OriginFor, + network_id: GenericNetworkId, + peer: MultiSigner, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + ensure!( + PendingPeerUpdate::::get(network_id).is_none(), + Error::::HasPendingPeerUpdate + ); + // Do nothing to ensure we have enough approvals for remove peer request + // Will be actually removed after request from sidechain + PendingPeerUpdate::::insert(network_id, peer); + // TODO: Send peer update + Ok(().into()) + } + + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::finish_remove_peer())] + pub fn finish_remove_peer(origin: OriginFor) -> DispatchResultWithPostInfo { + let CallOriginOutput { network_id, .. } = T::CallOrigin::ensure_origin(origin)?; + let peer = PendingPeerUpdate::::take(network_id) + .ok_or(Error::::DontHavePendingPeerUpdates)?; + Peers::::try_mutate(network_id, |peers| { + if let Some(peers) = peers { + ensure!(peers.remove_peer(&peer), Error::::PeerNotExists); + } else { + return Err(Error::::PalletNotInitialized); + } + Ok(()) + })?; + Ok(().into()) + } + + #[pallet::call_index(5)] + #[pallet::weight(::WeightInfo::finish_add_peer())] + pub fn finish_add_peer(origin: OriginFor) -> DispatchResultWithPostInfo { + let CallOriginOutput { network_id, .. } = T::CallOrigin::ensure_origin(origin)?; + let peer = PendingPeerUpdate::::take(network_id) + .ok_or(Error::::DontHavePendingPeerUpdates)?; + let peers = Peers::::get(network_id).ok_or(Error::::PalletNotInitialized)?; + ensure!(peers.contains(&peer), Error::::PeerNotExists); + Ok(().into()) + } + + #[pallet::call_index(6)] + #[pallet::weight(::WeightInfo::finish_remove_peer())] + pub fn remove_peer_internal( + origin: OriginFor, + peer: MultiSigner, + ) -> DispatchResultWithPostInfo { + let CallOriginOutput { network_id, .. } = T::CallOrigin::ensure_origin(origin)?; + Peers::::try_mutate(network_id, |peers| { + if let Some(peers) = peers { + ensure!(peers.remove_peer(&peer), Error::::PeerNotExists); + } else { + return Err(Error::::PalletNotInitialized); + } + Ok(()) + })?; + // TODO: Send peer update + Ok(().into()) + } + + #[pallet::call_index(7)] + #[pallet::weight(::WeightInfo::finish_add_peer())] + pub fn add_peer_internal( + origin: OriginFor, + peer: MultiSigner, + ) -> DispatchResultWithPostInfo { + let CallOriginOutput { network_id, .. } = T::CallOrigin::ensure_origin(origin)?; + Peers::::try_mutate(network_id, |peers| { + if let Some(peers) = peers { + if peers.contains(&peer) { + return Err(Error::::PeerExists); + } else { + ensure!(peers.add_peer(peer), Error::::TooMuchPeers); + } + } else { + return Err(Error::::NetworkNotSupported); + } + Ok(()) + })?; + // TODO: Send peer update + Ok(().into()) + } + + #[pallet::call_index(8)] + #[pallet::weight(::WeightInfo::add_peer())] + pub fn add_whitelisted_caller( + origin: OriginFor, + network_id: GenericNetworkId, + caller: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + Peers::::get(network_id).ok_or(Error::::NetworkNotSupported)?; + ensure!( + !Self::whitelist(network_id, caller.clone()), + Error::::CallerAlreadyWhitelisted + ); + Whitelist::::insert(network_id, caller, true); + Ok(().into()) + } + + #[pallet::call_index(9)] + #[pallet::weight(::WeightInfo::add_peer())] + pub fn remove_whitelisted_caller( + origin: OriginFor, + network_id: GenericNetworkId, + caller: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + Peers::::get(network_id).ok_or(Error::::NetworkNotSupported)?; + ensure!( + Self::whitelist(network_id, caller.clone()), + Error::::CallerIsNotWhitelisted + ); + Whitelist::::remove(network_id, caller); + Ok(().into()) + } + } +} + +impl Pallet { + fn verify_approval( + network_id: &GenericNetworkId, + data: &H256, + signature: &MultiSignature, + ) -> Result<(MultiSignatures, bool), Error> { + ensure!(signature.verify(*data), Error::::FailedToVerifySignature); + let peers = Peers::::get(network_id).ok_or(Error::::PalletNotInitialized)?; + ensure!( + peers.contains(&signature.public()), + Error::::PeerNotFound + ); + let mut approvals = if let Some(approvals) = Approvals::::get(network_id, data) { + approvals + } else { + peers.empty_signatures() + }; + ensure!( + !approvals.contains(&signature.public()), + Error::::AlreadyApproved + ); + ensure!( + approvals.add_signature(signature.clone()), + Error::::TooMuchApprovals + ); + let is_approved = + (approvals.len() as u32) >= bridge_types::utils::threshold(peers.len() as u32); + Ok((approvals, is_approved)) + } +} + +impl bridge_types::traits::Verifier for Pallet { + type Proof = MultiSignatures; + + fn verify( + network_id: GenericNetworkId, + commitment_hash: H256, + proof: &Self::Proof, + ) -> DispatchResult { + let this_network_id = T::ThisNetworkId::get(); + let peers = Peers::::get(network_id).ok_or(Error::::NetworkNotSupported)?; + let message_to_hash = (network_id, this_network_id, commitment_hash).encode(); + let message_hash = match peers { + MultiSigners::Ecdsa(_) => sp_io::hashing::keccak_256(&message_to_hash), + MultiSigners::Ed25519(_) => sp_io::hashing::sha2_256(&message_to_hash), + }; + ensure!( + proof.verify(&peers, message_hash.into()), + Error::::InvalidProof + ); + Ok(()) + } + + fn verify_weight(_proof: &Self::Proof) -> Weight { + ::WeightInfo::register_network() + } + + #[cfg(feature = "runtime-benchmarks")] + fn valid_proof() -> Option { + None + } +} diff --git a/pallets/bridge-signer/src/mock.rs b/pallets/bridge-signer/src/mock.rs new file mode 100644 index 00000000..d0c9afae --- /dev/null +++ b/pallets/bridge-signer/src/mock.rs @@ -0,0 +1,162 @@ +// 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 crate as bridge_signer; +use bridge_types::types::GenericAdditionalInboundData; +use bridge_types::GenericNetworkId; +use bridge_types::{traits::OutboundChannel, SubNetworkId}; +use frame_support::weights::Weight; +use frame_support::{parameter_types, traits::Everything}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::AccountId32; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + transaction_validity::TransactionPriority, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + BridgeSigner: bridge_signer::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; + pub const TestUnsignedPriority: TransactionPriority = 100; + pub const TestUnsignedLongevity: u64 = 100; + pub const BridgeMaxPeers: u32 = 50; + pub const ThisNetworkId: bridge_types::GenericNetworkId = bridge_types::GenericNetworkId::Sub(bridge_types::SubNetworkId::Mainnet); + pub const ApprovalCleanUpPeriod: u32 = 10; +} + +pub type AccountId = AccountId32; + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + 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 Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl bridge_signer::Config for Test { + type RuntimeEvent = RuntimeEvent; + type CallOrigin = TestCallOrigin; + type MaxPeers = BridgeMaxPeers; + type ThisNetworkId = ThisNetworkId; + type ApprovalCleanUpPeriod = ApprovalCleanUpPeriod; + type WeightInfo = (); +} + +pub struct TestOutboundChannel; +impl OutboundChannel for TestOutboundChannel { + fn submit( + _network_id: SubNetworkId, + _who: &system::RawOrigin, + _payload: &[u8], + _additional: (), + ) -> Result { + Ok([1; 32].into()) + } + + fn submit_weight() -> Weight { + Default::default() + } +} + +impl Default for RuntimeOrigin { + fn default() -> Self { + RuntimeOrigin::root() + } +} + +pub struct TestCallOrigin; +impl frame_support::traits::EnsureOrigin for TestCallOrigin { + type Success = + bridge_types::types::CallOriginOutput; + + fn try_origin(_o: OuterOrigin) -> Result { + Ok(bridge_types::types::CallOriginOutput { + network_id: SubNetworkId::Mainnet.into(), + message_id: [1; 32].into(), + timepoint: Default::default(), + additional: GenericAdditionalInboundData::Sub, + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(Default::default()) + } +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into(); + ext.register_extension(sp_keystore::KeystoreExt(std::sync::Arc::new( + sp_keystore::testing::KeyStore::new(), + ))); + + ext +} diff --git a/pallets/bridge-signer/src/tests.rs b/pallets/bridge-signer/src/tests.rs new file mode 100644 index 00000000..e6665bda --- /dev/null +++ b/pallets/bridge-signer/src/tests.rs @@ -0,0 +1,493 @@ +// 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 crate::{mock::*, Error}; +use bridge_types::{ + multisig::{MultiSignature, MultiSigner, MultiSigners}, + SubNetworkId, H256, +}; +use frame_support::{assert_noop, assert_ok}; +use sp_core::{bounded_vec, ecdsa, Pair}; + +fn test_peers() -> (MultiSigners, Vec) { + let pairs: Vec = vec![ + ecdsa::Pair::generate_with_phrase(Some("password")), + ecdsa::Pair::generate_with_phrase(Some("password1")), + ecdsa::Pair::generate_with_phrase(Some("password2")), + ecdsa::Pair::generate_with_phrase(Some("password3")), + ecdsa::Pair::generate_with_phrase(Some("password4")), + ecdsa::Pair::generate_with_phrase(Some("password5")), + ] + .into_iter() + .map(|(x, _, _)| x) + .collect(); + let mut peers = MultiSigners::Ecdsa(Default::default()); + for peer in pairs.iter() { + peers.add_peer(MultiSigner::Ecdsa(peer.public())); + } + (peers, pairs) +} + +fn test_signer() -> ecdsa::Pair { + ecdsa::Pair::generate_with_phrase(Some("something")).0 +} + +#[test] +fn it_works_register_network() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let peers = test_peers().0; + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + Default::default() + )); + + assert!(BridgeSigner::peers(network_id).is_some()); + assert!(BridgeSigner::peers(network_id).unwrap().len() == peers.len()); + }); +} + +#[test] +fn it_works_register_network_with_empty_peers() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let peers = MultiSigners::Ecdsa(Default::default()); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + Default::default() + )); + + assert!(BridgeSigner::peers(network_id).is_some()); + assert!(BridgeSigner::peers(network_id).unwrap().is_empty()); + }); +} + +#[test] +fn it_fails_register_network_alredy_initialized() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + test_peers().0.try_into().unwrap(), + Default::default() + )); + + assert_noop!( + BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + test_peers().0.try_into().unwrap(), + Default::default() + ), + Error::::PalletInitialized + ); + }); +} + +#[test] +fn it_works_approve() { + new_test_ext().execute_with(|| { + let sender = sp_keyring::AccountKeyring::Alice.to_account_id(); + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, pairs) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + bounded_vec![sender.clone()] + )); + + let data = [1u8; 32]; + let signature = MultiSignature::Ecdsa(pairs[0].public(), pairs[0].sign_prehashed(&data)); + assert!(BridgeSigner::peers(network_id) + .unwrap() + .contains(&pairs[0].public().into())); + assert!(BridgeSigner::approvals(network_id, H256::from(data)).is_none()); + + assert_ok!(BridgeSigner::approve( + RuntimeOrigin::signed(sender), + network_id, + H256::from(data), + signature, + )); + + assert!( + BridgeSigner::approvals(network_id, H256::from(data)) + .unwrap() + .len() + == 1 + ); + }); +} + +#[test] +fn it_fails_approve_nonexisted_peer() { + new_test_ext().execute_with(|| { + let sender = sp_keyring::AccountKeyring::Alice.to_account_id(); + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + bounded_vec![sender.clone()] + )); + + let data = [1u8; 32]; + let signature = + MultiSignature::Ecdsa(test_signer().public(), test_signer().sign_prehashed(&data)); + assert!(BridgeSigner::approvals(network_id, H256::from(data)).is_none()); + + assert_noop!( + BridgeSigner::approve( + RuntimeOrigin::signed(sender), + network_id, + H256::from(data), + signature, + ), + Error::::FailedToVerifySignature + ); + + assert!(BridgeSigner::approvals(network_id, H256::from(data)).is_none()); + }); +} + +#[test] +fn it_fails_approve_sign_already_exist() { + new_test_ext().execute_with(|| { + let sender = sp_keyring::AccountKeyring::Alice.to_account_id(); + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, pairs) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + bounded_vec![sender.clone()] + )); + + let data = [1u8; 32]; + let signature = MultiSignature::Ecdsa(pairs[0].public(), pairs[0].sign_prehashed(&data)); + assert!(BridgeSigner::approvals(network_id, H256::from(data)).is_none()); + + assert_ok!(BridgeSigner::approve( + RuntimeOrigin::signed(sender.clone()), + network_id, + H256::from(data), + signature.clone(), + )); + + assert!( + BridgeSigner::approvals(network_id, H256::from(data)) + .unwrap() + .len() + == 1 + ); + + assert_noop!( + BridgeSigner::approve( + RuntimeOrigin::signed(sender), + network_id, + H256::from(data), + signature, + ), + Error::::AlreadyApproved + ); + + assert!( + BridgeSigner::approvals(network_id, H256::from(data)) + .unwrap() + .len() + == 1 + ); + }); +} + +#[test] +fn it_works_add_peer() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + Default::default() + )); + + let new_peer = test_signer().public().into(); + assert_ok!(BridgeSigner::add_peer( + RuntimeOrigin::root(), + network_id, + new_peer, + )); + + assert!(BridgeSigner::pending_peer_update(network_id).is_some()); + }); +} + +#[test] +fn it_fails_add_peer_pending_update() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + Default::default() + )); + + let new_peer = test_signer().public().into(); + assert_ok!(BridgeSigner::add_peer( + RuntimeOrigin::root(), + network_id, + new_peer, + )); + + // cannot add another peer while pending peer update + let new_peer = test_signer().public().into(); + assert_noop!( + BridgeSigner::add_peer(RuntimeOrigin::root(), network_id, new_peer,), + Error::::HasPendingPeerUpdate + ); + + assert!(BridgeSigner::pending_peer_update(network_id).is_some()); + }); +} + +#[test] +fn it_fails_add_peer_already_exists() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + Default::default() + )); + + let peer = peers.signers()[0].clone(); + assert_noop!( + BridgeSigner::add_peer(RuntimeOrigin::root(), network_id, peer,), + Error::::PeerExists + ); + + assert!(BridgeSigner::pending_peer_update(network_id).is_none()); + }); +} + +#[test] +fn it_works_remove_peer() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + Default::default() + )); + + let peer = peers.signers()[0].clone(); + assert_ok!(BridgeSigner::remove_peer( + RuntimeOrigin::root(), + network_id, + peer, + )); + + assert!(BridgeSigner::pending_peer_update(network_id).is_some()); + }); +} + +#[test] +fn it_fails_remove_peer_pending_update() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + Default::default() + )); + + let peer = peers.signers()[0].clone(); + assert_ok!(BridgeSigner::remove_peer( + RuntimeOrigin::root(), + network_id, + peer, + )); + + // cannot remove another peer while pending peer update + let peer = peers.signers()[1].clone(); + assert_noop!( + BridgeSigner::remove_peer(RuntimeOrigin::root(), network_id, peer,), + Error::::HasPendingPeerUpdate + ); + + assert!(BridgeSigner::pending_peer_update(network_id).is_some()); + }); +} + +#[test] +fn it_works_finish_remove_peer() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + Default::default() + )); + + let peer = peers.signers()[0].clone(); + assert_ok!(BridgeSigner::remove_peer( + RuntimeOrigin::root(), + network_id, + peer.clone(), + )); + + assert!(BridgeSigner::pending_peer_update(network_id).is_some()); + + assert_ok!(BridgeSigner::finish_remove_peer(RuntimeOrigin::root())); + + assert!(BridgeSigner::pending_peer_update(network_id).is_none()); + assert!(!BridgeSigner::peers(network_id).unwrap().contains(&peer)); + }); +} + +#[test] +fn it_fails_finish_remove_peer_no_updates() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers.clone(), + Default::default() + )); + + assert!(BridgeSigner::pending_peer_update(network_id).is_none()); + + assert_noop!( + BridgeSigner::finish_remove_peer(RuntimeOrigin::root()), + Error::::DontHavePendingPeerUpdates + ); + }) +} + +#[test] +fn it_fails_finish_remove_not_initialized() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let peer = test_signer().public().into(); + + assert_ok!(BridgeSigner::remove_peer( + RuntimeOrigin::root(), + network_id, + peer, + )); + + assert_noop!( + BridgeSigner::finish_remove_peer(RuntimeOrigin::root()), + Error::::PalletNotInitialized + ); + }) +} + +#[test] +fn it_works_finish_add_peer() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + Default::default() + )); + + let new_peer: MultiSigner = test_signer().public().into(); + assert_ok!(BridgeSigner::add_peer( + RuntimeOrigin::root(), + network_id, + new_peer.clone(), + )); + + assert!(BridgeSigner::pending_peer_update(network_id).is_some()); + + assert_ok!(BridgeSigner::finish_add_peer(RuntimeOrigin::root(),)); + + assert!(BridgeSigner::pending_peer_update(network_id).is_none()); + assert!(BridgeSigner::peers(network_id).unwrap().contains(&new_peer)); + }); +} + +#[test] +fn it_fails_add_peer_no_pending_update() { + new_test_ext().execute_with(|| { + let network_id = bridge_types::GenericNetworkId::Sub(SubNetworkId::Mainnet); + let (peers, _) = test_peers(); + + assert_ok!(BridgeSigner::register_network( + RuntimeOrigin::root(), + network_id, + peers, + Default::default() + )); + + assert_noop!( + BridgeSigner::finish_add_peer(RuntimeOrigin::root()), + Error::::DontHavePendingPeerUpdates + ); + }); +} diff --git a/pallets/bridge-signer/src/weights.rs b/pallets/bridge-signer/src/weights.rs new file mode 100644 index 00000000..95ab7823 --- /dev/null +++ b/pallets/bridge-signer/src/weights.rs @@ -0,0 +1,236 @@ +// 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 bridge_data_signer +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-28, 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=bridge_data_signer +// --extrinsic=* +// --header=./misc/file_header.txt +// --template=./misc/pallet-weight-template.hbs +// --output=./bridge-data-signer.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 bridge_data_signer. +pub trait WeightInfo { + fn register_network() -> Weight; + fn add_peer() -> Weight; + fn remove_peer() -> Weight; + fn finish_add_peer() -> Weight; + fn finish_remove_peer() -> Weight; + fn approve() -> Weight; +} + +/// Weights for bridge_data_signer using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: BridgeDataSigner Peers (r:1 w:1) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + fn register_network() -> Weight { + // Proof Size summary in bytes: + // Measured: `175` + // Estimated: `4159` + // Minimum execution time: 15_661_000 picoseconds. + Weight::from_parts(18_111_000, 4159) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + /// Storage: BridgeDataSigner Peers (r:1 w:1) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + /// 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 add_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `367` + // Estimated: `12352` + // Minimum execution time: 16_741_000 picoseconds. + Weight::from_parts(17_451_000, 12352) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + /// 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 remove_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `264` + // Estimated: `7987` + // Minimum execution time: 14_101_000 picoseconds. + Weight::from_parts(14_631_000, 7987) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + fn finish_add_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `239` + // Estimated: `2509` + // Minimum execution time: 6_160_000 picoseconds. + Weight::from_parts(6_310_000, 2509) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + /// Storage: BridgeDataSigner Peers (r:1 w:1) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + fn finish_remove_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `362` + // Estimated: `6668` + // Minimum execution time: 8_700_000 picoseconds. + Weight::from_parts(9_131_000, 6668) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: BridgeDataSigner Peers (r:1 w:0) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + /// Storage: BridgeDataSigner Approvals (r:1 w:1) + /// Proof: BridgeDataSigner Approvals (max_values: None, max_size: Some(4966), added: 7441, mode: MaxEncodedLen) + fn approve() -> Weight { + // Proof Size summary in bytes: + // Measured: `336` + // Estimated: `11600` + // Minimum execution time: 50_042_000 picoseconds. + Weight::from_parts(51_072_000, 11600) + .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: BridgeDataSigner Peers (r:1 w:1) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + fn register_network() -> Weight { + // Proof Size summary in bytes: + // Measured: `175` + // Estimated: `4159` + // Minimum execution time: 15_661_000 picoseconds. + Weight::from_parts(18_111_000, 4159) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + /// Storage: BridgeDataSigner Peers (r:1 w:1) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + /// 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 add_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `367` + // Estimated: `12352` + // Minimum execution time: 16_741_000 picoseconds. + Weight::from_parts(17_451_000, 12352) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + /// 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 remove_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `264` + // Estimated: `7987` + // Minimum execution time: 14_101_000 picoseconds. + Weight::from_parts(14_631_000, 7987) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + fn finish_add_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `239` + // Estimated: `2509` + // Minimum execution time: 6_160_000 picoseconds. + Weight::from_parts(6_310_000, 2509) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: BridgeDataSigner PendingPeerUpdate (r:1 w:1) + /// Proof: BridgeDataSigner PendingPeerUpdate (max_values: None, max_size: Some(34), added: 2509, mode: MaxEncodedLen) + /// Storage: BridgeDataSigner Peers (r:1 w:1) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + fn finish_remove_peer() -> Weight { + // Proof Size summary in bytes: + // Measured: `362` + // Estimated: `6668` + // Minimum execution time: 8_700_000 picoseconds. + Weight::from_parts(9_131_000, 6668) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: BridgeDataSigner Peers (r:1 w:0) + /// Proof: BridgeDataSigner Peers (max_values: None, max_size: Some(1684), added: 4159, mode: MaxEncodedLen) + /// Storage: BridgeDataSigner Approvals (r:1 w:1) + /// Proof: BridgeDataSigner Approvals (max_values: None, max_size: Some(4966), added: 7441, mode: MaxEncodedLen) + fn approve() -> Weight { + // Proof Size summary in bytes: + // Measured: `336` + // Estimated: `11600` + // Minimum execution time: 50_042_000 picoseconds. + Weight::from_parts(51_072_000, 11600) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/pallets/types/src/lib.rs b/pallets/types/src/lib.rs index c4d60804..47d7a764 100644 --- a/pallets/types/src/lib.rs +++ b/pallets/types/src/lib.rs @@ -32,6 +32,7 @@ pub mod channel_abi; pub mod evm; +pub mod multisig; pub mod substrate; #[cfg(any(feature = "test", test))] pub mod test_utils; diff --git a/pallets/types/src/multisig.rs b/pallets/types/src/multisig.rs new file mode 100644 index 00000000..83d99eed --- /dev/null +++ b/pallets/types/src/multisig.rs @@ -0,0 +1,201 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use derivative::Derivative; +use scale_info::TypeInfo; +use sp_core::{ecdsa, ed25519, Get, RuntimeDebug, H256}; +use sp_runtime::{BoundedBTreeMap, BoundedBTreeSet}; + +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Derivative)] +#[derivative( + Debug(bound = ""), + Clone(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +#[scale_info(skip_type_params(MaxSigs))] +pub enum MultiSignatures> { + /// Substrate and EVM Bridge + Ecdsa(BoundedBTreeMap), + /// TON Bridge + Ed25519(BoundedBTreeMap), +} + +#[derive(RuntimeDebug, Clone, Decode, Encode, MaxEncodedLen, TypeInfo, PartialEq, Eq)] +pub enum MultiSignature { + /// Substrate and EVM Bridge + Ecdsa(ecdsa::Public, ecdsa::Signature), + /// TON Bridge + Ed25519(ed25519::Public, ed25519::Signature), +} + +#[derive(RuntimeDebug, Clone, Decode, Encode, MaxEncodedLen, TypeInfo, PartialEq, Eq)] +pub enum MultiSigner { + /// Substrate and EVM Bridge + Ecdsa(ecdsa::Public), + /// TON Bridge + Ed25519(ed25519::Public), +} + +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Derivative)] +#[derivative( + Debug(bound = ""), + Clone(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +#[scale_info(skip_type_params(MaxPeers))] +pub enum MultiSigners> { + /// Substrate and EVM Bridge + Ecdsa(BoundedBTreeSet), + /// TON Bridge + Ed25519(BoundedBTreeSet), +} + +impl MultiSignature { + pub fn verify(&self, msg: H256) -> bool { + match self { + Self::Ecdsa(pub_key, sig) => { + sp_io::crypto::ecdsa_verify_prehashed(sig, &msg.0, pub_key) + } + Self::Ed25519(pub_key, sig) => sp_io::crypto::ed25519_verify(sig, &msg.0, pub_key), + } + } + + pub fn public(&self) -> MultiSigner { + match self { + Self::Ecdsa(pub_key, _) => MultiSigner::Ecdsa(pub_key.clone()), + Self::Ed25519(pub_key, _) => MultiSigner::Ed25519(pub_key.clone()), + } + } +} + +impl> MultiSignatures { + pub fn verify(&self, signers: &MultiSigners, msg: H256) -> bool { + if self.len() < crate::utils::threshold(signers.len() as u32) as usize { + return false; + } + for sig in self.signatures() { + if !signers.contains(&sig.public()) { + return false; + } + if !sig.verify(msg) { + return false; + } + } + true + } + + pub fn add_signature(&mut self, sig: MultiSignature) -> bool { + match (self, sig) { + (Self::Ecdsa(sigs), MultiSignature::Ecdsa(pub_key, sig)) => { + if !matches!(sigs.try_insert(pub_key, sig), Ok(None)) { + return false; + } + } + (Self::Ed25519(sigs), MultiSignature::Ed25519(pub_key, sig)) => { + if !matches!(sigs.try_insert(pub_key, sig), Ok(None)) { + return false; + } + } + _ => return false, + } + true + } + + pub fn len(&self) -> usize { + match self { + Self::Ecdsa(sigs) => sigs.len(), + Self::Ed25519(sigs) => sigs.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn contains(&self, signer: &MultiSigner) -> bool { + match (self, signer) { + (Self::Ecdsa(sigs), MultiSigner::Ecdsa(pub_key)) => sigs.contains_key(pub_key), + (Self::Ed25519(sigs), MultiSigner::Ed25519(pub_key)) => sigs.contains_key(pub_key), + _ => false, + } + } + pub fn signatures(&self) -> Vec { + match self { + MultiSignatures::Ecdsa(sigs) => sigs + .iter() + .map(|(pk, sig)| MultiSignature::Ecdsa(pk.clone(), sig.clone())) + .collect(), + MultiSignatures::Ed25519(sigs) => sigs + .iter() + .map(|(pk, sig)| MultiSignature::Ed25519(pk.clone(), sig.clone())) + .collect(), + } + } +} + +impl> MultiSigners { + pub fn contains(&self, signer: &MultiSigner) -> bool { + match (self, signer) { + (Self::Ecdsa(pub_keys), MultiSigner::Ecdsa(pub_key)) => pub_keys.contains(pub_key), + (Self::Ed25519(pub_keys), MultiSigner::Ed25519(pub_key)) => pub_keys.contains(pub_key), + _ => false, + } + } + + pub fn empty_signatures(&self) -> MultiSignatures { + match self { + Self::Ecdsa(_) => MultiSignatures::Ecdsa(BoundedBTreeMap::new()), + Self::Ed25519(_) => MultiSignatures::Ecdsa(BoundedBTreeMap::new()), + } + } + + pub fn len(&self) -> usize { + match self { + Self::Ecdsa(pub_keys) => pub_keys.len(), + Self::Ed25519(pub_keys) => pub_keys.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn add_peer(&mut self, pub_key: MultiSigner) -> bool { + match (self, pub_key) { + (Self::Ecdsa(pub_keys), MultiSigner::Ecdsa(pub_key)) => { + return pub_keys.try_insert(pub_key).unwrap_or(false); + } + (Self::Ed25519(pub_keys), MultiSigner::Ed25519(pub_key)) => { + return pub_keys.try_insert(pub_key).unwrap_or(false); + } + _ => return false, + } + } + + pub fn remove_peer(&mut self, pub_key: &MultiSigner) -> bool { + match (self, pub_key) { + (Self::Ecdsa(pub_keys), MultiSigner::Ecdsa(pub_key)) => pub_keys.remove(pub_key), + (Self::Ed25519(pub_keys), MultiSigner::Ed25519(pub_key)) => pub_keys.remove(pub_key), + _ => false, + } + } + + pub fn signers(&self) -> Vec { + match self { + MultiSigners::Ecdsa(peers) => peers.iter().cloned().map(Into::into).collect(), + MultiSigners::Ed25519(peers) => peers.iter().cloned().map(Into::into).collect(), + } + } +} + +impl From for MultiSigner { + fn from(value: ecdsa::Public) -> Self { + MultiSigner::Ecdsa(value) + } +} + +impl From for MultiSigner { + fn from(value: ed25519::Public) -> Self { + MultiSigner::Ed25519(value) + } +} diff --git a/pallets/types/src/substrate.rs b/pallets/types/src/substrate.rs index 646bbff4..0d68f4a3 100644 --- a/pallets/types/src/substrate.rs +++ b/pallets/types/src/substrate.rs @@ -29,6 +29,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #![allow(clippy::large_enum_variant)] +use crate::multisig::MultiSigner; use crate::ton::{TonAddressWithPrefix, TonBalance}; use crate::{H160, H256}; use codec::{Decode, Encode}; @@ -217,6 +218,21 @@ impl SubstrateBridgeMessageEncode for MultisigVerifierCall { } } +/// Message to BridgeSigner +#[derive(Clone, RuntimeDebug, Encode, Decode, PartialEq, Eq, scale_info::TypeInfo)] +pub enum BridgeSignerCall { + AddPeer { peer: MultiSigner }, + RemovePeer { peer: MultiSigner }, + FinishRemovePeer, + FinishAddPeer, +} + +impl SubstrateBridgeMessageEncode for BridgeSignerCall { + fn prepare_message(self) -> Vec { + BridgeCall::BridgeSigner(self).encode() + } +} + /// Substrate bridge message payload #[derive(Clone, RuntimeDebug, Encode, Decode, PartialEq, Eq, scale_info::TypeInfo)] pub enum BridgeCall { @@ -227,6 +243,7 @@ pub enum BridgeCall { SubstrateApp(SubstrateAppCall), FAApp(FAAppCall), JettonApp(JettonAppCall), + BridgeSigner(BridgeSignerCall), } impl SubstrateBridgeMessageEncode for BridgeCall {