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
  • distance
  • elois-ci-binary-release
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-fix-85
  • elois-opti-cert
  • elois-remove-renewable-period
  • elois-rework-certs
  • elois-smoldot
  • elois-substrate-v0.9.23
  • elois-technical-commitee
  • hugo-cucumber-identity
  • master
  • no-bootnodes
  • poc-oneshot-accounts
  • release/runtime-100
  • release/runtime-200
  • ts-types
  • ud-time-64
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • v0.1.0
28 results
Show changes
Showing
with 3316 additions and 83 deletions
// 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(i: u32) -> Weight;
fn on_initialize_epoch(i: u32) -> Weight;
fn request() -> Weight;
}
// Insecure weights implementation, use it for tests only!
impl WeightInfo for () {
// Storage: ProvideRandomness CounterForRequestsIds (r:1 w:1)
// Storage: ProvideRandomness RequestIdProvider (r:1 w:1)
// Storage: ProvideRandomness RequestsIds (r:1 w:1)
// Storage: Babe EpochIndex (r:1 w:0)
// Storage: ProvideRandomness NexEpochHookIn (r:1 w:0)
// Storage: ProvideRandomness RequestsReadyAtEpoch (r:1 w:1)
fn request() -> Weight {
// Minimum execution time: 321_822 nanoseconds.
Weight::from_parts(338_919_000 as u64, 0)
.saturating_add(RocksDbWeight::get().reads(6 as u64))
.saturating_add(RocksDbWeight::get().writes(4 as u64))
}
// Storage: ProvideRandomness RequestsReadyAtNextBlock (r:1 w:1)
// Storage: Babe AuthorVrfRandomness (r:1 w:0)
// Storage: ProvideRandomness RequestsIds (r:1 w:1)
// Storage: ProvideRandomness CounterForRequestsIds (r:1 w:1)
// Storage: Account PendingRandomIdAssignments (r:1 w:0)
// Storage: ProvideRandomness NexEpochHookIn (r:1 w:1)
/// The range of component `i` is `[1, 100]`.
fn on_initialize(i: u32) -> Weight {
// Minimum execution time: 175_645 nanoseconds.
Weight::from_parts(461_442_906 as u64, 0)
// Standard Error: 1_523_561
.saturating_add(Weight::from_parts(43_315_015 as u64, 0).saturating_mul(i as u64))
.saturating_add(RocksDbWeight::get().reads(4 as u64))
.saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(i as u64)))
.saturating_add(RocksDbWeight::get().writes(3 as u64))
.saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(i as u64)))
}
fn on_initialize_epoch(i: u32) -> Weight {
// Minimum execution time: 175_645 nanoseconds.
Weight::from_parts(461_442_906 as u64, 0)
// Standard Error: 1_523_561
.saturating_add(Weight::from_parts(43_315_015 as u64, 0).saturating_mul(i as u64))
.saturating_add(RocksDbWeight::get().reads(4 as u64))
.saturating_add(RocksDbWeight::get().reads((2 as u64).saturating_mul(i as u64)))
.saturating_add(RocksDbWeight::get().writes(3 as u64))
.saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(i as u64)))
}
}
[package]
authors.workspace = true
description = "duniter pallet quota"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-quota"
repository.workspace = true
version.workspace = true
[features]
default = ["std"]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-identity/runtime-benchmarks",
"sp-membership/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-identity/try-runtime",
"sp-membership/try-runtime",
"sp-runtime/try-runtime",
]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"pallet-balances/std",
"pallet-identity/std",
"sp-membership/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
]
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-balances = { workspace = true }
pallet-identity = { workspace = true }
sp-membership = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
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/>.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::{account, v2::*};
use frame_support::traits::fungible::Mutate;
use sp_runtime::traits::One;
fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_has_event(generic_event.into());
}
#[benchmarks(
where
IdtyId<T>: From<u32>,
BalanceOf<T>: From<u64>,
T::AccountId: From<[u8; 32]>,
)]
mod benchmarks {
use super::*;
#[benchmark]
fn queue_refund() {
let account: T::AccountId = account("Alice", 1, 1);
let dummy_refund = Refund {
account: account.clone(),
identity: 0u32.into(),
amount: 20u64.into(),
};
let refund = Refund {
account,
identity: 1u32.into(),
amount: 10u64.into(),
};
// Complexity is bound to MAX_QUEUD_REFUNDS where an insertion is O(n-1)
for _ in 0..MAX_QUEUED_REFUNDS - 1 {
Pallet::<T>::queue_refund(dummy_refund.clone())
}
#[block]
{
Pallet::<T>::queue_refund(refund.clone());
}
assert_eq!(RefundQueue::<T>::get().last(), Some(refund).as_ref());
assert_eq!(RefundQueue::<T>::get().len() as u32, MAX_QUEUED_REFUNDS);
}
#[benchmark]
fn spend_quota() {
let idty_id: IdtyId<T> = 1u32.into();
let amount = 2u64;
let quota_amount = 10u64;
IdtyQuota::<T>::insert(
idty_id,
Quota {
last_use: BlockNumberFor::<T>::zero(),
amount: quota_amount.into(),
},
);
#[block]
{
Pallet::<T>::spend_quota(idty_id, amount.into());
}
let quota_growth =
sp_runtime::Perbill::from_rational(BlockNumberFor::<T>::one(), T::ReloadRate::get())
.mul_floor(T::MaxQuota::get());
assert_eq!(
IdtyQuota::<T>::get(idty_id).unwrap().amount,
quota_growth + quota_amount.into() - amount.into()
);
}
#[benchmark]
fn try_refund() {
let account: T::AccountId = account("Alice", 1, 1);
let idty_id: IdtyId<T> = 1u32.into();
IdtyQuota::<T>::insert(
idty_id,
Quota {
last_use: BlockNumberFor::<T>::zero(),
amount: 10u64.into(),
},
);
let _ = CurrencyOf::<T>::set_balance(&T::RefundAccount::get(), u32::MAX.into());
// The worst-case scenario is when the refund fails
// and can only be triggered if the account is dead,
// in this case by having no balance in the account.
let refund = Refund {
account: account.clone(),
identity: 1u32.into(),
amount: 10u64.into(),
};
#[block]
{
Pallet::<T>::try_refund(refund);
}
assert_has_event::<T>(Event::<T>::RefundFailed(account).into());
}
#[benchmark]
fn do_refund() {
let account: T::AccountId = account("Alice", 1, 1);
let _ = CurrencyOf::<T>::set_balance(&T::RefundAccount::get(), u32::MAX.into());
// The worst-case scenario is when the refund fails
// and can only be triggered if the account is dead,
// in this case by having no balance in the account.
let refund = Refund {
account: account.clone(),
identity: 1u32.into(),
amount: 10u64.into(),
};
#[block]
{
Pallet::<T>::try_refund(refund);
}
assert_has_event::<T>(Event::<T>::RefundFailed(account).into());
}
#[benchmark]
fn on_process_refund_queue() {
// The base weight consumed on processing refund queue when empty.
assert_eq!(RefundQueue::<T>::get().len() as u32, 0);
#[block]
{
Pallet::<T>::process_refund_queue(Weight::MAX);
}
}
#[benchmark]
fn on_process_refund_queue_elements(i: Linear<1, MAX_QUEUED_REFUNDS>) {
// The weight consumed on processing refund queue with one element.
// Can deduce the process_refund_queue overhead by subtracting try_refund weight.
let account: T::AccountId = account("Alice", 1, 1);
let idty_id: IdtyId<T> = 1u32.into();
IdtyQuota::<T>::insert(
idty_id,
Quota {
last_use: BlockNumberFor::<T>::zero(),
amount: 10u64.into(),
},
);
let _ = CurrencyOf::<T>::set_balance(&T::RefundAccount::get(), u32::MAX.into());
// The worst-case scenario is when the refund fails
// and can only be triggered if the account is dead,
// in this case by having no balance in the account.
let refund = Refund {
account: account.clone(),
identity: 1u32.into(),
amount: 10u64.into(),
};
for _ in 0..i {
Pallet::<T>::queue_refund(refund.clone());
}
assert_eq!(RefundQueue::<T>::get().len() as u32, i);
#[block]
{
Pallet::<T>::process_refund_queue(Weight::MAX);
}
assert_eq!(RefundQueue::<T>::get().len() as u32, 0);
assert_has_event::<T>(Event::<T>::RefundFailed(account).into());
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(crate::mock::QuotaConfig {
identities: vec![1, 2]
}),
crate::mock::Test
);
}
// 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 Quota Pallet
//!
//! ## Overview
//!
//! This pallet is designed to manage transaction fee refunds based on quotas allocated to identities within the Duniter identity system. Quotas are linked to transaction fees, ensuring efficient handling of fee refunds when transactions occur.
//!
//! ## Refund Mechanism
//!
//! When a transaction is processed:
//! - The `OnChargeTransaction` implementation in the `frame-executive` pallet is called.
//! - The `OnChargeTransaction` implementation in the `duniter-account` pallet checks if the paying account is linked to an identity.
//! - If linked, the `request_refund` function in the `quota` pallet evaluates the eligibility for fee refund based on the identity's quota.
//! - Eligible refunds are added to the `RefundQueue`, managed by `process_refund_queue` during the `on_idle` phase.
//! - Refunds are processed with `try_refund`, using quotas to refund fees via `spend_quota`, and then executing the refund through `do_refund` by transferring currency from the `RefundAccount` back to the paying account.
//!
//! ## Conditions for Refund
//!
//! Refunds are executed under the following conditions:
//! 1. The paying account is linked to an identity.
//! 2. Quotas are allocated to the identity and have a non-zero value after updates.
#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
mod traits;
mod weights;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
use frame_support::{
pallet_prelude::*,
traits::{Currency, ExistenceRequirement},
};
use frame_system::pallet_prelude::*;
use scale_info::prelude::vec::Vec;
use sp_runtime::traits::Zero;
pub use pallet::*;
pub use traits::*;
pub use weights::WeightInfo;
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
pub const MAX_QUEUED_REFUNDS: u32 = 256;
// Currency used for quota is the one of pallet balances
pub type CurrencyOf<T> = pallet_balances::Pallet<T>;
// Balance used for quota is the one associated to balance currency
pub type BalanceOf<T> =
<CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
// identity id is pallet identity idty_index
pub type IdtyId<T> = <T as pallet_identity::Config>::IdtyIndex;
#[pallet::pallet]
pub struct Pallet<T>(_);
// CONFIG //
#[pallet::config]
pub trait Config:
frame_system::Config + pallet_balances::Config + pallet_identity::Config
{
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Number of blocks after which the maximum quota is replenished.
type ReloadRate: Get<BlockNumberFor<Self>>;
/// Maximum amount of quota an identity can receive.
type MaxQuota: Get<BalanceOf<Self>>;
/// Account used to refund fees.
#[pallet::constant]
type RefundAccount: Get<Self::AccountId>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
}
// TYPES //
/// Represents a refund.
#[derive(Encode, Decode, Clone, TypeInfo, Debug, PartialEq, MaxEncodedLen)]
pub struct Refund<AccountId, IdtyId, Balance> {
/// Account to refund.
pub account: AccountId,
/// Identity to use quota.
pub identity: IdtyId,
/// Amount of refund.
pub amount: Balance,
}
/// Represents a quota.
#[derive(Encode, Decode, Clone, TypeInfo, Debug, PartialEq, MaxEncodedLen)]
pub struct Quota<BlockNumber, Balance> {
/// Block number of the last quota used.
pub last_use: BlockNumber,
/// Amount of remaining quota.
pub amount: Balance,
}
// STORAGE //
/// The quota for each identity.
#[pallet::storage]
#[pallet::getter(fn quota)]
pub type IdtyQuota<T: Config> =
StorageMap<_, Twox64Concat, IdtyId<T>, Quota<BlockNumberFor<T>, BalanceOf<T>>, OptionQuery>;
/// The fees waiting to be refunded.
#[pallet::storage]
pub type RefundQueue<T: Config> = StorageValue<
_,
BoundedVec<Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>, ConstU32<MAX_QUEUED_REFUNDS>>,
ValueQuery,
>;
// EVENTS //
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Transaction fees were refunded.
Refunded {
who: T::AccountId,
identity: IdtyId<T>,
amount: BalanceOf<T>,
},
/// No more quota available for refund.
NoQuotaForIdty(IdtyId<T>),
/// No more currency available for refund.
/// This scenario should never occur if the fees are intended for the refund account.
NoMoreCurrencyForRefund,
/// The refund has failed.
/// This scenario should rarely occur, except when the account was destroyed in the interim between the request and the refund.
RefundFailed(T::AccountId),
/// Refund queue was full.
RefundQueueFull,
}
// This pallet only contains the `on_idle` hook and no call.
// Hooks are infallible by definition, so there are no error. To monitor no-ops
// from inside the quota pallet, we use events as mentioned in
// https://substrate.stackexchange.com/questions/9854/emitting-errors-from-hooks-like-on-initialize
// PUBLIC FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Estimates the quota refund amount for an identity
/// The estimation simulate a refund request at the current block
pub fn estimate_quota_refund(idty_index: IdtyId<T>) -> BalanceOf<T> {
if is_eligible_for_refund::<T>(idty_index) {
if let Some(ref mut quota) = IdtyQuota::<T>::get(idty_index) {
Self::update_quota(quota);
quota.amount
} else {
Zero::zero()
}
} else {
Zero::zero()
}
}
}
// INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Adds a new refund request to the refund queue.
pub fn queue_refund(refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>) {
if RefundQueue::<T>::mutate(|v| v.try_push(refund)).is_err() {
Self::deposit_event(Event::RefundQueueFull);
}
}
/// Attempts to process a refund using available quota.
pub fn try_refund(queued_refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>) -> Weight {
// get the amount of quota that identity is able to spend
let amount = Self::spend_quota(queued_refund.identity, queued_refund.amount);
if amount.is_zero() {
// partial weight
return <T as pallet::Config>::WeightInfo::spend_quota();
}
// only perform refund if amount is not null
Self::do_refund(queued_refund, amount);
// total weight
<T as pallet::Config>::WeightInfo::spend_quota()
.saturating_add(<T as pallet::Config>::WeightInfo::do_refund())
}
/// Performs a refund operation for a specified non-null amount from the refund account to the requester's account.
// opti: more accurate estimation of consumed weight
pub fn do_refund(
queued_refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>,
amount: BalanceOf<T>,
) {
// take money from refund account
let res = CurrencyOf::<T>::withdraw(
&T::RefundAccount::get(),
amount,
frame_support::traits::WithdrawReasons::FEE, // a fee but in reverse
ExistenceRequirement::KeepAlive,
);
// if successful
if let Ok(imbalance) = res {
// perform refund
let res = CurrencyOf::<T>::resolve_into_existing(&queued_refund.account, imbalance);
match res {
// take money from refund account OK + refund account OK → event
Ok(_) => {
Self::deposit_event(Event::Refunded {
who: queued_refund.account,
identity: queued_refund.identity,
amount,
});
}
Err(imbalance) => {
// refund failed (for example account stopped existing) → handle dust
// give back to refund account (should not happen)
CurrencyOf::<T>::resolve_creating(&T::RefundAccount::get(), imbalance);
// if this event is observed, block should be examined carefully
Self::deposit_event(Event::RefundFailed(queued_refund.account));
}
}
} else {
// could not withdraw refund account
Self::deposit_event(Event::NoMoreCurrencyForRefund);
}
}
/// Processes as many refunds as possible from the refund queue within the supplied weight limit.
pub fn process_refund_queue(weight_limit: Weight) -> Weight {
RefundQueue::<T>::mutate(|queue| {
// The weight to process an empty queue
let mut total_weight = <T as pallet::Config>::WeightInfo::on_process_refund_queue();
// The weight to process one element without the actual try_refund weight
let overhead =
<T as pallet::Config>::WeightInfo::on_process_refund_queue_elements(2)
.saturating_sub(
<T as pallet::Config>::WeightInfo::on_process_refund_queue_elements(1),
)
.saturating_sub(<T as pallet::Config>::WeightInfo::try_refund());
// make sure that we have at least the time to handle one try_refund call
if queue.is_empty() {
return total_weight;
}
while total_weight.any_lt(weight_limit.saturating_sub(
<T as pallet::Config>::WeightInfo::try_refund().saturating_add(overhead),
)) {
let Some(queued_refund) = queue.pop() else {
break;
};
let consumed_weight = Self::try_refund(queued_refund);
total_weight = total_weight
.saturating_add(consumed_weight)
.saturating_add(overhead);
}
total_weight
})
}
/// Spends the quota of an identity by deducting the specified `amount` from its quota balance.
pub fn spend_quota(idty_id: IdtyId<T>, amount: BalanceOf<T>) -> BalanceOf<T> {
IdtyQuota::<T>::mutate_exists(idty_id, |quota| {
if let Some(ref mut quota) = quota {
Self::update_quota(quota);
Self::do_spend_quota(quota, amount)
} else {
// error event if identity has no quota
Self::deposit_event(Event::NoQuotaForIdty(idty_id));
BalanceOf::<T>::zero()
}
})
}
/// Update the quota according to the growth rate, maximum value, and last use.
fn update_quota(quota: &mut Quota<BlockNumberFor<T>, BalanceOf<T>>) {
let current_block = frame_system::pallet::Pallet::<T>::block_number();
let quota_growth = sp_runtime::Perbill::from_rational(
current_block - quota.last_use,
T::ReloadRate::get(),
)
.mul_floor(T::MaxQuota::get());
// mutate quota
quota.last_use = current_block;
quota.amount = core::cmp::min(quota.amount + quota_growth, T::MaxQuota::get());
}
/// Spend a certain amount of quota and return the amount that was spent.
fn do_spend_quota(
quota: &mut Quota<BlockNumberFor<T>, BalanceOf<T>>,
amount: BalanceOf<T>,
) -> BalanceOf<T> {
let old_amount = quota.amount;
// entire amount fit in remaining quota
if amount <= old_amount {
quota.amount -= amount;
amount
}
// all quota are spent and only partial refund is possible
else {
quota.amount = BalanceOf::<T>::zero();
old_amount
}
}
}
// GENESIS STUFF //
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub identities: Vec<IdtyId<T>>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
identities: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
for idty in self.identities.iter() {
IdtyQuota::<T>::insert(
idty,
Quota {
last_use: BlockNumberFor::<T>::zero(),
amount: BalanceOf::<T>::zero(),
},
);
}
}
}
// HOOKS //
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
// process refund queue if space left on block
fn on_idle(_block: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
Self::process_refund_queue(remaining_weight)
}
}
}
/// Implementing the refund fee trait for the pallet.
impl<T: Config> RefundFee<T> for Pallet<T> {
/// This implementation checks if the identity is eligible for a refund and queues the refund if so.
fn request_refund(account: T::AccountId, identity: IdtyId<T>, amount: BalanceOf<T>) {
if is_eligible_for_refund::<T>(identity) {
Self::queue_refund(Refund {
account,
identity,
amount,
})
}
}
}
/// Checks if an identity is eligible for a refund.
///
/// This function returns `true` only if the identity exists and has a status of `Member`.
/// If the identity does not exist or has a different status, it returns `false`, and the refund request will not be processed.
///
fn is_eligible_for_refund<T: pallet_identity::Config>(idty_index: IdtyId<T>) -> bool {
pallet_identity::Identities::<T>::get(idty_index).map_or_else(
|| false,
|id| id.status == pallet_identity::IdtyStatus::Member,
)
}
/// Implementing the on new membership event handler for the pallet.
impl<T: Config> sp_membership::traits::OnNewMembership<IdtyId<T>> for Pallet<T> {
/// This implementation initializes the identity quota for the newly created identity.
fn on_created(idty_index: &IdtyId<T>) {
IdtyQuota::<T>::insert(
idty_index,
Quota {
last_use: frame_system::pallet::Pallet::<T>::block_number(),
amount: BalanceOf::<T>::zero(),
},
);
}
fn on_renewed(_idty_index: &IdtyId<T>) {}
}
/// Implementing the on remove identity event handler for the pallet.
impl<T: Config> sp_membership::traits::OnRemoveMembership<IdtyId<T>> for Pallet<T> {
/// This implementation removes the identity quota associated with the removed identity.
fn on_removed(idty_id: &IdtyId<T>) -> Weight {
let mut weight = Weight::zero();
let mut add_db_reads_writes = |reads, writes| {
weight = weight.saturating_add(T::DbWeight::get().reads_writes(reads, writes));
};
IdtyQuota::<T>::remove(idty_id);
add_db_reads_writes(1, 1);
weight
}
}
// Copyright 2021 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/>.
// Note: most of this file is copy pasted from common pallet_config and other mocks
use super::*;
pub use crate::pallet as pallet_quota;
use frame_support::{
derive_impl, parameter_types,
traits::{Everything, OnFinalize, OnInitialize},
};
use frame_system as system;
use sp_core::{Pair, H256};
use sp_runtime::{
traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify},
BuildStorage, MultiSignature, MultiSigner,
};
type BlockNumber = u64;
type Balance = u64;
type Block = frame_system::mocking::MockBlock<Test>;
pub type Signature = MultiSignature;
pub type AccountPublic = <Signature as Verify>::Signer;
pub type AccountId = <AccountPublic as IdentifyAccount>::AccountId;
pub fn account(id: u8) -> AccountId {
let pair = sp_core::sr25519::Pair::from_seed(&[id; 32]);
MultiSigner::Sr25519(pair.public()).into_account()
}
// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test{
System: frame_system,
Quota: pallet_quota,
Balances: pallet_balances,
Identity: pallet_identity,
}
);
// QUOTA //
pub struct TreasuryAccountId;
impl frame_support::pallet_prelude::Get<AccountId> for TreasuryAccountId {
fn get() -> AccountId {
account(99)
}
}
parameter_types! {
pub const ReloadRate: u64 = 10;
pub const MaxQuota: u64 = 1000;
}
impl Config for Test {
type MaxQuota = MaxQuota;
type RefundAccount = TreasuryAccountId;
type ReloadRate = ReloadRate;
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
}
// SYSTEM //
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const SS58Prefix: u8 = 42;
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl system::Config for Test {
type AccountData = pallet_balances::AccountData<Balance>;
type AccountId = AccountId;
type BaseCallFilter = Everything;
type Block = Block;
type BlockHashCount = BlockHashCount;
type Hash = H256;
type Hashing = BlakeTwo256;
type Lookup = IdentityLookup<Self::AccountId>;
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type PalletInfo = PalletInfo;
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
type RuntimeOrigin = RuntimeOrigin;
type SS58Prefix = SS58Prefix;
}
// BALANCES //
parameter_types! {
pub const ExistentialDeposit: Balance = 1000;
pub const MaxLocks: u32 = 50;
}
impl pallet_balances::Config for Test {
type AccountStore = System;
type Balance = Balance;
type DoneSlashHandler = ();
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type FreezeIdentifier = ();
type MaxFreezes = ConstU32<0>;
type MaxLocks = MaxLocks;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type RuntimeEvent = RuntimeEvent;
type RuntimeFreezeReason = ();
type RuntimeHoldReason = ();
type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>;
}
// IDENTITY //
parameter_types! {
pub const ChangeOwnerKeyPeriod: u64 = 10;
pub const ConfirmPeriod: u64 = 2;
pub const ValidationPeriod: u64 = 3;
pub const AutorevocationPeriod: u64 = 5;
pub const DeletionPeriod: u64 = 7;
pub const IdtyCreationPeriod: u64 = 3;
}
pub struct IdtyNameValidatorTestImpl;
impl pallet_identity::traits::IdtyNameValidator for IdtyNameValidatorTestImpl {
fn validate(idty_name: &pallet_identity::IdtyName) -> bool {
idty_name.0.len() < 16
}
}
impl pallet_identity::Config for Test {
type AccountId32 = AccountId;
type AccountLinker = ();
type AutorevocationPeriod = AutorevocationPeriod;
type ChangeOwnerKeyPeriod = ChangeOwnerKeyPeriod;
type CheckAccountWorthiness = ();
type CheckIdtyCallAllowed = ();
type ConfirmPeriod = ConfirmPeriod;
type DeletionPeriod = DeletionPeriod;
type IdtyCreationPeriod = IdtyCreationPeriod;
type IdtyData = ();
type IdtyIndex = u64;
type IdtyNameValidator = IdtyNameValidatorTestImpl;
type OnKeyChange = ();
type OnNewIdty = ();
type OnRemoveIdty = ();
type RuntimeEvent = RuntimeEvent;
type Signature = Signature;
type Signer = AccountPublic;
type ValidationPeriod = ValidationPeriod;
type WeightInfo = ();
}
// Build genesis storage according to the mock runtime.
pub fn new_test_ext(gen_conf: pallet_quota::GenesisConfig<Test>) -> sp_io::TestExternalities {
RuntimeGenesisConfig {
system: SystemConfig::default(),
balances: BalancesConfig::default(),
quota: gen_conf,
identity: IdentityConfig::default(),
}
.build_storage()
.unwrap()
.into()
}
pub fn run_to_block(n: BlockNumber) {
while System::block_number() < n {
<frame_system::Pallet<Test> as OnFinalize<BlockNumber>>::on_finalize(System::block_number());
System::reset_events();
System::set_block_number(System::block_number() + 1);
<frame_system::Pallet<Test> as OnInitialize<BlockNumber>>::on_initialize(
System::block_number(),
);
}
}
// Copyright 2021 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::{mock::*, Weight};
use frame_support::traits::fungible::Mutate;
use sp_core::Get;
// Note: values for reload rate and max quota defined in mock file
// parameter_types! {
// pub const ReloadRate: u64 = 10;
// pub const MaxQuota: u64 = 1000;
// }
// pub const ExistentialDeposit: Balance = 1000;
/// test that quota are well initialized for genesis identities
#[test]
fn test_initial_quota() {
new_test_ext(QuotaConfig {
identities: vec![1, 2, 3],
})
.execute_with(|| {
run_to_block(1);
// quota initialized to 0,0 for a given identity
assert_eq!(
Quota::quota(1),
Some(pallet_quota::Quota {
last_use: 0,
amount: 0
})
);
// no initialized quota for standard account
assert_eq!(Quota::quota(4), None);
})
}
/// test that quota are updated according to the reload rate and max quota values
#[test]
fn test_update_quota() {
new_test_ext(QuotaConfig {
identities: vec![1, 2, 3],
})
.execute_with(|| {
// Block 1
run_to_block(1);
assert_eq!(
Quota::quota(1),
Some(pallet_quota::Quota {
last_use: 0,
amount: 0
})
);
// (spending 0 quota will lead to only update)
// assert zero quota spent
assert_eq!(Quota::spend_quota(1, 0), 0);
assert_eq!(
Quota::quota(1),
Some(pallet_quota::Quota {
last_use: 1, // used at block 1
// max quota × (current block - last use) / reload rate
amount: 100 // 1000 × 1 / 10 = 100
})
);
// Block 2
run_to_block(2);
assert_eq!(Quota::spend_quota(2, 0), 0);
assert_eq!(
Quota::quota(2),
Some(pallet_quota::Quota {
last_use: 2, // used at block 2
// max quota × (current block - last use) / reload rate
amount: 200 // 1000 × 2 / 10 = 200
})
);
// Block 20
run_to_block(20);
assert_eq!(Quota::spend_quota(2, 0), 0);
assert_eq!(
Quota::quota(2),
Some(pallet_quota::Quota {
last_use: 20, // used at block 20
// maximum quota is reached
// 1000 × (20 - 2) / 10 = 1800
amount: 1000 // min(1000, 1800)
})
);
})
}
/// test that right amount of quota is spent
#[test]
fn test_spend_quota() {
new_test_ext(QuotaConfig {
identities: vec![1, 2, 3],
})
.execute_with(|| {
// at block 5, quota are half loaded (500)
run_to_block(5);
// spending less than available
assert_eq!(Quota::spend_quota(1, 200), 200);
assert_eq!(
Quota::quota(1),
Some(pallet_quota::Quota {
last_use: 5,
amount: 300 // 500 - 200
})
);
// spending all available
assert_eq!(Quota::spend_quota(2, 500), 500);
assert_eq!(
Quota::quota(2),
Some(pallet_quota::Quota {
last_use: 5,
amount: 0 // 500 - 500
})
);
// spending more than available
assert_eq!(Quota::spend_quota(3, 1000), 500);
assert_eq!(
Quota::quota(3),
Some(pallet_quota::Quota {
last_use: 5,
amount: 0 // 500 - 500
})
);
})
}
/// test complete scenario with queue and process refund queue
#[test]
fn test_process_refund_queue() {
new_test_ext(QuotaConfig {
identities: vec![1, 2],
})
.execute_with(|| {
run_to_block(5);
// give enough currency to accounts and treasury and double check
Balances::set_balance(&account(1), 1000);
Balances::set_balance(&account(2), 1000);
Balances::set_balance(&account(3), 1000);
Balances::set_balance(
&<Test as pallet_quota::Config>::RefundAccount::get(),
10_000,
);
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
10_000
);
// fill in the refund queue
Quota::queue_refund(pallet_quota::Refund {
account: account(1),
identity: 1,
amount: 10,
});
Quota::queue_refund(pallet_quota::Refund {
account: account(2),
identity: 2,
amount: 1000,
});
Quota::queue_refund(pallet_quota::Refund {
account: account(3),
identity: 3,
amount: 666,
});
// process it
Quota::process_refund_queue(Weight::from(10));
// after processing, it should be empty
assert!(pallet_quota::RefundQueue::<Test>::get().is_empty());
// and we should observe the effects of refund
assert_eq!(Balances::free_balance(account(1)), 1010); // 1000 initial + 10 refunded
assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 500 refunded
assert_eq!(Balances::free_balance(account(3)), 1000); // only initial because no available quota
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
// initial minus refunds
10_000 - 500 - 10
);
// events
System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded {
who: account(1),
identity: 1,
amount: 10,
}));
System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::NoQuotaForIdty(3)));
})
}
/// test not enough currency in treasury
#[test]
fn test_not_enough_treasury() {
new_test_ext(QuotaConfig {
identities: vec![1],
})
.execute_with(|| {
run_to_block(5);
Balances::set_balance(&account(1), 1000);
Balances::set_balance(&<Test as pallet_quota::Config>::RefundAccount::get(), 1200);
Quota::queue_refund(pallet_quota::Refund {
account: account(1),
identity: 1,
amount: 500,
});
Quota::process_refund_queue(Weight::from(10));
// refund was not possible, would kill treasury
assert_eq!(Balances::free_balance(account(1)), 1000);
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
1200
);
// event
System::assert_has_event(RuntimeEvent::Quota(
pallet_quota::Event::NoMoreCurrencyForRefund,
));
// quotas were spent anyway, there is no refund for quotas when refund account is empty
assert_eq!(
Quota::quota(1),
Some(pallet_quota::Quota {
last_use: 5,
amount: 0
})
);
})
}
/// test complete scenario with queue and process refund queue weight with available quotas
#[test]
fn test_process_refund_queue_weight_with_quotas() {
new_test_ext(QuotaConfig {
identities: vec![1, 2, 3],
})
.execute_with(|| {
run_to_block(15);
// give enough currency to accounts and treasury and double check
Balances::set_balance(&account(1), 1000);
Balances::set_balance(&account(2), 1000);
Balances::set_balance(&account(3), 1000);
Balances::set_balance(
&<Test as pallet_quota::Config>::RefundAccount::get(),
10_000,
);
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
10_000
);
// fill in the refund queue
Quota::queue_refund(pallet_quota::Refund {
account: account(1),
identity: 10,
amount: 10,
});
Quota::queue_refund(pallet_quota::Refund {
account: account(2),
identity: 2,
amount: 500,
});
Quota::queue_refund(pallet_quota::Refund {
account: account(3),
identity: 3,
amount: 666,
});
// process it with only no weight
Quota::process_refund_queue(Weight::from(0));
// after processing, it should be of the same size
assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 3);
// process it with only 200 allowed weight
Quota::process_refund_queue(Weight::from_parts(200u64, 0));
// after processing, it should be of size 1 because total_weight += 25*2 by iteration and
// limit is total_weight < 200-100 so 2 elements can be processed
assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 1);
// and we should observe the effects of refund
assert_eq!(Balances::free_balance(account(3)), 1666); // 1000 initial + 666 refunded
assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 1500 refunded
assert_eq!(Balances::free_balance(account(1)), 1000); // only initial because no available weight to process
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
// initial minus refunds
10_000 - 666 - 500
);
// events
System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded {
who: account(3),
identity: 3,
amount: 666,
}));
System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded {
who: account(2),
identity: 2,
amount: 500,
}));
})
}
/// test complete scenario with queue and process refund queue weight with limited quotas
#[test]
fn test_process_refund_queue_weight_no_quotas() {
new_test_ext(QuotaConfig {
identities: vec![1, 2],
})
.execute_with(|| {
run_to_block(15);
// give enough currency to accounts and treasury and double check
Balances::set_balance(&account(1), 1000);
Balances::set_balance(&account(2), 1000);
Balances::set_balance(&account(3), 1000);
Balances::set_balance(
&<Test as pallet_quota::Config>::RefundAccount::get(),
10_000,
);
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
10_000
);
// fill in the refund queue
Quota::queue_refund(pallet_quota::Refund {
account: account(1),
identity: 10,
amount: 10,
});
Quota::queue_refund(pallet_quota::Refund {
account: account(2),
identity: 2,
amount: 500,
});
Quota::queue_refund(pallet_quota::Refund {
account: account(3),
identity: 3,
amount: 666,
});
// process it with only no weight
Quota::process_refund_queue(Weight::from(0));
// after processing, it should be of the same size
assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 3);
// process it with only 150 allowed weight
Quota::process_refund_queue(Weight::from_parts(150u64, 0));
// after processing, it should be of size 2 because try_refund weight is 25 (first in the queue with no quota) then 25*2 for the 2 other elements
// limit is total_weight < 150-100 so 2 elements can be processed
assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 1);
// and we should observe the effects of refund
assert_eq!(Balances::free_balance(account(3)), 1000); // 1000 initial only because no quota available
assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 500 refunded
assert_eq!(Balances::free_balance(account(1)), 1000); // only initial because no available weight to process
assert_eq!(
Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
// initial minus refunds
10_000 - 500
);
// events
System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded {
who: account(2),
identity: 2,
amount: 500,
}));
})
}
// Copyright 2021 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::*;
/// Trait for managing refund operations.
pub trait RefundFee<T: Config> {
/// Request a refund of a fee for a specific account and identity.
fn request_refund(account: T::AccountId, identity: IdtyId<T>, amount: BalanceOf<T>);
}
impl<T: Config> RefundFee<T> for () {
fn request_refund(_account: T::AccountId, _identity: IdtyId<T>, _amount: BalanceOf<T>) {}
}
// tmp
use frame_support::weights::Weight;
pub trait WeightInfo {
fn queue_refund() -> Weight;
fn spend_quota() -> Weight;
fn try_refund() -> Weight;
fn do_refund() -> Weight;
fn on_process_refund_queue() -> Weight;
fn on_process_refund_queue_elements(_i: u32) -> Weight;
}
impl WeightInfo for () {
fn queue_refund() -> Weight {
Weight::from_parts(100u64, 0)
}
fn spend_quota() -> Weight {
Weight::from_parts(25u64, 0)
}
fn try_refund() -> Weight {
Weight::from_parts(100u64, 0)
}
fn do_refund() -> Weight {
Weight::from_parts(25u64, 0)
}
fn on_process_refund_queue() -> Weight {
Weight::from_parts(1u64, 0)
}
fn on_process_refund_queue_elements(_i: u32) -> Weight {
Weight::from_parts(1u64, 0)
}
}
[package]
name = "pallet-session-benchmarking"
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
description = "FRAME sessions pallet benchmarking"
version.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true }
frame-benchmarking = { workspace = true, optional = true }
frame-system = { workspace = true }
pallet-session = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-runtime = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-system/std",
"pallet-session/std",
"sp-runtime/std",
]
try-runtime = [
"frame-system/try-runtime",
"pallet-session/try-runtime",
"sp-runtime/try-runtime",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
// Copyright 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 Session Benchmarking Pallet
//!
//! This crate provides benchmarks specifically for the `pallet-session` within Duniter. Unlike traditional setups, this implementation is decoupled from the `staking-pallet`, which is not utilized in Duniter's architecture. Instead, session management functionalities are integrated into the `authority-members` pallet.
//!
//! ## Note
//!
//! This crate is separated from the main codebase due to cyclic dependency issues, focusing solely on session-related benchmarking independent of staking-related functionalities.
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg(feature = "runtime-benchmarks")]
use codec::Decode;
use frame_benchmarking::{benchmarks, whitelisted_caller};
use frame_system::RawOrigin;
use pallet_session::*;
use scale_info::prelude::{vec, vec::Vec};
pub struct Pallet<T: Config>(pallet_session::Pallet<T>);
pub trait Config: pallet_session::Config {}
benchmarks! {
set_keys {
let caller: T::AccountId = whitelisted_caller();
frame_system::Pallet::<T>::inc_providers(&caller);
let keys = T::Keys::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()).unwrap();
let proof: Vec<u8> = vec![0,1,2,3];
}: _(RawOrigin::Signed(caller), keys, proof)
purge_keys {
let caller: T::AccountId = whitelisted_caller();
frame_system::Pallet::<T>::inc_providers(&caller);
let keys = T::Keys::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()).unwrap();
let proof: Vec<u8> = vec![0,1,2,3];
let _t = pallet_session::Pallet::<T>::set_keys(RawOrigin::Signed(caller.clone()).into(), keys, proof);
}: _(RawOrigin::Signed(caller))
}
[package]
name = "pallet-smith-members"
authors.workspace = true
description = "duniter pallet to handle offences"
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]
duniter-primitives = { workspace = true }
codec = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
log = { workspace = true }
pallet-authority-members = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-runtime = { workspace = true }
sp-staking = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"log/std",
"pallet-authority-members/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-staking/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-authority-members/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-authority-members/runtime-benchmarks",
"pallet-authority-members/try-runtime",
"sp-runtime/try-runtime",
]
[dev-dependencies]
maplit = { workspace = true, default-features = true }
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/>.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use crate::Pallet;
#[benchmarks(
where
T::IdtyIndex: From<u32>
)]
mod benchmarks {
use super::*;
fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_has_event(generic_event.into());
}
#[benchmark]
fn invite_smith() {
let issuer: T::IdtyIndex = 1.into();
let caller: T::AccountId = T::IdtyAttr::owner_key(issuer).unwrap();
Pallet::<T>::on_smith_goes_online(1.into());
// Should be the last identities from the local_testnet_config
let receiver: T::IdtyIndex = 6.into();
#[extrinsic_call]
_(RawOrigin::Signed(caller), receiver);
assert_has_event::<T>(Event::<T>::InvitationSent { receiver, issuer }.into());
}
#[benchmark]
fn accept_invitation() -> Result<(), BenchmarkError> {
let issuer: T::IdtyIndex = 1.into();
let caller: T::AccountId = T::IdtyAttr::owner_key(issuer).unwrap();
Pallet::<T>::on_smith_goes_online(1.into());
let caller_origin: <T as frame_system::Config>::RuntimeOrigin =
RawOrigin::Signed(caller.clone()).into();
// Should be the last identities from the local_testnet_config
let receiver: T::IdtyIndex = 6.into();
Pallet::<T>::invite_smith(caller_origin, receiver)?;
let issuer: T::IdtyIndex = 6.into();
let caller: T::AccountId = T::IdtyAttr::owner_key(issuer).unwrap();
#[extrinsic_call]
_(RawOrigin::Signed(caller));
assert_has_event::<T>(
Event::<T>::InvitationAccepted {
idty_index: receiver,
}
.into(),
);
Ok(())
}
#[benchmark]
fn certify_smith() -> Result<(), BenchmarkError> {
let issuer: T::IdtyIndex = 1.into();
let caller: T::AccountId = T::IdtyAttr::owner_key(issuer).unwrap();
Pallet::<T>::on_smith_goes_online(1.into());
let caller_origin: <T as frame_system::Config>::RuntimeOrigin =
RawOrigin::Signed(caller.clone()).into();
// Should be the last identities from the local_testnet_config
let receiver: T::IdtyIndex = 6.into();
Pallet::<T>::invite_smith(caller_origin, receiver)?;
let issuer: T::IdtyIndex = receiver;
let caller: T::AccountId = T::IdtyAttr::owner_key(issuer).unwrap();
let caller_origin: <T as frame_system::Config>::RuntimeOrigin =
RawOrigin::Signed(caller.clone()).into();
Pallet::<T>::accept_invitation(caller_origin)?;
let issuer: T::IdtyIndex = 1.into();
let caller: T::AccountId = T::IdtyAttr::owner_key(issuer).unwrap();
#[extrinsic_call]
_(RawOrigin::Signed(caller), receiver);
assert_has_event::<T>(Event::<T>::SmithCertAdded { receiver, issuer }.into());
Ok(())
}
#[benchmark]
fn on_removed_wot_member() {
let idty: T::IdtyIndex = 1.into();
assert!(Smiths::<T>::get(idty).is_some());
#[block]
{
Pallet::<T>::on_removed_wot_member(idty);
}
}
#[benchmark]
fn on_removed_wot_member_empty() {
let idty: T::IdtyIndex = 100.into();
assert!(Smiths::<T>::get(idty).is_none());
#[block]
{
Pallet::<T>::on_removed_wot_member(idty);
}
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(crate::GenesisConfig {
initial_smiths: maplit::btreemap![
1 => (false, vec![2, 3]),
2 => (false, vec![1, 3]),
3 => (false, vec![1, 2]),
],
}),
crate::mock::Runtime
);
}
use crate::{Config, CurrentSession, Pallet};
use pallet_authority_members::SessionIndex;
use sp_runtime::traits::Convert;
impl<T: Config> pallet_authority_members::OnOutgoingMember<T::MemberId> for Pallet<T> {
fn on_outgoing_member(member_id: T::MemberId) {
if let Some(member_id) = T::IdtyIdOfAuthorityId::convert(member_id) {
Pallet::<T>::on_smith_goes_offline(member_id);
}
}
}
/// As long as a Smith is in the authority set, he will not expire.
impl<T: Config> pallet_authority_members::OnIncomingMember<T::MemberId> for Pallet<T> {
fn on_incoming_member(member_id: T::MemberId) {
if let Some(member_id) = T::IdtyIdOfAuthorityId::convert(member_id) {
Pallet::<T>::on_smith_goes_online(member_id);
}
}
}
impl<T: Config> pallet_authority_members::OnNewSession for Pallet<T> {
fn on_new_session(index: SessionIndex) {
CurrentSession::<T>::put(index);
Pallet::<T>::on_exclude_expired_smiths(index);
}
}
// 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 Smith Pallet
//!
//! The Smith pallet in Duniter serves as a bridge between the `identity` and `authority-members` pallets.
//!
//! ## Overview
//!
//! The Smith pallet manages the certification and membership status of Smiths. Smiths are identities that have met certain requirements and play a critical role in the network's operations (block authoring, distance evaluation).
//!
//! ## Key Concepts
//!
//! ### Smith Status
//!
//! The status of an identity within the Smith pallet can be one of the following:
//! - **Invited**: The identity has been invited by a Smith but has not yet accepted the invitation.
//! - **Pending**: The identity has accepted the invitation and is pending to become a full Smith.
//! - **Smith**: The identity has fulfilled the requirements and is a full-fledged Smith, eligible to perform critical network functions.
//! - **Excluded**: The identity has been removed from the Smiths set but its certifications are retained for tracking purposes.
//!
//! ### Certifications
//!
//! Certifications are crucial in determining Smith status:
//! - An identity needs a minimum number of certifications to become a Smith (`MinCertForMembership`).
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod impls;
pub mod traits;
mod types;
pub mod weights;
mod benchmarking;
use codec::{Codec, Decode, Encode};
use duniter_primitives::Idty;
use frame_support::{
dispatch::DispatchResultWithPostInfo,
ensure,
pallet_prelude::{Get, RuntimeDebug, Weight},
};
use frame_system::{
ensure_signed,
pallet_prelude::{BlockNumberFor, OriginFor},
};
use scale_info::{
prelude::{collections::BTreeMap, fmt::Debug, vec, vec::Vec},
TypeInfo,
};
use sp_runtime::traits::{AtLeast32BitUnsigned, IsMember};
use crate::traits::OnSmithDelete;
pub use crate::weights::WeightInfo;
pub use pallet::*;
use pallet_authority_members::SessionIndex;
pub use types::*;
/// Reasons for the removal of a Smith identity.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithRemovalReason {
/// Membership was lost due to expiration or other reasons.
LostMembership,
/// Smith was offline for too long.
OfflineTooLong,
/// Smith was blacklisted.
Blacklisted,
}
/// Possible statuses of a Smith identity.
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithStatus {
/// The identity has been invited by a Smith but has not accepted yet.
Invited,
/// The identity has accepted to eventually become a Smith.
Pending,
/// The identity has reached the requirements to become a Smith and can now perform Smith operations.
Smith,
/// The identity has been removed from the Smiths set but is kept to track its certifications.
Excluded,
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{pallet_prelude::*, traits::StorageVersion};
use pallet_authority_members::SessionIndex;
use sp_runtime::traits::{Convert, IsMember};
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 {
/// Trait to check if identity is a WoT members.
type IsWoTMember: IsMember<Self::IdtyIndex>;
type OnSmithDelete: traits::OnSmithDelete<Self::IdtyIndex>;
/// The overarching event type for this pallet.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// A short identity index type.
type IdtyIndex: Parameter
+ Member
+ AtLeast32BitUnsigned
+ Codec
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ Debug
+ MaxEncodedLen;
/// Identifier type for an authority-member.
type MemberId: Copy + Ord + MaybeSerializeDeserialize + Parameter;
/// Something that gives the IdtyIndex of an AccountId and reverse.
type IdtyAttr: duniter_primitives::Idty<Self::IdtyIndex, Self::AccountId>;
/// Something that gives the AccountId of an identity.
type IdtyIdOfAuthorityId: Convert<Self::MemberId, Option<Self::IdtyIndex>>;
/// Maximum number of active certifications per issuer.
#[pallet::constant]
type MaxByIssuer: Get<u32>;
/// Minimum number of certifications required to become a Smith.
#[pallet::constant]
type MinCertForMembership: Get<u32>;
/// Maximum duration of inactivity allowed before a Smith is removed.
#[pallet::constant]
type SmithInactivityMaxDuration: Get<u32>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
}
/// Events type.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// An identity is being inivited to become a smith.
InvitationSent {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
/// The invitation has been accepted.
InvitationAccepted { idty_index: T::IdtyIndex },
/// Certification received
SmithCertAdded {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
/// Certification lost
SmithCertRemoved {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
/// A smith gathered enough certifications to become an authority (can call `go_online()`).
SmithMembershipAdded { idty_index: T::IdtyIndex },
/// A smith has been removed from the smiths set.
SmithMembershipRemoved { idty_index: T::IdtyIndex },
}
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub initial_smiths: BTreeMap<T::IdtyIndex, (bool, Vec<T::IdtyIndex>)>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
initial_smiths: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
CurrentSession::<T>::put(0);
let mut cert_meta_by_issuer = BTreeMap::<T::IdtyIndex, Vec<T::IdtyIndex>>::new();
for (receiver, (is_online, issuers)) in &self.initial_smiths {
// Forbid self-cert
assert!(
!issuers.contains(receiver),
"Identity cannot certify it-self."
);
let mut issuers_: Vec<_> = Vec::with_capacity(issuers.len());
for issuer in issuers {
// Count issued certs
cert_meta_by_issuer
.entry(*issuer)
.or_insert(vec![])
.push(*receiver);
issuers_.push(*issuer);
}
// Write CertsByReceiver
issuers_.sort();
let issuers_count = issuers_.len();
let smith_status = if issuers_count >= T::MinCertForMembership::get() as usize {
SmithStatus::Smith
} else {
SmithStatus::Pending
};
Smiths::<T>::insert(
receiver,
SmithMeta {
status: smith_status,
expires_on: if *is_online {
None
} else {
Some(CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get())
},
issued_certs: vec![],
received_certs: issuers_,
last_online: None,
},
);
// if smith is offline, schedule expire
if !*is_online {
ExpiresOn::<T>::append(
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get(),
receiver,
);
}
}
for (issuer, issued_certs) in cert_meta_by_issuer {
// Write CertsByIssuer
Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.issued_certs = issued_certs;
}
});
}
}
}
/// The Smith metadata for each identity.
#[pallet::storage]
#[pallet::getter(fn smiths)]
pub type Smiths<T: Config> = StorageMap<
_,
Twox64Concat,
T::IdtyIndex,
SmithMeta<T::IdtyIndex, BlockNumberFor<T>>,
OptionQuery,
>;
/// The indexes of Smith to remove at a given session.
#[pallet::storage]
#[pallet::getter(fn expires_on)]
pub type ExpiresOn<T: Config> =
StorageMap<_, Twox64Concat, SessionIndex, Vec<T::IdtyIndex>, OptionQuery>;
/// The current session index.
#[pallet::storage]
#[pallet::getter(fn current_session)]
pub type CurrentSession<T: Config> = StorageValue<_, SessionIndex, ValueQuery>;
// ERRORS //
#[pallet::error]
pub enum Error<T> {
/// Issuer of anything (invitation, acceptance, certification) must have an identity ID
OriginMustHaveAnIdentity,
/// Issuer must be known as a potential smith
OriginHasNeverBeenInvited,
/// Invitation is reseverd to smiths
InvitationIsASmithPrivilege,
/// Invitation is reseverd to online smiths
InvitationIsAOnlineSmithPrivilege,
/// Invitation must not have been accepted yet
InvitationAlreadyAccepted,
/// Invitation of an already known smith is forbidden except if it has been excluded
InvitationOfExistingNonExcluded,
/// Invitation of a non-member (of the WoT) is forbidden
InvitationOfNonMember,
/// Certification cannot be made on someone who has not accepted an invitation
CertificationMustBeAgreed,
/// Certification cannot be made on excluded
CertificationOnExcludedIsForbidden,
/// Issuer must be a smith
CertificationIsASmithPrivilege,
/// Only online smiths can certify
CertificationIsAOnlineSmithPrivilege,
/// Smith cannot certify itself
CertificationOfSelfIsForbidden,
/// Receiver must be invited by another smith
CertificationReceiverMustHaveBeenInvited,
/// Receiver must not already have this certification
CertificationAlreadyExists,
/// A smith has a limited stock of certifications
CertificationStockFullyConsumed,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Invite a member of the Web of Trust to attempt becoming a Smith.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::invite_smith())]
pub fn invite_smith(
origin: OriginFor<T>,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let issuer =
T::IdtyAttr::idty_index(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_invite_smith(issuer, receiver)?;
Self::do_invite_smith(issuer, receiver);
Ok(().into())
}
/// Accept an invitation to become a Smith (must have been invited first).
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::accept_invitation())]
pub fn accept_invitation(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let receiver =
T::IdtyAttr::idty_index(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_accept_invitation(receiver)?;
Self::do_accept_invitation(receiver)?;
Ok(().into())
}
/// Certify an invited Smith, which can lead the certified to become a Smith.
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::certify_smith())]
pub fn certify_smith(
origin: OriginFor<T>,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let issuer =
T::IdtyAttr::idty_index(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_certify_smith(issuer, receiver)?;
Self::do_certify_smith(receiver, issuer);
Ok(().into())
}
}
}
impl<T: Config> Pallet<T> {
/// Check conditions before inviting a potential Smith.
fn check_invite_smith(
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let issuer = Smiths::<T>::get(issuer).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
ensure!(
issuer.status == SmithStatus::Smith,
Error::<T>::InvitationIsASmithPrivilege
);
ensure!(
issuer.expires_on.is_none(),
Error::<T>::InvitationIsAOnlineSmithPrivilege
);
if let Some(receiver_meta) = Smiths::<T>::get(receiver) {
ensure!(
receiver_meta.status == SmithStatus::Excluded,
Error::<T>::InvitationOfExistingNonExcluded
);
}
ensure!(
T::IsWoTMember::is_member(&receiver),
Error::<T>::InvitationOfNonMember
);
Ok(().into())
}
/// Perform the invitation of a potential Smith.
fn do_invite_smith(issuer: T::IdtyIndex, receiver: T::IdtyIndex) {
let new_expires_on = CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
let mut existing = Smiths::<T>::get(receiver).unwrap_or_default();
existing.status = SmithStatus::Invited;
existing.expires_on = Some(new_expires_on);
existing.received_certs = vec![];
Smiths::<T>::insert(receiver, existing);
ExpiresOn::<T>::append(new_expires_on, receiver);
Self::deposit_event(Event::<T>::InvitationSent { issuer, receiver });
}
/// Check conditions before accepting an invitation to become a Smith.
fn check_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
let pretender_status = Smiths::<T>::get(receiver)
.ok_or(Error::<T>::OriginHasNeverBeenInvited)?
.status;
ensure!(
pretender_status == SmithStatus::Invited,
Error::<T>::InvitationAlreadyAccepted
);
Ok(().into())
}
/// Accept the invitation to become a Smith.
fn do_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.status = SmithStatus::Pending;
}
});
Self::deposit_event(Event::<T>::InvitationAccepted {
idty_index: receiver,
});
Ok(().into())
}
/// Check conditions before certifying a potential Smith.
fn check_certify_smith(
issuer_index: T::IdtyIndex,
receiver_index: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
ensure!(
issuer_index != receiver_index,
Error::<T>::CertificationOfSelfIsForbidden
);
let issuer = Smiths::<T>::get(issuer_index).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
ensure!(
issuer.status == SmithStatus::Smith,
Error::<T>::CertificationIsASmithPrivilege
);
ensure!(
issuer.expires_on.is_none(),
Error::<T>::CertificationIsAOnlineSmithPrivilege
);
let issued_certs = issuer.issued_certs.len();
ensure!(
issued_certs < T::MaxByIssuer::get() as usize,
Error::<T>::CertificationStockFullyConsumed
);
let receiver = Smiths::<T>::get(receiver_index)
.ok_or(Error::<T>::CertificationReceiverMustHaveBeenInvited)?;
ensure!(
receiver.status != SmithStatus::Invited,
Error::<T>::CertificationMustBeAgreed
);
ensure!(
receiver.status != SmithStatus::Excluded,
Error::<T>::CertificationOnExcludedIsForbidden
);
ensure!(
receiver
.received_certs
.binary_search(&issuer_index)
.is_err(),
Error::<T>::CertificationAlreadyExists
);
Ok(().into())
}
/// Perform certification of a potential Smith by another Smith.
fn do_certify_smith(receiver: T::IdtyIndex, issuer: T::IdtyIndex) {
// - adds a certification in issuer issued list
Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.issued_certs.push(receiver);
smith_meta.issued_certs.sort();
}
});
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
// - adds a certification in receiver received list
smith_meta.received_certs.push(issuer);
smith_meta.received_certs.sort();
Self::deposit_event(Event::<T>::SmithCertAdded { issuer, receiver });
// - receiving a certification either lead us to Pending or Smith status
let previous_status = smith_meta.status;
smith_meta.status =
if smith_meta.received_certs.len() >= T::MinCertForMembership::get() as usize {
// - if the number of certification received by the receiver is enough, win the Smith status (or keep it)
SmithStatus::Smith
} else {
// - otherwise we are (still) a pending smith
SmithStatus::Pending
};
if previous_status != SmithStatus::Smith {
// - postpone the expiration: a Pending smith cannot do anything but wait
// this postponement is here to ease the process of becoming a smith
let new_expires_on =
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
smith_meta.expires_on = Some(new_expires_on);
ExpiresOn::<T>::append(new_expires_on, receiver);
}
// - if the status is smith but wasn't, notify that smith gained membership
if smith_meta.status == SmithStatus::Smith && previous_status != SmithStatus::Smith
{
Self::deposit_event(Event::<T>::SmithMembershipAdded {
idty_index: receiver,
});
}
// TODO: (optimization) unschedule old expiry
}
});
}
/// Handle the removal of Smiths whose expiration time has been reached at a given session index.
fn on_exclude_expired_smiths(at: SessionIndex) {
if let Some(smiths_to_remove) = ExpiresOn::<T>::take(at) {
for smith in smiths_to_remove {
if let Some(smith_meta) = Smiths::<T>::get(smith) {
if let Some(expires_on) = smith_meta.expires_on {
if expires_on == at {
Self::_do_exclude_smith(smith, SmithRemovalReason::OfflineTooLong);
}
}
}
}
}
}
/// Handle actions upon the removal of a Web of Trust member.
pub fn on_removed_wot_member(idty_index: T::IdtyIndex) -> Weight {
let mut weight = T::WeightInfo::on_removed_wot_member_empty();
if Smiths::<T>::get(idty_index).is_some() {
Self::_do_exclude_smith(idty_index, SmithRemovalReason::LostMembership);
weight = weight.saturating_add(T::WeightInfo::on_removed_wot_member());
}
weight
}
/// Perform the exclusion of a Smith.
fn _do_exclude_smith(receiver: T::IdtyIndex, reason: SmithRemovalReason) {
let mut lost_certs = vec![];
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.expires_on = None;
smith_meta.status = SmithStatus::Excluded;
for cert in &smith_meta.received_certs {
lost_certs.push(*cert);
}
smith_meta.received_certs = vec![];
// N.B.: the issued certs are kept in case the smith joins back
}
});
// We remove the lost certs from their issuer's stock
for lost_cert_issuer in lost_certs {
Smiths::<T>::mutate(lost_cert_issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
if let Ok(index) = smith_meta.issued_certs.binary_search(&receiver) {
smith_meta.issued_certs.remove(index);
Self::deposit_event(Event::<T>::SmithCertRemoved {
issuer: lost_cert_issuer,
receiver,
});
}
}
});
}
// Deletion done: notify (authority-members) for cascading
T::OnSmithDelete::on_smith_delete(receiver, reason);
Self::deposit_event(Event::<T>::SmithMembershipRemoved {
idty_index: receiver,
});
}
/// Handle the event when a Smith goes online.
pub fn on_smith_goes_online(idty_index: T::IdtyIndex) {
Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
if smith_meta.expires_on.is_some() {
// As long as the smith is online, it cannot expire
smith_meta.expires_on = None;
smith_meta.last_online = None;
}
}
});
}
/// Handle the event when a Smith goes offline.
pub fn on_smith_goes_offline(idty_index: T::IdtyIndex) {
Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
// Smith can go offline after main membership expiry
// in this case, there is no scheduled expiry since it is already excluded
if smith_meta.status != SmithStatus::Excluded {
// schedule expiry
let new_expires_on =
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
smith_meta.expires_on = Some(new_expires_on);
let block_number = frame_system::pallet::Pallet::<T>::block_number();
smith_meta.last_online = Some(block_number);
ExpiresOn::<T>::append(new_expires_on, idty_index);
}
}
});
}
/// Provide whether the given identity index is a Smith.
fn provide_is_member(idty_id: &T::IdtyIndex) -> bool {
Smiths::<T>::get(idty_id).map_or(false, |smith| smith.status == SmithStatus::Smith)
}
}
impl<T: Config> sp_runtime::traits::IsMember<T::IdtyIndex> for Pallet<T> {
fn is_member(idty_id: &T::IdtyIndex) -> bool {
Self::provide_is_member(idty_id)
}
}
// 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_smith_members};
use frame_support::{
derive_impl,
pallet_prelude::Hooks,
parameter_types,
traits::{ConstU32, ConstU64},
weights::{constants::RocksDbWeight, Weight},
};
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, ConvertInto, IdentityLookup, IsMember},
BuildStorage, Perbill,
};
parameter_types! {
pub static OnOffencePerbill: Vec<Perbill> = Default::default();
pub static OffenceWeight: Weight = Default::default();
}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub struct Runtime {
System: frame_system,
Smith: pallet_smith_members,
}
);
#[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;
}
pub struct EveryoneExceptIdZero;
impl IsMember<u64> for EveryoneExceptIdZero {
fn is_member(member_id: &u64) -> bool {
member_id != &0 && member_id != &10
}
}
impl pallet_smith_members::Config for Runtime {
type IdtyAttr = ();
type IdtyIdOfAuthorityId = ConvertInto;
type IdtyIndex = u64;
type IsWoTMember = EveryoneExceptIdZero;
type MaxByIssuer = ConstU32<3>;
type MemberId = u64;
type MinCertForMembership = ConstU32<2>;
type OnSmithDelete = ();
type RuntimeEvent = RuntimeEvent;
type SmithInactivityMaxDuration = ConstU32<5>;
type WeightInfo = ();
}
pub fn new_test_ext(
genesis_config: crate::pallet::GenesisConfig<Runtime>,
) -> sp_io::TestExternalities {
RuntimeGenesisConfig {
system: SystemConfig::default(),
smith: genesis_config,
}
.build_storage()
.unwrap()
.into()
}
pub fn run_to_block(n: u64) {
while System::block_number() < n {
Smith::on_finalize(System::block_number());
System::on_finalize(System::block_number());
System::reset_events();
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
Smith::on_initialize(System::block_number());
}
}
// 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, run_to_block, Runtime, RuntimeEvent, RuntimeOrigin, System};
use frame_support::{assert_err, assert_ok};
use crate::SmithStatus::{Excluded, Invited, Pending, Smith};
#[cfg(test)]
use maplit::btreemap;
use pallet_authority_members::OnNewSession;
#[test]
fn process_to_become_a_smith_and_lose_it() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
// Events cannot be recorded on genesis
run_to_block(1);
// State before
assert_eq!(Smiths::<Runtime>::get(5), None);
// Try to invite
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::InvitationSent {
receiver: 5,
issuer: 1,
}));
// Accept invitation
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::InvitationAccepted {
idty_index: 5,
}));
// State after
assert_eq!(
Smiths::<Runtime>::get(5).unwrap(),
SmithMeta {
status: SmithStatus::Pending,
expires_on: Some(5),
issued_certs: vec![],
received_certs: vec![],
last_online: None,
}
);
// Then certification 1/2
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertAdded {
receiver: 5,
issuer: 1,
}));
assert_eq!(
Smiths::<Runtime>::get(5).unwrap(),
SmithMeta {
status: SmithStatus::Pending,
expires_on: Some(5),
issued_certs: vec![],
received_certs: vec![1],
last_online: None,
}
);
// Then certification 2/2
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertAdded {
receiver: 5,
issuer: 1,
}));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipAdded { idty_index: 5 },
));
assert_eq!(
Smiths::<Runtime>::get(5).unwrap(),
SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(5),
issued_certs: vec![],
received_certs: vec![1, 2],
last_online: None,
}
);
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_offline(1);
Pallet::<Runtime>::on_smith_goes_offline(2);
// On session 4 everything if fine
Pallet::<Runtime>::on_new_session(4);
assert!(Smiths::<Runtime>::get(1).is_some());
assert!(Smiths::<Runtime>::get(2).is_some());
assert!(Smiths::<Runtime>::get(5).is_some());
// On session 5 no more smiths because of lack of activity
Pallet::<Runtime>::on_new_session(5);
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipRemoved { idty_index: 1 },
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 2,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 3,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 4,
}));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipRemoved { idty_index: 2 },
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 2,
issuer: 3,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 2,
issuer: 4,
}));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipRemoved { idty_index: 5 },
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 3,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 5,
issuer: 1,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 5,
issuer: 2,
}));
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: Some(1),
})
);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: Some(1),
})
);
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: None,
})
);
});
}
#[test]
fn avoid_multiple_events_for_becoming_smith() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_smith_goes_online(3);
// Events cannot be recorded on genesis
run_to_block(1);
// State before
assert_eq!(Smiths::<Runtime>::get(5), None);
// Try to invite
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipAdded { idty_index: 5 },
));
run_to_block(2);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
5
));
// Should not be promoted again
assert!(!System::events().iter().any(|record| record.event
== RuntimeEvent::Smith(Event::<Runtime>::SmithMembershipAdded { idty_index: 5 },)));
});
}
#[test]
fn should_have_checks_on_certify() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![4]),
4 => (false, vec![1, 2]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
// Initially
assert_eq!(
Smiths::<Runtime>::get(1).unwrap(),
SmithMeta {
status: Smith,
expires_on: None,
issued_certs: vec![4],
received_certs: vec![2, 3, 4],
last_online: None,
}
);
assert_eq!(
Smiths::<Runtime>::get(2).unwrap(),
SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![1, 4],
received_certs: vec![3, 4],
last_online: None,
}
);
assert_eq!(
Smiths::<Runtime>::get(3).unwrap(),
SmithMeta {
status: Pending,
expires_on: Some(5),
issued_certs: vec![1, 2],
received_certs: vec![4],
last_online: None,
}
);
assert_eq!(
Smiths::<Runtime>::get(4).unwrap(),
SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![1, 2, 3],
received_certs: vec![1, 2],
last_online: None,
}
);
// Tries all possible errors
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(0), 1),
Error::<Runtime>::OriginHasNeverBeenInvited
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 1),
Error::<Runtime>::CertificationOfSelfIsForbidden
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(3), 5),
Error::<Runtime>::CertificationIsASmithPrivilege
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 6),
Error::<Runtime>::CertificationReceiverMustHaveBeenInvited
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 4),
Error::<Runtime>::CertificationAlreadyExists
);
// #3: state before
assert_eq!(
Smiths::<Runtime>::get(3).unwrap(),
SmithMeta {
status: Pending,
expires_on: Some(5),
issued_certs: vec![1, 2],
received_certs: vec![4],
last_online: None,
}
);
// Try to certify #3
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
3
));
// #3: state after
assert_eq!(
Smiths::<Runtime>::get(3).unwrap(),
SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(5),
issued_certs: vec![1, 2],
received_certs: vec![1, 4],
last_online: None,
}
);
});
}
#[test]
fn smith_activity_postpones_expiration() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![]),
4 => (false, vec![])
],
})
.execute_with(|| {
// On session 4 everything is fine
Pallet::<Runtime>::on_new_session(4);
assert!(Smiths::<Runtime>::get(1).is_some());
assert!(Smiths::<Runtime>::get(2).is_some());
// Smith #2 is online but not #1
Pallet::<Runtime>::on_smith_goes_online(2);
// On session 5: exclusion for lack of activity
Pallet::<Runtime>::on_new_session(5);
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: None,
})
);
// issued_certs is empty because #1 was excluded
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Smith,
expires_on: None,
issued_certs: vec![],
received_certs: vec![3, 4],
last_online: None,
})
);
// Smith #2 goes offline
Pallet::<Runtime>::on_new_session(6);
Pallet::<Runtime>::on_smith_goes_offline(2);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(11),
issued_certs: vec![],
received_certs: vec![3, 4],
last_online: Some(0),
})
);
// Still not expired on session 10
Pallet::<Runtime>::on_new_session(10);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(11),
issued_certs: vec![],
received_certs: vec![3, 4],
last_online: Some(0),
})
);
// But expired on session 11
Pallet::<Runtime>::on_new_session(11);
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: None,
})
);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: Some(0),
})
);
});
}
#[test]
fn smith_coming_back_recovers_its_issued_certs() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 4]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Not activity for Smith #2
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(3);
Pallet::<Runtime>::on_smith_goes_online(4);
// Smith #2 gets excluded
Pallet::<Runtime>::on_new_session(5);
// The issued certs are preserved
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: Excluded,
expires_on: None,
issued_certs: vec![1],
received_certs: vec![],
last_online: None,
})
);
// Smith #2 comes back
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 2));
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
2
)));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
2
));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
2
));
// Smith #2 is back with its issued certs recovered, but not its received certs
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: Smith,
expires_on: Some(10),
issued_certs: vec![1],
received_certs: vec![1, 3],
last_online: None,
})
);
Pallet::<Runtime>::on_smith_goes_online(2);
// We can verify it with the stock rule
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
3
));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
4
));
// Max stock is reached (3 = 1 recovered + 2 new)
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(2), 5),
Error::<Runtime>::CertificationStockFullyConsumed
);
});
}
#[test]
fn certifying_on_different_status() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_smith_goes_online(3);
// State before
assert_eq!(Smiths::<Runtime>::get(5), None);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 5),
Error::<Runtime>::CertificationReceiverMustHaveBeenInvited
);
// After invitation
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Invited);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 5),
Error::<Runtime>::CertificationMustBeAgreed
);
// After acceptation
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Pending);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Pending);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Smith);
// After being a smith
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
5
));
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_new_session(5);
assert_eq!(Smiths::<Runtime>::get(1).unwrap().status, Smith);
assert_eq!(Smiths::<Runtime>::get(2).unwrap().status, Smith);
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Excluded);
// After being excluded
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 5),
Error::<Runtime>::CertificationOnExcludedIsForbidden
);
});
}
#[test]
fn certifying_an_online_smith() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_smith_goes_online(3);
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
Pallet::<Runtime>::on_new_session(2);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
Pallet::<Runtime>::on_new_session(3);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
// Smith can expire
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: Smith,
expires_on: Some(8),
issued_certs: vec![],
received_certs: vec![1, 2],
last_online: None,
})
);
assert_eq!(ExpiresOn::<Runtime>::get(7), Some(vec![5]));
assert_eq!(ExpiresOn::<Runtime>::get(8), Some(vec![5]));
Pallet::<Runtime>::on_smith_goes_online(5);
// After going online, the expiration disappears
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: Smith,
expires_on: None,
issued_certs: vec![],
received_certs: vec![1, 2],
last_online: None,
})
);
// ExpiresOn is not unscheduled, but as expires_on has switched to None it's not a problem
assert_eq!(ExpiresOn::<Runtime>::get(7), Some(vec![5]));
assert_eq!(ExpiresOn::<Runtime>::get(8), Some(vec![5]));
// We can receive certification without postponing the expiration (because we are online)
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
5
));
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: Smith,
expires_on: None,
issued_certs: vec![],
received_certs: vec![1, 2, 3],
last_online: None,
})
);
});
}
/// Test that scheduled expiration is removed after session
#[test]
fn expires_on_cleans_up() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (true, vec![2, 3]),
2 => (true, vec![1,3]),
3 => (true, vec![1,2]),
],
})
.execute_with(|| {
// Alice goes offline, and is set to expire
Pallet::<Runtime>::on_smith_goes_offline(1);
// The expiration block is present in smith data
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![2, 3],
received_certs: vec![2, 3],
last_online: Some(0),
})
);
// It is also present in ExpiresOn schedule
assert_eq!(ExpiresOn::<Runtime>::get(5), Some(vec![1]));
// Go to expiration session
Pallet::<Runtime>::on_new_session(5);
// Alice is expired
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Excluded,
expires_on: None,
issued_certs: vec![2, 3],
received_certs: vec![],
last_online: Some(0),
})
);
// ExpiresOn is clean
assert_eq!(ExpiresOn::<Runtime>::get(5), None);
});
}
#[test]
fn invitation_on_non_wot_member() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
// State before
assert_eq!(Smiths::<Runtime>::get(10), None);
// After invitation
assert_err!(
Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 10),
Error::<Runtime>::InvitationOfNonMember
);
});
}
#[test]
fn losing_wot_membership_cascades_to_smith_members() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// State before
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![3],
received_certs: vec![2, 3, 4],
last_online: None,
})
);
assert_eq!(
Smiths::<Runtime>::get(1).unwrap().issued_certs,
Vec::<u64>::from([3])
);
assert_eq!(
Smiths::<Runtime>::get(2).unwrap().issued_certs,
Vec::<u64>::from([1, 3])
);
assert_eq!(
Smiths::<Runtime>::get(3).unwrap().issued_certs,
Vec::<u64>::from([1, 2])
);
assert_eq!(
Smiths::<Runtime>::get(4).unwrap().issued_certs,
Vec::<u64>::from([1, 2])
);
Pallet::<Runtime>::on_removed_wot_member(1);
// Excluded
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Excluded,
expires_on: None,
issued_certs: vec![3],
received_certs: vec![],
last_online: None,
})
);
// Issued certifications updated for certifiers of 1
assert_eq!(
Smiths::<Runtime>::get(1).unwrap().issued_certs,
Vec::<u64>::from([3])
);
assert_eq!(
Smiths::<Runtime>::get(2).unwrap().issued_certs,
Vec::<u64>::from([3])
);
assert_eq!(
Smiths::<Runtime>::get(3).unwrap().issued_certs,
Vec::<u64>::from([2])
);
assert_eq!(
Smiths::<Runtime>::get(4).unwrap().issued_certs,
Vec::<u64>::from([2])
);
});
}
use crate::SmithRemovalReason;
/// Trait for handling actions when a Smith is deleted.
pub trait OnSmithDelete<IdtyIndex> {
/// Handle the deletion of a smith.
fn on_smith_delete(idty_index: IdtyIndex, reason: SmithRemovalReason);
}
impl<IdtyIndex> OnSmithDelete<IdtyIndex> for () {
fn on_smith_delete(_: IdtyIndex, _: SmithRemovalReason) {}
}
// Copyright 2021 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/>.
//! Various basic types for use in the identity pallet.
use crate::SmithStatus;
use codec::{Decode, Encode};
use frame_support::pallet_prelude::*;
use scale_info::{prelude::vec::Vec, TypeInfo};
use sp_staking::SessionIndex;
/// Represents a certification metadata attached to a Smith identity.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct SmithMeta<IdtyIndex, BlockNumber> {
/// Current status of the Smith.
pub status: SmithStatus,
/// The session at which the Smith will expire (for lack of validation activity).
pub expires_on: Option<SessionIndex>,
/// Certifications issued to other Smiths.
pub issued_certs: Vec<IdtyIndex>,
/// Certifications received from other Smiths.
pub received_certs: Vec<IdtyIndex>,
/// Last online time.
pub last_online: Option<BlockNumber>,
}
/// By default, a smith has the least possible privileges
impl<IdtyIndex, BlockNumber> Default for SmithMeta<IdtyIndex, BlockNumber> {
fn default() -> Self {
Self {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: Vec::<IdtyIndex>::new(),
received_certs: Vec::<IdtyIndex>::new(),
last_online: Default::default(),
}
}
}
// 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::Weight;
/// Weight functions needed for pallet.
pub trait WeightInfo {
fn invite_smith() -> Weight;
fn accept_invitation() -> Weight;
fn certify_smith() -> Weight;
fn on_removed_wot_member() -> Weight;
fn on_removed_wot_member_empty() -> Weight;
}
impl WeightInfo for () {
fn invite_smith() -> Weight {
Weight::zero()
}
fn accept_invitation() -> Weight {
Weight::zero()
}
fn certify_smith() -> Weight {
Weight::zero()
}
fn on_removed_wot_member() -> Weight {
Weight::zero()
}
fn on_removed_wot_member_empty() -> Weight {
Weight::zero()
}
}
[package]
authors = ['librelois <c@elo.tf>']
description = 'FRAME pallet universal dividend accounts storage.'
edition = '2018'
homepage = 'https://substrate.dev'
license = 'AGPL-3.0'
name = 'pallet-ud-accounts-storage'
readme = 'README.md'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
[features]
default = ['std']
runtime-benchmarks = ['frame-benchmarking']
std = [
'codec/std',
'frame-support/std',
'frame-system/std',
'frame-benchmarking/std',
"sp-std/std",
]
try-runtime = ['frame-support/try-runtime']
[dependencies]
# substrate
scale-info = { version = "1.0", default-features = false, features = ["derive"] }
[dependencies.codec]
default-features = false
features = ['derive']
package = 'parity-scale-codec'
version = '2.3.1'
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/librelois/substrate.git'
optional = true
branch = 'duniter-monthly-2022-02'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dependencies.frame-system]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
### DOC ###
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
[dev-dependencies.serde]
version = '1.0.119'
### DEV ###
[dev-dependencies.pallet-balances]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dev-dependencies.sp-core]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dev-dependencies.sp-io]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dev-dependencies.sp-runtime]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'