From 3765dbc6bcc0332659765d4648237ec4fd1c3f83 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Fri, 25 Oct 2024 12:36:02 +0900 Subject: [PATCH] feat: Add in-mem UniqueMultimap for testing (#84) * feat: Add in-mem UniqueMultimap for testing * test: Add test for UnifyAccount signed extension * chore: Fix clippy errors * test: Add unit tests for in-mem UniqueMultimap --- frame/babel/Cargo.toml | 2 +- frame/babel/src/cosmos/address.rs | 6 +- frame/babel/src/cosmos/precompile.rs | 2 +- frame/babel/src/extensions/unify_account.rs | 61 ++++++-- frame/babel/src/lib.rs | 6 +- frame/babel/src/traits.rs | 39 +++++ frame/multimap/src/traits.rs | 3 + frame/multimap/src/traits/in_mem.rs | 165 ++++++++++++++++++++ 8 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 frame/babel/src/traits.rs create mode 100644 frame/multimap/src/traits/in_mem.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 6aeb92de..75ff80ef 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -49,7 +49,7 @@ sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable24 sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409", default-features = false } [dev-dependencies] -hex = "0.4.3" +const-hex = "1.13" # substrate pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409" } pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409" } diff --git a/frame/babel/src/cosmos/address.rs b/frame/babel/src/cosmos/address.rs index 11581911..30107729 100644 --- a/frame/babel/src/cosmos/address.rs +++ b/frame/babel/src/cosmos/address.rs @@ -42,7 +42,7 @@ where pub struct AccountToAddr(PhantomData); impl Convert, String> for AccountToAddr where - T: pallet_cosmwasm::Config + unify_account::Config, + T: pallet_cosmwasm::Config + unify_account::Config>, { fn convert(account: AccountIdOf) -> String { let addresses = T::AddressMap::get(&account); @@ -61,7 +61,7 @@ where impl Convert, ()>> for AccountToAddr where - T: pallet_cosmwasm::Config + unify_account::Config, + T: pallet_cosmwasm::Config + unify_account::Config>, { fn convert(address: String) -> Result, ()> { let (_hrp, address_raw) = acc_address_from_bech32(&address).map_err(|_| ())?; @@ -71,7 +71,7 @@ where impl Convert, Result, ()>> for AccountToAddr where - T: pallet_cosmwasm::Config + unify_account::Config, + T: pallet_cosmwasm::Config + unify_account::Config>, { fn convert(address: Vec) -> Result, ()> { match address.len() { diff --git a/frame/babel/src/cosmos/precompile.rs b/frame/babel/src/cosmos/precompile.rs index 9771c9d3..68180039 100644 --- a/frame/babel/src/cosmos/precompile.rs +++ b/frame/babel/src/cosmos/precompile.rs @@ -141,6 +141,6 @@ mod tests { let ExecuteMsg::Dispatch { input } = serde_json_wasm::from_slice(message.as_bytes()).unwrap(); - assert_eq!(input, hex::decode("0a030090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe220f0000c16ff28623").unwrap()); + assert_eq!(input, const_hex::decode("0a030090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe220f0000c16ff28623").unwrap()); } } diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index b0b275c4..6fa2f487 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -16,9 +16,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::traits::AccountIdProvider; use core::marker::PhantomData; use frame_support::traits::tokens::{fungible, Fortitude, Preservation}; -use np_babel::{CosmosAddress, EthereumAddress, VarAddress}; +use np_babel::VarAddress; use pallet_multimap::traits::UniqueMultimap; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -28,12 +29,14 @@ use sp_runtime::{ transaction_validity::{TransactionValidityError, ValidTransaction}, }; +type AccountIdOf = ::AccountId; + /// A configuration for UnifyAccount signed extension. -pub trait Config: frame_system::Config + TryInto> { +pub trait Config: AccountIdProvider + TryInto> { /// A map from account to addresses. - type AddressMap: UniqueMultimap; + type AddressMap: UniqueMultimap, VarAddress>; /// Drain account balance when unifying accounts. - type DrainBalance: DrainBalance; + type DrainBalance: DrainBalance>; } /// Unifies the accounts associated with the same public key. @@ -56,11 +59,11 @@ impl UnifyAccount { } impl UnifyAccount { - pub fn unify_ecdsa(who: &T::AccountId) -> Result<(), &'static str> { + pub fn unify_ecdsa(who: &AccountIdOf) -> Result<(), &'static str> { if let Ok(public) = who.clone().try_into() { #[cfg(feature = "ethereum")] { - let address = EthereumAddress::from(public); + let address = np_babel::EthereumAddress::from(public); let interim = address.clone().into_account_truncating(); T::DrainBalance::drain_balance(&interim, who)?; T::AddressMap::try_insert(who, VarAddress::Ethereum(address)) @@ -68,7 +71,7 @@ impl UnifyAccount { } #[cfg(feature = "cosmos")] { - let address = CosmosAddress::from(public); + let address = np_babel::CosmosAddress::from(public); let interim = address.clone().into_account_truncating(); T::DrainBalance::drain_balance(&interim, who)?; T::AddressMap::try_insert(who, VarAddress::Cosmos(address)) @@ -105,8 +108,11 @@ impl core::fmt::Debug for UnifyAccount { } } -impl SignedExtension for UnifyAccount { - type AccountId = T::AccountId; +impl SignedExtension for UnifyAccount +where + T: Config + frame_system::Config>, +{ + type AccountId = AccountIdOf; type Call = T::RuntimeCall; type AdditionalSigned = (); type Pre = (); @@ -167,3 +173,40 @@ where .map(|_| amount) } } + +#[cfg(test)] +mod tests { + use super::*; + use np_runtime::{AccountId32, MultiSigner}; + + type AccountId = AccountId32; + + struct MockConfig; + + impl Config for MockConfig { + type AddressMap = pallet_multimap::traits::in_mem::UniqueMultimap; + type DrainBalance = (); + } + + impl AccountIdProvider for MockConfig { + type AccountId = AccountId; + } + + fn dev_public() -> ecdsa::Public { + const_hex::decode_to_array( + b"02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f", + ) + .unwrap() + .into() + } + + #[test] + fn unify_ecdsa_works() { + let who = AccountId::from(dev_public()); + let _ = UnifyAccount::::unify_ecdsa(&who); + let cosmos = VarAddress::Cosmos(dev_public().into()); + assert_eq!(::AddressMap::find_key(cosmos), Some(who.clone())); + let ethereum = VarAddress::Ethereum(dev_public().into()); + assert_eq!(::AddressMap::find_key(ethereum), Some(who)); + } +} diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index bc3938e9..8f65fb9e 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -21,13 +21,15 @@ extern crate alloc; +#[cfg(test)] +mod mock; + #[cfg(feature = "cosmos")] pub mod cosmos; #[cfg(feature = "ethereum")] pub mod ethereum; pub mod extensions; -#[cfg(test)] -mod mock; +pub mod traits; pub use extensions::unify_account::UnifyAccount; pub use np_babel::VarAddress; diff --git a/frame/babel/src/traits.rs b/frame/babel/src/traits.rs new file mode 100644 index 00000000..d596a4f1 --- /dev/null +++ b/frame/babel/src/traits.rs @@ -0,0 +1,39 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use core::fmt::Debug; +use frame_support::{ + sp_runtime::traits::{MaybeDisplay, MaybeSerializeDeserialize, Member}, + Parameter, +}; +use parity_scale_codec::MaxEncodedLen; + +/// Trait for providing the account id type. +pub trait AccountIdProvider { + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; +} + +impl AccountIdProvider for T { + type AccountId = T::AccountId; +} diff --git a/frame/multimap/src/traits.rs b/frame/multimap/src/traits.rs index 88719dfa..60bce77f 100644 --- a/frame/multimap/src/traits.rs +++ b/frame/multimap/src/traits.rs @@ -17,6 +17,9 @@ use crate::*; +#[cfg(feature = "std")] +pub mod in_mem; + use alloc::collections::BTreeSet; use frame_support::ensure; use parity_scale_codec::{Codec, EncodeLike, FullCodec}; diff --git a/frame/multimap/src/traits/in_mem.rs b/frame/multimap/src/traits/in_mem.rs new file mode 100644 index 00000000..1e4f9e12 --- /dev/null +++ b/frame/multimap/src/traits/in_mem.rs @@ -0,0 +1,165 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{EncodeLike, FullCodec}; +use std::{ + cell::RefCell, + collections::{BTreeMap, BTreeSet}, + marker::PhantomData, +}; + +thread_local! { + static UNIQUE_MULTIMAP: RefCell, BTreeSet>>> = const { RefCell::new(BTreeMap::new()) }; + static UNIQUE_MULTIMAP_INDEX: RefCell, Vec>> = const { RefCell::new(BTreeMap::new()) }; +} + +pub struct UniqueMultimap(PhantomData<(K, V)>); + +impl super::UniqueMultimap for UniqueMultimap { + type Error = &'static str; + + fn try_insert, ValArg: EncodeLike>( + key: KeyArg, + value: ValArg, + ) -> Result { + let key = key.encode(); + let value = value.encode(); + match UNIQUE_MULTIMAP_INDEX.with(|index| { + let mut index = index.borrow_mut(); + if let Some(existing_key) = index.get(&value) { + if existing_key != &key { + Err("Duplicate value") + } else { + Ok(false) + } + } else { + index.insert(value.clone(), key.clone()); + Ok(true) + } + }) { + Ok(true) => { + UNIQUE_MULTIMAP.with(|map| { + let mut map = map.borrow_mut(); + map.entry(key).or_default().insert(value); + }); + Ok(true) + }, + Ok(false) => Ok(false), + Err(e) => Err(e), + } + } + + fn get>(key: KeyArg) -> BTreeSet { + let key = key.encode(); + UNIQUE_MULTIMAP.with(|map| { + map.borrow() + .get(&key) + .map(|values| { + values + .iter() + .map(|value| V::decode(&mut &value[..]).expect("Decoding failed")) + .collect() + }) + .unwrap_or_default() + }) + } + + fn find_key>(value: ValArg) -> Option { + let value = value.encode(); + UNIQUE_MULTIMAP_INDEX.with(|index| { + index + .borrow() + .get(&value) + .map(|key| K::decode(&mut &key[..]).expect("Decoding failed")) + }) + } + + fn remove, ValArg: EncodeLike>(key: KeyArg, value: ValArg) -> bool { + let key = key.encode(); + let value = value.encode(); + if UNIQUE_MULTIMAP_INDEX.with(|index| { + let mut index = index.borrow_mut(); + index.remove(&value).is_some() + }) { + UNIQUE_MULTIMAP.with(|map| { + let mut map = map.borrow_mut(); + if let Some(values) = map.get_mut(&key) { + values.remove(&value); + } + }); + true + } else { + false + } + } + + fn remove_all>(key: KeyArg) -> bool { + let key = key.encode(); + if let Some(values) = UNIQUE_MULTIMAP.with(|map| map.borrow_mut().remove(&key)) { + UNIQUE_MULTIMAP_INDEX.with(|index| { + let mut index = index.borrow_mut(); + for value in values { + index.remove(&value); + } + }); + true + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use crate::traits::{in_mem::UniqueMultimap, UniqueMultimap as _}; + use std::collections::BTreeSet; + + type Multimap = UniqueMultimap; + + #[test] + fn unique_multimap_insert() { + assert_eq!(Multimap::try_insert("alice".to_string(), 1), Ok(true)); + assert_eq!(Multimap::try_insert("alice".to_string(), 1), Ok(false)); + assert_eq!(Multimap::try_insert("bob".to_string(), 1), Err("Duplicate value")); + assert_eq!(Multimap::try_insert("alice".to_string(), 2), Ok(true)); + assert_eq!(Multimap::try_insert("bob".to_string(), 3), Ok(true)); + assert_eq!(Multimap::try_insert("bob".to_string(), 1), Err("Duplicate value")); + } + + #[test] + fn unique_multimap_get() { + let _ = Multimap::try_insert("alice".to_string(), 1); + let _ = Multimap::try_insert("alice".to_string(), 2); + let _ = Multimap::try_insert("bob".to_string(), 3); + assert_eq!(Multimap::get("alice"), BTreeSet::from([1, 2])); + assert_eq!(Multimap::find_key(1), Some("alice".to_string())); + assert_eq!(Multimap::find_key(3), Some("bob".to_string())); + } + + #[test] + fn unique_multimap_remove() { + let _ = Multimap::try_insert("alice".to_string(), 1); + let _ = Multimap::try_insert("alice".to_string(), 2); + let _ = Multimap::try_insert("alice".to_string(), 3); + assert!(Multimap::remove("alice", 2)); + assert_eq!(Multimap::get("alice"), BTreeSet::from([1, 3])); + assert!(!Multimap::remove("alice", 2)); + assert!(Multimap::remove_all("alice")); + assert!(Multimap::get("alice").is_empty()); + assert!(!Multimap::remove_all("alice")); + } +}