diff --git a/Cargo.lock b/Cargo.lock index f7fbb6a6aa5792381f4df4cc842aeac7eb13d9b1..254ee0a24bbc32b55572ef554a979c4c39b2a5a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1454,6 +1454,7 @@ dependencies = [ "clap", "clap_complete", "common-runtime", + "duniter-storage-node", "frame-benchmarking", "frame-benchmarking-cli", "futures 0.3.19", @@ -1542,6 +1543,39 @@ dependencies = [ "sp-std", ] +[[package]] +name = "duniter-storage-node" +version = "3.0.0" +dependencies = [ + "duniter-storage-primitives", + "hex-literal", + "memory-db", + "parity-scale-codec", + "sc-client-api", + "sp-core", + "sp-runtime", + "sp-state-machine", + "sp-trie", + "trie-db", +] + +[[package]] +name = "duniter-storage-primitives" +version = "3.0.0" +dependencies = [ + "async-trait", + "frame-support", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-inherents", + "sp-io", + "sp-runtime", + "sp-std", + "sp-trie", +] + [[package]] name = "dyn-clonable" version = "0.9.0" @@ -4945,6 +4979,30 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-duniter-storage" +version = "3.0.0" +dependencies = [ + "duniter-storage-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "maplit", + "memory-db", + "pallet-balances", + "pallet-provide-randomness", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-state-machine", + "sp-std", + "sp-trie", + "trie-db", +] + [[package]] name = "pallet-duniter-test-parameters" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index e826d2d80ed88674a13b1742c87d25018cb92445..f08dab0524658a6c03d0b4d1cdf9254b5da398da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ sp-trie = { git = "https://github.com/librelois/substrate.git", branch = "dunite [dependencies] # local dependencies common-runtime = { path = 'runtime/common' } +duniter-storage-node = { path = 'node/duniter-storage' } g1-runtime = { path = 'runtime/g1', optional = true } gdev-runtime = { path = 'runtime/gdev', optional = true } gtest-runtime = { path = 'runtime/gtest', optional = true } @@ -124,15 +125,18 @@ resolver = "2" members = [ 'end2end-tests', 'pallets/certification', + 'pallets/duniter-storage', 'pallets/duniter-test-parameters', 'pallets/duniter-test-parameters/macro', 'pallets/duniter-wot', 'pallets/identity', - 'pallets/membership', - 'pallets/authority-members', + 'pallets/membership', + 'pallets/authority-members', + 'pallets/provide-randomness', 'pallets/ud-accounts-storage', 'pallets/universal-dividend', - 'pallets/upgrade-origin', + 'pallets/upgrade-origin', + 'primitives/duniter-storage', 'primitives/membership', 'runtime/common', 'runtime/gdev', diff --git a/node/duniter-storage/Cargo.toml b/node/duniter-storage/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..0d8c2968039e4a318a9c64b1a3452fe849bd6a9f --- /dev/null +++ b/node/duniter-storage/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = ["librelois <c@elo.tf>"] +description = "duniter-storage client part" +edition = "2018" +homepage = "https://substrate.dev" +license = "AGPL-3.0" +name = "duniter-storage-node" +repository = "https://git.duniter.org/nodes/rust/duniter-v2s" +version = "3.0.0" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# local +duniter-storage-primitives = { path = "../../primitives/duniter-storage" } + +# crates.io +codec = { package = 'parity-scale-codec', version = "2.3.1", features = ["derive"] } +hex-literal = "0.3" +memory-db = { version = "0.27.0" } +trie-db = { version = "0.22.6" } + +# substrate +sc-client-api = { git = "https://github.com/librelois/substrate.git", branch = "duniter-monthly-2022-02" } +sp-core = { git = "https://github.com/librelois/substrate.git", branch = "duniter-monthly-2022-02" } +sp-runtime = { git = "https://github.com/librelois/substrate.git", branch = "duniter-monthly-2022-02" } +sp-state-machine = { git = "https://github.com/librelois/substrate.git", branch = "duniter-monthly-2022-02" } +sp-trie = { git = "https://github.com/librelois/substrate.git", branch = "duniter-monthly-2022-02" } diff --git a/node/duniter-storage/src/lib.rs b/node/duniter-storage/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..df5e71acbc94640652619d59593bd92448b959a9 --- /dev/null +++ b/node/duniter-storage/src/lib.rs @@ -0,0 +1,103 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use codec::Decode; +use sc_client_api::backend::StorageProvider; +use sc_client_api::{Backend as BackendT, StateBackend, StorageKey}; +use sp_runtime::{ + generic::BlockId, + traits::{BlakeTwo256, Block as BlockT}, +}; + +/// Prefix of Storage value DuniterStorage.Delta +// twox128("DuniterStorage") ++ twox128("TrieDelta") +const DUNITER_STORAGE_DELTA_STORAGE_KEY: [u8; 32] = + hex_literal::hex!("54893a1f81bcfacb273bd40750b36f5291ff17e39390d8c052a516c276ec74d6"); + +type MemoryDB = memory_db::MemoryDB< + BlakeTwo256, + memory_db::HashKey<BlakeTwo256>, + trie_db::DBValue, + memory_db::NoopTracker<trie_db::DBValue>, +>; + +/// Create a new [`duniter-storage-primitives::InherentDataProvider`] at the given block. +pub fn duniter_storage_inherent_data_provider<Backend, Block, Client>( + client: &Client, + parent: Block::Hash, +) -> Result<duniter_storage_primitives::InherentDataProvider, sc_client_api::blockchain::Error> +where + Backend: BackendT<Block>, + Block: BlockT, + Client: StorageProvider<Block, Backend>, +{ + let maybe_storage_data = client.storage( + &BlockId::Hash(parent), + &StorageKey(DUNITER_STORAGE_DELTA_STORAGE_KEY.to_vec()), + )?; + + let maybe_delta = if let Some(storage_data) = maybe_storage_data { + Some( + duniter_storage_primitives::Delta::decode(&mut &storage_data.0[..]).map_err(|e| { + sc_client_api::blockchain::Error::Backend(format!( + "duniter_storage_inherent_data_provider: fail to decode Delta from storage: {}", + e + )) + })?, + ) + } else { + None + }; + + let maybe_proof = if let Some(delta) = maybe_delta { + let (db, root) = MemoryDB::default_with_root(); + let trie_backend = sp_state_machine::TrieBackend::new(db, root); + Some(generate_trie_proof( + &trie_backend, + delta.iter().map(|entry| &entry.0), + )?) + } else { + None + }; + + Ok(duniter_storage_primitives::InherentDataProvider::new( + maybe_proof, + )) +} + +fn generate_trie_proof<K, S>( + trie_backend: &sp_state_machine::TrieBackend<S, BlakeTwo256>, + keys: impl Iterator<Item = K>, +) -> Result<sp_trie::CompactProof, sc_client_api::blockchain::Error> +where + K: AsRef<[u8]>, + S: sp_state_machine::TrieBackendStorage<BlakeTwo256>, +{ + let proving_backend = sp_state_machine::ProvingBackend::new(trie_backend); + for key in keys { + let _ = proving_backend.storage(key.as_ref()); + } + let trie_proof = proving_backend + .extract_proof() + .into_compact_proof::<BlakeTwo256>(*trie_backend.root()) + .map_err(|e| { + sc_client_api::blockchain::Error::Backend(format!( + "duniter_storage_inherent_data_provider: fail to generate compact proof: {}", + e + )) + })?; + Ok(trie_proof) +} diff --git a/node/src/service.rs b/node/src/service.rs index d3d68763b2c1cd8ecea7bfcfb21b285eb3c9eb59..85dca57f5c1db122a50e9e47e08ed71b7a475a65 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -404,6 +404,9 @@ where Vec::default(), )); + // TODO push duniter-storage network protocol here + // config.network.extra_sets.push(duniter_storage_peers_set_config); + let (network, system_rpc_tx, network_starter) = sc_service::build_network(sc_service::BuildNetworkParams { config: &config, @@ -503,18 +506,23 @@ where commands_stream, select_chain, consensus_data_provider: Some(Box::new(babe_consensus_data_provider)), - create_inherent_data_providers: move |_, _| { - let client = client_clone.clone(); + create_inherent_data_providers: move |parent, _| { + let client_clone = client_clone.clone(); async move { let timestamp = manual_seal::consensus::timestamp::SlotTimestampProvider::new_babe( - client.clone(), + client_clone.clone(), ) .map_err(|err| format!("{:?}", err))?; let babe = sp_consensus_babe::inherents::InherentDataProvider::new( timestamp.slot().into(), ); - Ok((timestamp, babe)) + let duniter_storage = + duniter_storage_node::duniter_storage_inherent_data_provider( + &*client_clone, + parent, + )?; + Ok((timestamp, babe, duniter_storage)) } }, }), @@ -550,7 +558,13 @@ where slot_duration, ); - Ok((timestamp, slot, uncles)) + let duniter_storage = + duniter_storage_node::duniter_storage_inherent_data_provider( + &*client_clone, + parent, + )?; + + Ok((timestamp, slot, uncles, duniter_storage)) } }, force_authoring, diff --git a/pallets/duniter-storage/Cargo.toml b/pallets/duniter-storage/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..3649f6e9e9e7f461cb660c5b52f5a8a5e9081e06 --- /dev/null +++ b/pallets/duniter-storage/Cargo.toml @@ -0,0 +1,110 @@ +[package] +authors = ['librelois <c@elo.tf>'] +description = 'FRAME pallet duniter storage.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'pallet-duniter-storage' +readme = 'README.md' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[features] +default = ['std'] +runtime-benchmarks = ['frame-benchmarking'] +std = [ + 'codec/std', + 'duniter-storage-primitives/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'pallet-provide-randomness/std', + 'serde', + 'sp-core/std', + 'sp-io/std', + 'sp-runtime/std', + 'sp-std/std', + 'sp-trie/std' +] +try-runtime = ['frame-support/try-runtime'] + +[dependencies] +# local +duniter-storage-primitives = { path = "../../primitives/duniter-storage", default-features = false } +pallet-provide-randomness = { path = "../provide-randomness", default-features = false } + +# crates.io +codec = { package = 'parity-scale-codec', version = "2.3.1", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + +# substrate +[dependencies.frame-benchmarking] +default-features = false +git = 'https://github.com/librelois/substrate.git' +optional = true +branch = 'duniter-monthly-2022-02' + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.serde] +version = "1.0.101" +optional = true +features = ["derive"] + +[dependencies.sp-core] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-io] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-trie] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +### DOC ### + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +### DEV ### + +[dev-dependencies] +maplit = "1.0.2" +memory-db = { version = "0.27.0" } +serde = "1.0.119" +trie-db = { version = "0.22.6" } + +[dev-dependencies.pallet-balances] +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dev-dependencies.sp-io] +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dev-dependencies.sp-state-machine] +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' diff --git a/pallets/duniter-storage/src/lib.rs b/pallets/duniter-storage/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ab5f524e41bad572307a3301944a190f53ddda9 --- /dev/null +++ b/pallets/duniter-storage/src/lib.rs @@ -0,0 +1,646 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::type_complexity)] + +//pub mod traits; +mod types; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/*#[cfg(feature = "runtime-benchmarks")] +mod benchmarking;*/ + +pub use pallet::*; +pub use types::*; + +//use crate::traits::*; +use pallet_provide_randomness::RequestId; +use sp_io::hashing::blake2_256; +use sp_std::prelude::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use duniter_storage_primitives::*; + use frame_support::pallet_prelude::*; + use frame_support::traits::{Currency, NamedReservableCurrency, Randomness, StorageVersion}; + use frame_system::pallet_prelude::*; + use sp_core::H256; + use sp_runtime::traits::{BlakeTwo256, Convert, IdentifyAccount, One, Verify}; + + pub type BalanceOf<T> = + <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; + pub type UserProofOf<T> = UserProof<<T as Config>::UserSigner, <T as Config>::UserSignature>; + + const EMPTY_ROOT: H256 = H256([ + 3, 23, 10, 46, 117, 151, 183, 183, 227, 216, 76, 5, 57, 29, 19, 154, 98, 177, 87, 231, 135, + 134, 216, 192, 130, 242, 157, 207, 76, 17, 19, 20, + ]); + + const RESERVE_ID: [u8; 8] = [b'd', b'u', b'n', b'i', b's', b't', b'o', b'r']; + + /// 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<T>(_); + + // CONFIG // + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_provide_randomness::Config { + /// Origin authorized to modify the replication factor + type ChangeReplicationFactorOrigin: EnsureOrigin<Self::Origin>; + /// The currency mechanism. + type Currency: NamedReservableCurrency<Self::AccountId, ReserveIdentifier = [u8; 8]>; + #[pallet::constant] + type UserDeposit: Get<BalanceOf<Self>>; + #[pallet::constant] + type EntryExpiration: Get<Self::BlockNumber>; + /// The overarching event type. + type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>; + /// Minimum duration of participation for a provider + type MinProviderDuration: Get<Self::BlockNumber>; + #[pallet::constant] + type MinKeyLen: Get<u32>; + #[pallet::constant] + type ProviderRegistrationDeposit: Get<BalanceOf<Self>>; + /// Something that identifies a provider + type ProviderId: Copy + Default + MaybeSerializeDeserialize + Parameter + Ord; + /// Something that returns the ProviderId of an account, if the account does not have the + /// right to be Provider, the implementation must return none + type ProviderIdOf: Convert<Self::AccountId, Option<Self::ProviderId>>; + /// A safe source of randomness + type Randomness: Randomness<H256, Self::BlockNumber>; + /// Signing key of user payload + type UserSigner: Parameter + IdentifyAccount<AccountId = Self::AccountId>; + /// Signature of user payload + type UserSignature: Parameter + Verify<Signer = Self::UserSigner>; + } + + // STORAGE // + + #[pallet::storage] + #[pallet::getter(fn trie_delta)] + pub type TrieDelta<T: Config> = StorageValue<_, duniter_storage_primitives::Delta, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn trie_root)] + pub type TrieRoot<T: Config> = StorageValue<_, H256, ValueQuery>; + + #[pallet::storage] + pub type TrieRootUpdated<T: Config> = StorageValue<_, bool, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn pending_providers)] + pub type PendingProviders<T: Config> = + StorageMap<_, Twox64Concat, RequestId, (T::ProviderId, ProviderEndpoint), OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn providers)] + pub type Providers<T: Config> = CountedStorageMap< + _, + Twox64Concat, + T::ProviderId, + ProviderInfo<T::BlockNumber>, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn provider_by_pos)] + pub type ProviderByPos<T: Config> = StorageMap<_, Identity, H256, T::ProviderId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn provider_by_reversed_pos)] + pub type ProviderByReversedPos<T: Config> = + StorageMap<_, Identity, H256, T::ProviderId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn replication_factor_half)] + pub type ReplicationFactorHalf<T: Config> = StorageValue<_, u32, ValueQuery>; + + // GENESIS STUFF // + + #[pallet::genesis_config] + pub struct GenesisConfig<T: Config> { + pub providers: Vec<(T::ProviderId, ProviderEndpoint)>, + pub replication_factor: u32, + } + + #[cfg(feature = "std")] + impl<T: Config> Default for GenesisConfig<T> { + fn default() -> Self { + Self { + providers: Vec::new(), + replication_factor: 0, + } + } + } + + #[pallet::genesis_build] + impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { + fn build(&self) { + assert!(self.replication_factor >= 2); + assert!(self.replication_factor % 2 == 0); + assert!(self.providers.len() >= self.replication_factor as usize); + + ReplicationFactorHalf::<T>::put(self.replication_factor / 2); + for (provider_id, endpoint) in &self.providers { + Providers::<T>::insert( + provider_id, + ProviderInfo { + endpoint: endpoint.clone(), + pos: H256(blake2_256(&(provider_id, endpoint).encode())), + unregisterable_on: T::MinProviderDuration::get(), + }, + ); + } + TrieRoot::<T>::put(EMPTY_ROOT); + } + } + + // EVENTS // + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + /// An entry has been inserted or updated + /// [owner, key, deposit, value_or_hash] + EntryUpserted { + owner: T::AccountId, + key: Key, + value_hash: H256, + }, + /// A user has updated his deposit for an entry + /// [owner, key, deposit, value_or_hash] + EntryDeleted { owner: T::AccountId, key: Key }, + /// A user pruned an entry that had expired + /// [who, entry_owner, key] + EntryPruned { + who: T::AccountId, + owner: T::AccountId, + key: Key, + }, + /// A new provider has requested registration + /// [id, endpoint] + NewProviderPending { + id: T::ProviderId, + endpoint: ProviderEndpoint, + }, + /// A new provider has been registered + /// [provider_id, provider_position] + ProviderRegistered { id: T::ProviderId, pos: H256 }, + /// A provider has been unregistered + /// [provider_id, provider_position] + ProviderUnregistered { id: T::ProviderId, pos: H256 }, + } + + // ERRORS // + + #[pallet::error] + pub enum Error<T> { + /// Entry already inserted + AlreadyInserted, + /// Provider already registered + AlreadyRegistered, + /// Fail to compute new trie root + FailToComputeNewTrieRoot, + /// Key too short + KeyTooShort, + /// Invalid compact proof + InvalidCompactProof, + /// Invalid trie root + InvalidTrieRoot, + /// Invalid user signature + InvalidUserSig, + /// Not enough providers + NotEnoughProviders, + /// Entry not exist + NotExist, + /// The entry has not yet expired + NotExpiredYet, + /// This account has no right to be a provider + NoProviderId, + /// Provider not registered + NotRegistered, + /// Provider not unregisterable + NotUnregisterable, + /// Uneven replication factor + UnevenReplicationFactor, + /// Unsufficient deposit + UnsufficientDeposit, + } + + // HOOKS // + + #[pallet::hooks] + impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { + fn on_initialize(_n: T::BlockNumber) -> Weight { + if TrieDelta::<T>::get().is_empty() { + TrieRootUpdated::<T>::put(true); + } else { + TrieRootUpdated::<T>::put(false); + } + // on_finalize is accounting here + T::DbWeight::get().reads_writes(2, 1) + } + fn on_finalize(_n: T::BlockNumber) { + if !TrieRootUpdated::<T>::get() { + panic!("invalid block: trie root was not updated!") + } + } + } + + // CALLS // + + #[pallet::call] + impl<T: Config> Pallet<T> { + #[pallet::weight(100_000_000)] + pub fn change_replication_factor( + origin: OriginFor<T>, + new_replication_factor: u32, + ) -> DispatchResultWithPostInfo { + // Verification phase // + T::ChangeReplicationFactorOrigin::ensure_origin(origin)?; + ensure!( + new_replication_factor % 2 == 0, + Error::<T>::UnevenReplicationFactor + ); + ensure!( + Providers::<T>::count() >= new_replication_factor, + Error::<T>::NotEnoughProviders + ); + + ReplicationFactorHalf::<T>::put(new_replication_factor / 2); + + Ok(().into()) + } + #[pallet::weight(100_000_000)] + pub fn provider_register( + origin: OriginFor<T>, + endpoint: ProviderEndpoint, + ) -> DispatchResultWithPostInfo { + // Verification phase // + let who = ensure_signed(origin)?; + + let provider_id = + T::ProviderIdOf::convert(who.clone()).ok_or(Error::<T>::NoProviderId)?; + ensure!( + !Providers::<T>::contains_key(&provider_id), + Error::<T>::AlreadyRegistered + ); + + // Apply phase // + let deposit = T::ProviderRegistrationDeposit::get(); + <T as pallet::Config>::Currency::reserve_named(&RESERVE_ID, &who, deposit)?; + let salt = H256(blake2_256(&(RESERVE_ID, provider_id).encode())); + let request_id = pallet_provide_randomness::Pallet::<T>::do_request( + &who, + pallet_provide_randomness::RandomnessType::RandomnessFromTwoEpochsAgo, + salt, + )?; + PendingProviders::<T>::insert(request_id, (provider_id, endpoint.clone())); + Self::deposit_event(Event::NewProviderPending { + id: provider_id, + endpoint, + }); + + Ok(().into()) + } + #[pallet::weight(100_000_000)] + pub fn provider_update_deposit( + origin: OriginFor<T>, + target: T::AccountId, + ) -> DispatchResultWithPostInfo { + // Verification phase // + let who = ensure_signed(origin)?; + + let provider_id = T::ProviderIdOf::convert(who).ok_or(Error::<T>::NoProviderId)?; + ensure!( + Providers::<T>::contains_key(&provider_id), + Error::<T>::NotRegistered + ); + + let provider_id = + T::ProviderIdOf::convert(target.clone()).ok_or(Error::<T>::NoProviderId)?; + let provider_info = + Providers::<T>::get(&provider_id).ok_or(Error::<T>::NotRegistered)?; + + // Apply phase // + let old_deposit = + <T as pallet::Config>::Currency::reserved_balance_named(&RESERVE_ID, &target); + let new_deposit = T::ProviderRegistrationDeposit::get(); + #[allow(clippy::comparison_chain)] + if new_deposit > old_deposit { + if <T as pallet::Config>::Currency::reserve_named( + &RESERVE_ID, + &target, + new_deposit - old_deposit, + ) + .is_err() + { + // Remove provider + Self::do_remove_provider( + provider_id, + &target, + Some(old_deposit), + provider_info.pos, + ); + } + } else if new_deposit < old_deposit { + <T as pallet::Config>::Currency::unreserve_named( + &RESERVE_ID, + &target, + old_deposit - new_deposit, + ); + } + + Ok(().into()) + } + #[pallet::weight(100_000_000)] + pub fn provider_update_info( + origin: OriginFor<T>, + endpoint: ProviderEndpoint, + ) -> DispatchResultWithPostInfo { + // Verification phase // + let who = ensure_signed(origin)?; + + let provider_id = + T::ProviderIdOf::convert(who.clone()).ok_or(Error::<T>::NoProviderId)?; + let mut provider_info = + Providers::<T>::get(&provider_id).ok_or(Error::<T>::NotRegistered)?; + + // Apply phase // + let new_deposit = T::ProviderRegistrationDeposit::get(); + let old_deposit = + <T as pallet::Config>::Currency::reserved_balance_named(&RESERVE_ID, &who); + provider_info.endpoint = endpoint; + + if new_deposit > old_deposit + && <T as pallet::Config>::Currency::reserve_named( + &RESERVE_ID, + &who, + new_deposit - old_deposit, + ) + .is_err() + { + Self::do_remove_provider(provider_id, &who, Some(old_deposit), provider_info.pos); + } else { + // Write new provider info + Providers::<T>::insert(provider_id, provider_info); + } + + Ok(().into()) + } + #[pallet::weight(100_000_000)] + pub fn provider_unregister(origin: OriginFor<T>) -> DispatchResultWithPostInfo { + // Verification phase // + let who = ensure_signed(origin)?; + + let provider_id = + T::ProviderIdOf::convert(who.clone()).ok_or(Error::<T>::NoProviderId)?; + let provider_info = + Providers::<T>::get(&provider_id).ok_or(Error::<T>::NotRegistered)?; + let block_number = frame_system::Pallet::<T>::block_number(); + ensure!( + block_number >= provider_info.unregisterable_on, + Error::<T>::NotUnregisterable + ); + ensure!( + Providers::<T>::count() > ReplicationFactorHalf::<T>::get() * 2, + Error::<T>::NotEnoughProviders + ); + + // Apply phase // + Self::do_remove_provider(provider_id, &who, None, provider_info.pos); + + Ok(().into()) + } + + #[pallet::weight(10_000_000_000)] + pub fn provider_declare_changes( + origin: OriginFor<T>, + users_proof: Vec<UserProofOf<T>>, + ) -> DispatchResult { + // Verification phase // + let who = ensure_signed(origin)?; + + let provider_id = T::ProviderIdOf::convert(who).ok_or(Error::<T>::NoProviderId)?; + ensure!( + Providers::<T>::get(&provider_id).is_some(), + Error::<T>::NotRegistered + ); + + // TODO check each user proof + for user_proof in &users_proof { + ensure!(user_proof.verify_signature(), Error::<T>::InvalidUserSig,); + // The use of `ensure_reserved_named` allows to update the deposit amount in case a + // deposit has already been made. + <T as pallet::Config>::Currency::ensure_reserved_named( + &RESERVE_ID, + &user_proof.user_account(), + T::UserDeposit::get(), + )?; + } + + // Apply phase // + let block_number = frame_system::Pallet::<T>::block_number(); + let delta: duniter_storage_primitives::Delta = users_proof + .iter() + .map(|user_proof| user_proof.to_delta(block_number + One::one())) + .collect(); + TrieDelta::<T>::put(delta); + + for user_proof in &users_proof { + Self::do_set_entry( + user_proof.user_account(), + user_proof.key().clone(), + user_proof.maybe_value_hash(), + ) + } + + Ok(()) + } + + // TODO add call prune to apply entries expiration + + // TODO add calls for moderators + + #[pallet::weight(( + 1_000_000_000, + DispatchClass::Mandatory + ))] + pub fn author_update_root( + origin: OriginFor<T>, + compact_proof: sp_trie::CompactProof, + ) -> DispatchResult { + // Verification phase // + ensure_none(origin)?; + + let (storage_proof, root) = compact_proof + .to_storage_proof::<BlakeTwo256>(None) + .map_err(|_| Error::<T>::InvalidCompactProof)?; + + ensure!(root == TrieRoot::<T>::get(), Error::<T>::InvalidTrieRoot); + + let mut trie = storage_proof.into_memory_db::<BlakeTwo256>(); + let new_root = sp_trie::delta_trie_root::<sp_trie::Layout<BlakeTwo256>, _, _, _, _, _>( + &mut trie, + root, + TrieDelta::<T>::get() + .iter() + .map(|(key, maybe_value)| (key.as_ref(), maybe_value.map(|value| value.0))), + ) + .map_err(|_| Error::<T>::FailToComputeNewTrieRoot)?; + + // Apply phase // + TrieRoot::<T>::put(new_root); + TrieRootUpdated::<T>::put(true); + + Ok(()) + } + } + + #[pallet::inherent] + impl<T: Config> ProvideInherent for Pallet<T> { + type Call = Call<T>; + type Error = InherentError; + const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; + + fn create_inherent(data: &InherentData) -> Option<Self::Call> { + let maybe_inherent_data = data + .get_data::<InherentType>(&INHERENT_IDENTIFIER) + .expect("DuniterStorage inherent data not correctly encoded"); + + maybe_inherent_data.map(|inherent_data| Call::author_update_root { + compact_proof: inherent_data, + }) + } + + fn is_inherent(call: &Self::Call) -> bool { + matches!(call, Call::author_update_root { .. }) + } + } + + // PUBLIC FUNCTIONS // + + impl<T: Config> Pallet<T> {} + + // INTERNAL FUNCTIONS // + + impl<T: Config> Pallet<T> { + fn do_set_entry(owner: T::AccountId, key: Key, maybe_value_hash: Option<H256>) { + if let Some(value_hash) = maybe_value_hash { + Self::deposit_event(Event::EntryUpserted { + owner, + key, + value_hash, + }); + } else { + Self::deposit_event(Event::EntryDeleted { owner, key }); + } + } + fn do_remove_provider( + id: T::ProviderId, + owner_key: &T::AccountId, + maybe_deposit: Option<BalanceOf<T>>, + pos: H256, + ) { + <T as pallet::Config>::Currency::unreserve_named( + &RESERVE_ID, + owner_key, + maybe_deposit.unwrap_or_else(|| { + <T as pallet::Config>::Currency::reserved_balance_named(&RESERVE_ID, owner_key) + }), + ); + ProviderByPos::<T>::remove(pos); + ProviderByReversedPos::<T>::remove(reverse_hash(pos)); + Providers::<T>::remove(id); + Self::deposit_event(Event::ProviderUnregistered { id, pos }); + } + + /*fn provider_neighbors(provider_pos: H256) -> Vec<T::ProviderId> { + let replication_factor_half = ReplicationFactorHalf::<T>::get(); + let mut neighbors = Vec::with_capacity(replication_factor_half as usize * 2); + neighbors.extend( + ProviderByPos::<T>::iter_from(provider_pos.0.to_vec()) + .chain(ProviderByPos::<T>::iter()) + .take(replication_factor_half as usize) + .map(|(_k, v)| v), + ); + neighbors.extend( + ProviderByReversedPos::<T>::iter_from(provider_pos.0.to_vec()) + .chain(ProviderByReversedPos::<T>::iter()) + .take(replication_factor_half as usize) + .map(|(_k, v)| v), + ); + neighbors.sort_unstable(); + neighbors + } + fn provider_range(provider_pos: H256) -> HashsRange { + let replication_factor_half = ReplicationFactorHalf::<T>::get(); + ( + ProviderByPos::<T>::iter_keys_from(provider_pos.0.to_vec()) + .chain(ProviderByPos::<T>::iter_keys()) + .take(replication_factor_half as usize) + .last() + .unwrap_or(H256([0; 32])), + ProviderByReversedPos::<T>::iter_keys_from(provider_pos.0.to_vec()) + .chain(ProviderByReversedPos::<T>::iter_keys()) + .take(replication_factor_half as usize) + .last() + .unwrap_or(H256([255; 32])), + ) + }*/ + } + + impl<T: Config> pallet_provide_randomness::OnFilledRandomness for Pallet<T> { + fn on_filled_randomness(request_id: RequestId, randomness: H256) -> Weight { + if let Some((provider_id, provider_endpoint)) = PendingProviders::<T>::take(&request_id) + { + let provider_pos = randomness; + let block_number = frame_system::Pallet::<T>::block_number(); + let unregisterable_on = block_number + T::MinProviderDuration::get(); + Providers::<T>::insert( + provider_id, + ProviderInfo { + endpoint: provider_endpoint, + pos: provider_pos, + unregisterable_on, + }, + ); + ProviderByPos::<T>::insert(provider_pos, provider_id); + ProviderByReversedPos::<T>::insert(reverse_hash(provider_pos), provider_id); + Self::deposit_event(Event::ProviderRegistered { + id: provider_id, + pos: provider_pos, + }); + 500_000 + } else { + 50_000 + } + } + } +} diff --git a/pallets/duniter-storage/src/mock.rs b/pallets/duniter-storage/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..502e8b864342beaaa7dc9fbaba6ce58f2dade0dd --- /dev/null +++ b/pallets/duniter-storage/src/mock.rs @@ -0,0 +1,189 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use super::*; +use crate::{self as pallet_duniter_storage}; +use frame_support::{ + parameter_types, + traits::{Everything, OnFinalize, OnInitialize}, +}; +use frame_system as system; +use sp_core::H256; +use sp_io::hashing::blake2_256; +use sp_runtime::{ + testing::{Header, TestSignature, UintAuthorityId}, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +type AccountId = u64; +type Balance = u64; +type Block = frame_system::mocking::MockBlock<Test>; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; + +// 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<T>}, + Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, + DuniterStorage: pallet_duniter_storage::{Pallet, Call, Config<T>, Storage, Event<T>}, + Randomness: pallet_provide_randomness::{Pallet, Call, Storage, Event}, + } +); + +// System +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData<Balance>; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +// Balances +parameter_types! { + pub const ExistentialDeposit: Balance = 1; + pub const MaxLocks: u32 = 50; +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>; + type MaxLocks = MaxLocks; + type MaxReserves = frame_support::pallet_prelude::ConstU32<1>; + type ReserveIdentifier = [u8; 8]; +} + +// DuniterStorage +parameter_types! { + pub const UserDeposit: u64 = 100; + pub const ProviderRegistrationDeposit: u64 = 500; +} + +pub struct TestRandomness; +impl frame_support::traits::Randomness<H256, u64> for TestRandomness { + fn random(subject: &[u8]) -> (H256, u64) { + (H256(blake2_256(subject)), 0) + } +} + +impl pallet_duniter_storage::Config for Test { + type ChangeReplicationFactorOrigin = frame_system::EnsureRoot<AccountId>; + type Currency = Balances; + type EntryExpiration = frame_support::traits::ConstU64<20>; + type Event = Event; + type MinProviderDuration = frame_support::traits::ConstU64<10>; + type MinKeyLen = frame_support::pallet_prelude::ConstU32<4>; + type ProviderRegistrationDeposit = ProviderRegistrationDeposit; + type ProviderId = AccountId; + type ProviderIdOf = sp_runtime::traits::ConvertInto; + type Randomness = TestRandomness; + type UserDeposit = UserDeposit; + type UserSigner = UintAuthorityId; + type UserSignature = TestSignature; +} + +pub struct FakeRandomness; +impl frame_support::traits::Randomness<Option<H256>, u64> for FakeRandomness { + fn random(_subject: &[u8]) -> (Option<H256>, u64) { + todo!() + } +} +impl frame_support::traits::Randomness<H256, u64> for FakeRandomness { + fn random(_subject: &[u8]) -> (H256, u64) { + todo!() + } +} + +// Randomness +parameter_types! { + pub const GetCurrentEpochIndex: u64 = 0; +} +impl pallet_provide_randomness::Config for Test { + type Currency = Balances; + type Event = Event; + type GetCurrentEpochIndex = GetCurrentEpochIndex; + type MaxRequests = frame_support::pallet_prelude::ConstU32<10>; + type RequestPrice = frame_support::traits::ConstU64<100>; + type OnFilledRandomness = DuniterStorage; + type OnUnbalanced = (); + type CurrentBlockRandomness = FakeRandomness; + type RandomnessFromOneEpochAgo = FakeRandomness; +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext(replication_factor: u32) -> sp_io::TestExternalities { + GenesisConfig { + system: SystemConfig::default(), + balances: pallet_balances::GenesisConfig { + balances: (0..replication_factor + 3) + .map(|i| (i as u64, 800)) + .collect(), + }, + duniter_storage: pallet_duniter_storage::GenesisConfig { + providers: (0..replication_factor) + .map(|i| (i as u64, ProviderEndpoint::default())) + .collect(), + replication_factor, + }, + } + .build_storage() + .unwrap() + .into() +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + //Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + //Balances::on_initialize(System::block_number()); + } +} diff --git a/pallets/duniter-storage/src/tests.rs b/pallets/duniter-storage/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..97dc85df8687e04525528afb4bbbd6901b41d678 --- /dev/null +++ b/pallets/duniter-storage/src/tests.rs @@ -0,0 +1,350 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use crate::mock::Event as RuntimeEvent; +use crate::mock::*; +use crate::{Error, Event, UserProof, UserProofOf}; +use codec::Encode; +use frame_support::{assert_err, assert_ok, BoundedVec}; +use sp_core::H256; +use sp_io::hashing::blake2_256; +use sp_runtime::testing::{TestSignature, UintAuthorityId}; +use sp_runtime::traits::BlakeTwo256; +use sp_state_machine::Backend; +use std::convert::TryFrom; + +fn create_user_proof(key: &[u8], value: Option<&[u8]>) -> UserProofOf<Test> { + let genesis_hash = H256::default(); + let data_nonce = 0; + let key = BoundedVec::<u8, _>::try_from(key.to_vec()).unwrap(); + let value = value.map(|value| (value.len() as u32, H256(blake2_256(value)))); + let signer = UintAuthorityId(0); + let sig_payload = (genesis_hash, data_nonce, &key, value, &signer).encode(); + UserProof { + genesis_hash, + data_nonce, + key, + value, + signer, + signature: TestSignature(0, sig_payload), + } +} + +fn generate_trie_proof<S: sp_state_machine::TrieBackendStorage<BlakeTwo256>>( + trie_backend: &sp_state_machine::TrieBackend<S, BlakeTwo256>, + keys: Vec<&[u8]>, +) -> sp_trie::CompactProof { + let proving_backend = sp_state_machine::ProvingBackend::new(trie_backend); + for key in keys { + let _ = proving_backend.storage(key); + } + let trie_proof = proving_backend + .extract_proof() + .into_compact_proof::<BlakeTwo256>(*trie_backend.root()) + .expect("fail to generate compact proof"); + println!("trie_proof_len={}", trie_proof.encoded_size()); + trie_proof +} + +fn to_leaf(value: &[u8]) -> Vec<u8> { + duniter_storage_primitives::compute_leaf_hash( + System::block_number() + 1, + H256(blake2_256(value)), + ) + .0 + .to_vec() +} + +#[test] +fn test_genesis_build() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + // Verify state + assert_eq!(DuniterStorage::replication_factor_half(), 1); + assert!(DuniterStorage::providers(0).is_some()); + assert!(DuniterStorage::providers(1).is_some()); + assert!(DuniterStorage::providers(2).is_none()); + }); +} + +#[test] +fn test_uneven_replication_factor() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::change_replication_factor(Origin::root(), 3), + Error::<Test>::UnevenReplicationFactor + ); + }); +} + +#[test] +fn test_not_enough_providers_to_increase_replication_factor() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::change_replication_factor(Origin::root(), 10), + Error::<Test>::NotEnoughProviders + ); + }); +} + +#[test] +fn test_already_registered() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::provider_register(Origin::signed(1), Default::default()), + Error::<Test>::AlreadyRegistered + ); + }); +} + +type MemoryDB = memory_db::MemoryDB< + BlakeTwo256, + memory_db::HashKey<BlakeTwo256>, + trie_db::DBValue, + memory_db::NoopTracker<trie_db::DBValue>, +>; + +#[test] +fn test_provider_declare_changes() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + + // Create initial trie + let (db, empty_root) = MemoryDB::default_with_root(); + let mut trie_backend = sp_state_machine::TrieBackend::new(db, empty_root); + println!("empty_root={:?}", empty_root.0); + + // try to insert 3 new entries, should success + assert_ok!(DuniterStorage::provider_declare_changes( + Origin::signed(1), + vec![ + create_user_proof(b"k1", Some(b"v1")), + create_user_proof(b"k2", Some(b"v2")), + create_user_proof(b"k3", Some(b"v3")), + ], + ),); + + // compute the new root onchain + assert_ok!(DuniterStorage::author_update_root( + Origin::none(), + generate_trie_proof(&trie_backend, vec![b"k1", b"k2", b"k3"]) + ),); + + // Apply changes in the trie + trie_backend.insert(vec![( + None, + vec![ + (b"k1".to_vec(), Some(to_leaf(b"v1"))), + (b"k2".to_vec(), Some(to_leaf(b"v2"))), + (b"k3".to_vec(), Some(to_leaf(b"v3"))), + ], + )]); + + // Check the now root onchain + let new_expected_root = trie_backend.root(); + assert_eq!(DuniterStorage::trie_root(), *new_expected_root); + + // try to (insert k4, modify k3,remove k2) with valid trie proof, should success + assert_ok!(DuniterStorage::provider_declare_changes( + Origin::signed(1), + vec![ + create_user_proof(b"k4", Some(b"v4")), + create_user_proof(b"k3", Some(b"v3.2")), + create_user_proof(b"k2", None), + ], + ),); + + // Try to compute the new root onchain with an empty proof, should fail + assert_err!( + DuniterStorage::author_update_root( + Origin::none(), + sp_trie::CompactProof { + encoded_nodes: vec![vec![]] + } //empty proof + ), + Error::<Test>::InvalidCompactProof + ); + + // Compute the new root onchain + assert_ok!(DuniterStorage::author_update_root( + Origin::none(), + generate_trie_proof(&trie_backend, vec![b"k2", b"k3", b"k4"]) + ),); + + // Apply changes in the trie + trie_backend.insert(vec![( + None, + vec![ + (b"k2".to_vec(), None), + (b"k3".to_vec(), Some(to_leaf(b"v3.2"))), + (b"k4".to_vec(), Some(to_leaf(b"v4"))), + ], + )]); + + // Check the now root onchain + let new_expected_root = trie_backend.root(); + assert_eq!(DuniterStorage::trie_root(), *new_expected_root); + }); +} + +#[test] +fn test_provider_register_ok() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + //println!("{}:?", Balances::free_balance()); + assert_ok!(DuniterStorage::provider_register( + Origin::signed(2), + Default::default() + ),); + assert_eq!(System::events().len(), 3); + // Deposit reserved to be à provider + assert_eq!( + System::events()[0].event, + RuntimeEvent::Balances(pallet_balances::Event::Reserved { + who: 2, + amount: 500 + }) + ); + // Amount pays to generate a random position + assert_eq!( + System::events()[1].event, + RuntimeEvent::Balances(pallet_balances::Event::Withdraw { + who: 2, + amount: 100 + }) + ); + assert_eq!( + System::events()[2].event, + RuntimeEvent::DuniterStorage(Event::NewProviderPending { + id: 2, + endpoint: Default::default(), + }) + ); + }); +} + +#[test] +fn test_provider_update_deposit_origin_not_registered() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::provider_update_deposit(Origin::signed(3), 1), + Error::<Test>::NotRegistered + ); + }); +} + +#[test] +fn test_provider_update_deposit_target_not_registered() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::provider_update_deposit(Origin::signed(1), 3), + Error::<Test>::NotRegistered + ); + }); +} + +#[test] +fn test_provider_update_deposit_ok() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_ok!(DuniterStorage::provider_update_deposit( + Origin::signed(0), + 1 + ),); + assert_eq!(System::events().len(), 1); + assert_eq!( + System::events()[0].event, + RuntimeEvent::Balances(pallet_balances::Event::Reserved { + who: 1, + amount: 500 + }) + ); + run_to_block(2); + assert_ok!(DuniterStorage::provider_update_deposit( + Origin::signed(0), + 1 + ),); + assert_eq!(System::events().len(), 0); + }); +} + +#[test] +fn test_provider_update_info_not_registered() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::provider_update_info(Origin::signed(3), Default::default()), + Error::<Test>::NotRegistered + ); + }); +} + +#[test] +fn test_provider_update_info_ok() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_ok!(DuniterStorage::provider_update_info( + Origin::signed(1), + Default::default() + ),); + assert_eq!(System::events().len(), 1); + assert_eq!( + System::events()[0].event, + RuntimeEvent::Balances(pallet_balances::Event::Reserved { + who: 1, + amount: 500 + }) + ); + }); +} + +#[test] +fn test_unregister_not_registered() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::provider_unregister(Origin::signed(3)), + Error::<Test>::NotRegistered + ); + }); +} + +#[test] +fn test_not_unregisterable() { + new_test_ext(2).execute_with(|| { + run_to_block(1); + assert_err!( + DuniterStorage::provider_unregister(Origin::signed(1)), + Error::<Test>::NotUnregisterable + ); + }); +} + +#[test] +fn test_not_enough_providers() { + new_test_ext(2).execute_with(|| { + run_to_block(10); + assert_err!( + DuniterStorage::provider_unregister(Origin::signed(1)), + Error::<Test>::NotEnoughProviders + ); + }); +} diff --git a/pallets/duniter-storage/src/types.rs b/pallets/duniter-storage/src/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..b6ac0d9c0e2ee1f164d4e9f9d6ac63d472ff98c3 --- /dev/null +++ b/pallets/duniter-storage/src/types.rs @@ -0,0 +1,132 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +//! Various basic types for use in the duniter-storage pallet. + +use codec::{Decode, Encode, MaxEncodedLen}; +use duniter_storage_primitives::Key; +use frame_support::pallet_prelude::*; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_runtime::traits::{IdentifyAccount, Verify}; + +#[derive( + Encode, Decode, Default, Clone, PartialEq, Eq, MaxEncodedLen, Ord, PartialOrd, RuntimeDebug, +)] +pub struct ProviderEndpoint(pub BoundedVec<u8, ConstU32<100>>); + +impl scale_info::TypeInfo for ProviderEndpoint { + type Identity = str; + + fn type_info() -> scale_info::Type { + Self::Identity::type_info() + } +} + +#[cfg(feature = "std")] +impl serde::Serialize for ProviderEndpoint { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str( + std::str::from_utf8(&self.0[..]) + .map_err(|e| serde::ser::Error::custom(format!("{:?}", e)))?, + ) + } +} + +#[cfg(feature = "std")] +impl<'de> serde::Deserialize<'de> for ProviderEndpoint { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + use std::convert::TryFrom as _; + Ok(ProviderEndpoint( + BoundedVec::try_from(String::deserialize(deserializer)?.as_bytes().to_vec()) + .map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?, + )) + } +} + +#[derive( + Clone, Encode, Decode, Default, PartialEq, Eq, PartialOrd, Ord, RuntimeDebug, TypeInfo, +)] +pub struct ProviderInfo<BlockNumber> { + pub endpoint: ProviderEndpoint, + pub pos: H256, + pub unregisterable_on: BlockNumber, +} + +#[derive(Clone, Encode, Decode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +pub struct UserProof<Signer, Signature> { + pub(super) genesis_hash: H256, + pub(super) data_nonce: u32, + pub(super) key: Key, + pub(super) value: Option<(u32, H256)>, + pub(super) signer: Signer, + pub(super) signature: Signature, +} + +impl<Signer, Signature> UserProof<Signer, Signature> { + pub fn to_delta<BlockNumber: Encode>(&self, block_number: BlockNumber) -> (Key, Option<H256>) { + ( + self.key.clone(), + if let Some((_value_size, value_hash)) = self.value { + Some(duniter_storage_primitives::compute_leaf_hash( + block_number, + value_hash, + )) + } else { + None + }, + ) + } +} + +impl<Signer, Signature> UserProof<Signer, Signature> { + pub fn key(&self) -> &Key { + &self.key + } + pub fn maybe_value_hash(&self) -> Option<H256> { + if let Some((_value_size, value_hash)) = self.value { + Some(value_hash) + } else { + None + } + } +} + +impl<Signer: Clone + IdentifyAccount, Signature> UserProof<Signer, Signature> { + pub fn user_account(&self) -> <Signer as IdentifyAccount>::AccountId { + self.signer.clone().into_account() + } +} + +impl<Signer, Signature> UserProof<Signer, Signature> +where + Signer: Clone + Encode + IdentifyAccount, + Signature: Encode + Verify<Signer = Signer>, +{ + pub fn verify_signature(&self) -> bool { + self.using_encoded(|bytes| { + let sig_begin = bytes.len().saturating_sub(self.signature.encoded_size()); + self.signature + .verify(&bytes[..sig_begin], &self.signer.clone().into_account()) + }) + } +} diff --git a/primitives/duniter-storage/Cargo.toml b/primitives/duniter-storage/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..cf256358c9cee530d4b643d32a8d3cdd35cd7fba --- /dev/null +++ b/primitives/duniter-storage/Cargo.toml @@ -0,0 +1,78 @@ +[package] +authors = ['librelois <c@elo.tf>'] +description = 'primitives for duniter-storage.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'duniter-storage-primitives' +readme = 'README.md' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[features] +default = ['std'] +std = [ + 'async-trait', + 'codec/std', + 'frame-support/std', + 'sp-api/std', + 'sp-core/std', + 'sp-io/std', + 'sp-inherents/std', + 'sp-runtime/std', + 'sp-std/std', + 'sp-trie/std', +] + +[dependencies] + +# crates.io +async-trait = { version = "0.1.50", optional = true } +codec = { package = 'parity-scale-codec', version = "2.3.1", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + +# substrate +[dependencies.frame-support] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-api] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-core] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-io] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-inherents] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +[dependencies.sp-trie] +default-features = false +git = 'https://github.com/librelois/substrate.git' +branch = 'duniter-monthly-2022-02' + +### DOC ### + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] diff --git a/primitives/duniter-storage/src/lib.rs b/primitives/duniter-storage/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..5c1ae526bdb7361e5012c847ef64cb22abe63ca6 --- /dev/null +++ b/primitives/duniter-storage/src/lib.rs @@ -0,0 +1,93 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +//! Duniter Storage primitives. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod util; + +pub use sp_inherents::Error; +pub use util::*; + +use codec::{Decode, Encode}; +use frame_support::{traits::ConstU32, BoundedVec}; +use sp_core::H256; +use sp_inherents::{InherentData, InherentIdentifier, IsFatalError}; +use sp_std::vec::Vec; + +pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"dunistor"; + +pub const MAX_KEY_LEN: u32 = 255; + +pub type Key = BoundedVec<u8, ConstU32<MAX_KEY_LEN>>; + +pub type Delta = Vec<(BoundedVec<u8, ConstU32<MAX_KEY_LEN>>, Option<H256>)>; + +pub type InherentType = sp_trie::CompactProof; + +/// +#[derive(Encode, sp_runtime::RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Decode))] +pub enum InherentError {} + +impl IsFatalError for InherentError { + fn is_fatal_error(&self) -> bool { + true + } +} + +pub struct InherentDataProvider { + maybe_proof: Option<sp_trie::CompactProof>, +} + +impl InherentDataProvider { + pub fn new(maybe_proof: Option<sp_trie::CompactProof>) -> Self { + Self { maybe_proof } + } +} + +#[cfg(feature = "std")] +#[async_trait::async_trait] +impl sp_inherents::InherentDataProvider for InherentDataProvider { + fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> { + if let Some(proof) = &self.maybe_proof { + inherent_data.put_data(INHERENT_IDENTIFIER, proof) + } else { + Ok(()) + } + } + + async fn try_handle_error( + &self, + identifier: &InherentIdentifier, + mut error: &[u8], + ) -> Option<Result<(), Error>> { + if *identifier != INHERENT_IDENTIFIER { + return None; + } + + let error = InherentError::decode(&mut error).ok()?; + + Some(Err(Error::Application(Box::from(format!("{:?}", error))))) + } +} + +pub type HashsRange = (H256, H256); + +pub fn compute_leaf_hash<BlockNumber: Encode>(block_number: BlockNumber, value_hash: H256) -> H256 { + (block_number, value_hash).using_encoded(|bytes| H256(sp_core::blake2_256(bytes))) +} diff --git a/primitives/duniter-storage/src/util.rs b/primitives/duniter-storage/src/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..558856553e34d7b05956aaf96eb0c6575313074e --- /dev/null +++ b/primitives/duniter-storage/src/util.rs @@ -0,0 +1,34 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +//! Duniter Storage util functions. + +use crate::*; + +pub fn reverse_hash(mut hash: H256) -> H256 { + for i in 0..32 { + hash.0[i] = !hash.0[i]; + } + hash +} + +pub fn range_contains(range: HashsRange, key_hash: H256) -> bool { + if range.0 > range.1 { + key_hash >= range.0 || key_hash < range.1 + } else { + key_hash >= range.0 && key_hash < range.1 + } +}