Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1000i100-test
  • 105_gitlab_container_registry
  • cgeek/issue-297-cpu
  • ci_cache
  • debug/podman
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-smoldot
  • feature/dc-dump
  • feature/distance-rule
  • feature/show_milestone
  • fix-252
  • fix_picked_up_file_in_runtime_release
  • gdev-800-tests
  • hugo-release/runtime-701
  • hugo-tmp-dockerfile-cache
  • hugo/195-doc
  • hugo/195-graphql-schema
  • hugo/distance-precompute
  • hugo/endpoint-gossip
  • hugo/tmp-0.9.1
  • master
  • network/gdev-800
  • network/gdev-802
  • network/gdev-803
  • network/gdev-900
  • network/gtest-1000
  • pini-check-password
  • release/client-800.2
  • release/hugo-chainspec-gdev5
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • release/runtime-401
  • release/runtime-500
  • release/runtime-600
  • release/runtime-700
  • release/runtime-701
  • release/runtime-800
  • runtime/gtest-1000
  • tests/distance-with-oracle
  • tuxmain/anonymous-tx
  • tuxmain/benchmark-distance
  • tuxmain/fix-change-owner-key
  • update-docker-compose-rpc-squid-names
  • upgradable-multisig
  • gdev-800
  • gdev-800-0.8.0
  • gdev-802
  • gdev-803
  • gdev-900-0.10.0
  • gdev-900-0.10.1
  • gdev-900-0.9.0
  • gdev-900-0.9.1
  • gdev-900-0.9.2
  • gtest-1000
  • gtest-1000-0.11.0
  • gtest-1000-0.11.1
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • runtime-401
  • runtime-500
  • runtime-600
  • runtime-700
  • runtime-701
  • runtime-800
  • runtime-800-backup
  • runtime-800-bis
  • runtime-801
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
  • v0.4.1
88 results

Target

Select target project
  • nodes/rust/duniter-v2s
  • llaq/lc-core-substrate
  • pini-gh/duniter-v2s
  • vincentux/duniter-v2s
  • mildred/duniter-v2s
  • d0p1/duniter-v2s
  • bgallois/duniter-v2s
  • Nicolas80/duniter-v2s
8 results
Select Git revision
  • archive_upgrade_polkadot_v0.9.42
  • david-wot-scenarios-cucumber
  • distance
  • elois-ci-binary-release
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-fix-85
  • elois-fix-idty-post-genesis
  • elois-fix-sufficients-change-owner-key
  • elois-opti-cert
  • elois-remove-renewable-period
  • elois-revoc-with-old-key
  • elois-rework-certs
  • elois-smish-members-cant-change-or-rem-idty
  • elois-smoldot
  • elois-substrate-v0.9.23
  • elois-technical-commitee
  • hugo-gtest
  • hugo-remove-duniter-account
  • hugo-rework-genesis
  • hugo-tmp
  • jrx/workspace_tomls
  • master
  • no-bootnodes
  • pallet-benchmark
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • test-gen-new-owner-key-msg
  • ts-types
  • ud-time-64
  • upgrade_polkadot_v0.9.42
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
52 results
Show changes
Showing
with 2006 additions and 853 deletions
...@@ -16,63 +16,93 @@ ...@@ -16,63 +16,93 @@
use crate::*; use crate::*;
use frame_support::pallet_prelude::*; use frame_support::pallet_prelude::*;
use impl_trait_for_tuples::impl_for_tuples;
/// Trait defining operations for checking if identity-related calls are allowed.
pub trait CheckIdtyCallAllowed<T: Config> { pub trait CheckIdtyCallAllowed<T: Config> {
/// Check if creating an identity is allowed.
fn check_create_identity(creator: T::IdtyIndex) -> Result<(), DispatchError>; fn check_create_identity(creator: T::IdtyIndex) -> Result<(), DispatchError>;
fn check_confirm_identity(idty_index: T::IdtyIndex) -> Result<(), DispatchError>;
fn check_validate_identity(idty_index: T::IdtyIndex) -> Result<(), DispatchError>;
fn check_change_identity_address(idty_index: T::IdtyIndex) -> Result<(), DispatchError>;
fn check_remove_identity(idty_index: T::IdtyIndex) -> Result<(), DispatchError>;
} }
#[impl_for_tuples(5)] impl<T: Config> CheckIdtyCallAllowed<T> for () {
impl<T: Config> CheckIdtyCallAllowed<T> for Tuple { fn check_create_identity(_creator: T::IdtyIndex) -> Result<(), DispatchError> {
fn check_create_identity(creator: T::IdtyIndex) -> Result<(), DispatchError> {
for_tuples!( #( Tuple::check_create_identity(creator)?; )* );
Ok(()) Ok(())
} }
fn check_confirm_identity(idty_index: T::IdtyIndex) -> Result<(), DispatchError> {
for_tuples!( #( Tuple::check_confirm_identity(idty_index)?; )* );
Ok(())
}
fn check_validate_identity(idty_index: T::IdtyIndex) -> Result<(), DispatchError> {
for_tuples!( #( Tuple::check_validate_identity(idty_index)?; )* );
Ok(())
} }
fn check_change_identity_address(idty_index: T::IdtyIndex) -> Result<(), DispatchError> {
for_tuples!( #( Tuple::check_change_identity_address(idty_index)?; )* ); /// Trait to check the worthiness of an account.
Ok(()) pub trait CheckAccountWorthiness<T: Config> {
/// Check the worthiness of an account.
fn check_account_worthiness(account: &T::AccountId) -> Result<(), DispatchError>;
/// Set an account as worthy. Only available for runtime benchmarks.
#[cfg(feature = "runtime-benchmarks")]
fn set_worthy(account: &T::AccountId);
} }
fn check_remove_identity(idty_index: T::IdtyIndex) -> Result<(), DispatchError> {
for_tuples!( #( Tuple::check_remove_identity(idty_index)?; )* ); impl<T: Config> CheckAccountWorthiness<T> for () {
fn check_account_worthiness(_account: &T::AccountId) -> Result<(), DispatchError> {
Ok(()) Ok(())
} }
#[cfg(feature = "runtime-benchmarks")]
fn set_worthy(_account: &T::AccountId) {}
} }
/// Trait defining operations for validating identity names.
pub trait IdtyNameValidator { pub trait IdtyNameValidator {
/// Validate an identity name.
fn validate(idty_name: &IdtyName) -> bool; fn validate(idty_name: &IdtyName) -> bool;
} }
pub trait OnIdtyChange<T: Config> { /// Trait defining behavior for handling new identities creation.
fn on_idty_change(idty_index: T::IdtyIndex, idty_event: &IdtyEvent<T>) -> Weight; pub trait OnNewIdty<T: Config> {
/// Called when a new identity is created.
fn on_created(idty_index: &T::IdtyIndex, creator: &T::IdtyIndex);
} }
#[impl_for_tuples(5)] /// Trait defining behavior for handling removed identities.
#[allow(clippy::let_and_return)] /// As the weight accounting can be complicated it should be done
impl<T: Config> OnIdtyChange<T> for Tuple { /// at the handler level.
fn on_idty_change(idty_index: T::IdtyIndex, idty_event: &IdtyEvent<T>) -> Weight { pub trait OnRemoveIdty<T: Config> {
let mut weight = Weight::zero(); /// Called when an identity is removed.
for_tuples!( #( weight = weight.saturating_add(Tuple::on_idty_change(idty_index, idty_event)); )* ); fn on_removed(idty_index: &T::IdtyIndex) -> Weight;
weight /// Called when an identity is revoked.
fn on_revoked(idty_index: &T::IdtyIndex) -> Weight;
} }
impl<T: Config> OnNewIdty<T> for () {
fn on_created(_idty_index: &T::IdtyIndex, _creator: &T::IdtyIndex) {}
} }
pub trait RemoveIdentityConsumers<IndtyIndex> { impl<T: Config> OnRemoveIdty<T> for () {
fn remove_idty_consumers(idty_index: IndtyIndex) -> Weight; fn on_removed(_idty_index: &T::IdtyIndex) -> Weight {
Weight::zero()
} }
impl<IndtyIndex> RemoveIdentityConsumers<IndtyIndex> for () {
fn remove_idty_consumers(_: IndtyIndex) -> Weight { fn on_revoked(_idty_index: &T::IdtyIndex) -> Weight {
Weight::zero() Weight::zero()
} }
} }
/// Trait defining operations for linking identities to accounts.
pub trait LinkIdty<AccountId, IdtyIndex> {
/// Links an identity to an account.
fn link_identity(account_id: &AccountId, idty_index: IdtyIndex) -> Result<(), DispatchError>;
}
impl<AccountId, IdtyIndex> LinkIdty<AccountId, IdtyIndex> for () {
fn link_identity(_: &AccountId, _: IdtyIndex) -> Result<(), DispatchError> {
Ok(())
}
}
/// A trait for handling identity owner key changes.
pub trait KeyChange<T: Config> {
fn on_changed(id: T::IdtyIndex, account_id: T::AccountId) -> Result<(), DispatchError>;
}
impl<T: Config> KeyChange<T> for () {
/// Called when an identity's owner key has changed.
fn on_changed(_id: T::IdtyIndex, _account_id: T::AccountId) -> Result<(), DispatchError> {
Ok(())
}
}
...@@ -16,90 +16,151 @@ ...@@ -16,90 +16,151 @@
//! Various basic types for use in the identity pallet. //! Various basic types for use in the identity pallet.
use codec::{Decode, Encode}; use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::pallet_prelude::*; use frame_support::pallet_prelude::*;
use scale_info::TypeInfo; use scale_info::{prelude::vec::Vec, TypeInfo};
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sp_std::vec::Vec;
/// Internal events related to identity.
pub enum IdtyEvent<T: crate::Config> { pub enum IdtyEvent<T: crate::Config> {
Created { creator: T::IdtyIndex }, /// Creation of a new identity by another.
Confirmed, // pallet account links account to identity
Validated, // pallet wot adds certification
ChangedOwnerKey { new_owner_key: T::AccountId }, // pallet quota adds storage item for this identity
Removed { status: IdtyStatus }, Created {
/// Identity of the creator.
creator: T::IdtyIndex,
/// Account of the identity owner.
owner_key: T::AccountId,
},
/// Removing an identity (unvalidated or revoked).
// pallet wot removes associated certifications if status is not revoked
// pallet quota removes associated quota
// pallet smith-members exclude smith
Removed {
/// Status of the identity.
status: IdtyStatus,
},
// TODO add a way to unlink accounts corresponding to revoked or removed identities
} }
#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, PartialOrd, Ord, RuntimeDebug)] /// Reasons for revocation.
pub struct IdtyName(pub Vec<u8>); #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum RevocationReason {
impl scale_info::TypeInfo for IdtyName { /// Revoked by root (e.g., governance or migration).
type Identity = str; Root,
/// Revoked by user action (revocation document).
fn type_info() -> scale_info::Type { User,
Self::Identity::type_info() /// Revoked due to inactive period.
Expired,
} }
/// Reasons for removal.
#[derive(Encode, Decode, Clone, DecodeWithMemTracking, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum RemovalReason {
/// Removed by root.
Root,
/// Removed because unconfirmed.
Unconfirmed,
/// Removed because unvalidated.
Unvalidated,
/// Removed automatically after revocation buffer.
Revoked,
} }
#[cfg(feature = "std")] /// Represents the name of an identity, ASCII encoded.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Default,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
RuntimeDebug,
Serialize,
Deserialize,
TypeInfo,
)]
pub struct IdtyName(pub Vec<u8>);
impl From<&str> for IdtyName { impl From<&str> for IdtyName {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Self(s.as_bytes().to_vec()) Self(s.as_bytes().to_vec())
} }
} }
#[cfg(feature = "std")] /// State of an identity.
impl serde::Serialize for IdtyName { #[derive(
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { Encode,
std::str::from_utf8(&self.0) Decode,
.map_err(|e| serde::ser::Error::custom(format!("{:?}", e)))? Default,
.serialize(serializer) Clone,
} Copy,
} PartialEq,
Eq,
#[cfg(feature = "std")] RuntimeDebug,
impl<'de> serde::Deserialize<'de> for IdtyName { TypeInfo,
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> { Deserialize,
Ok(Self(String::deserialize(de)?.as_bytes().to_vec())) Serialize,
} )]
}
#[cfg_attr(feature = "std", derive(Deserialize, Serialize))]
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum IdtyStatus { pub enum IdtyStatus {
Created, /// Created through a first certification but unconfirmed.
ConfirmedByOwner, #[default]
Validated, Unconfirmed,
} /// Confirmed by key owner with a name published but unvalidated.
impl Default for IdtyStatus { Unvalidated,
fn default() -> Self { /// Member of the main web of trust.
IdtyStatus::Created // (there must be a membership in membership pallet storage)
} Member,
/// Not a member of the main web of trust, auto-revocation planned.
NotMember,
/// Revoked manually or automatically, deletion possible.
Revoked,
} }
#[cfg_attr(feature = "std", derive(Debug, Deserialize, Serialize))] /// Identity value structure.
#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] ///
/// Represents the value associated with an identity, akin to key/value pairs.
#[derive(Serialize, Deserialize, Debug, Encode, Decode, Clone, PartialEq, Eq, TypeInfo)]
pub struct IdtyValue<BlockNumber, AccountId, IdtyData> { pub struct IdtyValue<BlockNumber, AccountId, IdtyData> {
/// Data shared between pallets defined by runtime.
/// Only contains `first_eligible_ud` in our case.
pub data: IdtyData, pub data: IdtyData,
/// Block before which creating a new identity is not allowed.
pub next_creatable_identity_on: BlockNumber, pub next_creatable_identity_on: BlockNumber,
/// Previous owner key of this identity (optional).
pub old_owner_key: Option<(AccountId, BlockNumber)>, pub old_owner_key: Option<(AccountId, BlockNumber)>,
/// Current owner key of this identity.
pub owner_key: AccountId, pub owner_key: AccountId,
pub removable_on: BlockNumber, /// Next action scheduled on identity.
///
/// `0` if no action is scheduled.
pub next_scheduled: BlockNumber,
/// Current status of the identity (until validation).
pub status: IdtyStatus, pub status: IdtyStatus,
} }
/// Reprensent the payload to define a new owner key.
#[derive(Clone, Copy, Encode, RuntimeDebug)] #[derive(Clone, Copy, Encode, RuntimeDebug)]
pub struct NewOwnerKeyPayload<'a, AccountId, IdtyIndex, Hash> { pub struct IdtyIndexAccountIdPayload<'a, AccountId, IdtyIndex, Hash> {
// Avoid replay attack between networks /// Hash of the genesis block.
// Used to avoid replay attacks across networks.
pub genesis_hash: &'a Hash, pub genesis_hash: &'a Hash,
/// Identity index.
pub idty_index: IdtyIndex, pub idty_index: IdtyIndex,
/// Old owner key of the identity.
pub old_owner_key: &'a AccountId, pub old_owner_key: &'a AccountId,
} }
/// Represents the payload for identity revocation.
#[derive(Clone, Copy, Encode, Decode, PartialEq, Eq, TypeInfo, RuntimeDebug)] #[derive(Clone, Copy, Encode, Decode, PartialEq, Eq, TypeInfo, RuntimeDebug)]
pub struct RevocationPayload<IdtyIndex, Hash> { pub struct RevocationPayload<IdtyIndex, Hash> {
// Avoid replay attack between networks /// Hash of the genesis block.
// Used to avoid replay attacks across networks.
pub genesis_hash: Hash, pub genesis_hash: Hash,
/// Identity index.
pub idty_index: IdtyIndex, pub idty_index: IdtyIndex,
} }
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::unnecessary_cast)]
use frame_support::weights::{constants::RocksDbWeight, Weight};
pub trait WeightInfo {
fn create_identity() -> Weight;
fn confirm_identity() -> Weight;
fn change_owner_key() -> Weight;
fn revoke_identity() -> Weight;
fn revoke_identity_legacy() -> Weight;
fn prune_item_identities_names(i: u32) -> Weight;
fn fix_sufficients() -> Weight;
fn link_account() -> Weight;
fn on_initialize() -> Weight;
fn do_revoke_identity_noop() -> Weight;
fn do_revoke_identity() -> Weight;
fn do_remove_identity_noop() -> Weight;
fn do_remove_identity_handler() -> Weight;
fn do_remove_identity() -> Weight;
fn prune_identities_noop() -> Weight;
fn prune_identities_none() -> Weight;
fn prune_identities_err() -> Weight;
fn membership_removed() -> Weight;
}
// Insecure weights implementation, use it for tests only!
impl WeightInfo for () {
fn create_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1165`
// Estimated: `7105`
// Minimum execution time: 1_643_969_000 picoseconds.
Weight::from_parts(1_781_521_000, 0)
.saturating_add(Weight::from_parts(0, 7105))
.saturating_add(RocksDbWeight::get().reads(14))
.saturating_add(RocksDbWeight::get().writes(12))
}
fn confirm_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `661`
// Estimated: `6601`
// Minimum execution time: 564_892_000 picoseconds.
Weight::from_parts(588_761_000, 0)
.saturating_add(Weight::from_parts(0, 6601))
.saturating_add(RocksDbWeight::get().reads(5))
.saturating_add(RocksDbWeight::get().writes(4))
}
fn change_owner_key() -> Weight {
// Proof Size summary in bytes:
// Measured: `837`
// Estimated: `6777`
// Minimum execution time: 991_641_000 picoseconds.
Weight::from_parts(1_071_332_000, 0)
.saturating_add(Weight::from_parts(0, 6777))
.saturating_add(RocksDbWeight::get().reads(7))
.saturating_add(RocksDbWeight::get().writes(5))
}
fn revoke_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `778`
// Estimated: `6718`
// Minimum execution time: 829_174_000 picoseconds.
Weight::from_parts(869_308_000, 0)
.saturating_add(Weight::from_parts(0, 6718))
.saturating_add(RocksDbWeight::get().reads(6))
.saturating_add(RocksDbWeight::get().writes(6))
}
fn prune_item_identities_names(i: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 51_362_000 picoseconds.
Weight::from_parts(80_389_000, 0)
.saturating_add(Weight::from_parts(0, 0))
// Standard Error: 75_232
.saturating_add(Weight::from_parts(30_016_649, 0).saturating_mul(i.into()))
.saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(i.into())))
}
fn fix_sufficients() -> Weight {
// Proof Size summary in bytes:
// Measured: `67`
// Estimated: `3591`
// Minimum execution time: 154_343_000 picoseconds.
Weight::from_parts(156_117_000, 0)
.saturating_add(Weight::from_parts(0, 3591))
.saturating_add(RocksDbWeight::get().reads(1))
.saturating_add(RocksDbWeight::get().writes(1))
}
fn link_account() -> Weight {
// Proof Size summary in bytes:
// Measured: `307`
// Estimated: `3772`
// Minimum execution time: 538_773_000 picoseconds.
Weight::from_parts(591_354_000, 0)
.saturating_add(Weight::from_parts(0, 3772))
.saturating_add(RocksDbWeight::get().reads(3))
.saturating_add(RocksDbWeight::get().writes(1))
}
fn on_initialize() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 4_529_000 picoseconds.
Weight::from_parts(7_360_000, 0).saturating_add(Weight::from_parts(0, 0))
}
fn do_revoke_identity_noop() -> Weight {
// Proof Size summary in bytes:
// Measured: `269`
// Estimated: `3734`
// Minimum execution time: 103_668_000 picoseconds.
Weight::from_parts(107_679_000, 0)
.saturating_add(Weight::from_parts(0, 3734))
.saturating_add(RocksDbWeight::get().reads(1))
}
fn do_revoke_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1525`
// Estimated: `7465`
// Minimum execution time: 2_204_911_000 picoseconds.
Weight::from_parts(2_225_493_000, 0)
.saturating_add(Weight::from_parts(0, 7465))
.saturating_add(RocksDbWeight::get().reads(17))
.saturating_add(RocksDbWeight::get().writes(20))
}
fn revoke_identity_legacy() -> Weight {
// Proof Size summary in bytes:
// Measured: `1525`
// Estimated: `7465`
// Minimum execution time: 2_204_911_000 picoseconds.
Weight::from_parts(2_225_493_000, 0)
.saturating_add(Weight::from_parts(0, 7465))
.saturating_add(RocksDbWeight::get().reads(17))
.saturating_add(RocksDbWeight::get().writes(20))
}
fn do_remove_identity_noop() -> Weight {
// Proof Size summary in bytes:
// Measured: `269`
// Estimated: `3734`
// Minimum execution time: 104_296_000 picoseconds.
Weight::from_parts(115_316_000, 0)
.saturating_add(Weight::from_parts(0, 3734))
.saturating_add(RocksDbWeight::get().reads(1))
}
fn do_remove_identity_handler() -> Weight {
// Proof Size summary in bytes:
// Measured: `269`
// Estimated: `3734`
// Minimum execution time: 104_296_000 picoseconds.
Weight::from_parts(115_316_000, 0)
.saturating_add(Weight::from_parts(0, 3734))
.saturating_add(RocksDbWeight::get().reads(1))
}
fn do_remove_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1432`
// Estimated: `6192`
// Minimum execution time: 2_870_497_000 picoseconds.
Weight::from_parts(4_159_994_000, 0)
.saturating_add(Weight::from_parts(0, 6192))
.saturating_add(RocksDbWeight::get().reads(16))
.saturating_add(RocksDbWeight::get().writes(22))
}
fn prune_identities_noop() -> Weight {
// Proof Size summary in bytes:
// Measured: `108`
// Estimated: `3573`
// Minimum execution time: 68_859_000 picoseconds.
Weight::from_parts(71_836_000, 0)
.saturating_add(Weight::from_parts(0, 3573))
.saturating_add(RocksDbWeight::get().reads(1))
}
fn prune_identities_none() -> Weight {
// Proof Size summary in bytes:
// Measured: `292`
// Estimated: `3757`
// Minimum execution time: 178_332_000 picoseconds.
Weight::from_parts(186_982_000, 0)
.saturating_add(Weight::from_parts(0, 3757))
.saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(1))
}
fn prune_identities_err() -> Weight {
// Proof Size summary in bytes:
// Measured: `1177`
// Estimated: `4642`
// Minimum execution time: 1_427_848_000 picoseconds.
Weight::from_parts(2_637_229_000, 0)
.saturating_add(Weight::from_parts(0, 4642))
.saturating_add(RocksDbWeight::get().reads(8))
.saturating_add(RocksDbWeight::get().writes(8))
}
fn membership_removed() -> Weight {
// Proof Size summary in bytes:
// Measured: `1177`
// Estimated: `4642`
// Minimum execution time: 1_427_848_000 picoseconds.
Weight::from_parts(2_637_229_000, 0)
.saturating_add(Weight::from_parts(0, 4642))
.saturating_add(RocksDbWeight::get().reads(8))
.saturating_add(RocksDbWeight::get().writes(8))
}
}
[package] [package]
authors = ['librelois <c@elo.tf>'] authors.workspace = true
description = 'FRAME pallet membership.' description = "duniter pallet membership"
edition = "2021" edition.workspace = true
homepage = 'https://duniter.org' homepage.workspace = true
license = 'AGPL-3.0' license.workspace = true
name = 'pallet-membership' name = "pallet-membership"
readme = 'README.md' repository.workspace = true
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version.workspace = true
version = '3.0.0'
[features] [features]
default = ['std'] default = ["std"]
runtime-benchmarks = ['frame-benchmarking'] runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
std = [ std = [
'codec/std', "codec/std",
'frame-support/std', "frame-benchmarking?/std",
'frame-system/std', "frame-support/std",
'frame-benchmarking/std', "frame-system/std",
'serde', "scale-info/std",
'sp-core/std', "sp-core/std",
'sp-membership/std', "sp-io/std",
'sp-runtime/std', "sp-membership/std",
'sp-std/std', "sp-runtime/std",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-membership/try-runtime",
"sp-runtime/try-runtime",
] ]
try-runtime = ['frame-support/try-runtime']
[dependencies]
sp-membership = { path = "../../primitives/membership", default-features = false }
# substrate
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
[dependencies.codec]
default-features = false
features = ['derive']
package = 'parity-scale-codec'
version = "3.1.5"
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/duniter/substrate'
optional = true
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-system]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.serde]
version = "1.0.101"
optional = true
features = ["derive"]
[dependencies.sp-core]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
### DOC ###
[package.metadata.docs.rs] [package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu'] targets = ["x86_64-unknown-linux-gnu"]
[dev-dependencies.serde]
version = '1.0.119'
### DEV ###
[dev-dependencies.maplit] [dependencies]
version = '1.0.2' duniter-primitives = { workspace = true }
codec = { workspace = true, features = ["derive"] }
[dev-dependencies.sp-io] frame-benchmarking = { workspace = true, optional = true }
default-features = false frame-support = { workspace = true }
git = 'https://github.com/duniter/substrate' frame-system = { workspace = true }
branch = 'duniter-substrate-v0.9.32' scale-info = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-membership = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
maplit = { workspace = true, default-features = true }
sp-io = { workspace = true, default-features = true }
# Duniter membership pallet
Duniter membership is related to duniter Web of Trust and more specific than [parity membership pallet](https://github.com/paritytech/substrate/tree/master/frame/membership). It is used only internally by the identity, WoT, and distance pallets. In particular, it is adding the concept of "pending membership" which is an intermediate state where the identity is waiting to become member.
\ No newline at end of file
// Copyright 2021-2022 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_system::pallet_prelude::BlockNumberFor;
use crate::Pallet;
#[benchmarks(
where
T::IdtyId: From<u32>,
BlockNumberFor<T>: From<u32>,
)]
mod benchmarks {
use super::*;
// TODO membership add and renewal should be included to distance on_new_session as worst case scenario
#[benchmark]
fn on_initialize() {
// Base weight of an empty initialize
#[block]
{
Pallet::<T>::on_initialize(BlockNumberFor::<T>::zero());
}
}
#[benchmark]
fn expire_memberships(i: Linear<0, 3>) {
// Limited by the number of validators
// Arbitrarily high, to be in the worst case of wot instance,
// this will overcount the weight in hooks see https://git.duniter.org/nodes/rust/duniter-v2s/-/issues/167
let block_number: BlockNumberFor<T> = 10_000_000.into();
frame_system::pallet::Pallet::<T>::set_block_number(block_number);
let mut idties: Vec<T::IdtyId> = Vec::new();
for j in 1..i + 1 {
let j: T::IdtyId = j.into();
Membership::<T>::insert(j, MembershipData::<BlockNumberFor<T>>::default());
idties.push(j);
}
MembershipsExpireOn::<T>::insert(block_number, idties);
assert_eq!(
MembershipsExpireOn::<T>::get(block_number).len(),
i as usize
);
#[block]
{
Pallet::<T>::expire_memberships(block_number);
}
assert_eq!(MembershipsExpireOn::<T>::get(block_number).len(), 0_usize);
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(crate::mock::MembershipConfig {
memberships: maplit::btreemap![
3 => crate::MembershipData {
expire_on: 3,
},
],
}),
crate::mock::Test
);
}
...@@ -14,6 +14,14 @@ ...@@ -14,6 +14,14 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Duniter Membership Pallet
//!
//! The Duniter Membership Pallet is closely integrated with the Duniter Web of Trust (WoT) and is tailored specifically for Duniter, in contrast to the [Parity Membership Pallet](https://github.com/paritytech/substrate/tree/master/frame/membership). It operates exclusively within the Duniter ecosystem and is utilized internally by the Identity, Web of Trust, and Distance Pallets.
//!
//! ## Main Web of Trust (WoT)
//!
//! The Membership Pallet manages all aspects related to the membership of identities within the Duniter Web of Trust. Unlike traditional membership systems, it does not expose any external calls to users. Instead, its functionalities are accessible through distance evaluations provided by the Distance Oracle.
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
...@@ -23,72 +31,105 @@ mod mock; ...@@ -23,72 +31,105 @@ mod mock;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
/*#[cfg(feature = "runtime-benchmarks")] mod benchmarking;
mod benchmarking;*/
pub mod weights;
pub use pallet::*; pub use pallet::*;
pub use weights::WeightInfo;
use frame_support::dispatch::Weight; use frame_support::pallet_prelude::{Weight, *};
use frame_support::error::BadOrigin; use scale_info::prelude::{collections::BTreeMap, vec::Vec};
use frame_support::pallet_prelude::*; use sp_membership::{traits::*, MembershipData};
use frame_system::RawOrigin;
use sp_membership::traits::*;
use sp_membership::MembershipData;
use sp_runtime::traits::Zero; use sp_runtime::traits::Zero;
use sp_std::prelude::*;
#[cfg(feature = "std")]
use std::collections::BTreeMap;
#[cfg(feature = "runtime-benchmarks")]
pub trait SetupBenchmark<IdtyId, AccountId> {
fn force_valid_distance_status(idty_index: &IdtyId);
fn add_cert(_issuer: &IdtyId, _receiver: &IdtyId);
}
#[cfg(feature = "runtime-benchmarks")]
impl<IdtyId, AccountId> SetupBenchmark<IdtyId, AccountId> for () {
fn force_valid_distance_status(_idty_id: &IdtyId) {}
fn add_cert(_issuer: &IdtyId, _receiver: &IdtyId) {}
}
/// Represent reasons for the removal of membership.
#[derive(Encode, Decode, Clone, DecodeWithMemTracking, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum MembershipRemovalReason {
/// Indicates membership was removed because it reached the end of its life.
Expired,
/// Indicates membership was explicitly revoked.
Revoked,
/// Indicates membership was removed because the received certifications count fell below the threshold.
NotEnoughCerts,
/// Indicates membership was removed due to system reasons (e.g., consumers, authority members, or root).
System,
}
#[allow(unreachable_patterns)]
#[frame_support::pallet] #[frame_support::pallet]
pub mod pallet { pub mod pallet {
use super::*; use super::*;
use frame_support::traits::StorageVersion; use frame_support::traits::StorageVersion;
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
use sp_runtime::traits::Convert;
/// The current storage version. /// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet] #[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::storage_version(STORAGE_VERSION)] #[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info] #[pallet::without_storage_info]
pub struct Pallet<T, I = ()>(_); pub struct Pallet<T>(_);
// CONFIG // // CONFIG //
#[pallet::config] #[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config { pub trait Config: frame_system::Config {
/// Ask the runtime whether the identity can perform membership operations /// Check if the identity can perform membership operations.
type CheckCallAllowed: CheckCallAllowed<Self::IdtyId>; type CheckMembershipOpAllowed: CheckMembershipOpAllowed<Self::IdtyId>;
/// Something that identifies an identity
/// Something that identifies an identity.
type IdtyId: Copy + MaybeSerializeDeserialize + Parameter + Ord; type IdtyId: Copy + MaybeSerializeDeserialize + Parameter + Ord;
/// Something that give the IdtyId on an account id
type IdtyIdOf: Convert<Self::AccountId, Option<Self::IdtyId>>; /// Something that gives the IdtyId of an AccountId and reverse.
/// Optional metadata type IdtyAttr: duniter_primitives::Idty<Self::IdtyId, Self::AccountId>;
type MetaData: Default + Parameter + Validate<Self::AccountId>;
/// Maximum lifespan of a single membership (in number of blocks).
#[pallet::constant] #[pallet::constant]
/// Maximum life span of a non-renewable membership (in number of blocks) type MembershipPeriod: Get<BlockNumberFor<Self>>;
type MembershipPeriod: Get<Self::BlockNumber>;
/// On event handler /// Minimum delay to wait before renewing membership, i.e., asking for distance evaluation.
type OnEvent: OnEvent<Self::IdtyId, Self::MetaData>;
#[pallet::constant] #[pallet::constant]
/// Maximum period (in number of blocks), where an identity can remain pending subscription. type MembershipRenewalPeriod: Get<BlockNumberFor<Self>>;
type PendingMembershipPeriod: Get<Self::BlockNumber>;
/// Because this pallet emits events, it depends on the runtime's definition of an event. /// Handler called when a new membership is created or renewed.
type RuntimeEvent: From<Event<Self, I>> type OnNewMembership: OnNewMembership<Self::IdtyId>;
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Handler called when a membership is revoked or removed.
type OnRemoveMembership: OnRemoveMembership<Self::IdtyId>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
/// Benchmark setup handler for runtime benchmarks (feature-dependent).
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkSetupHandler: SetupBenchmark<Self::IdtyId, Self::AccountId>;
} }
// GENESIS STUFF // // GENESIS STUFF //
#[pallet::genesis_config] #[pallet::genesis_config]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { pub struct GenesisConfig<T: Config> {
pub memberships: BTreeMap<T::IdtyId, MembershipData<T::BlockNumber>>, pub memberships: BTreeMap<T::IdtyId, MembershipData<BlockNumberFor<T>>>,
} }
#[cfg(feature = "std")] impl<T: Config> Default for GenesisConfig<T> {
impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
fn default() -> Self { fn default() -> Self {
Self { Self {
memberships: Default::default(), memberships: Default::default(),
...@@ -97,301 +138,213 @@ pub mod pallet { ...@@ -97,301 +138,213 @@ pub mod pallet {
} }
#[pallet::genesis_build] #[pallet::genesis_build]
impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> { impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) { fn build(&self) {
for (idty_id, membership_data) in &self.memberships { for (idty_id, membership_data) in &self.memberships {
MembershipsExpireOn::<T, I>::append(membership_data.expire_on, idty_id); MembershipsExpireOn::<T>::append(membership_data.expire_on, idty_id);
Membership::<T, I>::insert(idty_id, membership_data); Membership::<T>::insert(idty_id, membership_data);
} }
} }
} }
// STORAGE // // STORAGE //
/// The membership data for each identity.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn membership)] #[pallet::getter(fn membership)]
pub type Membership<T: Config<I>, I: 'static = ()> = pub type Membership<T: Config> = CountedStorageMap<
CountedStorageMap<_, Twox64Concat, T::IdtyId, MembershipData<T::BlockNumber>, OptionQuery>; _,
Twox64Concat,
T::IdtyId,
MembershipData<BlockNumberFor<T>>,
OptionQuery,
>;
/// The identities of memberships to expire at a given block.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn memberships_expire_on)] #[pallet::getter(fn memberships_expire_on)]
pub type MembershipsExpireOn<T: Config<I>, I: 'static = ()> = pub type MembershipsExpireOn<T: Config> =
StorageMap<_, Twox64Concat, T::BlockNumber, Vec<T::IdtyId>, ValueQuery>; StorageMap<_, Twox64Concat, BlockNumberFor<T>, Vec<T::IdtyId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn pending_membership)]
pub type PendingMembership<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::IdtyId, T::MetaData, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn pending_memberships_expire_on)]
pub type PendingMembershipsExpireOn<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::BlockNumber, Vec<T::IdtyId>, ValueQuery>;
// EVENTS // // EVENTS //
#[pallet::event] #[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> { pub enum Event<T: Config> {
/// A membership has acquired /// A membership was added.
/// [idty_id] MembershipAdded {
MembershipAcquired(T::IdtyId), member: T::IdtyId,
/// A membership has expired expire_on: BlockNumberFor<T>,
/// [idty_id] },
MembershipExpired(T::IdtyId), /// A membership was renewed.
/// A membership has renewed MembershipRenewed {
/// [idty_id] member: T::IdtyId,
MembershipRenewed(T::IdtyId), expire_on: BlockNumberFor<T>,
/// An identity requested membership },
/// [idty_id] /// A membership was removed.
MembershipRequested(T::IdtyId), MembershipRemoved {
/// A membership has revoked member: T::IdtyId,
/// [idty_id] reason: MembershipRemovalReason,
MembershipRevoked(T::IdtyId), },
/// A pending membership request has expired
/// [idty_id]
PendingMembershipExpired(T::IdtyId),
} }
// ERRORS// // ERRORS//
#[pallet::error] #[pallet::error]
pub enum Error<T, I = ()> { pub enum Error<T> {
/// Invalid meta data /// Membership not found, can not renew.
InvalidMetaData,
/// Identity id not found
IdtyIdNotFound,
/// Membership already acquired
MembershipAlreadyAcquired,
/// Membership already requested
MembershipAlreadyRequested,
/// Membership not found
MembershipNotFound, MembershipNotFound,
/// Origin not allowed to use this identity /// Already member, can not add membership.
OriginNotAllowedToUseIdty, AlreadyMember,
/// Membership request not found
MembershipRequestNotFound,
} }
// HOOKS // // HOOKS //
#[pallet::hooks] #[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> { impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: T::BlockNumber) -> Weight { fn on_initialize(n: BlockNumberFor<T>) -> Weight {
if n > T::BlockNumber::zero() { if n > BlockNumberFor::<T>::zero() {
Self::expire_pending_memberships(n) + Self::expire_memberships(n) T::WeightInfo::on_initialize().saturating_add(Self::expire_memberships(n))
} else { } else {
Weight::zero() T::WeightInfo::on_initialize()
} }
} }
} }
// CALLS // // // CALLS //
// #[pallet::call]
#[pallet::call] // impl<T: Config> Pallet<T> {
impl<T: Config<I>, I: 'static> Pallet<T, I> { // // no calls for membership pallet
#[pallet::weight(1_000_000_000)] // }
pub fn force_request_membership(
origin: OriginFor<T>,
idty_id: T::IdtyId,
metadata: T::MetaData,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
Self::do_request_membership(idty_id, metadata)
}
#[pallet::weight(1_000_000_000)] // INTERNAL FUNCTIONS //
pub fn request_membership( impl<T: Config> Pallet<T> {
origin: OriginFor<T>, /// Unschedules membership expiry.
metadata: T::MetaData, fn unschedule_membership_expiry(idty_id: T::IdtyId, block_number: BlockNumberFor<T>) {
) -> DispatchResultWithPostInfo { let mut scheduled = MembershipsExpireOn::<T>::get(block_number);
let who = ensure_signed(origin)?;
let idty_id = T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T, I>::IdtyIdNotFound)?; if let Some(pos) = scheduled.iter().position(|x| *x == idty_id) {
if !metadata.validate(&who) { scheduled.swap_remove(pos);
return Err(Error::<T, I>::InvalidMetaData.into()); MembershipsExpireOn::<T>::set(block_number, scheduled);
} }
T::CheckCallAllowed::check_idty_allowed_to_request_membership(&idty_id)?;
Self::do_request_membership(idty_id, metadata)
} }
#[pallet::weight(1_000_000_000)] /// Insert membership and schedule its expiry.
pub fn claim_membership( fn insert_membership_and_schedule_expiry(idty_id: T::IdtyId) -> BlockNumberFor<T> {
origin: OriginFor<T>, let block_number = frame_system::pallet::Pallet::<T>::block_number();
maybe_idty_id: Option<T::IdtyId>, let expire_on = block_number + T::MembershipPeriod::get();
) -> DispatchResultWithPostInfo {
// Verify phase
let idty_id = Self::ensure_origin_and_get_idty_id(origin, maybe_idty_id)?;
ensure!(
!Membership::<T, I>::contains_key(idty_id),
Error::<T, I>::MembershipAlreadyAcquired
);
T::CheckCallAllowed::check_idty_allowed_to_claim_membership(&idty_id)?;
let metadata = PendingMembership::<T, I>::take(idty_id)
.ok_or(Error::<T, I>::MembershipRequestNotFound)?;
// Apply phase
Self::do_renew_membership_inner(idty_id);
Self::deposit_event(Event::MembershipAcquired(idty_id));
T::OnEvent::on_event(&sp_membership::Event::MembershipAcquired(idty_id, metadata));
Ok(().into()) Membership::<T>::insert(idty_id, MembershipData { expire_on });
MembershipsExpireOn::<T>::append(expire_on, idty_id);
expire_on
} }
#[pallet::weight(1_000_000_000)] /// Check if membership can be claimed.
pub fn renew_membership( pub fn check_add_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
origin: OriginFor<T>, // no-op is error
maybe_idty_id: Option<T::IdtyId>,
) -> DispatchResultWithPostInfo {
// Verify phase
let idty_id = Self::ensure_origin_and_get_idty_id(origin, maybe_idty_id)?;
ensure!( ensure!(
Self::get_membership(&idty_id).is_some(), Membership::<T>::get(idty_id).is_none(),
Error::<T, I>::MembershipNotFound Error::<T>::AlreadyMember
); );
T::CheckCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?; // check status and enough certifications
T::CheckMembershipOpAllowed::check_add_membership(idty_id)?;
let _ = Self::do_renew_membership(idty_id); Ok(())
Ok(().into())
} }
#[pallet::weight(1_000_000_000)] /// Check if membership renewal is allowed.
pub fn revoke_membership( pub fn check_renew_membership(
origin: OriginFor<T>, idty_id: T::IdtyId,
maybe_idty_id: Option<T::IdtyId>, ) -> Result<MembershipData<BlockNumberFor<T>>, DispatchError> {
) -> DispatchResultWithPostInfo { let membership_data =
// Verify phase Membership::<T>::get(idty_id).ok_or(Error::<T>::MembershipNotFound)?;
let idty_id = Self::ensure_origin_and_get_idty_id(origin, maybe_idty_id)?;
// Apply phase // enough certifications
if Self::remove_membership(&idty_id) { T::CheckMembershipOpAllowed::check_renew_membership(idty_id)?;
Self::deposit_event(Event::MembershipRevoked(idty_id)); Ok(membership_data)
T::OnEvent::on_event(&sp_membership::Event::MembershipRevoked(idty_id));
} }
Ok(().into()) /// Attempt to add membership.
} pub fn try_add_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
Self::check_add_membership(idty_id)?;
Self::do_add_membership(idty_id);
Ok(())
} }
// INTERNAL FUNCTIONS // /// Attempt to renew membership.
pub fn try_renew_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
impl<T: Config<I>, I: 'static> Pallet<T, I> { let membership_data = Self::check_renew_membership(idty_id)?;
pub(super) fn do_renew_membership(idty_id: T::IdtyId) -> Weight { Self::do_renew_membership(idty_id, membership_data);
let total_weight = Self::do_renew_membership_inner(idty_id); Ok(())
Self::deposit_event(Event::MembershipRenewed(idty_id));
T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id));
total_weight
} }
fn do_renew_membership_inner(idty_id: T::IdtyId) -> Weight {
let block_number = frame_system::pallet::Pallet::<T>::block_number();
let expire_on = block_number + T::MembershipPeriod::get();
Self::insert_membership(idty_id, MembershipData { expire_on }); /// Perform membership addition.
MembershipsExpireOn::<T, I>::append(expire_on, idty_id); fn do_add_membership(idty_id: T::IdtyId) {
Weight::zero() let expire_on = Self::insert_membership_and_schedule_expiry(idty_id);
} Self::deposit_event(Event::MembershipAdded {
fn do_request_membership( member: idty_id,
idty_id: T::IdtyId, expire_on,
metadata: T::MetaData, });
) -> DispatchResultWithPostInfo { T::OnNewMembership::on_created(&idty_id);
if PendingMembership::<T, I>::contains_key(idty_id) {
return Err(Error::<T, I>::MembershipAlreadyRequested.into());
}
if Membership::<T, I>::contains_key(idty_id) {
return Err(Error::<T, I>::MembershipAlreadyAcquired.into());
} }
let block_number = frame_system::pallet::Pallet::<T>::block_number(); /// Perform membership renewal.
let expire_on = block_number + T::PendingMembershipPeriod::get(); fn do_renew_membership(
idty_id: T::IdtyId,
PendingMembership::<T, I>::insert(idty_id, metadata); membership_data: MembershipData<BlockNumberFor<T>>,
PendingMembershipsExpireOn::<T, I>::append(expire_on, idty_id); ) {
Self::deposit_event(Event::MembershipRequested(idty_id)); Self::unschedule_membership_expiry(idty_id, membership_data.expire_on);
T::OnEvent::on_event(&sp_membership::Event::MembershipRequested(idty_id)); let expire_on = Self::insert_membership_and_schedule_expiry(idty_id);
Self::deposit_event(Event::MembershipRenewed {
Ok(().into()) member: idty_id,
} expire_on,
fn ensure_origin_and_get_idty_id( });
origin: OriginFor<T>, T::OnNewMembership::on_renewed(&idty_id);
maybe_idty_id: Option<T::IdtyId>,
) -> Result<T::IdtyId, DispatchError> {
match origin.into() {
Ok(RawOrigin::Root) => {
maybe_idty_id.ok_or_else(|| Error::<T, I>::IdtyIdNotFound.into())
}
Ok(RawOrigin::Signed(account_id)) => T::IdtyIdOf::convert(account_id)
.ok_or_else(|| Error::<T, I>::IdtyIdNotFound.into()),
_ => Err(BadOrigin.into()),
}
} }
fn expire_memberships(block_number: T::BlockNumber) -> Weight {
let mut total_weight: Weight = Weight::zero();
for idty_id in MembershipsExpireOn::<T, I>::take(block_number) { /// Perform membership removal.
if let Some(member_data) = Self::get_membership(&idty_id) { pub fn do_remove_membership(idty_id: T::IdtyId, reason: MembershipRemovalReason) -> Weight {
if member_data.expire_on == block_number { let mut weight = T::DbWeight::get().reads_writes(2, 3);
Self::remove_membership(&idty_id); if let Some(membership_data) = Membership::<T>::take(idty_id) {
Self::deposit_event(Event::MembershipExpired(idty_id)); Self::unschedule_membership_expiry(idty_id, membership_data.expire_on);
total_weight += Self::deposit_event(Event::MembershipRemoved {
T::OnEvent::on_event(&sp_membership::Event::MembershipExpired(idty_id)); member: idty_id,
} reason,
});
weight += T::OnRemoveMembership::on_removed(&idty_id);
} }
weight
} }
total_weight /// Perform membership expiry scheduled at the given block number.
} pub fn expire_memberships(block_number: BlockNumberFor<T>) -> Weight {
fn expire_pending_memberships(block_number: T::BlockNumber) -> Weight { let mut expired_idty_count = 0u32;
let mut total_weight: Weight = Weight::zero();
for idty_id in PendingMembershipsExpireOn::<T, I>::take(block_number) { for idty_id in MembershipsExpireOn::<T>::take(block_number) {
if PendingMembership::<T, I>::take(idty_id).is_some() { // remove membership (take)
Self::deposit_event(Event::PendingMembershipExpired(idty_id)); Self::do_remove_membership(idty_id, MembershipRemovalReason::Expired);
total_weight += T::OnEvent::on_event( expired_idty_count += 1;
&sp_membership::Event::PendingMembershipExpired(idty_id),
);
}
} }
T::WeightInfo::expire_memberships(expired_idty_count)
total_weight
} }
pub(super) fn is_member_inner(idty_id: &T::IdtyId) -> bool { /// Check if an identity is a member.
Membership::<T, I>::contains_key(idty_id) pub fn is_member(idty_id: &T::IdtyId) -> bool {
} Membership::<T>::contains_key(idty_id)
fn insert_membership(idty_id: T::IdtyId, membership_data: MembershipData<T::BlockNumber>) {
Membership::<T, I>::insert(idty_id, membership_data);
}
fn get_membership(idty_id: &T::IdtyId) -> Option<MembershipData<T::BlockNumber>> {
Membership::<T, I>::try_get(idty_id).ok()
}
fn remove_membership(idty_id: &T::IdtyId) -> bool {
Membership::<T, I>::take(idty_id).is_some()
} }
} }
} }
impl<T: Config<I>, I: 'static> IsInPendingMemberships<T::IdtyId> for Pallet<T, I> { // implement traits
fn is_in_pending_memberships(idty_id: T::IdtyId) -> bool {
PendingMembership::<T, I>::contains_key(idty_id)
}
}
impl<T: Config<I>, I: 'static> sp_runtime::traits::IsMember<T::IdtyId> for Pallet<T, I> { impl<T: Config> sp_runtime::traits::IsMember<T::IdtyId> for Pallet<T> {
fn is_member(idty_id: &T::IdtyId) -> bool { fn is_member(idty_id: &T::IdtyId) -> bool {
Self::is_member_inner(idty_id) Self::is_member(idty_id)
} }
} }
impl<T: Config<I>, I: 'static> MembersCount for Pallet<T, I> { impl<T: Config> MembersCount for Pallet<T> {
fn members_count() -> u32 { fn members_count() -> u32 {
Membership::<T, I>::count() Membership::<T>::count()
} }
} }
...@@ -16,14 +16,13 @@ ...@@ -16,14 +16,13 @@
use crate::{self as pallet_membership}; use crate::{self as pallet_membership};
use frame_support::{ use frame_support::{
parameter_types, derive_impl, parameter_types,
traits::{Everything, OnFinalize, OnInitialize}, traits::{Everything, OnFinalize, OnInitialize},
}; };
use frame_system as system; use frame_system as system;
use sp_core::H256; use sp_core::H256;
use sp_runtime::{ use sp_runtime::{
testing::Header, traits::{BlakeTwo256, IdentityLookup},
traits::{BlakeTwo256, ConvertInto, IdentityLookup},
BuildStorage, BuildStorage,
}; };
...@@ -31,17 +30,12 @@ type AccountId = u64; ...@@ -31,17 +30,12 @@ type AccountId = u64;
type BlockNumber = u64; type BlockNumber = u64;
type Block = frame_system::mocking::MockBlock<Test>; type Block = frame_system::mocking::MockBlock<Test>;
pub type IdtyId = u64; pub type IdtyId = u64;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
// Configure a mock runtime to test the pallet. // Configure a mock runtime to test the pallet.
frame_support::construct_runtime!( frame_support::construct_runtime!(
pub enum Test where pub enum Test{
Block = Block, System: frame_system,
NodeBlock = Block, Membership: pallet_membership,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
DefaultMembership: pallet_membership::{Pallet, Call, Event<T>, Storage, Config<T>},
} }
); );
...@@ -50,54 +44,48 @@ parameter_types! { ...@@ -50,54 +44,48 @@ parameter_types! {
pub const SS58Prefix: u8 = 42; pub const SS58Prefix: u8 = 42;
} }
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl system::Config for Test { impl system::Config for Test {
type AccountId = AccountId;
type BaseCallFilter = Everything; type BaseCallFilter = Everything;
type BlockWeights = (); type Block = Block;
type BlockLength = (); type BlockHashCount = BlockHashCount;
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Index = u64;
type BlockNumber = BlockNumber;
type Hash = H256; type Hash = H256;
type Hashing = BlakeTwo256; type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>; type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header; type MaxConsumers = frame_support::traits::ConstU32<16>;
type RuntimeEvent = RuntimeEvent; type Nonce = u64;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo; type PalletInfo = PalletInfo;
type AccountData = (); type RuntimeCall = RuntimeCall;
type OnNewAccount = (); type RuntimeEvent = RuntimeEvent;
type OnKilledAccount = (); type RuntimeOrigin = RuntimeOrigin;
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix; type SS58Prefix = SS58Prefix;
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
} }
parameter_types! { parameter_types! {
pub const MembershipPeriod: BlockNumber = 5; pub const MembershipPeriod: BlockNumber = 5;
pub const PendingMembershipPeriod: BlockNumber = 3; pub const MembershipRenewalPeriod: BlockNumber = 2;
} }
impl pallet_membership::Config for Test { impl pallet_membership::Config for Test {
type CheckCallAllowed = (); #[cfg(feature = "runtime-benchmarks")]
type BenchmarkSetupHandler = ();
type CheckMembershipOpAllowed = ();
type IdtyAttr = ();
type IdtyId = IdtyId; type IdtyId = IdtyId;
type IdtyIdOf = ConvertInto;
type MembershipPeriod = MembershipPeriod; type MembershipPeriod = MembershipPeriod;
type MetaData = (); type MembershipRenewalPeriod = MembershipRenewalPeriod;
type OnEvent = (); type OnNewMembership = ();
type PendingMembershipPeriod = PendingMembershipPeriod; type OnRemoveMembership = ();
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
} }
// Build genesis storage according to the mock runtime. // Build genesis storage according to the mock runtime.
pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::TestExternalities { pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::TestExternalities {
GenesisConfig { RuntimeGenesisConfig {
system: SystemConfig::default(), system: SystemConfig::default(),
default_membership: gen_conf, membership: gen_conf,
} }
.build_storage() .build_storage()
.unwrap() .unwrap()
...@@ -106,11 +94,11 @@ pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io:: ...@@ -106,11 +94,11 @@ pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::
pub fn run_to_block(n: u64) { pub fn run_to_block(n: u64) {
while System::block_number() < n { while System::block_number() < n {
DefaultMembership::on_finalize(System::block_number()); Membership::on_finalize(System::block_number());
System::on_finalize(System::block_number()); System::on_finalize(System::block_number());
System::reset_events(); System::reset_events();
System::set_block_number(System::block_number() + 1); System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number()); System::on_initialize(System::block_number());
DefaultMembership::on_initialize(System::block_number()); Membership::on_initialize(System::block_number());
} }
} }
...@@ -14,16 +14,13 @@ ...@@ -14,16 +14,13 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::mock::*; use crate::{mock::*, Error, Event, MembershipRemovalReason};
use crate::{Error, Event}; use frame_support::{assert_noop, assert_ok};
use frame_support::assert_ok;
use maplit::btreemap; use maplit::btreemap;
use sp_membership::traits::*; use sp_membership::{traits::*, MembershipData};
use sp_membership::MembershipData;
use sp_runtime::traits::IsMember;
fn default_gen_conf() -> DefaultMembershipConfig { fn default_gen_conf() -> MembershipConfig {
DefaultMembershipConfig { MembershipConfig {
memberships: btreemap![ memberships: btreemap![
0 => MembershipData { 0 => MembershipData {
expire_on: 3, expire_on: 3,
...@@ -38,166 +35,127 @@ fn test_genesis_build() { ...@@ -38,166 +35,127 @@ fn test_genesis_build() {
run_to_block(1); run_to_block(1);
// Verify state // Verify state
assert_eq!( assert_eq!(
DefaultMembership::membership(0), Membership::membership(0),
Some(MembershipData { expire_on: 3 }) Some(MembershipData { expire_on: 3 })
); );
assert_eq!(DefaultMembership::members_count(), 1); assert_eq!(Membership::members_count(), 1);
}); });
} }
/// test membership expiration
// membership should expire
#[test] #[test]
fn test_membership_already_acquired() { fn test_membership_expiration() {
new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1);
// Merbership 0 cannot be reclaimed
assert_eq!(
DefaultMembership::claim_membership(RuntimeOrigin::signed(0), None),
Err(Error::<Test, _>::MembershipAlreadyAcquired.into())
);
});
}
#[test]
fn test_membership_request_not_found() {
new_test_ext(default_gen_conf()).execute_with(|| { new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1); // Membership 0 should not expired on block #2
// Merbership 0 cannot be reclaimed run_to_block(2);
assert_eq!( assert!(Membership::is_member(&0));
DefaultMembership::claim_membership(RuntimeOrigin::signed(1), None), // Membership 0 should expire on block #3
Err(Error::<Test, _>::MembershipRequestNotFound.into()) run_to_block(3);
); assert!(!Membership::is_member(&0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
member: 0,
reason: MembershipRemovalReason::Expired,
}));
}); });
} }
/// test membership renewal (triggered automatically after distance evaluation)
#[test] #[test]
fn test_membership_renewal() { fn test_membership_renewal() {
new_test_ext(default_gen_conf()).execute_with(|| { new_test_ext(default_gen_conf()).execute_with(|| {
// membership still valid at block 2
run_to_block(2); run_to_block(2);
// Merbership 0 can be renewable on block #2 assert!(Membership::is_member(&0));
assert_ok!(DefaultMembership::renew_membership( // Membership 0 can be renewed
RuntimeOrigin::signed(0), assert_ok!(Membership::try_renew_membership(0));
None System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRenewed {
),); member: 0,
assert_eq!( expire_on: 2 + <Test as crate::Config>::MembershipPeriod::get(),
System::events()[0].event, }));
RuntimeEvent::DefaultMembership(Event::MembershipRenewed(0)) // membership should not expire at block 3 to 6 because it has been renewed
); run_to_block(3);
assert!(Membership::is_member(&0));
run_to_block(6);
assert!(Membership::is_member(&0));
// membership should expire at block 7 (2+5)
run_to_block(7);
assert!(!Membership::is_member(&0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
member: 0,
reason: MembershipRemovalReason::Expired,
}));
}); });
} }
/// test membership renewal for non member identity
#[test] #[test]
fn test_membership_expiration() { fn test_membership_renewal_nope() {
new_test_ext(default_gen_conf()).execute_with(|| { new_test_ext(default_gen_conf()).execute_with(|| {
// Merbership 0 should not expired on block #2
run_to_block(2); run_to_block(2);
assert!(DefaultMembership::is_member(&0),); assert!(!Membership::is_member(&1));
// Merbership 0 should expire on block #3 // Membership 1 can not be renewed
run_to_block(3); assert_noop!(
assert!(!DefaultMembership::is_member(&0),); Membership::try_renew_membership(1),
assert_eq!( Error::<Test>::MembershipNotFound,
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipExpired(0))
); );
run_to_block(3);
assert!(!Membership::is_member(&1));
}); });
} }
/// test membership revocation
#[test] #[test]
fn test_membership_revocation() { fn test_membership_revocation() {
new_test_ext(default_gen_conf()).execute_with(|| { new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1); run_to_block(1);
// Merbership 0 can be revocable on block #1 // Membership 0 can be revocable on block #1
assert_ok!(DefaultMembership::revoke_membership( Membership::do_remove_membership(0, MembershipRemovalReason::Revoked);
RuntimeOrigin::signed(0), System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
None member: 0,
),); reason: MembershipRemovalReason::Revoked,
assert_eq!( }));
System::events()[0].event, assert_eq!(Membership::membership(0), None);
RuntimeEvent::DefaultMembership(Event::MembershipRevoked(0))
); // Membership 0 can re-claim membership
// Membership 0 can re-request membership
run_to_block(5); run_to_block(5);
assert_ok!(DefaultMembership::request_membership( assert_ok!(Membership::try_add_membership(0));
RuntimeOrigin::signed(0), System::assert_has_event(RuntimeEvent::Membership(Event::MembershipAdded {
() member: 0,
),); expire_on: 5 + <Test as crate::Config>::MembershipPeriod::get(),
assert_eq!( }));
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
);
}); });
} }
#[test] /// test membership workflow
fn test_pending_membership_expiration() { // - claim membership
new_test_ext(Default::default()).execute_with(|| { // - renew membership
// Idty 0 request membership // - membership expiry
run_to_block(1);
assert_ok!(DefaultMembership::request_membership(
RuntimeOrigin::signed(0),
()
),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
);
// Then, idty 0 shold still in pending memberships until PendingMembershipPeriod ended
run_to_block(PendingMembershipPeriod::get());
assert!(DefaultMembership::is_in_pending_memberships(0),);
// Then, idty 0 request should expire after PendingMembershipPeriod
run_to_block(1 + PendingMembershipPeriod::get());
assert!(!DefaultMembership::is_in_pending_memberships(0),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::PendingMembershipExpired(0))
);
})
}
#[test] #[test]
fn test_membership_workflow() { fn test_membership_workflow() {
new_test_ext(Default::default()).execute_with(|| { new_test_ext(Default::default()).execute_with(|| {
// Idty 0 request membership // - Then, idty 0 claim membership
run_to_block(1);
assert_ok!(DefaultMembership::request_membership(
RuntimeOrigin::signed(0),
()
),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
);
// Then, idty 0 claim membership
run_to_block(2); run_to_block(2);
assert_ok!(DefaultMembership::claim_membership( assert_ok!(Membership::try_add_membership(0));
RuntimeOrigin::signed(0), System::assert_has_event(RuntimeEvent::Membership(Event::MembershipAdded {
None member: 0,
),); expire_on: 2 + <Test as crate::Config>::MembershipPeriod::get(),
assert_eq!( }));
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipAcquired(0))
);
// Then, idty 0 claim renewal, should success // - Then, idty 0 claim renewal, should success
run_to_block(2); run_to_block(2);
assert_ok!(DefaultMembership::renew_membership( assert_ok!(Membership::try_renew_membership(0));
RuntimeOrigin::signed(0),
None // idty 0 should still be member until membership period ended
),); run_to_block(6); // 2 + 5 - 1
assert!(Membership::is_member(&0));
// Then, idty 0 shoul still member until membership period ended
run_to_block(2 + MembershipPeriod::get() - 1); // - Then, idty 0 should expire after membership period
assert!(DefaultMembership::is_member(&0)); run_to_block(7); // 2 + 5
assert!(!Membership::is_member(&0));
// Then, idty 0 shoul expire after membership period System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
run_to_block(2 + MembershipPeriod::get()); member: 0,
assert!(!DefaultMembership::is_member(&0),); reason: MembershipRemovalReason::Expired,
assert_eq!( }));
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipExpired(0))
);
}); });
} }
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::unnecessary_cast)]
use frame_support::weights::{constants::RocksDbWeight, Weight};
/// Weight functions needed for pallet_universal_dividend.
pub trait WeightInfo {
fn on_initialize() -> Weight;
fn expire_memberships(_i: u32) -> Weight;
}
// Insecure weights implementation, use it for tests only!
impl WeightInfo for () {
fn on_initialize() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 4_012_000 picoseconds.
Weight::from_parts(4_629_000, 0).saturating_add(Weight::from_parts(0, 0))
}
fn expire_memberships(i: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `567 + i * (23 ±0)`
// Estimated: `6583 + i * (2499 ±0)`
// Minimum execution time: 86_925_000 picoseconds.
Weight::from_parts(89_056_000, 0)
.saturating_add(Weight::from_parts(0, 6583))
// Standard Error: 2_429_589
.saturating_add(Weight::from_parts(295_368_241, 0).saturating_mul(i.into()))
.saturating_add(RocksDbWeight::get().reads(3))
.saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(i.into())))
.saturating_add(RocksDbWeight::get().writes(5))
.saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(i.into())))
.saturating_add(Weight::from_parts(0, 2499).saturating_mul(i.into()))
}
}
[package]
name = "pallet-offences"
authors.workspace = true
description = "duniter pallet to handle offences. fork from paritytechnologies offences pallet"
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, features = ["derive"] }
log = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-runtime = { workspace = true }
sp-staking = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"frame-support/std",
"frame-system/std",
"log/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-staking/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
]
[dev-dependencies]
sp-core = { workspace = true, default-features = true }
sp-io = { workspace = true, default-features = true }
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Duniter Offences Pallet
//!
//! This pallet is a fork of the Substrate `offences` pallet, customized to align with the offence rules specified by the `authority-member` pallet rather than the Substrate `staking` pallet.
//!
//! ## Offences Processing
//!
//! The Duniter Offences Pallet manages various types of offences as follows:
//!
//! - **`im-online` Pallet Offences**: Offences from the `im-online` pallet necessitate disconnection of the offender.
//!
//! - **Other Offences**: For all other offences, the pallet enforces:
//! - Disconnection of the offender.
//! - Addition of the offender to a blacklist.
//! - Authorization from a designated origin to remove offenders from the blacklist.
//!
//! ## Offences Triage and Slashing Execution
//!
//! This pallet handles the triage of offences, categorizing them based on predefined rules. The actual execution of slashing and other punitive measures is delegated to the `authority-member` pallet.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
use core::marker::PhantomData;
use codec::Encode;
use frame_support::weights::Weight;
use scale_info::prelude::vec::Vec;
use sp_runtime::traits::Hash;
use sp_staking::offence::{Kind, Offence, OffenceDetails, OffenceError, ReportOffence};
pub use pallet::*;
pub mod traits;
use self::traits::*;
/// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`.
type OpaqueTimeSlot = Vec<u8>;
/// A type alias for a report identifier.
type ReportIdOf<T> = <T as frame_system::Config>::Hash;
pub enum SlashStrategy {
Disconnect,
Blacklist,
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
/// The pallet's config trait.
#[pallet::config]
pub trait Config: frame_system::Config {
/// The overarching event type.
type RuntimeEvent: From<Event> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Full identification of the validator.
type IdentificationTuple: Parameter;
/// A handler called for every offence report.
type OnOffenceHandler: OnOffenceHandler<Self::AccountId, Self::IdentificationTuple, Weight>;
}
/// The primary structure that holds all offence records keyed by report identifiers.
#[pallet::storage]
#[pallet::getter(fn reports)]
pub type Reports<T: Config> = StorageMap<
_,
Twox64Concat,
ReportIdOf<T>,
OffenceDetails<T::AccountId, T::IdentificationTuple>,
>;
/// A vector of reports of the same kind that happened at the same time slot.
#[pallet::storage]
pub type ConcurrentReportsIndex<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
Kind,
Twox64Concat,
OpaqueTimeSlot,
Vec<ReportIdOf<T>>,
ValueQuery,
>;
/// Events type.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event {
/// An offense was reported during the specified time slot. This event is not deposited for duplicate slashes.
Offence {
kind: Kind,
timeslot: OpaqueTimeSlot,
},
}
}
impl<T, O> ReportOffence<T::AccountId, T::IdentificationTuple, O> for Pallet<T>
where
T: Config,
O: Offence<T::IdentificationTuple>,
{
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
let offenders = offence.offenders();
let time_slot = offence.time_slot();
// Go through all offenders in the offence report and find all offenders that were spotted
// in unique reports.
let TriageOutcome {
concurrent_offenders,
} = match Self::triage_offence_report::<O>(reporters, &time_slot, offenders) {
Some(triage) => triage,
None => return Err(OffenceError::DuplicateReport),
};
// Define the slash strategy.
let slash_strategy = if O::ID == *b"im-online:offlin" {
SlashStrategy::Disconnect
} else {
SlashStrategy::Blacklist
};
T::OnOffenceHandler::on_offence(
&concurrent_offenders,
slash_strategy,
offence.session_index(),
);
Self::deposit_event(Event::Offence {
kind: O::ID,
timeslot: time_slot.encode(),
});
Ok(())
}
fn is_known_offence(offenders: &[T::IdentificationTuple], time_slot: &O::TimeSlot) -> bool {
let any_unknown = offenders.iter().any(|offender| {
let report_id = Self::report_id::<O>(time_slot, offender);
!<Reports<T>>::contains_key(report_id)
});
!any_unknown
}
}
impl<T: Config> Pallet<T> {
/// Compute the ID for the given report properties.
///
/// The report id depends on the offence kind, time slot and the id of offender.
fn report_id<O: Offence<T::IdentificationTuple>>(
time_slot: &O::TimeSlot,
offender: &T::IdentificationTuple,
) -> ReportIdOf<T> {
(O::ID, time_slot.encode(), offender).using_encoded(T::Hashing::hash)
}
/// Triages the offence report and returns the set of offenders that was involved in unique
/// reports along with the list of the concurrent offences.
fn triage_offence_report<O: Offence<T::IdentificationTuple>>(
reporters: Vec<T::AccountId>,
time_slot: &O::TimeSlot,
offenders: Vec<T::IdentificationTuple>,
) -> Option<TriageOutcome<T>> {
let mut storage = ReportIndexStorage::<T, O>::load(time_slot);
let mut any_new = false;
for offender in offenders {
let report_id = Self::report_id::<O>(time_slot, &offender);
if !<Reports<T>>::contains_key(report_id) {
any_new = true;
<Reports<T>>::insert(
report_id,
OffenceDetails {
offender,
reporters: reporters.clone(),
},
);
storage.insert(report_id);
}
}
if any_new {
// Load report details for the all reports happened at the same time.
let concurrent_offenders = storage
.concurrent_reports
.iter()
.filter_map(<Reports<T>>::get)
.collect::<Vec<_>>();
storage.save();
Some(TriageOutcome {
concurrent_offenders,
})
} else {
None
}
}
}
struct TriageOutcome<T: Config> {
/// Other reports for the same report kinds.
concurrent_offenders: Vec<OffenceDetails<T::AccountId, T::IdentificationTuple>>,
}
/// An auxiliary struct for working with storage of indexes localized for a specific offence
/// kind (specified by the `O` type parameter).
///
/// This struct is responsible for aggregating storage writes and the underlying storage should not
/// accessed directly meanwhile.
#[must_use = "The changes are not saved without called `save`"]
struct ReportIndexStorage<T: Config, O: Offence<T::IdentificationTuple>> {
opaque_time_slot: OpaqueTimeSlot,
concurrent_reports: Vec<ReportIdOf<T>>,
_phantom: PhantomData<O>,
}
impl<T: Config, O: Offence<T::IdentificationTuple>> ReportIndexStorage<T, O> {
/// Preload indexes from the storage for the specific `time_slot` and the kind of the offence.
fn load(time_slot: &O::TimeSlot) -> Self {
let opaque_time_slot = time_slot.encode();
let concurrent_reports = <ConcurrentReportsIndex<T>>::get(O::ID, &opaque_time_slot);
Self {
opaque_time_slot,
concurrent_reports,
_phantom: Default::default(),
}
}
/// Insert a new report to the index.
fn insert(&mut self, report_id: ReportIdOf<T>) {
// Update the list of concurrent reports.
self.concurrent_reports.push(report_id);
}
/// Dump the indexes to the storage.
fn save(self) {
<ConcurrentReportsIndex<T>>::insert(
O::ID,
&self.opaque_time_slot,
&self.concurrent_reports,
);
}
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::{self as pallet_offences, Config, SlashStrategy};
use codec::Encode;
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU32, ConstU64},
weights::{constants::RocksDbWeight, Weight},
};
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage, Perbill,
};
use sp_staking::{
offence::{Kind, OffenceDetails},
SessionIndex,
};
pub struct OnOffenceHandler;
parameter_types! {
pub static OnOffencePerbill: Vec<Perbill> = Default::default();
pub static OffenceWeight: Weight = Default::default();
}
impl<Reporter, Offender> pallet_offences::OnOffenceHandler<Reporter, Offender, Weight>
for OnOffenceHandler
{
fn on_offence(
_offenders: &[OffenceDetails<Reporter, Offender>],
_strategy: SlashStrategy,
_offence_session: SessionIndex,
) -> Weight {
OffenceWeight::get()
}
}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub struct Runtime {
System: frame_system,
Offences: pallet_offences,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type AccountId = u64;
type BaseCallFilter = frame_support::traits::Everything;
type Block = Block;
type BlockHashCount = ConstU64<250>;
type DbWeight = RocksDbWeight;
type Hash = H256;
type Hashing = BlakeTwo256;
type Lookup = IdentityLookup<Self::AccountId>;
type MaxConsumers = ConstU32<16>;
type Nonce = u64;
type PalletInfo = PalletInfo;
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
type RuntimeOrigin = RuntimeOrigin;
}
impl Config for Runtime {
type IdentificationTuple = u64;
type OnOffenceHandler = OnOffenceHandler;
type RuntimeEvent = RuntimeEvent;
}
pub fn new_test_ext() -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::<Runtime>::default()
.build_storage()
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
pub const KIND: [u8; 16] = *b"test_report_1234";
/// Returns all offence details for the specific `kind` happened at the specific time slot.
pub fn offence_reports(kind: Kind, time_slot: u128) -> Vec<OffenceDetails<u64, u64>> {
<crate::ConcurrentReportsIndex<Runtime>>::get(kind, time_slot.encode())
.into_iter()
.map(|report_id| {
<crate::Reports<Runtime>>::get(report_id)
.expect("dangling report id is found in ConcurrentReportsIndex")
})
.collect()
}
#[derive(Clone)]
pub struct Offence {
pub validator_set_count: u32,
pub offenders: Vec<u64>,
pub time_slot: u128,
}
impl pallet_offences::Offence<u64> for Offence {
type TimeSlot = u128;
const ID: pallet_offences::Kind = KIND;
fn offenders(&self) -> Vec<u64> {
self.offenders.clone()
}
fn validator_set_count(&self) -> u32 {
self.validator_set_count
}
fn time_slot(&self) -> u128 {
self.time_slot
}
fn session_index(&self) -> SessionIndex {
1
}
fn slash_fraction(&self, offenders_count: u32) -> Perbill {
Perbill::from_percent(5 + offenders_count * 100 / self.validator_set_count)
}
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use crate::mock::{new_test_ext, offence_reports, Offence, Offences, RuntimeEvent, System, KIND};
use frame_system::{EventRecord, Phase};
#[test]
fn should_report_an_authority_and_trigger_on_offence_and_add_to_blacklist() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5, 9],
};
// when
Offences::report_offence(vec![], offence).unwrap();
// then
assert_eq!(
offence_reports(KIND, time_slot),
vec![
OffenceDetails {
offender: 5,
reporters: vec![]
},
OffenceDetails {
offender: 9,
reporters: vec![]
}
]
);
});
}
#[test]
fn should_not_report_the_same_authority_twice_in_the_same_slot() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
Offences::report_offence(vec![], offence.clone()).unwrap();
// when
// report for the second time
assert_eq!(
Offences::report_offence(vec![], offence),
Err(OffenceError::DuplicateReport)
);
// then
assert_eq!(
offence_reports(KIND, time_slot),
vec![OffenceDetails {
offender: 5,
reporters: vec![]
},]
);
});
}
#[test]
fn should_report_in_different_time_slot() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let mut offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
Offences::report_offence(vec![], offence.clone()).unwrap();
System::assert_last_event(
Event::Offence {
kind: KIND,
timeslot: time_slot.encode(),
}
.into(),
);
// when
// report for the second time
offence.time_slot += 1;
Offences::report_offence(vec![], offence.clone()).unwrap();
// then
System::assert_last_event(
Event::Offence {
kind: KIND,
timeslot: offence.time_slot.encode(),
}
.into(),
);
});
}
#[test]
fn should_deposit_event() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
// when
Offences::report_offence(vec![], offence).unwrap();
// then
assert_eq!(
System::events(),
vec![EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::Offences(crate::Event::Offence {
kind: KIND,
timeslot: time_slot.encode()
}),
topics: vec![],
}]
);
});
}
#[test]
fn doesnt_deposit_event_for_dups() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
Offences::report_offence(vec![], offence.clone()).unwrap();
// when
// report for the second time
assert_eq!(
Offences::report_offence(vec![], offence),
Err(OffenceError::DuplicateReport)
);
// then
// there is only one event.
assert_eq!(
System::events(),
vec![EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::Offences(crate::Event::Offence {
kind: KIND,
timeslot: time_slot.encode()
}),
topics: vec![],
}]
);
});
}
#[test]
fn reports_if_an_offence_is_dup() {
new_test_ext().execute_with(|| {
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = |time_slot, offenders| Offence {
validator_set_count: 5,
time_slot,
offenders,
};
let mut test_offence = offence(time_slot, vec![0]);
// the report for authority 0 at time slot 42 should not be a known
// offence
assert!(
!<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence.offenders,
&test_offence.time_slot
)
);
// we report an offence for authority 0 at time slot 42
Offences::report_offence(vec![], test_offence.clone()).unwrap();
// the same report should be a known offence now
assert!(
<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence.offenders,
&test_offence.time_slot
)
);
// and reporting it again should yield a duplicate report error
assert_eq!(
Offences::report_offence(vec![], test_offence.clone()),
Err(OffenceError::DuplicateReport)
);
// after adding a new offender to the offence report
test_offence.offenders.push(1);
// it should not be a known offence anymore
assert!(
!<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence.offenders,
&test_offence.time_slot
)
);
// and reporting it again should work without any error
assert_eq!(
Offences::report_offence(vec![], test_offence.clone()),
Ok(())
);
// creating a new offence for the same authorities on the next slot
// should be considered a new offence and thefore not known
let test_offence_next_slot = offence(time_slot + 1, vec![0, 1]);
assert!(
!<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence_next_slot.offenders,
&test_offence_next_slot.time_slot
)
);
});
}
#[test]
fn should_properly_count_offences() {
// We report two different authorities for the same issue. Ultimately, the 1st authority
// should have `count` equal 2 and the count of the 2nd one should be equal to 1.
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence1 = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
let offence2 = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![4],
};
Offences::report_offence(vec![], offence1).unwrap();
// when
// report for the second time
Offences::report_offence(vec![], offence2).unwrap();
// then
// the 1st authority should have count 2 and the 2nd one should be reported only once.
assert_eq!(
offence_reports(KIND, time_slot),
vec![
OffenceDetails {
offender: 5,
reporters: vec![]
},
OffenceDetails {
offender: 4,
reporters: vec![]
},
]
);
});
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S 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.
//
// Duniter-v2S 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 Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use sp_staking::{offence::OffenceDetails, SessionIndex};
use crate::SlashStrategy;
/// Trait for handling offences.
pub trait OnOffenceHandler<Reporter, Offender, Res> {
/// Handle an offence committed by one or more offenders.
fn on_offence(
offenders: &[OffenceDetails<Reporter, Offender>],
slash_strategy: SlashStrategy,
session: SessionIndex,
) -> Res;
}
[package] [package]
authors = ['librelois <c@elo.tf>'] authors.workspace = true
description = 'FRAME pallet oneshot account.' description = "duniter pallet oneshot account"
edition = '2018' edition.workspace = true
homepage = 'https://duniter.org' homepage.workspace = true
license = 'AGPL-3.0' license.workspace = true
name = 'pallet-oneshot-account' name = "pallet-oneshot-account"
readme = 'README.md' repository.workspace = true
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version.workspace = true
version = '3.0.0'
[features] [features]
default = ['std'] default = ["std"]
runtime-benchmarks = [ runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks", "frame-benchmarking/runtime-benchmarks",
"pallet-balances", "frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-transaction-payment/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/runtime-benchmarks",
"pallet-balances/try-runtime",
"pallet-transaction-payment/try-runtime",
"sp-runtime/try-runtime",
] ]
std = [ std = [
'codec/std', "codec/std",
'frame-support/std', "frame-benchmarking?/std",
'frame-system/std', "frame-support/std",
'frame-benchmarking/std', "frame-system/std",
'sp-core/std', "log/std",
'sp-io/std', "pallet-balances/std",
'sp-runtime/std', "pallet-transaction-payment/std",
'sp-std/std', "scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
] ]
try-runtime = ['frame-support/try-runtime']
[dependencies]
# crates.io
codec = { package = 'parity-scale-codec', version = "3.1.5", default-features = false, features = ["derive"] }
log = { version = "0.4.14", default-features = false }
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
# benchmarks
pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', optional = true, default-features = false }
# substrate
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/duniter/substrate'
optional = true
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-system]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.pallet-transaction-payment]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-core]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-io]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
### DOC ###
[package.metadata.docs.rs] [package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu'] default-features = false
targets = ["x86_64-unknown-linux-gnu"]
### DEV ###
[dev-dependencies.sp-io]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dev-dependencies.pallet-balances] [dependencies]
git = 'https://github.com/duniter/substrate' codec = { workspace = true, features = ["derive"] }
branch = 'duniter-substrate-v0.9.32' frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
log = { workspace = true }
pallet-balances = { workspace = true }
pallet-transaction-payment = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
pallet-balances = { workspace = true, default-features = true }
# Duniter oneshot account pallet
Duniter provides light accounts without `AccountInfo` (nonce, consumers, providers, sufficients, free, reserved, misc_frozen, fee_frozen) that can only be consumed once. This should reduce transaction weight and then fees. The use case is anonymous accounts or physical supports.
\ No newline at end of file
...@@ -15,110 +15,118 @@ ...@@ -15,110 +15,118 @@
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg(feature = "runtime-benchmarks")] #![cfg(feature = "runtime-benchmarks")]
#![allow(clippy::multiple_bound_locations)]
use super::*; use super::*;
use frame_benchmarking::{account, benchmarks, whitelisted_caller}; use frame_benchmarking::{account, v2::*, whitelisted_caller};
use frame_support::pallet_prelude::IsType; use frame_support::{pallet_prelude::IsType, traits::fungible::Mutate};
use frame_support::traits::Get;
use frame_system::RawOrigin; use frame_system::RawOrigin;
use pallet_balances::Pallet as Balances; use pallet_balances::Pallet as Balances;
use crate::Pallet; use crate::Pallet;
const SEED: u32 = 0; type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
benchmarks! { #[benchmarks(
where_clause { where where
T: pallet_balances::Config, T: pallet_balances::Config,
T::Balance: From<u64>, T::Balance: From<u64>,
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance> BalanceOf<T>: IsType<T::Balance>+From<T::Balance>
} )]
create_oneshot_account { mod benchmarks {
use super::*;
#[benchmark]
fn create_oneshot_account() {
let existential_deposit = T::ExistentialDeposit::get(); let existential_deposit = T::ExistentialDeposit::get();
let caller = whitelisted_caller(); let caller = whitelisted_caller();
// Give some multiple of the existential deposit
let balance = existential_deposit.saturating_mul((2).into()); let balance = existential_deposit.saturating_mul((2).into());
let _ = T::Currency::make_free_balance_be(&caller, balance.into()); let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
&caller,
let recipient: T::AccountId = account("recipient", 0, SEED); balance.into(),
);
let recipient: T::AccountId = account("recipient", 0, 1);
let recipient_lookup: <T::Lookup as StaticLookup>::Source = let recipient_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient.clone()); T::Lookup::unlookup(recipient.clone());
let transfer_amount = existential_deposit; let transfer_amount = existential_deposit;
}: _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount.into())
verify { #[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()),
recipient_lookup,
transfer_amount.into(),
);
assert_eq!(Balances::<T>::free_balance(&caller), transfer_amount); assert_eq!(Balances::<T>::free_balance(&caller), transfer_amount);
assert_eq!(OneshotAccounts::<T>::get(&recipient), Some(transfer_amount.into())); assert_eq!(
} OneshotAccounts::<T>::get(&recipient),
where_clause { where Some(transfer_amount.into())
T: pallet_balances::Config, );
T::Balance: From<u64>,
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>+From<T::Balance>
} }
consume_oneshot_account {
#[benchmark]
fn consume_oneshot_account() {
let existential_deposit = T::ExistentialDeposit::get(); let existential_deposit = T::ExistentialDeposit::get();
let caller: T::AccountId = whitelisted_caller(); let caller: T::AccountId = whitelisted_caller();
// Give some multiple of the existential deposit
let balance = existential_deposit.saturating_mul((2).into()); let balance = existential_deposit.saturating_mul((2).into());
OneshotAccounts::<T>::insert( OneshotAccounts::<T>::insert(caller.clone(), Into::<BalanceOf<T>>::into(balance));
caller.clone(),
Into::<<T::Currency as Currency<T::AccountId>>::Balance>::into(balance)
);
// Deposit into a normal account is more expensive than into a oneshot account // Deposit into a normal account is more expensive than into a oneshot account
// so we create the recipient account with an existential deposit. // so we create the recipient account with an existential deposit.
let recipient: T::AccountId = account("recipient", 0, SEED); let recipient: T::AccountId = account("recipient", 0, 1);
let recipient_lookup: <T::Lookup as StaticLookup>::Source = let recipient_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient.clone()); T::Lookup::unlookup(recipient.clone());
let _ = T::Currency::make_free_balance_be(&recipient, existential_deposit.into()); let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
}: _( &recipient,
existential_deposit.into(),
);
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()), RawOrigin::Signed(caller.clone()),
T::BlockNumber::zero(), BlockNumberFor::<T>::zero(),
Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient_lookup) Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient_lookup),
) );
verify {
assert_eq!(OneshotAccounts::<T>::get(&caller), None); assert_eq!(OneshotAccounts::<T>::get(&caller), None);
assert_eq!( assert_eq!(
Balances::<T>::free_balance(&recipient), Balances::<T>::free_balance(&recipient),
existential_deposit.saturating_mul((3).into()) existential_deposit.saturating_mul((3).into())
); );
} }
where_clause { where
T: pallet_balances::Config, #[benchmark]
T::Balance: From<u64>, fn consume_oneshot_account_with_remaining() {
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>+From<T::Balance>
}
consume_oneshot_account_with_remaining {
let existential_deposit = T::ExistentialDeposit::get(); let existential_deposit = T::ExistentialDeposit::get();
let caller: T::AccountId = whitelisted_caller(); let caller: T::AccountId = whitelisted_caller();
// Give some multiple of the existential deposit
let balance = existential_deposit.saturating_mul((2).into()); let balance = existential_deposit.saturating_mul((2).into());
OneshotAccounts::<T>::insert( OneshotAccounts::<T>::insert(caller.clone(), Into::<BalanceOf<T>>::into(balance));
caller.clone(),
Into::<<T::Currency as Currency<T::AccountId>>::Balance>::into(balance)
);
// Deposit into a normal account is more expensive than into a oneshot account // Deposit into a normal account is more expensive than into a oneshot account
// so we create the recipient accounts with an existential deposits. // so we create the recipient accounts with an existential deposits.
let recipient1: T::AccountId = account("recipient1", 0, SEED); let recipient1: T::AccountId = account("recipient1", 0, 1);
let recipient1_lookup: <T::Lookup as StaticLookup>::Source = let recipient1_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient1.clone()); T::Lookup::unlookup(recipient1.clone());
let _ = T::Currency::make_free_balance_be(&recipient1, existential_deposit.into()); let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
let recipient2: T::AccountId = account("recipient2", 1, SEED); &recipient1,
existential_deposit.into(),
);
let recipient2: T::AccountId = account("recipient2", 1, 1);
let recipient2_lookup: <T::Lookup as StaticLookup>::Source = let recipient2_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient2.clone()); T::Lookup::unlookup(recipient2.clone());
let _ = T::Currency::make_free_balance_be(&recipient2, existential_deposit.into()); let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
}: _( &recipient2,
existential_deposit.into(),
);
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()), RawOrigin::Signed(caller.clone()),
T::BlockNumber::zero(), BlockNumberFor::<T>::zero(),
Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient1_lookup), Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient1_lookup),
Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient2_lookup), Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient2_lookup),
existential_deposit.into() existential_deposit.into(),
) );
verify {
assert_eq!(OneshotAccounts::<T>::get(&caller), None); assert_eq!(OneshotAccounts::<T>::get(&caller), None);
assert_eq!( assert_eq!(
Balances::<T>::free_balance(&recipient1), Balances::<T>::free_balance(&recipient1),
...@@ -130,9 +138,5 @@ benchmarks! { ...@@ -130,9 +138,5 @@ benchmarks! {
); );
} }
impl_benchmark_test_suite!( impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
Pallet,
crate::mock::new_test_ext(),
crate::mock::Test
);
} }
...@@ -16,17 +16,24 @@ ...@@ -16,17 +16,24 @@
use crate::Config; use crate::Config;
use codec::{Decode, Encode}; use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::dispatch::DispatchInfo; use frame_support::{dispatch::DispatchInfo, pallet_prelude::Weight, traits::IsSubType};
use frame_support::traits::IsSubType;
//use frame_system::Config; //use frame_system::Config;
use scale_info::TypeInfo; use scale_info::{
prelude::fmt::{Debug, Formatter},
TypeInfo,
};
use sp_runtime::{ use sp_runtime::{
traits::{DispatchInfoOf, Dispatchable, SignedExtension}, traits::{
transaction_validity::{TransactionValidity, TransactionValidityError}, AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf,
TransactionExtension, ValidateResult,
},
transaction_validity::{TransactionSource, TransactionValidityError},
DispatchResult,
}; };
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] /// Wrapper around `frame_system::CheckNonce<T>`.
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(Runtime))] #[scale_info(skip_type_params(Runtime))]
pub struct CheckNonce<T: Config>(pub frame_system::CheckNonce<T>); pub struct CheckNonce<T: Config>(pub frame_system::CheckNonce<T>);
...@@ -36,57 +43,83 @@ impl<T: Config> From<frame_system::CheckNonce<T>> for CheckNonce<T> { ...@@ -36,57 +43,83 @@ impl<T: Config> From<frame_system::CheckNonce<T>> for CheckNonce<T> {
} }
} }
impl<T: Config> sp_std::fmt::Debug for CheckNonce<T> { impl<T: Config> Debug for CheckNonce<T> {
#[cfg(feature = "std")] #[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { fn fmt(&self, f: &mut Formatter) -> scale_info::prelude::fmt::Result {
write!(f, "CheckNonce({})", self.0 .0) write!(f, "CheckNonce({})", self.0 .0)
} }
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { fn fmt(&self, _: &mut Formatter) -> scale_info::prelude::fmt::Result {
Ok(()) Ok(())
} }
} }
impl<T: Config + TypeInfo> SignedExtension for CheckNonce<T> impl<T: Config + TypeInfo> TransactionExtension<T::RuntimeCall> for CheckNonce<T>
where where
T::RuntimeCall: Dispatchable<Info = DispatchInfo> + IsSubType<crate::Call<T>>, T::RuntimeCall: Dispatchable<Info = DispatchInfo>,
<T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
T::RuntimeCall: IsSubType<crate::Call<T>>,
{ {
type AccountId = <T as frame_system::Config>::AccountId; type Implicit = ();
type Call = <T as frame_system::Config>::RuntimeCall; type Pre = <frame_system::CheckNonce<T> as TransactionExtension<T::RuntimeCall>>::Pre;
type AdditionalSigned = (); type Val = <frame_system::CheckNonce<T> as TransactionExtension<T::RuntimeCall>>::Val;
type Pre = ();
const IDENTIFIER: &'static str = "CheckNonce"; const IDENTIFIER: &'static str = "CheckNonce";
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { fn validate(
self.0.additional_signed() &self,
origin: <T as frame_system::Config>::RuntimeOrigin,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
self_implicit: Self::Implicit,
inherited_implication: &impl sp_runtime::traits::Implication,
source: TransactionSource,
) -> ValidateResult<Self::Val, T::RuntimeCall> {
self.0.validate(
origin,
call,
info,
len,
self_implicit,
inherited_implication,
source,
)
}
fn weight(&self, origin: &T::RuntimeCall) -> Weight {
self.0.weight(origin)
} }
fn pre_dispatch( fn prepare(
self, self,
who: &Self::AccountId, val: Self::Val,
call: &Self::Call, origin: &T::RuntimeOrigin,
info: &DispatchInfoOf<Self::Call>, call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize, len: usize,
) -> Result<(), TransactionValidityError> { ) -> Result<Self::Pre, TransactionValidityError> {
if let Some( if let Some(
crate::Call::consume_oneshot_account { .. } crate::Call::consume_oneshot_account { .. }
| crate::Call::consume_oneshot_account_with_remaining { .. }, | crate::Call::consume_oneshot_account_with_remaining { .. },
) = call.is_sub_type() ) = call.is_sub_type()
{ {
Ok(()) Ok(Self::Pre::NonceChecked)
} else { } else {
self.0.pre_dispatch(who, call, info, len) self.0.prepare(val, origin, call, info, len)
} }
} }
fn validate( fn post_dispatch_details(
&self, pre: Self::Pre,
who: &Self::AccountId, info: &DispatchInfo,
call: &Self::Call, post_info: &PostDispatchInfoOf<T::RuntimeCall>,
info: &DispatchInfoOf<Self::Call>,
len: usize, len: usize,
) -> TransactionValidity { result: &DispatchResult,
self.0.validate(who, call, info, len) ) -> Result<Weight, TransactionValidityError> {
<frame_system::CheckNonce<T> as TransactionExtension<T::RuntimeCall>>::post_dispatch_details(
pre, info, post_info, len, result,
)
} }
} }
...@@ -14,6 +14,10 @@ ...@@ -14,6 +14,10 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Duniter Oneshot Account Pallet
//!
//! Duniter Oneshot Account Pallet introduces lightweight accounts that do not utilize `AccountInfo`, including fields like nonce, consumers, providers, sufficients, free, reserved. These accounts are designed for single-use scenarios, aiming to reduce transaction weight and associated fees. The primary use cases include anonymous transactions and physical support scenarios where lightweight and disposable accounts are beneficial.
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking; mod benchmarking;
...@@ -21,31 +25,35 @@ mod check_nonce; ...@@ -21,31 +25,35 @@ mod check_nonce;
#[cfg(test)] #[cfg(test)]
mod mock; mod mock;
mod types; mod types;
pub mod weights;
pub use check_nonce::CheckNonce; pub use check_nonce::CheckNonce;
pub use pallet::*; pub use pallet::*;
pub use types::*; pub use types::*;
pub use weights::WeightInfo;
use frame_support::pallet_prelude::*; use frame_support::{
use frame_support::traits::{ pallet_prelude::*,
Currency, ExistenceRequirement, Imbalance, IsSubType, WithdrawReasons, traits::{
fungible,
fungible::{Balanced, Credit, Inspect},
tokens::{Fortitude, Precision, Preservation},
Imbalance, IsSubType,
},
}; };
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
use pallet_transaction_payment::OnChargeTransaction; use pallet_transaction_payment::OnChargeTransaction;
use sp_runtime::traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, Zero}; use sp_runtime::traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, Zero};
use sp_std::convert::TryInto;
type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
#[allow(unreachable_patterns)]
#[frame_support::pallet] #[frame_support::pallet]
pub mod pallet { pub mod pallet {
use super::*; use super::*;
use frame_support::traits::StorageVersion;
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet] #[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info] #[pallet::without_storage_info]
pub struct Pallet<T>(_); pub struct Pallet<T>(_);
...@@ -53,22 +61,26 @@ pub mod pallet { ...@@ -53,22 +61,26 @@ pub mod pallet {
#[pallet::config] #[pallet::config]
pub trait Config: frame_system::Config + pallet_transaction_payment::Config { pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
type Currency: Currency<Self::AccountId>; /// The currency type.
type Currency: fungible::Balanced<Self::AccountId> + fungible::Mutate<Self::AccountId>;
/// A handler for charging transactions.
type InnerOnChargeTransaction: OnChargeTransaction<Self>; type InnerOnChargeTransaction: OnChargeTransaction<Self>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
} }
// STORAGE // // STORAGE //
/// The balance for each oneshot account.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn oneshot_account)] #[pallet::getter(fn oneshot_account)]
pub type OneshotAccounts<T: Config> = StorageMap< pub type OneshotAccounts<T: Config> =
_, StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, OptionQuery>;
Blake2_128Concat,
T::AccountId,
<T::Currency as Currency<T::AccountId>>::Balance,
OptionQuery,
>;
// EVENTS // // EVENTS //
...@@ -76,25 +88,22 @@ pub mod pallet { ...@@ -76,25 +88,22 @@ pub mod pallet {
#[pallet::event] #[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> { pub enum Event<T: Config> {
/// A oneshot account was created.
OneshotAccountCreated { OneshotAccountCreated {
account: T::AccountId, account: T::AccountId,
balance: <T::Currency as Currency<T::AccountId>>::Balance, balance: BalanceOf<T>,
creator: T::AccountId, creator: T::AccountId,
}, },
/// A oneshot account was consumed.
OneshotAccountConsumed { OneshotAccountConsumed {
account: T::AccountId, account: T::AccountId,
dest1: ( dest1: (T::AccountId, BalanceOf<T>),
T::AccountId, dest2: Option<(T::AccountId, BalanceOf<T>)>,
<T::Currency as Currency<T::AccountId>>::Balance,
),
dest2: Option<(
T::AccountId,
<T::Currency as Currency<T::AccountId>>::Balance,
)>,
}, },
/// A withdrawal was executed on a oneshot account.
Withdraw { Withdraw {
account: T::AccountId, account: T::AccountId,
balance: <T::Currency as Currency<T::AccountId>>::Balance, balance: BalanceOf<T>,
}, },
} }
...@@ -102,19 +111,19 @@ pub mod pallet { ...@@ -102,19 +111,19 @@ pub mod pallet {
#[pallet::error] #[pallet::error]
pub enum Error<T> { pub enum Error<T> {
/// Block height is in the future /// Block height is in the future.
BlockHeightInFuture, BlockHeightInFuture,
/// Block height is too old /// Block height is too old.
BlockHeightTooOld, BlockHeightTooOld,
/// Destination account does not exist /// Destination account does not exist.
DestAccountNotExist, DestAccountNotExist,
/// Destination account has balance less than existential deposit /// Destination account has a balance less than the existential deposit.
ExistentialDeposit, ExistentialDeposit,
/// Source account has insufficient balance /// Source account has insufficient balance.
InsufficientBalance, InsufficientBalance,
/// Destination oneshot account already exists /// Destination oneshot account already exists.
OneshotAccountAlreadyCreated, OneshotAccountAlreadyCreated,
/// Source oneshot account does not exist /// Source oneshot account does not exist.
OneshotAccountNotExist, OneshotAccountNotExist,
} }
...@@ -127,17 +136,18 @@ pub mod pallet { ...@@ -127,17 +136,18 @@ pub mod pallet {
/// - `balance`: The balance to be transfered to this oneshot account. /// - `balance`: The balance to be transfered to this oneshot account.
/// ///
/// Origin account is kept alive. /// Origin account is kept alive.
#[pallet::weight(500_000_000)] #[pallet::call_index(0)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::create_oneshot_account())]
pub fn create_oneshot_account( pub fn create_oneshot_account(
origin: OriginFor<T>, origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source, dest: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] value: <T::Currency as Currency<T::AccountId>>::Balance, #[pallet::compact] value: BalanceOf<T>,
) -> DispatchResult { ) -> DispatchResult {
let transactor = ensure_signed(origin)?; let transactor = ensure_signed(origin)?;
let dest = T::Lookup::lookup(dest)?; let dest = T::Lookup::lookup(dest)?;
ensure!( ensure!(
value >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), value >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit Error::<T>::ExistentialDeposit
); );
ensure!( ensure!(
...@@ -145,11 +155,12 @@ pub mod pallet { ...@@ -145,11 +155,12 @@ pub mod pallet {
Error::<T>::OneshotAccountAlreadyCreated Error::<T>::OneshotAccountAlreadyCreated
); );
<T::Currency as Currency<T::AccountId>>::withdraw( let _ = T::Currency::withdraw(
&transactor, &transactor,
value, value,
WithdrawReasons::TRANSFER, Precision::Exact,
ExistenceRequirement::KeepAlive, Preservation::Preserve,
Fortitude::Polite,
)?; )?;
OneshotAccounts::<T>::insert(&dest, value); OneshotAccounts::<T>::insert(&dest, value);
Self::deposit_event(Event::OneshotAccountCreated { Self::deposit_event(Event::OneshotAccountCreated {
...@@ -160,15 +171,17 @@ pub mod pallet { ...@@ -160,15 +171,17 @@ pub mod pallet {
Ok(()) Ok(())
} }
/// Consume a oneshot account and transfer its balance to an account /// Consume a oneshot account and transfer its balance to an account
/// ///
/// - `block_height`: Must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks) /// - `block_height`: Must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks)
/// - `dest`: The destination account. /// - `dest`: The destination account.
/// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account. /// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account.
#[pallet::weight(500_000_000)] #[pallet::call_index(1)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::consume_oneshot_account())]
pub fn consume_oneshot_account( pub fn consume_oneshot_account(
origin: OriginFor<T>, origin: OriginFor<T>,
block_height: T::BlockNumber, block_height: BlockNumberFor<T>,
dest: Account<<T::Lookup as StaticLookup>::Source>, dest: Account<<T::Lookup as StaticLookup>::Source>,
) -> DispatchResult { ) -> DispatchResult {
let transactor = ensure_signed(origin)?; let transactor = ensure_signed(origin)?;
...@@ -201,8 +214,8 @@ pub mod pallet { ...@@ -201,8 +214,8 @@ pub mod pallet {
balance: value, balance: value,
creator: transactor.clone(), creator: transactor.clone(),
}); });
} else { } else if frame_system::Pallet::<T>::providers(&dest) > 0 {
<T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest, value)?; let _ = T::Currency::deposit(&dest, value, Precision::Exact)?;
} }
OneshotAccounts::<T>::remove(&transactor); OneshotAccounts::<T>::remove(&transactor);
Self::deposit_event(Event::OneshotAccountConsumed { Self::deposit_event(Event::OneshotAccountConsumed {
...@@ -213,6 +226,7 @@ pub mod pallet { ...@@ -213,6 +226,7 @@ pub mod pallet {
Ok(()) Ok(())
} }
/// Consume a oneshot account then transfer some amount to an account, /// Consume a oneshot account then transfer some amount to an account,
/// and the remaining amount to another account. /// and the remaining amount to another account.
/// ///
...@@ -223,13 +237,14 @@ pub mod pallet { ...@@ -223,13 +237,14 @@ pub mod pallet {
/// - `dest2`: The second destination account. /// - `dest2`: The second destination account.
/// - `dest2_is_oneshot`: If set to `true`, then a oneshot account is created at `dest2`. Else, `dest2` has to be an existing account. /// - `dest2_is_oneshot`: If set to `true`, then a oneshot account is created at `dest2`. Else, `dest2` has to be an existing account.
/// - `balance1`: The amount transfered to `dest`, the leftover being transfered to `dest2`. /// - `balance1`: The amount transfered to `dest`, the leftover being transfered to `dest2`.
#[pallet::weight(500_000_000)] #[pallet::call_index(2)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::consume_oneshot_account_with_remaining())]
pub fn consume_oneshot_account_with_remaining( pub fn consume_oneshot_account_with_remaining(
origin: OriginFor<T>, origin: OriginFor<T>,
block_height: T::BlockNumber, block_height: BlockNumberFor<T>,
dest: Account<<T::Lookup as StaticLookup>::Source>, dest: Account<<T::Lookup as StaticLookup>::Source>,
remaining_to: Account<<T::Lookup as StaticLookup>::Source>, remaining_to: Account<<T::Lookup as StaticLookup>::Source>,
#[pallet::compact] balance: <T::Currency as Currency<T::AccountId>>::Balance, #[pallet::compact] balance: BalanceOf<T>,
) -> DispatchResult { ) -> DispatchResult {
let transactor = ensure_signed(origin)?; let transactor = ensure_signed(origin)?;
...@@ -264,12 +279,12 @@ pub mod pallet { ...@@ -264,12 +279,12 @@ pub mod pallet {
Error::<T>::OneshotAccountAlreadyCreated Error::<T>::OneshotAccountAlreadyCreated
); );
ensure!( ensure!(
balance1 >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), balance1 >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit Error::<T>::ExistentialDeposit
); );
} else { } else {
ensure!( ensure!(
!<T::Currency as Currency<T::AccountId>>::free_balance(&dest1).is_zero(), !T::Currency::balance(&dest1).is_zero(),
Error::<T>::DestAccountNotExist Error::<T>::DestAccountNotExist
); );
} }
...@@ -279,7 +294,7 @@ pub mod pallet { ...@@ -279,7 +294,7 @@ pub mod pallet {
Error::<T>::OneshotAccountAlreadyCreated Error::<T>::OneshotAccountAlreadyCreated
); );
ensure!( ensure!(
balance2 >= <T::Currency as Currency<T::AccountId>>::minimum_balance(), balance2 >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit Error::<T>::ExistentialDeposit
); );
OneshotAccounts::<T>::insert(&dest2, balance2); OneshotAccounts::<T>::insert(&dest2, balance2);
...@@ -288,8 +303,8 @@ pub mod pallet { ...@@ -288,8 +303,8 @@ pub mod pallet {
balance: balance2, balance: balance2,
creator: transactor.clone(), creator: transactor.clone(),
}); });
} else { } else if frame_system::Pallet::<T>::providers(&dest2) > 0 {
<T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest2, balance2)?; let _ = T::Currency::deposit(&dest2, balance2, Precision::Exact)?;
} }
if dest1_is_oneshot { if dest1_is_oneshot {
OneshotAccounts::<T>::insert(&dest1, balance1); OneshotAccounts::<T>::insert(&dest1, balance1);
...@@ -298,8 +313,8 @@ pub mod pallet { ...@@ -298,8 +313,8 @@ pub mod pallet {
balance: balance1, balance: balance1,
creator: transactor.clone(), creator: transactor.clone(),
}); });
} else { } else if frame_system::Pallet::<T>::providers(&dest1) > 0 {
<T::Currency as Currency<T::AccountId>>::deposit_into_existing(&dest1, balance1)?; let _ = T::Currency::deposit(&dest1, balance1, Precision::Exact)?;
} }
OneshotAccounts::<T>::remove(&transactor); OneshotAccounts::<T>::remove(&transactor);
Self::deposit_event(Event::OneshotAccountConsumed { Self::deposit_event(Event::OneshotAccountConsumed {
...@@ -318,12 +333,23 @@ where ...@@ -318,12 +333,23 @@ where
T::RuntimeCall: IsSubType<Call<T>>, T::RuntimeCall: IsSubType<Call<T>>,
T::InnerOnChargeTransaction: OnChargeTransaction< T::InnerOnChargeTransaction: OnChargeTransaction<
T, T,
Balance = <T::Currency as Currency<T::AccountId>>::Balance, Balance = BalanceOf<T>,
LiquidityInfo = Option<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>, LiquidityInfo = Option<Credit<T::AccountId, T::Currency>>,
>, >,
{ {
type Balance = <T::Currency as Currency<T::AccountId>>::Balance; type Balance = BalanceOf<T>;
type LiquidityInfo = Option<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>; type LiquidityInfo = Option<Credit<T::AccountId, T::Currency>>;
fn can_withdraw_fee(
who: &T::AccountId,
call: &T::RuntimeCall,
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
fee: Self::Balance,
tip: Self::Balance,
) -> Result<(), TransactionValidityError> {
T::InnerOnChargeTransaction::can_withdraw_fee(who, call, dispatch_info, fee, tip)
}
fn withdraw_fee( fn withdraw_fee(
who: &T::AccountId, who: &T::AccountId,
call: &T::RuntimeCall, call: &T::RuntimeCall,
...@@ -347,10 +373,7 @@ where ...@@ -347,10 +373,7 @@ where
account: who.clone(), account: who.clone(),
balance: fee, balance: fee,
}); });
// TODO return Ok(Some(Imbalance::zero()));
return Ok(Some(
<T::Currency as Currency<T::AccountId>>::NegativeImbalance::zero(),
));
} }
} }
Err(TransactionValidityError::Invalid( Err(TransactionValidityError::Invalid(
...@@ -360,6 +383,7 @@ where ...@@ -360,6 +383,7 @@ where
T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip) T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip)
} }
} }
fn correct_and_deposit_fee( fn correct_and_deposit_fee(
who: &T::AccountId, who: &T::AccountId,
dispatch_info: &DispatchInfoOf<T::RuntimeCall>, dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
...@@ -377,4 +401,14 @@ where ...@@ -377,4 +401,14 @@ where
already_withdrawn, already_withdrawn,
) )
} }
#[cfg(feature = "runtime-benchmarks")]
fn endow_account(who: &T::AccountId, amount: Self::Balance) {
T::InnerOnChargeTransaction::endow_account(who, amount);
}
#[cfg(feature = "runtime-benchmarks")]
fn minimum_balance() -> Self::Balance {
T::InnerOnChargeTransaction::minimum_balance()
}
} }