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 2434 additions and 647 deletions
// Copyright 2021 Axiom-Team
//
// This file is part of Substrate-Libre-Currency.
// This file is part of Duniter-v2S.
//
// Substrate-Libre-Currency is free software: you can redistribute it and/or modify
// 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.
//
// Substrate-Libre-Currency is distributed in the hope that it will be useful,
// 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Duniter Membership Pallet
//!
//! The Duniter Membership Pallet is closely integrated with the Duniter Web of Trust (WoT) and is tailored specifically for Duniter, in contrast to the [Parity Membership Pallet](https://github.com/paritytech/substrate/tree/master/frame/membership). It operates exclusively within the Duniter ecosystem and is utilized internally by the Identity, Web of Trust, and Distance Pallets.
//!
//! ## Main Web of Trust (WoT)
//!
//! The Membership Pallet manages all aspects related to the membership of identities within the Duniter Web of Trust. Unlike traditional membership systems, it does not expose any external calls to users. Instead, its functionalities are accessible through distance evaluations provided by the Distance Oracle.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::type_complexity)]
......@@ -23,77 +31,105 @@ mod mock;
#[cfg(test)]
mod tests;
/*#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;*/
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
use frame_support::dispatch::Weight;
use frame_support::error::BadOrigin;
use frame_support::pallet_prelude::*;
use frame_system::RawOrigin;
use sp_membership::traits::*;
use sp_membership::MembershipData;
use frame_support::pallet_prelude::{Weight, *};
use scale_info::prelude::{collections::BTreeMap, vec::Vec};
use sp_membership::{traits::*, MembershipData};
use sp_runtime::traits::Zero;
use sp_std::prelude::*;
#[cfg(feature = "std")]
use std::collections::BTreeMap;
#[cfg(feature = "runtime-benchmarks")]
pub trait SetupBenchmark<IdtyId, AccountId> {
fn force_valid_distance_status(idty_index: &IdtyId);
fn add_cert(_issuer: &IdtyId, _receiver: &IdtyId);
}
#[cfg(feature = "runtime-benchmarks")]
impl<IdtyId, AccountId> SetupBenchmark<IdtyId, AccountId> for () {
fn force_valid_distance_status(_idty_id: &IdtyId) {}
fn add_cert(_issuer: &IdtyId, _receiver: &IdtyId) {}
}
/// Represent reasons for the removal of membership.
#[derive(Encode, Decode, Clone, DecodeWithMemTracking, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum MembershipRemovalReason {
/// Indicates membership was removed because it reached the end of its life.
Expired,
/// Indicates membership was explicitly revoked.
Revoked,
/// Indicates membership was removed because the received certifications count fell below the threshold.
NotEnoughCerts,
/// Indicates membership was removed due to system reasons (e.g., consumers, authority members, or root).
System,
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::traits::StorageVersion;
use frame_system::pallet_prelude::*;
use sp_runtime::traits::Convert;
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T, I = ()>(_);
pub struct Pallet<T>(_);
// CONFIG //
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
type IsIdtyAllowedToRenewMembership: IsIdtyAllowedToRenewMembership<Self::IdtyId>;
type IsIdtyAllowedToRequestMembership: IsIdtyAllowedToRequestMembership<Self::IdtyId>;
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>;
/// Something that identifies an identity
pub trait Config: frame_system::Config {
/// Check if the identity can perform membership operations.
type CheckMembershipOpAllowed: CheckMembershipOpAllowed<Self::IdtyId>;
/// Something that identifies an identity.
type IdtyId: Copy + MaybeSerializeDeserialize + Parameter + Ord;
/// Something that give the IdtyId on an account id
type IdtyIdOf: Convert<Self::AccountId, Option<Self::IdtyId>>;
/// Optional metadata
type MetaData: Parameter + Validate<Self::AccountId>;
#[pallet::constant]
/// Maximum life span of a non-renewable membership (in number of blocks)
type MembershipPeriod: Get<Self::BlockNumber>;
/// On event handler
type OnEvent: OnEvent<Self::IdtyId, Self::MetaData>;
#[pallet::constant]
/// Maximum period (in number of blocks), where an identity can remain pending subscription.
type PendingMembershipPeriod: Get<Self::BlockNumber>;
/// Something that gives the IdtyId of an AccountId and reverse.
type IdtyAttr: duniter_primitives::Idty<Self::IdtyId, Self::AccountId>;
/// Maximum lifespan of a single membership (in number of blocks).
#[pallet::constant]
/// Duration after which a membership is renewable
type RenewablePeriod: Get<Self::BlockNumber>;
type MembershipPeriod: Get<BlockNumberFor<Self>>;
/// Minimum delay to wait before renewing membership, i.e., asking for distance evaluation.
#[pallet::constant]
/// Minimum duration (in number of blocks between a revocation and a new entry request
type RevocationPeriod: Get<Self::BlockNumber>;
type MembershipRenewalPeriod: Get<BlockNumberFor<Self>>;
/// Handler called when a new membership is created or renewed.
type OnNewMembership: OnNewMembership<Self::IdtyId>;
/// Handler called when a membership is revoked or removed.
type OnRemoveMembership: OnRemoveMembership<Self::IdtyId>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
/// Benchmark setup handler for runtime benchmarks (feature-dependent).
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkSetupHandler: SetupBenchmark<Self::IdtyId, Self::AccountId>;
}
// GENESIS STUFF //
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub memberships: BTreeMap<T::IdtyId, MembershipData<T::BlockNumber>>,
pub struct GenesisConfig<T: Config> {
pub memberships: BTreeMap<T::IdtyId, MembershipData<BlockNumberFor<T>>>,
}
#[cfg(feature = "std")]
impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
memberships: Default::default(),
......@@ -102,358 +138,213 @@ pub mod pallet {
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> {
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
for (idty_id, membership_data) in &self.memberships {
MembershipsExpireOn::<T, I>::append(membership_data.expire_on, idty_id);
Membership::<T, I>::insert(idty_id, membership_data);
MembershipsExpireOn::<T>::append(membership_data.expire_on, idty_id);
Membership::<T>::insert(idty_id, membership_data);
}
}
}
// STORAGE //
/// The membership data for each identity.
#[pallet::storage]
#[pallet::getter(fn membership)]
pub type Membership<T: Config<I>, I: 'static = ()> =
CountedStorageMap<_, Twox64Concat, T::IdtyId, MembershipData<T::BlockNumber>, OptionQuery>;
pub type Membership<T: Config> = CountedStorageMap<
_,
Twox64Concat,
T::IdtyId,
MembershipData<BlockNumberFor<T>>,
OptionQuery,
>;
/// The identities of memberships to expire at a given block.
#[pallet::storage]
#[pallet::getter(fn memberships_expire_on)]
pub type MembershipsExpireOn<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::BlockNumber, Vec<T::IdtyId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn pending_membership)]
pub type PendingMembership<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::IdtyId, T::MetaData, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn pending_memberships_expire_on)]
pub type PendingMembershipsExpireOn<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::BlockNumber, Vec<T::IdtyId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn revoked_membership)]
pub type RevokedMembership<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::IdtyId, (), OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn revoked_memberships_pruned_on)]
pub type RevokedMembershipsPrunedOn<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>;
pub type MembershipsExpireOn<T: Config> =
StorageMap<_, Twox64Concat, BlockNumberFor<T>, Vec<T::IdtyId>, ValueQuery>;
// EVENTS //
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
/// A membership has acquired
/// [idty_id]
MembershipAcquired(T::IdtyId),
/// A membership has expired
/// [idty_id]
MembershipExpired(T::IdtyId),
/// A membership has renewed
/// [idty_id]
MembershipRenewed(T::IdtyId),
/// An identity requested membership
/// [idty_id]
MembershipRequested(T::IdtyId),
/// A membership has revoked
/// [idty_id]
MembershipRevoked(T::IdtyId),
/// A pending membership request has expired
/// [idty_id]
PendingMembershipExpired(T::IdtyId),
pub enum Event<T: Config> {
/// A membership was added.
MembershipAdded {
member: T::IdtyId,
expire_on: BlockNumberFor<T>,
},
/// A membership was renewed.
MembershipRenewed {
member: T::IdtyId,
expire_on: BlockNumberFor<T>,
},
/// A membership was removed.
MembershipRemoved {
member: T::IdtyId,
reason: MembershipRemovalReason,
},
}
// ERRORS//
#[pallet::error]
pub enum Error<T, I = ()> {
/// Identity not allowed to request membership
IdtyNotAllowedToRequestMembership,
/// Identity not allowed to renew membership
IdtyNotAllowedToRenewMembership,
/// Invalid meta data
InvalidMetaData,
/// Identity id not found
IdtyIdNotFound,
/// Membership already acquired
MembershipAlreadyAcquired,
/// Membership already requested
MembershipAlreadyRequested,
/// Membership not yet renewable
MembershipNotYetRenewable,
/// Membership not found
pub enum Error<T> {
/// Membership not found, can not renew.
MembershipNotFound,
/// Origin not allowed to use this identity
OriginNotAllowedToUseIdty,
/// Membership request not found
MembershipRequestNotFound,
/// Membership revoked recently
MembershipRevokedRecently,
/// Already member, can not add membership.
AlreadyMember,
}
// HOOKS //
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
fn on_initialize(n: T::BlockNumber) -> Weight {
if n > T::BlockNumber::zero() {
Self::expire_pending_memberships(n)
+ Self::expire_memberships(n)
+ Self::prune_revoked_memberships(n)
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
if n > BlockNumberFor::<T>::zero() {
T::WeightInfo::on_initialize().saturating_add(Self::expire_memberships(n))
} else {
0
}
}
}
// CALLS //
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::weight(1_000_000_000)]
pub fn force_request_membership(
origin: OriginFor<T>,
idty_id: T::IdtyId,
metadata: T::MetaData,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
Self::do_request_membership(idty_id, metadata)
}
#[pallet::weight(1_000_000_000)]
pub fn request_membership(
origin: OriginFor<T>,
metadata: T::MetaData,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let idty_id = T::IdtyIdOf::convert(who.clone()).ok_or(Error::<T, I>::IdtyIdNotFound)?;
if !metadata.validate(&who) {
return Err(Error::<T, I>::InvalidMetaData.into());
}
if !T::IsIdtyAllowedToRequestMembership::is_idty_allowed_to_request_membership(&idty_id)
{
return Err(Error::<T, I>::IdtyNotAllowedToRequestMembership.into());
}
Self::do_request_membership(idty_id, metadata)
}
#[pallet::weight(1_000_000_000)]
pub fn claim_membership(
origin: OriginFor<T>,
maybe_idty_id: Option<T::IdtyId>,
) -> DispatchResultWithPostInfo {
// Verify phase
let idty_id = Self::ensure_origin_and_get_idty_id(origin, maybe_idty_id)?;
if Membership::<T, I>::contains_key(&idty_id) {
return Err(Error::<T, I>::MembershipAlreadyAcquired.into());
}
let metadata = PendingMembership::<T, I>::take(&idty_id)
.ok_or(Error::<T, I>::MembershipRequestNotFound)?;
// Apply phase
Self::do_renew_membership_inner(idty_id);
Self::deposit_event(Event::MembershipAcquired(idty_id));
T::OnEvent::on_event(&sp_membership::Event::MembershipAcquired(idty_id, metadata));
Ok(().into())
T::WeightInfo::on_initialize()
}
#[pallet::weight(1_000_000_000)]
pub fn renew_membership(
origin: OriginFor<T>,
maybe_idty_id: Option<T::IdtyId>,
) -> DispatchResultWithPostInfo {
// Verify phase
let idty_id = Self::ensure_origin_and_get_idty_id(origin, maybe_idty_id)?;
if !T::IsIdtyAllowedToRenewMembership::is_idty_allowed_to_renew_membership(&idty_id) {
return Err(Error::<T, I>::IdtyNotAllowedToRenewMembership.into());
}
if let Some(membership_data) = Self::get_membership(&idty_id) {
let block_number = frame_system::pallet::Pallet::<T>::block_number();
if membership_data.renewable_on > block_number {
return Err(Error::<T, I>::MembershipNotYetRenewable.into());
}
} else {
return Err(Error::<T, I>::MembershipNotFound.into());
}
let _ = Self::do_renew_membership(idty_id);
// // CALLS //
// #[pallet::call]
// impl<T: Config> Pallet<T> {
// // no calls for membership pallet
// }
Ok(().into())
}
#[pallet::weight(1_000_000_000)]
pub fn revoke_membership(
origin: OriginFor<T>,
maybe_idty_id: Option<T::IdtyId>,
) -> DispatchResultWithPostInfo {
// Verify phase
let idty_id = Self::ensure_origin_and_get_idty_id(origin, maybe_idty_id)?;
// Apply phase
let _ = Self::do_revoke_membership(idty_id);
// INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Unschedules membership expiry.
fn unschedule_membership_expiry(idty_id: T::IdtyId, block_number: BlockNumberFor<T>) {
let mut scheduled = MembershipsExpireOn::<T>::get(block_number);
Ok(().into())
if let Some(pos) = scheduled.iter().position(|x| *x == idty_id) {
scheduled.swap_remove(pos);
MembershipsExpireOn::<T>::set(block_number, scheduled);
}
}
// INTERNAL FUNCTIONS //
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub(super) fn do_renew_membership(idty_id: T::IdtyId) -> Weight {
let total_weight = Self::do_renew_membership_inner(idty_id);
Self::deposit_event(Event::MembershipRenewed(idty_id));
T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id));
total_weight
}
fn do_renew_membership_inner(idty_id: T::IdtyId) -> Weight {
/// Insert membership and schedule its expiry.
fn insert_membership_and_schedule_expiry(idty_id: T::IdtyId) -> BlockNumberFor<T> {
let block_number = frame_system::pallet::Pallet::<T>::block_number();
let expire_on = block_number + T::MembershipPeriod::get();
let renewable_on = block_number + T::RenewablePeriod::get();
Self::insert_membership(
idty_id,
MembershipData {
expire_on,
renewable_on,
},
);
MembershipsExpireOn::<T, I>::append(expire_on, idty_id);
0
}
fn do_request_membership(
idty_id: T::IdtyId,
metadata: T::MetaData,
) -> DispatchResultWithPostInfo {
if PendingMembership::<T, I>::contains_key(&idty_id) {
return Err(Error::<T, I>::MembershipAlreadyRequested.into());
}
if Membership::<T, I>::contains_key(&idty_id) {
return Err(Error::<T, I>::MembershipAlreadyAcquired.into());
}
if RevokedMembership::<T, I>::contains_key(&idty_id) {
return Err(Error::<T, I>::MembershipRevokedRecently.into());
Membership::<T>::insert(idty_id, MembershipData { expire_on });
MembershipsExpireOn::<T>::append(expire_on, idty_id);
expire_on
}
let block_number = frame_system::pallet::Pallet::<T>::block_number();
let expire_on = block_number + T::PendingMembershipPeriod::get();
PendingMembership::<T, I>::insert(idty_id, metadata);
PendingMembershipsExpireOn::<T, I>::append(expire_on, idty_id);
Self::deposit_event(Event::MembershipRequested(idty_id));
T::OnEvent::on_event(&sp_membership::Event::MembershipRequested(idty_id));
/// Check if membership can be claimed.
pub fn check_add_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
// no-op is error
ensure!(
Membership::<T>::get(idty_id).is_none(),
Error::<T>::AlreadyMember
);
Ok(().into())
// check status and enough certifications
T::CheckMembershipOpAllowed::check_add_membership(idty_id)?;
Ok(())
}
pub(super) fn do_revoke_membership(idty_id: T::IdtyId) -> Weight {
if Self::remove_membership(&idty_id) {
if T::RevocationPeriod::get() > Zero::zero() {
let block_number = frame_system::pallet::Pallet::<T>::block_number();
let pruned_on = block_number + T::RevocationPeriod::get();
RevokedMembership::<T, I>::insert(idty_id, ());
RevokedMembershipsPrunedOn::<T, I>::append(pruned_on, idty_id);
}
Self::deposit_event(Event::MembershipRevoked(idty_id));
T::OnEvent::on_event(&sp_membership::Event::MembershipRevoked(idty_id));
}
/// Check if membership renewal is allowed.
pub fn check_renew_membership(
idty_id: T::IdtyId,
) -> Result<MembershipData<BlockNumberFor<T>>, DispatchError> {
let membership_data =
Membership::<T>::get(idty_id).ok_or(Error::<T>::MembershipNotFound)?;
0
}
fn ensure_origin_and_get_idty_id(
origin: OriginFor<T>,
maybe_idty_id: Option<T::IdtyId>,
) -> Result<T::IdtyId, DispatchError> {
match origin.into() {
Ok(RawOrigin::Root) => {
maybe_idty_id.ok_or_else(|| Error::<T, I>::IdtyIdNotFound.into())
}
Ok(RawOrigin::Signed(account_id)) => T::IdtyIdOf::convert(account_id)
.ok_or_else(|| Error::<T, I>::IdtyIdNotFound.into()),
_ => Err(BadOrigin.into()),
// enough certifications
T::CheckMembershipOpAllowed::check_renew_membership(idty_id)?;
Ok(membership_data)
}
}
fn expire_memberships(block_number: T::BlockNumber) -> Weight {
let mut total_weight: Weight = 0;
for idty_id in MembershipsExpireOn::<T, I>::take(block_number) {
if let Some(member_data) = Self::get_membership(&idty_id) {
if member_data.expire_on == block_number {
Self::remove_membership(&idty_id);
Self::deposit_event(Event::MembershipExpired(idty_id));
total_weight +=
T::OnEvent::on_event(&sp_membership::Event::MembershipExpired(idty_id));
}
}
/// Attempt to add membership.
pub fn try_add_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
Self::check_add_membership(idty_id)?;
Self::do_add_membership(idty_id);
Ok(())
}
total_weight
/// Attempt to renew membership.
pub fn try_renew_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> {
let membership_data = Self::check_renew_membership(idty_id)?;
Self::do_renew_membership(idty_id, membership_data);
Ok(())
}
fn expire_pending_memberships(block_number: T::BlockNumber) -> Weight {
let mut total_weight: Weight = 0;
for idty_id in PendingMembershipsExpireOn::<T, I>::take(block_number) {
PendingMembership::<T, I>::remove(&idty_id);
Self::deposit_event(Event::PendingMembershipExpired(idty_id));
total_weight +=
T::OnEvent::on_event(&sp_membership::Event::PendingMembershipExpired(idty_id));
/// Perform membership addition.
fn do_add_membership(idty_id: T::IdtyId) {
let expire_on = Self::insert_membership_and_schedule_expiry(idty_id);
Self::deposit_event(Event::MembershipAdded {
member: idty_id,
expire_on,
});
T::OnNewMembership::on_created(&idty_id);
}
total_weight
/// Perform membership renewal.
fn do_renew_membership(
idty_id: T::IdtyId,
membership_data: MembershipData<BlockNumberFor<T>>,
) {
Self::unschedule_membership_expiry(idty_id, membership_data.expire_on);
let expire_on = Self::insert_membership_and_schedule_expiry(idty_id);
Self::deposit_event(Event::MembershipRenewed {
member: idty_id,
expire_on,
});
T::OnNewMembership::on_renewed(&idty_id);
}
fn prune_revoked_memberships(block_number: T::BlockNumber) -> Weight {
let total_weight: Weight = 0;
if let Some(identities_ids) = RevokedMembershipsPrunedOn::<T, I>::take(block_number) {
for idty_id in identities_ids {
RevokedMembership::<T, I>::remove(idty_id);
/// Perform membership removal.
pub fn do_remove_membership(idty_id: T::IdtyId, reason: MembershipRemovalReason) -> Weight {
let mut weight = T::DbWeight::get().reads_writes(2, 3);
if let Some(membership_data) = Membership::<T>::take(idty_id) {
Self::unschedule_membership_expiry(idty_id, membership_data.expire_on);
Self::deposit_event(Event::MembershipRemoved {
member: idty_id,
reason,
});
weight += T::OnRemoveMembership::on_removed(&idty_id);
}
weight
}
total_weight
}
/// Perform membership expiry scheduled at the given block number.
pub fn expire_memberships(block_number: BlockNumberFor<T>) -> Weight {
let mut expired_idty_count = 0u32;
pub(super) fn is_member_inner(idty_id: &T::IdtyId) -> bool {
Membership::<T, I>::contains_key(idty_id)
}
fn insert_membership(idty_id: T::IdtyId, membership_data: MembershipData<T::BlockNumber>) {
Membership::<T, I>::insert(idty_id, membership_data);
for idty_id in MembershipsExpireOn::<T>::take(block_number) {
// remove membership (take)
Self::do_remove_membership(idty_id, MembershipRemovalReason::Expired);
expired_idty_count += 1;
}
fn get_membership(idty_id: &T::IdtyId) -> Option<MembershipData<T::BlockNumber>> {
Membership::<T, I>::try_get(idty_id).ok()
T::WeightInfo::expire_memberships(expired_idty_count)
}
fn remove_membership(idty_id: &T::IdtyId) -> bool {
Membership::<T, I>::take(idty_id).is_some()
/// Check if an identity is a member.
pub fn is_member(idty_id: &T::IdtyId) -> bool {
Membership::<T>::contains_key(idty_id)
}
}
}
impl<T: Config<I>, I: 'static> IsInPendingMemberships<T::IdtyId> for Pallet<T, I> {
fn is_in_pending_memberships(idty_id: T::IdtyId) -> bool {
PendingMembership::<T, I>::contains_key(idty_id)
}
}
// implement traits
impl<T: Config<I>, I: 'static> sp_runtime::traits::IsMember<T::IdtyId> for Pallet<T, I> {
impl<T: Config> sp_runtime::traits::IsMember<T::IdtyId> for Pallet<T> {
fn is_member(idty_id: &T::IdtyId) -> bool {
Self::is_member_inner(idty_id)
Self::is_member(idty_id)
}
}
impl<T: Config<I>, I: 'static> MembersCount for Pallet<T, I> {
impl<T: Config> MembersCount for Pallet<T> {
fn members_count() -> u32 {
Membership::<T, I>::count()
Membership::<T>::count()
}
}
// Copyright 2021 Axiom-Team
//
// This file is part of Substrate-Libre-Currency.
// This file is part of Duniter-v2S.
//
// Substrate-Libre-Currency is free software: you can redistribute it and/or modify
// 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.
//
// Substrate-Libre-Currency is distributed in the hope that it will be useful,
// 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::{self as pallet_membership};
use frame_support::{
parameter_types,
derive_impl, parameter_types,
traits::{Everything, OnFinalize, OnInitialize},
};
use frame_system as system;
use sp_core::H256;
use sp_runtime::{
testing::Header,
traits::{BlakeTwo256, ConvertInto, IdentityLookup},
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};
......@@ -31,17 +30,12 @@ type AccountId = u64;
type BlockNumber = u64;
type Block = frame_system::mocking::MockBlock<Test>;
pub type IdtyId = u64;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
DefaultMembership: pallet_membership::{Pallet, Call, Event<T>, Storage, Config<T>},
pub enum Test{
System: frame_system,
Membership: pallet_membership,
}
);
......@@ -50,59 +44,48 @@ parameter_types! {
pub const SS58Prefix: u8 = 42;
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl system::Config for Test {
type AccountId = AccountId;
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type Origin = Origin;
type Call = Call;
type Index = u64;
type BlockNumber = BlockNumber;
type Block = Block;
type BlockHashCount = BlockHashCount;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = Event;
type BlockHashCount = BlockHashCount;
type Version = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
type Nonce = u64;
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
type RuntimeOrigin = RuntimeOrigin;
type SS58Prefix = SS58Prefix;
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
parameter_types! {
pub const MembershipPeriod: BlockNumber = 5;
pub const PendingMembershipPeriod: BlockNumber = 3;
pub const RenewablePeriod: BlockNumber = 2;
pub const RevocationPeriod: BlockNumber = 4;
pub const MembershipRenewalPeriod: BlockNumber = 2;
}
impl pallet_membership::Config for Test {
type IsIdtyAllowedToRenewMembership = ();
type IsIdtyAllowedToRequestMembership = ();
type Event = Event;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkSetupHandler = ();
type CheckMembershipOpAllowed = ();
type IdtyAttr = ();
type IdtyId = IdtyId;
type IdtyIdOf = ConvertInto;
type MembershipPeriod = MembershipPeriod;
type MetaData = ();
type OnEvent = ();
type PendingMembershipPeriod = PendingMembershipPeriod;
type RenewablePeriod = RenewablePeriod;
type RevocationPeriod = RevocationPeriod;
type MembershipRenewalPeriod = MembershipRenewalPeriod;
type OnNewMembership = ();
type OnRemoveMembership = ();
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
}
// Build genesis storage according to the mock runtime.
pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::TestExternalities {
GenesisConfig {
RuntimeGenesisConfig {
system: SystemConfig::default(),
default_membership: gen_conf,
membership: gen_conf,
}
.build_storage()
.unwrap()
......@@ -111,11 +94,11 @@ pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::
pub fn run_to_block(n: u64) {
while System::block_number() < n {
DefaultMembership::on_finalize(System::block_number());
Membership::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());
DefaultMembership::on_initialize(System::block_number());
Membership::on_initialize(System::block_number());
}
}
// Copyright 2021 Axiom-Team
//
// This file is part of Substrate-Libre-Currency.
// This file is part of Duniter-v2S.
//
// Substrate-Libre-Currency is free software: you can redistribute it and/or modify
// 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.
//
// Substrate-Libre-Currency is distributed in the hope that it will be useful,
// 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::mock::Event as RuntimeEvent;
use crate::mock::*;
use crate::{Error, Event};
use frame_support::assert_ok;
use crate::{mock::*, Error, Event, MembershipRemovalReason};
use frame_support::{assert_noop, assert_ok};
use maplit::btreemap;
use sp_membership::traits::*;
use sp_membership::MembershipData;
use sp_runtime::traits::IsMember;
use sp_membership::{traits::*, MembershipData};
fn default_gen_conf() -> DefaultMembershipConfig {
DefaultMembershipConfig {
fn default_gen_conf() -> MembershipConfig {
MembershipConfig {
memberships: btreemap![
0 => MembershipData {
expire_on: 3,
renewable_on: 2
}
],
}
......@@ -40,177 +35,127 @@ fn test_genesis_build() {
run_to_block(1);
// Verify state
assert_eq!(
DefaultMembership::membership(0),
Some(MembershipData {
expire_on: 3,
renewable_on: 2
})
);
assert_eq!(DefaultMembership::members_count(), 1);
});
}
#[test]
fn test_membership_not_yet_renewable() {
new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1);
// Merbership 0 cannot be renewed before #2
assert_eq!(
DefaultMembership::renew_membership(Origin::signed(0), None),
Err(Error::<Test, _>::MembershipNotYetRenewable.into())
);
});
}
#[test]
fn test_membership_already_acquired() {
new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1);
// Merbership 0 cannot be reclaimed
assert_eq!(
DefaultMembership::claim_membership(Origin::signed(0), None),
Err(Error::<Test, _>::MembershipAlreadyAcquired.into())
Membership::membership(0),
Some(MembershipData { expire_on: 3 })
);
assert_eq!(Membership::members_count(), 1);
});
}
/// test membership expiration
// membership should expire
#[test]
fn test_membership_request_not_found() {
fn test_membership_expiration() {
new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1);
// Merbership 0 cannot be reclaimed
assert_eq!(
DefaultMembership::claim_membership(Origin::signed(1), None),
Err(Error::<Test, _>::MembershipRequestNotFound.into())
);
// Membership 0 should not expired on block #2
run_to_block(2);
assert!(Membership::is_member(&0));
// Membership 0 should expire on block #3
run_to_block(3);
assert!(!Membership::is_member(&0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
member: 0,
reason: MembershipRemovalReason::Expired,
}));
});
}
/// test membership renewal (triggered automatically after distance evaluation)
#[test]
fn test_membership_renewal() {
new_test_ext(default_gen_conf()).execute_with(|| {
// membership still valid at block 2
run_to_block(2);
// Merbership 0 can be renewable on block #2
assert_ok!(DefaultMembership::renew_membership(Origin::signed(0), None),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRenewed(0))
);
assert!(Membership::is_member(&0));
// Membership 0 can be renewed
assert_ok!(Membership::try_renew_membership(0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRenewed {
member: 0,
expire_on: 2 + <Test as crate::Config>::MembershipPeriod::get(),
}));
// membership should not expire at block 3 to 6 because it has been renewed
run_to_block(3);
assert!(Membership::is_member(&0));
run_to_block(6);
assert!(Membership::is_member(&0));
// membership should expire at block 7 (2+5)
run_to_block(7);
assert!(!Membership::is_member(&0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
member: 0,
reason: MembershipRemovalReason::Expired,
}));
});
}
/// test membership renewal for non member identity
#[test]
fn test_membership_expiration() {
fn test_membership_renewal_nope() {
new_test_ext(default_gen_conf()).execute_with(|| {
// Merbership 0 should not expired on block #2
run_to_block(2);
assert!(DefaultMembership::is_member(&0),);
// Merbership 0 should expire on block #3
run_to_block(3);
assert!(!DefaultMembership::is_member(&0),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipExpired(0))
assert!(!Membership::is_member(&1));
// Membership 1 can not be renewed
assert_noop!(
Membership::try_renew_membership(1),
Error::<Test>::MembershipNotFound,
);
run_to_block(3);
assert!(!Membership::is_member(&1));
});
}
/// test membership revocation
#[test]
fn test_membership_revocation() {
new_test_ext(default_gen_conf()).execute_with(|| {
run_to_block(1);
// Merbership 0 can be revocable on block #1
assert_ok!(DefaultMembership::revoke_membership(
Origin::signed(0),
None
),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRevoked(0))
);
// Membership 0 can't request membership before the end of RevokePeriod (1 + 4 = 5)
run_to_block(2);
assert_eq!(
DefaultMembership::request_membership(Origin::signed(0), ()),
Err(Error::<Test, _>::MembershipRevokedRecently.into())
);
// Membership 0 can request membership after the end of RevokePeriod (1 + 4 = 5)
// Membership 0 can be revocable on block #1
Membership::do_remove_membership(0, MembershipRemovalReason::Revoked);
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
member: 0,
reason: MembershipRemovalReason::Revoked,
}));
assert_eq!(Membership::membership(0), None);
// Membership 0 can re-claim membership
run_to_block(5);
assert_ok!(DefaultMembership::request_membership(Origin::signed(0), ()),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
);
assert_ok!(Membership::try_add_membership(0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipAdded {
member: 0,
expire_on: 5 + <Test as crate::Config>::MembershipPeriod::get(),
}));
});
}
#[test]
fn test_pending_membership_expiration() {
new_test_ext(Default::default()).execute_with(|| {
// Idty 0 request membership
run_to_block(1);
assert_ok!(DefaultMembership::request_membership(Origin::signed(0), ()),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
);
// Then, idty 0 shold still in pending memberships until PendingMembershipPeriod ended
run_to_block(PendingMembershipPeriod::get());
assert!(DefaultMembership::is_in_pending_memberships(0),);
// Then, idty 0 request should expire after PendingMembershipPeriod
run_to_block(1 + PendingMembershipPeriod::get());
assert!(!DefaultMembership::is_in_pending_memberships(0),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::PendingMembershipExpired(0))
);
})
}
/// test membership workflow
// - claim membership
// - renew membership
// - membership expiry
#[test]
fn test_membership_workflow() {
new_test_ext(Default::default()).execute_with(|| {
// Idty 0 request membership
run_to_block(1);
assert_ok!(DefaultMembership::request_membership(Origin::signed(0), ()),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
);
// Then, idty 0 claim membership
// - Then, idty 0 claim membership
run_to_block(2);
assert_ok!(DefaultMembership::claim_membership(Origin::signed(0), None),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipAcquired(0))
);
// Then, idty 0 claim renewal, should fail
run_to_block(3);
assert_eq!(
DefaultMembership::renew_membership(Origin::signed(0), None),
Err(Error::<Test, _>::MembershipNotYetRenewable.into())
);
// Then, idty 0 claim renewal after renewable period, should success
run_to_block(2 + RenewablePeriod::get());
assert_ok!(DefaultMembership::renew_membership(Origin::signed(0), None),);
assert_ok!(Membership::try_add_membership(0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipAdded {
member: 0,
expire_on: 2 + <Test as crate::Config>::MembershipPeriod::get(),
}));
// Then, idty 0 shoul still member until membership period ended
run_to_block(2 + RenewablePeriod::get() + MembershipPeriod::get() - 1);
assert!(DefaultMembership::is_member(&0));
// Then, idty 0 shoul expire after membership period
run_to_block(2 + RenewablePeriod::get() + MembershipPeriod::get());
assert!(!DefaultMembership::is_member(&0),);
assert_eq!(
System::events()[0].event,
RuntimeEvent::DefaultMembership(Event::MembershipExpired(0))
);
// - Then, idty 0 claim renewal, should success
run_to_block(2);
assert_ok!(Membership::try_renew_membership(0));
// idty 0 should still be member until membership period ended
run_to_block(6); // 2 + 5 - 1
assert!(Membership::is_member(&0));
// - Then, idty 0 should expire after membership period
run_to_block(7); // 2 + 5
assert!(!Membership::is_member(&0));
System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved {
member: 0,
reason: MembershipRemovalReason::Expired,
}));
});
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::unnecessary_cast)]
use frame_support::weights::{constants::RocksDbWeight, Weight};
/// Weight functions needed for pallet_universal_dividend.
pub trait WeightInfo {
fn on_initialize() -> Weight;
fn expire_memberships(_i: u32) -> Weight;
}
// Insecure weights implementation, use it for tests only!
impl WeightInfo for () {
fn on_initialize() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 4_012_000 picoseconds.
Weight::from_parts(4_629_000, 0).saturating_add(Weight::from_parts(0, 0))
}
fn expire_memberships(i: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `567 + i * (23 ±0)`
// Estimated: `6583 + i * (2499 ±0)`
// Minimum execution time: 86_925_000 picoseconds.
Weight::from_parts(89_056_000, 0)
.saturating_add(Weight::from_parts(0, 6583))
// Standard Error: 2_429_589
.saturating_add(Weight::from_parts(295_368_241, 0).saturating_mul(i.into()))
.saturating_add(RocksDbWeight::get().reads(3))
.saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(i.into())))
.saturating_add(RocksDbWeight::get().writes(5))
.saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(i.into())))
.saturating_add(Weight::from_parts(0, 2499).saturating_mul(i.into()))
}
}
[package]
name = "pallet-offences"
authors.workspace = true
description = "duniter pallet to handle offences. fork from paritytechnologies offences pallet"
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, features = ["derive"] }
log = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-runtime = { workspace = true }
sp-staking = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"frame-support/std",
"frame-system/std",
"log/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-staking/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
]
[dev-dependencies]
sp-core = { workspace = true, default-features = true }
sp-io = { workspace = true, default-features = true }
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Duniter Offences Pallet
//!
//! This pallet is a fork of the Substrate `offences` pallet, customized to align with the offence rules specified by the `authority-member` pallet rather than the Substrate `staking` pallet.
//!
//! ## Offences Processing
//!
//! The Duniter Offences Pallet manages various types of offences as follows:
//!
//! - **`im-online` Pallet Offences**: Offences from the `im-online` pallet necessitate disconnection of the offender.
//!
//! - **Other Offences**: For all other offences, the pallet enforces:
//! - Disconnection of the offender.
//! - Addition of the offender to a blacklist.
//! - Authorization from a designated origin to remove offenders from the blacklist.
//!
//! ## Offences Triage and Slashing Execution
//!
//! This pallet handles the triage of offences, categorizing them based on predefined rules. The actual execution of slashing and other punitive measures is delegated to the `authority-member` pallet.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
use core::marker::PhantomData;
use codec::Encode;
use frame_support::weights::Weight;
use scale_info::prelude::vec::Vec;
use sp_runtime::traits::Hash;
use sp_staking::offence::{Kind, Offence, OffenceDetails, OffenceError, ReportOffence};
pub use pallet::*;
pub mod traits;
use self::traits::*;
/// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`.
type OpaqueTimeSlot = Vec<u8>;
/// A type alias for a report identifier.
type ReportIdOf<T> = <T as frame_system::Config>::Hash;
pub enum SlashStrategy {
Disconnect,
Blacklist,
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
/// The pallet's config trait.
#[pallet::config]
pub trait Config: frame_system::Config {
/// The overarching event type.
type RuntimeEvent: From<Event> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Full identification of the validator.
type IdentificationTuple: Parameter;
/// A handler called for every offence report.
type OnOffenceHandler: OnOffenceHandler<Self::AccountId, Self::IdentificationTuple, Weight>;
}
/// The primary structure that holds all offence records keyed by report identifiers.
#[pallet::storage]
#[pallet::getter(fn reports)]
pub type Reports<T: Config> = StorageMap<
_,
Twox64Concat,
ReportIdOf<T>,
OffenceDetails<T::AccountId, T::IdentificationTuple>,
>;
/// A vector of reports of the same kind that happened at the same time slot.
#[pallet::storage]
pub type ConcurrentReportsIndex<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
Kind,
Twox64Concat,
OpaqueTimeSlot,
Vec<ReportIdOf<T>>,
ValueQuery,
>;
/// Events type.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event {
/// An offense was reported during the specified time slot. This event is not deposited for duplicate slashes.
Offence {
kind: Kind,
timeslot: OpaqueTimeSlot,
},
}
}
impl<T, O> ReportOffence<T::AccountId, T::IdentificationTuple, O> for Pallet<T>
where
T: Config,
O: Offence<T::IdentificationTuple>,
{
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
let offenders = offence.offenders();
let time_slot = offence.time_slot();
// Go through all offenders in the offence report and find all offenders that were spotted
// in unique reports.
let TriageOutcome {
concurrent_offenders,
} = match Self::triage_offence_report::<O>(reporters, &time_slot, offenders) {
Some(triage) => triage,
None => return Err(OffenceError::DuplicateReport),
};
// Define the slash strategy.
let slash_strategy = if O::ID == *b"im-online:offlin" {
SlashStrategy::Disconnect
} else {
SlashStrategy::Blacklist
};
T::OnOffenceHandler::on_offence(
&concurrent_offenders,
slash_strategy,
offence.session_index(),
);
Self::deposit_event(Event::Offence {
kind: O::ID,
timeslot: time_slot.encode(),
});
Ok(())
}
fn is_known_offence(offenders: &[T::IdentificationTuple], time_slot: &O::TimeSlot) -> bool {
let any_unknown = offenders.iter().any(|offender| {
let report_id = Self::report_id::<O>(time_slot, offender);
!<Reports<T>>::contains_key(report_id)
});
!any_unknown
}
}
impl<T: Config> Pallet<T> {
/// Compute the ID for the given report properties.
///
/// The report id depends on the offence kind, time slot and the id of offender.
fn report_id<O: Offence<T::IdentificationTuple>>(
time_slot: &O::TimeSlot,
offender: &T::IdentificationTuple,
) -> ReportIdOf<T> {
(O::ID, time_slot.encode(), offender).using_encoded(T::Hashing::hash)
}
/// Triages the offence report and returns the set of offenders that was involved in unique
/// reports along with the list of the concurrent offences.
fn triage_offence_report<O: Offence<T::IdentificationTuple>>(
reporters: Vec<T::AccountId>,
time_slot: &O::TimeSlot,
offenders: Vec<T::IdentificationTuple>,
) -> Option<TriageOutcome<T>> {
let mut storage = ReportIndexStorage::<T, O>::load(time_slot);
let mut any_new = false;
for offender in offenders {
let report_id = Self::report_id::<O>(time_slot, &offender);
if !<Reports<T>>::contains_key(report_id) {
any_new = true;
<Reports<T>>::insert(
report_id,
OffenceDetails {
offender,
reporters: reporters.clone(),
},
);
storage.insert(report_id);
}
}
if any_new {
// Load report details for the all reports happened at the same time.
let concurrent_offenders = storage
.concurrent_reports
.iter()
.filter_map(<Reports<T>>::get)
.collect::<Vec<_>>();
storage.save();
Some(TriageOutcome {
concurrent_offenders,
})
} else {
None
}
}
}
struct TriageOutcome<T: Config> {
/// Other reports for the same report kinds.
concurrent_offenders: Vec<OffenceDetails<T::AccountId, T::IdentificationTuple>>,
}
/// An auxiliary struct for working with storage of indexes localized for a specific offence
/// kind (specified by the `O` type parameter).
///
/// This struct is responsible for aggregating storage writes and the underlying storage should not
/// accessed directly meanwhile.
#[must_use = "The changes are not saved without called `save`"]
struct ReportIndexStorage<T: Config, O: Offence<T::IdentificationTuple>> {
opaque_time_slot: OpaqueTimeSlot,
concurrent_reports: Vec<ReportIdOf<T>>,
_phantom: PhantomData<O>,
}
impl<T: Config, O: Offence<T::IdentificationTuple>> ReportIndexStorage<T, O> {
/// Preload indexes from the storage for the specific `time_slot` and the kind of the offence.
fn load(time_slot: &O::TimeSlot) -> Self {
let opaque_time_slot = time_slot.encode();
let concurrent_reports = <ConcurrentReportsIndex<T>>::get(O::ID, &opaque_time_slot);
Self {
opaque_time_slot,
concurrent_reports,
_phantom: Default::default(),
}
}
/// Insert a new report to the index.
fn insert(&mut self, report_id: ReportIdOf<T>) {
// Update the list of concurrent reports.
self.concurrent_reports.push(report_id);
}
/// Dump the indexes to the storage.
fn save(self) {
<ConcurrentReportsIndex<T>>::insert(
O::ID,
&self.opaque_time_slot,
&self.concurrent_reports,
);
}
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::{self as pallet_offences, Config, SlashStrategy};
use codec::Encode;
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU32, ConstU64},
weights::{constants::RocksDbWeight, Weight},
};
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage, Perbill,
};
use sp_staking::{
offence::{Kind, OffenceDetails},
SessionIndex,
};
pub struct OnOffenceHandler;
parameter_types! {
pub static OnOffencePerbill: Vec<Perbill> = Default::default();
pub static OffenceWeight: Weight = Default::default();
}
impl<Reporter, Offender> pallet_offences::OnOffenceHandler<Reporter, Offender, Weight>
for OnOffenceHandler
{
fn on_offence(
_offenders: &[OffenceDetails<Reporter, Offender>],
_strategy: SlashStrategy,
_offence_session: SessionIndex,
) -> Weight {
OffenceWeight::get()
}
}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub struct Runtime {
System: frame_system,
Offences: pallet_offences,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type AccountId = u64;
type BaseCallFilter = frame_support::traits::Everything;
type Block = Block;
type BlockHashCount = ConstU64<250>;
type DbWeight = RocksDbWeight;
type Hash = H256;
type Hashing = BlakeTwo256;
type Lookup = IdentityLookup<Self::AccountId>;
type MaxConsumers = ConstU32<16>;
type Nonce = u64;
type PalletInfo = PalletInfo;
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
type RuntimeOrigin = RuntimeOrigin;
}
impl Config for Runtime {
type IdentificationTuple = u64;
type OnOffenceHandler = OnOffenceHandler;
type RuntimeEvent = RuntimeEvent;
}
pub fn new_test_ext() -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::<Runtime>::default()
.build_storage()
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
pub const KIND: [u8; 16] = *b"test_report_1234";
/// Returns all offence details for the specific `kind` happened at the specific time slot.
pub fn offence_reports(kind: Kind, time_slot: u128) -> Vec<OffenceDetails<u64, u64>> {
<crate::ConcurrentReportsIndex<Runtime>>::get(kind, time_slot.encode())
.into_iter()
.map(|report_id| {
<crate::Reports<Runtime>>::get(report_id)
.expect("dangling report id is found in ConcurrentReportsIndex")
})
.collect()
}
#[derive(Clone)]
pub struct Offence {
pub validator_set_count: u32,
pub offenders: Vec<u64>,
pub time_slot: u128,
}
impl pallet_offences::Offence<u64> for Offence {
type TimeSlot = u128;
const ID: pallet_offences::Kind = KIND;
fn offenders(&self) -> Vec<u64> {
self.offenders.clone()
}
fn validator_set_count(&self) -> u32 {
self.validator_set_count
}
fn time_slot(&self) -> u128 {
self.time_slot
}
fn session_index(&self) -> SessionIndex {
1
}
fn slash_fraction(&self, offenders_count: u32) -> Perbill {
Perbill::from_percent(5 + offenders_count * 100 / self.validator_set_count)
}
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use crate::mock::{new_test_ext, offence_reports, Offence, Offences, RuntimeEvent, System, KIND};
use frame_system::{EventRecord, Phase};
#[test]
fn should_report_an_authority_and_trigger_on_offence_and_add_to_blacklist() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5, 9],
};
// when
Offences::report_offence(vec![], offence).unwrap();
// then
assert_eq!(
offence_reports(KIND, time_slot),
vec![
OffenceDetails {
offender: 5,
reporters: vec![]
},
OffenceDetails {
offender: 9,
reporters: vec![]
}
]
);
});
}
#[test]
fn should_not_report_the_same_authority_twice_in_the_same_slot() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
Offences::report_offence(vec![], offence.clone()).unwrap();
// when
// report for the second time
assert_eq!(
Offences::report_offence(vec![], offence),
Err(OffenceError::DuplicateReport)
);
// then
assert_eq!(
offence_reports(KIND, time_slot),
vec![OffenceDetails {
offender: 5,
reporters: vec![]
},]
);
});
}
#[test]
fn should_report_in_different_time_slot() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let mut offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
Offences::report_offence(vec![], offence.clone()).unwrap();
System::assert_last_event(
Event::Offence {
kind: KIND,
timeslot: time_slot.encode(),
}
.into(),
);
// when
// report for the second time
offence.time_slot += 1;
Offences::report_offence(vec![], offence.clone()).unwrap();
// then
System::assert_last_event(
Event::Offence {
kind: KIND,
timeslot: offence.time_slot.encode(),
}
.into(),
);
});
}
#[test]
fn should_deposit_event() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
// when
Offences::report_offence(vec![], offence).unwrap();
// then
assert_eq!(
System::events(),
vec![EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::Offences(crate::Event::Offence {
kind: KIND,
timeslot: time_slot.encode()
}),
topics: vec![],
}]
);
});
}
#[test]
fn doesnt_deposit_event_for_dups() {
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
Offences::report_offence(vec![], offence.clone()).unwrap();
// when
// report for the second time
assert_eq!(
Offences::report_offence(vec![], offence),
Err(OffenceError::DuplicateReport)
);
// then
// there is only one event.
assert_eq!(
System::events(),
vec![EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::Offences(crate::Event::Offence {
kind: KIND,
timeslot: time_slot.encode()
}),
topics: vec![],
}]
);
});
}
#[test]
fn reports_if_an_offence_is_dup() {
new_test_ext().execute_with(|| {
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence = |time_slot, offenders| Offence {
validator_set_count: 5,
time_slot,
offenders,
};
let mut test_offence = offence(time_slot, vec![0]);
// the report for authority 0 at time slot 42 should not be a known
// offence
assert!(
!<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence.offenders,
&test_offence.time_slot
)
);
// we report an offence for authority 0 at time slot 42
Offences::report_offence(vec![], test_offence.clone()).unwrap();
// the same report should be a known offence now
assert!(
<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence.offenders,
&test_offence.time_slot
)
);
// and reporting it again should yield a duplicate report error
assert_eq!(
Offences::report_offence(vec![], test_offence.clone()),
Err(OffenceError::DuplicateReport)
);
// after adding a new offender to the offence report
test_offence.offenders.push(1);
// it should not be a known offence anymore
assert!(
!<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence.offenders,
&test_offence.time_slot
)
);
// and reporting it again should work without any error
assert_eq!(
Offences::report_offence(vec![], test_offence.clone()),
Ok(())
);
// creating a new offence for the same authorities on the next slot
// should be considered a new offence and thefore not known
let test_offence_next_slot = offence(time_slot + 1, vec![0, 1]);
assert!(
!<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
&test_offence_next_slot.offenders,
&test_offence_next_slot.time_slot
)
);
});
}
#[test]
fn should_properly_count_offences() {
// We report two different authorities for the same issue. Ultimately, the 1st authority
// should have `count` equal 2 and the count of the 2nd one should be equal to 1.
new_test_ext().execute_with(|| {
// given
let time_slot = 42;
assert_eq!(offence_reports(KIND, time_slot), vec![]);
let offence1 = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![5],
};
let offence2 = Offence {
validator_set_count: 5,
time_slot,
offenders: vec![4],
};
Offences::report_offence(vec![], offence1).unwrap();
// when
// report for the second time
Offences::report_offence(vec![], offence2).unwrap();
// then
// the 1st authority should have count 2 and the 2nd one should be reported only once.
assert_eq!(
offence_reports(KIND, time_slot),
vec![
OffenceDetails {
offender: 5,
reporters: vec![]
},
OffenceDetails {
offender: 4,
reporters: vec![]
},
]
);
});
}
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use sp_staking::{offence::OffenceDetails, SessionIndex};
use crate::SlashStrategy;
/// Trait for handling offences.
pub trait OnOffenceHandler<Reporter, Offender, Res> {
/// Handle an offence committed by one or more offenders.
fn on_offence(
offenders: &[OffenceDetails<Reporter, Offender>],
slash_strategy: SlashStrategy,
session: SessionIndex,
) -> Res;
}
[package]
authors.workspace = true
description = "duniter pallet oneshot account"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-oneshot-account"
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-transaction-payment/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/runtime-benchmarks",
"pallet-balances/try-runtime",
"pallet-transaction-payment/try-runtime",
"sp-runtime/try-runtime",
]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"log/std",
"pallet-balances/std",
"pallet-transaction-payment/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
]
[package.metadata.docs.rs]
default-features = false
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 }
log = { workspace = true }
pallet-balances = { workspace = true }
pallet-transaction-payment = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
pallet-balances = { workspace = true, default-features = true }
// Copyright 2021-2022 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg(feature = "runtime-benchmarks")]
#![allow(clippy::multiple_bound_locations)]
use super::*;
use frame_benchmarking::{account, v2::*, whitelisted_caller};
use frame_support::{pallet_prelude::IsType, traits::fungible::Mutate};
use frame_system::RawOrigin;
use pallet_balances::Pallet as Balances;
use crate::Pallet;
type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
#[benchmarks(
where
T: pallet_balances::Config,
T::Balance: From<u64>,
BalanceOf<T>: IsType<T::Balance>+From<T::Balance>
)]
mod benchmarks {
use super::*;
#[benchmark]
fn create_oneshot_account() {
let existential_deposit = T::ExistentialDeposit::get();
let caller = whitelisted_caller();
let balance = existential_deposit.saturating_mul((2).into());
let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
&caller,
balance.into(),
);
let recipient: T::AccountId = account("recipient", 0, 1);
let recipient_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient.clone());
let transfer_amount = existential_deposit;
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()),
recipient_lookup,
transfer_amount.into(),
);
assert_eq!(Balances::<T>::free_balance(&caller), transfer_amount);
assert_eq!(
OneshotAccounts::<T>::get(&recipient),
Some(transfer_amount.into())
);
}
#[benchmark]
fn consume_oneshot_account() {
let existential_deposit = T::ExistentialDeposit::get();
let caller: T::AccountId = whitelisted_caller();
let balance = existential_deposit.saturating_mul((2).into());
OneshotAccounts::<T>::insert(caller.clone(), Into::<BalanceOf<T>>::into(balance));
// Deposit into a normal account is more expensive than into a oneshot account
// so we create the recipient account with an existential deposit.
let recipient: T::AccountId = account("recipient", 0, 1);
let recipient_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient.clone());
let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
&recipient,
existential_deposit.into(),
);
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()),
BlockNumberFor::<T>::zero(),
Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient_lookup),
);
assert_eq!(OneshotAccounts::<T>::get(&caller), None);
assert_eq!(
Balances::<T>::free_balance(&recipient),
existential_deposit.saturating_mul((3).into())
);
}
#[benchmark]
fn consume_oneshot_account_with_remaining() {
let existential_deposit = T::ExistentialDeposit::get();
let caller: T::AccountId = whitelisted_caller();
let balance = existential_deposit.saturating_mul((2).into());
OneshotAccounts::<T>::insert(caller.clone(), Into::<BalanceOf<T>>::into(balance));
// Deposit into a normal account is more expensive than into a oneshot account
// so we create the recipient accounts with an existential deposits.
let recipient1: T::AccountId = account("recipient1", 0, 1);
let recipient1_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient1.clone());
let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
&recipient1,
existential_deposit.into(),
);
let recipient2: T::AccountId = account("recipient2", 1, 1);
let recipient2_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient2.clone());
let _ = <<T as pallet::Config>::Currency as Mutate<T::AccountId>>::set_balance(
&recipient2,
existential_deposit.into(),
);
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()),
BlockNumberFor::<T>::zero(),
Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient1_lookup),
Account::<<T::Lookup as StaticLookup>::Source>::Normal(recipient2_lookup),
existential_deposit.into(),
);
assert_eq!(OneshotAccounts::<T>::get(&caller), None);
assert_eq!(
Balances::<T>::free_balance(&recipient1),
existential_deposit.saturating_mul((2).into())
);
assert_eq!(
Balances::<T>::free_balance(&recipient2),
existential_deposit.saturating_mul((2).into())
);
}
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
}
// 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::Config;
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::{dispatch::DispatchInfo, pallet_prelude::Weight, traits::IsSubType};
//use frame_system::Config;
use scale_info::{
prelude::fmt::{Debug, Formatter},
TypeInfo,
};
use sp_runtime::{
traits::{
AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf,
TransactionExtension, ValidateResult,
},
transaction_validity::{TransactionSource, TransactionValidityError},
DispatchResult,
};
/// Wrapper around `frame_system::CheckNonce<T>`.
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(Runtime))]
pub struct CheckNonce<T: Config>(pub frame_system::CheckNonce<T>);
impl<T: Config> From<frame_system::CheckNonce<T>> for CheckNonce<T> {
fn from(check_nonce: frame_system::CheckNonce<T>) -> Self {
Self(check_nonce)
}
}
impl<T: Config> Debug for CheckNonce<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut Formatter) -> scale_info::prelude::fmt::Result {
write!(f, "CheckNonce({})", self.0 .0)
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut Formatter) -> scale_info::prelude::fmt::Result {
Ok(())
}
}
impl<T: Config + TypeInfo> TransactionExtension<T::RuntimeCall> for CheckNonce<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo>,
<T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
T::RuntimeCall: IsSubType<crate::Call<T>>,
{
type Implicit = ();
type Pre = <frame_system::CheckNonce<T> as TransactionExtension<T::RuntimeCall>>::Pre;
type Val = <frame_system::CheckNonce<T> as TransactionExtension<T::RuntimeCall>>::Val;
const IDENTIFIER: &'static str = "CheckNonce";
fn validate(
&self,
origin: <T as frame_system::Config>::RuntimeOrigin,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
self_implicit: Self::Implicit,
inherited_implication: &impl sp_runtime::traits::Implication,
source: TransactionSource,
) -> ValidateResult<Self::Val, T::RuntimeCall> {
self.0.validate(
origin,
call,
info,
len,
self_implicit,
inherited_implication,
source,
)
}
fn weight(&self, origin: &T::RuntimeCall) -> Weight {
self.0.weight(origin)
}
fn prepare(
self,
val: Self::Val,
origin: &T::RuntimeOrigin,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
if let Some(
crate::Call::consume_oneshot_account { .. }
| crate::Call::consume_oneshot_account_with_remaining { .. },
) = call.is_sub_type()
{
Ok(Self::Pre::NonceChecked)
} else {
self.0.prepare(val, origin, call, info, len)
}
}
fn post_dispatch_details(
pre: Self::Pre,
info: &DispatchInfo,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
len: usize,
result: &DispatchResult,
) -> Result<Weight, TransactionValidityError> {
<frame_system::CheckNonce<T> as TransactionExtension<T::RuntimeCall>>::post_dispatch_details(
pre, info, post_info, len, result,
)
}
}
// 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/>.
//! # Duniter Oneshot Account Pallet
//!
//! Duniter Oneshot Account Pallet introduces lightweight accounts that do not utilize `AccountInfo`, including fields like nonce, consumers, providers, sufficients, free, reserved. These accounts are designed for single-use scenarios, aiming to reduce transaction weight and associated fees. The primary use cases include anonymous transactions and physical support scenarios where lightweight and disposable accounts are beneficial.
#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
mod check_nonce;
#[cfg(test)]
mod mock;
mod types;
pub mod weights;
pub use check_nonce::CheckNonce;
pub use pallet::*;
pub use types::*;
pub use weights::WeightInfo;
use frame_support::{
pallet_prelude::*,
traits::{
fungible,
fungible::{Balanced, Credit, Inspect},
tokens::{Fortitude, Precision, Preservation},
Imbalance, IsSubType,
},
};
use frame_system::pallet_prelude::*;
use pallet_transaction_payment::OnChargeTransaction;
use sp_runtime::traits::{DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, Zero};
type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
// CONFIG //
#[pallet::config]
pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
/// The currency type.
type Currency: fungible::Balanced<Self::AccountId> + fungible::Mutate<Self::AccountId>;
/// A handler for charging transactions.
type InnerOnChargeTransaction: OnChargeTransaction<Self>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
}
// STORAGE //
/// The balance for each oneshot account.
#[pallet::storage]
#[pallet::getter(fn oneshot_account)]
pub type OneshotAccounts<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, OptionQuery>;
// EVENTS //
#[allow(clippy::type_complexity)]
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A oneshot account was created.
OneshotAccountCreated {
account: T::AccountId,
balance: BalanceOf<T>,
creator: T::AccountId,
},
/// A oneshot account was consumed.
OneshotAccountConsumed {
account: T::AccountId,
dest1: (T::AccountId, BalanceOf<T>),
dest2: Option<(T::AccountId, BalanceOf<T>)>,
},
/// A withdrawal was executed on a oneshot account.
Withdraw {
account: T::AccountId,
balance: BalanceOf<T>,
},
}
// ERRORS //
#[pallet::error]
pub enum Error<T> {
/// Block height is in the future.
BlockHeightInFuture,
/// Block height is too old.
BlockHeightTooOld,
/// Destination account does not exist.
DestAccountNotExist,
/// Destination account has a balance less than the existential deposit.
ExistentialDeposit,
/// Source account has insufficient balance.
InsufficientBalance,
/// Destination oneshot account already exists.
OneshotAccountAlreadyCreated,
/// Source oneshot account does not exist.
OneshotAccountNotExist,
}
// CALLS //
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Create an account that can only be consumed once
///
/// - `dest`: The oneshot account to be created.
/// - `balance`: The balance to be transfered to this oneshot account.
///
/// Origin account is kept alive.
#[pallet::call_index(0)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::create_oneshot_account())]
pub fn create_oneshot_account(
origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] value: BalanceOf<T>,
) -> DispatchResult {
let transactor = ensure_signed(origin)?;
let dest = T::Lookup::lookup(dest)?;
ensure!(
value >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit
);
ensure!(
OneshotAccounts::<T>::get(&dest).is_none(),
Error::<T>::OneshotAccountAlreadyCreated
);
let _ = T::Currency::withdraw(
&transactor,
value,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)?;
OneshotAccounts::<T>::insert(&dest, value);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest,
balance: value,
creator: transactor,
});
Ok(())
}
/// Consume a oneshot account and transfer its balance to an account
///
/// - `block_height`: Must be a recent block number. The limit is `BlockHashCount` in the past. (this is to prevent replay attacks)
/// - `dest`: The destination account.
/// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account.
#[pallet::call_index(1)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::consume_oneshot_account())]
pub fn consume_oneshot_account(
origin: OriginFor<T>,
block_height: BlockNumberFor<T>,
dest: Account<<T::Lookup as StaticLookup>::Source>,
) -> DispatchResult {
let transactor = ensure_signed(origin)?;
let (dest, dest_is_oneshot) = match dest {
Account::Normal(account) => (account, false),
Account::Oneshot(account) => (account, true),
};
let dest = T::Lookup::lookup(dest)?;
let value = OneshotAccounts::<T>::take(&transactor)
.ok_or(Error::<T>::OneshotAccountNotExist)?;
ensure!(
block_height <= frame_system::Pallet::<T>::block_number(),
Error::<T>::BlockHeightInFuture
);
ensure!(
frame_system::pallet::BlockHash::<T>::contains_key(block_height),
Error::<T>::BlockHeightTooOld
);
if dest_is_oneshot {
ensure!(
OneshotAccounts::<T>::get(&dest).is_none(),
Error::<T>::OneshotAccountAlreadyCreated
);
OneshotAccounts::<T>::insert(&dest, value);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest.clone(),
balance: value,
creator: transactor.clone(),
});
} else if frame_system::Pallet::<T>::providers(&dest) > 0 {
let _ = T::Currency::deposit(&dest, value, Precision::Exact)?;
}
OneshotAccounts::<T>::remove(&transactor);
Self::deposit_event(Event::OneshotAccountConsumed {
account: transactor,
dest1: (dest, value),
dest2: None,
});
Ok(())
}
/// Consume a oneshot account then transfer some amount to an account,
/// and the remaining amount to another account.
///
/// - `block_height`: Must be a recent block number.
/// The limit is `BlockHashCount` in the past. (this is to prevent replay attacks)
/// - `dest`: The destination account.
/// - `dest_is_oneshot`: If set to `true`, then a oneshot account is created at `dest`. Else, `dest` has to be an existing account.
/// - `dest2`: The second destination account.
/// - `dest2_is_oneshot`: If set to `true`, then a oneshot account is created at `dest2`. Else, `dest2` has to be an existing account.
/// - `balance1`: The amount transfered to `dest`, the leftover being transfered to `dest2`.
#[pallet::call_index(2)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::consume_oneshot_account_with_remaining())]
pub fn consume_oneshot_account_with_remaining(
origin: OriginFor<T>,
block_height: BlockNumberFor<T>,
dest: Account<<T::Lookup as StaticLookup>::Source>,
remaining_to: Account<<T::Lookup as StaticLookup>::Source>,
#[pallet::compact] balance: BalanceOf<T>,
) -> DispatchResult {
let transactor = ensure_signed(origin)?;
let (dest1, dest1_is_oneshot) = match dest {
Account::Normal(account) => (account, false),
Account::Oneshot(account) => (account, true),
};
let dest1 = T::Lookup::lookup(dest1)?;
let (dest2, dest2_is_oneshot) = match remaining_to {
Account::Normal(account) => (account, false),
Account::Oneshot(account) => (account, true),
};
let dest2 = T::Lookup::lookup(dest2)?;
let value = OneshotAccounts::<T>::take(&transactor)
.ok_or(Error::<T>::OneshotAccountNotExist)?;
let balance1 = balance;
ensure!(value > balance1, Error::<T>::InsufficientBalance);
let balance2 = value.saturating_sub(balance1);
ensure!(
block_height <= frame_system::Pallet::<T>::block_number(),
Error::<T>::BlockHeightInFuture
);
ensure!(
frame_system::pallet::BlockHash::<T>::contains_key(block_height),
Error::<T>::BlockHeightTooOld
);
if dest1_is_oneshot {
ensure!(
OneshotAccounts::<T>::get(&dest1).is_none(),
Error::<T>::OneshotAccountAlreadyCreated
);
ensure!(
balance1 >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit
);
} else {
ensure!(
!T::Currency::balance(&dest1).is_zero(),
Error::<T>::DestAccountNotExist
);
}
if dest2_is_oneshot {
ensure!(
OneshotAccounts::<T>::get(&dest2).is_none(),
Error::<T>::OneshotAccountAlreadyCreated
);
ensure!(
balance2 >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit
);
OneshotAccounts::<T>::insert(&dest2, balance2);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest2.clone(),
balance: balance2,
creator: transactor.clone(),
});
} else if frame_system::Pallet::<T>::providers(&dest2) > 0 {
let _ = T::Currency::deposit(&dest2, balance2, Precision::Exact)?;
}
if dest1_is_oneshot {
OneshotAccounts::<T>::insert(&dest1, balance1);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest1.clone(),
balance: balance1,
creator: transactor.clone(),
});
} else if frame_system::Pallet::<T>::providers(&dest1) > 0 {
let _ = T::Currency::deposit(&dest1, balance1, Precision::Exact)?;
}
OneshotAccounts::<T>::remove(&transactor);
Self::deposit_event(Event::OneshotAccountConsumed {
account: transactor,
dest1: (dest1, balance1),
dest2: Some((dest2, balance2)),
});
Ok(())
}
}
}
impl<T: Config> OnChargeTransaction<T> for Pallet<T>
where
T::RuntimeCall: IsSubType<Call<T>>,
T::InnerOnChargeTransaction: OnChargeTransaction<
T,
Balance = BalanceOf<T>,
LiquidityInfo = Option<Credit<T::AccountId, T::Currency>>,
>,
{
type Balance = BalanceOf<T>;
type LiquidityInfo = Option<Credit<T::AccountId, T::Currency>>;
fn can_withdraw_fee(
who: &T::AccountId,
call: &T::RuntimeCall,
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
fee: Self::Balance,
tip: Self::Balance,
) -> Result<(), TransactionValidityError> {
T::InnerOnChargeTransaction::can_withdraw_fee(who, call, dispatch_info, fee, tip)
}
fn withdraw_fee(
who: &T::AccountId,
call: &T::RuntimeCall,
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
fee: Self::Balance,
tip: Self::Balance,
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
if let Some(
Call::consume_oneshot_account { .. }
| Call::consume_oneshot_account_with_remaining { .. },
) = call.is_sub_type()
{
if fee.is_zero() {
return Ok(None);
}
if let Some(balance) = OneshotAccounts::<T>::get(who) {
if balance >= fee {
OneshotAccounts::<T>::insert(who, balance.saturating_sub(fee));
Self::deposit_event(Event::Withdraw {
account: who.clone(),
balance: fee,
});
return Ok(Some(Imbalance::zero()));
}
}
Err(TransactionValidityError::Invalid(
InvalidTransaction::Payment,
))
} else {
T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip)
}
}
fn correct_and_deposit_fee(
who: &T::AccountId,
dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
corrected_fee: Self::Balance,
tip: Self::Balance,
already_withdrawn: Self::LiquidityInfo,
) -> Result<(), TransactionValidityError> {
T::InnerOnChargeTransaction::correct_and_deposit_fee(
who,
dispatch_info,
post_info,
corrected_fee,
tip,
already_withdrawn,
)
}
#[cfg(feature = "runtime-benchmarks")]
fn endow_account(who: &T::AccountId, amount: Self::Balance) {
T::InnerOnChargeTransaction::endow_account(who, amount);
}
#[cfg(feature = "runtime-benchmarks")]
fn minimum_balance() -> Self::Balance {
T::InnerOnChargeTransaction::minimum_balance()
}
}
// 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::{self as pallet_oneshot_account};
use frame_support::{derive_impl, parameter_types, traits::Everything, weights::IdentityFee};
use frame_system as system;
use pallet_transaction_payment::FungibleAdapter;
use sp_core::{ConstU32, H256};
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};
type Balance = u64;
type Block = frame_system::mocking::MockBlock<Test>;
// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
OneshotAccount: pallet_oneshot_account,
}
);
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 = u64;
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;
}
parameter_types! {
pub const ExistentialDeposit: Balance = 10;
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>;
}
impl pallet_transaction_payment::Config for Test {
type FeeMultiplierUpdate = ();
type LengthToFee = IdentityFee<u64>;
type OnChargeTransaction = OneshotAccount;
type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>;
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type WeightToFee = IdentityFee<u64>;
}
impl pallet_oneshot_account::Config for Test {
type Currency = Balances;
type InnerOnChargeTransaction = FungibleAdapter<Balances, ()>;
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
}
// Build genesis storage according to the mock runtime.
#[allow(dead_code)]
pub fn new_test_ext() -> sp_io::TestExternalities {
RuntimeGenesisConfig {
system: SystemConfig::default(),
balances: BalancesConfig::default(), // FIXME (explicit absence of oneshot account in genesis)
transaction_payment: TransactionPaymentConfig::default(),
}
.build_storage()
.unwrap()
.into()
}
// 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 codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::pallet_prelude::*;
/// The type of account.
#[derive(Clone, Decode, Encode, DecodeWithMemTracking, PartialEq, RuntimeDebug, TypeInfo)]
pub enum Account<AccountId> {
/// Normal account type.
Normal(AccountId),
/// Oneshot account type.
Oneshot(AccountId),
}
// Copyright 2021-2022 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::unnecessary_cast)]
use frame_support::weights::{constants::RocksDbWeight, Weight};
/// Weight functions needed for pallet_universal_dividend.
pub trait WeightInfo {
fn create_oneshot_account() -> Weight;
fn consume_oneshot_account() -> Weight;
fn consume_oneshot_account_with_remaining() -> Weight;
}
// Insecure weights implementation, use it for tests only!
impl WeightInfo for () {
// Storage: OneshotAccount OneshotAccounts (r:1 w:1)
fn create_oneshot_account() -> Weight {
(Weight::from_parts(45_690_000, 0))
.saturating_add(RocksDbWeight::get().reads(1))
.saturating_add(RocksDbWeight::get().writes(1))
}
// Storage: OneshotAccount OneshotAccounts (r:1 w:1)
// Storage: System BlockHash (r:1 w:0)
// Storage: System Account (r:1 w:1)
fn consume_oneshot_account() -> Weight {
(Weight::from_parts(50_060_000, 0))
.saturating_add(RocksDbWeight::get().reads(3))
.saturating_add(RocksDbWeight::get().writes(2))
}
// Storage: OneshotAccount OneshotAccounts (r:1 w:1)
// Storage: System BlockHash (r:1 w:0)
// Storage: System Account (r:2 w:2)
fn consume_oneshot_account_with_remaining() -> Weight {
(Weight::from_parts(69_346_000, 0))
.saturating_add(RocksDbWeight::get().reads(4))
.saturating_add(RocksDbWeight::get().writes(3))
}
}
[package]
authors = ['librelois <c@elo.tf>']
description = 'FRAME pallet to provide randomness to users.'
edition = '2018'
homepage = 'https://substrate.dev'
license = 'AGPL-3.0'
name = 'pallet-provide-randomness'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
authors.workspace = true
description = "duniter pallet to provide randomness to users"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-provide-randomness"
repository.workspace = true
version.workspace = true
[features]
default = ['std']
runtime-benchmarks = ['frame-benchmarking']
default = ["std"]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/try-runtime",
"sp-runtime/try-runtime",
]
std = [
'codec/std',
'frame-support/std',
'frame-system/std',
'frame-benchmarking/std',
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"pallet-balances/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-std/std",
"sp-runtime/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-core]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dependencies.sp-io]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
[dependencies.sp-runtime]
default-features = false
git = 'https://github.com/librelois/substrate.git'
branch = 'duniter-monthly-2022-02'
### DOC ###
scale-info = { workspace = true, features = ["derive"] }
codec = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-runtime = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
pallet-balances = { workspace = true }
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
default-features = false
targets = ["x86_64-unknown-linux-gnu"]
// 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")]
#![allow(clippy::multiple_bound_locations)]
use super::*;
use frame_benchmarking::{v2::*, whitelisted_caller};
use frame_support::{
ensure,
pallet_prelude::IsType,
sp_runtime::{traits::One, Saturating},
traits::{fungible::Mutate, Get, OnInitialize},
};
use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
use sp_core::H256;
use crate::Pallet;
#[benchmarks(
where
T: pallet_balances::Config,
T::Balance: From<u64>,
BalanceOf<T>: IsType<T::Balance>,
BlockNumberFor<T>: 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());
}
fn add_requests_next_block<T: Config>(i: u32) -> Result<(), &'static str> {
for _ in 0..i {
let salt: H256 = H256([0; 32]);
let request_id = RequestIdProvider::<T>::mutate(|next_request_id| {
core::mem::replace(next_request_id, next_request_id.saturating_add(1))
});
RequestsIds::<T>::insert(request_id, ());
RequestsReadyAtNextBlock::<T>::append(Request { request_id, salt });
}
Ok(())
}
fn add_requests_next_epoch<T: Config>(i: u32) -> Result<(), &'static str> {
for _ in 0..i {
let salt: H256 = H256([0; 32]);
let request_id = RequestIdProvider::<T>::mutate(|next_request_id| {
core::mem::replace(next_request_id, next_request_id.saturating_add(1))
});
RequestsIds::<T>::insert(request_id, ());
RequestsReadyAtEpoch::<T>::append(
T::GetCurrentEpochIndex::get(),
Request { request_id, salt },
);
}
Ok(())
}
#[benchmark]
fn request() {
// Get account
let caller: T::AccountId = whitelisted_caller();
// Provide deposit
let existential_deposit = T::ExistentialDeposit::get();
let balance = existential_deposit.saturating_mul((200).into());
let _ = T::Currency::set_balance(&caller, balance.into());
// Set randomness parameters
let random = RandomnessType::RandomnessFromOneEpochAgo;
let salt: H256 = H256([1; 32]);
#[extrinsic_call]
_(RawOrigin::Signed(caller), random, salt);
let request_id = RequestIdProvider::<T>::get() - 1;
assert_has_event::<T>(
Event::RequestedRandomness {
request_id,
salt,
r#type: random,
}
.into(),
);
}
#[benchmark]
fn on_initialize(i: Linear<1, { T::MaxRequests::get() }>) -> Result<(), BenchmarkError> {
add_requests_next_block::<T>(i)?;
ensure!(RequestsIds::<T>::count() == i, "List not filled properly.");
ensure!(
RequestsReadyAtNextBlock::<T>::get().len() == i as usize,
"List not filled properly."
);
let next_epoch_hook_in = NexEpochHookIn::<T>::mutate(|next_in| {
core::mem::replace(next_in, next_in.saturating_sub(1))
});
ensure!(next_epoch_hook_in != 1, "Will be next epoch.");
#[block]
{
Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
}
ensure!(RequestsIds::<T>::count() == 0, "List not processed.");
ensure!(
RequestsReadyAtNextBlock::<T>::get().is_empty(),
"List not processed."
);
Ok(())
}
#[benchmark]
fn on_initialize_epoch(i: Linear<1, { T::MaxRequests::get() }>) -> Result<(), BenchmarkError> {
add_requests_next_epoch::<T>(i)?;
ensure!(
RequestsReadyAtNextBlock::<T>::get().is_empty(),
"List not filled properly."
);
ensure!(RequestsIds::<T>::count() == i, "List not filled properly.");
ensure!(
RequestsReadyAtEpoch::<T>::get(T::GetCurrentEpochIndex::get()).len() == i as usize,
"List not filled properly."
);
NexEpochHookIn::<T>::mutate(|next_in| core::mem::replace(next_in, 1));
#[block]
{
Pallet::<T>::on_initialize(1.into());
}
ensure!(RequestsIds::<T>::count() == 0, "List not processed.");
ensure!(
RequestsReadyAtEpoch::<T>::get(T::GetCurrentEpochIndex::get()).is_empty(),
"List not processed properly."
);
Ok(())
}
}
// Copyright 2021 Axiom-Team
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Substrate-Libre-Currency.
// This file is part of Duniter-v2S.
//
// Substrate-Libre-Currency is free software: you can redistribute it and/or modify
// 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.
//
// Substrate-Libre-Currency is distributed in the hope that it will be useful,
// 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Provides Randomness Pallet
//!
//! The Provides Randomness Pallet facilitates the generation of randomness within the Duniter blockchain.
//!
//! This pallet manages randomness requests and emits events upon requesting and fulfilling randomness.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::boxed_local)]
mod benchmarking;
mod types;
pub mod weights;
use frame_support::pallet_prelude::Weight;
use frame_support::{
pallet_prelude::Weight,
traits::{
fungible::{self, Balanced, Credit},
tokens::{Fortitude, Precision, Preservation},
},
};
use scale_info::prelude::vec::Vec;
use sp_core::H256;
use sp_std::prelude::*;
pub use pallet::*;
pub use types::*;
pub use weights::WeightInfo;
pub type RequestId = u64;
......@@ -33,31 +49,28 @@ pub trait OnFilledRandomness {
}
impl OnFilledRandomness for () {
fn on_filled_randomness(_: RequestId, _: H256) -> Weight {
0
Weight::zero()
}
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::traits::{
Currency, ExistenceRequirement, OnUnbalanced, Randomness, StorageVersion, WithdrawReasons,
use frame_support::{
pallet_prelude::*,
traits::{OnUnbalanced, Randomness, StorageVersion},
};
use frame_system::pallet_prelude::*;
use sp_core::H256;
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::NegativeImbalance;
type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
......@@ -65,45 +78,61 @@ pub mod pallet {
/// Configuration trait.
#[pallet::config]
pub trait Config: frame_system::Config<Hash = H256> {
// The currency
type Currency: Currency<Self::AccountId>;
/// The overarching event type.
type Event: From<Event> + IsType<<Self as frame_system::Config>::Event>;
/// Get the current epoch index
// The currency type.
type Currency: fungible::Balanced<Self::AccountId> + fungible::Mutate<Self::AccountId>;
/// Type providing the current epoch index.
type GetCurrentEpochIndex: Get<u64>;
/// Maximum number of not yet filled requests
/// Maximum number of not yet filled requests.
#[pallet::constant]
type MaxRequests: Get<u32>;
/// The price of a request
/// The price of a request.
#[pallet::constant]
type RequestPrice: Get<BalanceOf<Self>>;
/// On filled randomness
/// Handler called when randomness is filled.
type OnFilledRandomness: OnFilledRandomness;
/// Handler for the unbalanced reduction when the requestor pays fees.
type OnUnbalanced: OnUnbalanced<NegativeImbalanceOf<Self>>;
/// A safe source of randomness from the current block
type CurrentBlockRandomness: Randomness<Option<H256>, Self::BlockNumber>;
/// A safe source of randomness from one epoch ago
type RandomnessFromOneEpochAgo: Randomness<H256, Self::BlockNumber>;
/// Handler for unbalanced reduction when the requestor pays fees.
type OnUnbalanced: OnUnbalanced<Credit<Self::AccountId, Self::Currency>>;
/// A safe source of randomness from the parent block.
type ParentBlockRandomness: Randomness<Option<H256>, BlockNumberFor<Self>>;
/// A safe source of randomness from one epoch ago.
type RandomnessFromOneEpochAgo: Randomness<H256, BlockNumberFor<Self>>;
/// The overarching event type.
type RuntimeEvent: From<Event> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
}
// STORAGE //
/// The number of blocks before the next epoch.
#[pallet::storage]
pub(super) type NexEpochHookIn<T: Config> = StorageValue<_, u8, ValueQuery>;
/// The request ID.
#[pallet::storage]
pub(super) type RequestIdProvider<T: Config> = StorageValue<_, RequestId, ValueQuery>;
/// The requests that will be fulfilled at the next block.
#[pallet::storage]
#[pallet::getter(fn requests_ready_at_next_block)]
pub type RequestsReadyAtNextBlock<T: Config> = StorageValue<_, Vec<Request>, ValueQuery>;
/// The requests that will be fulfilled at the next epoch.
#[pallet::storage]
#[pallet::getter(fn requests_ready_at_epoch)]
pub type RequestsReadyAtEpoch<T: Config> =
StorageMap<_, Twox64Concat, u64, Vec<Request>, ValueQuery>;
/// The requests being processed.
#[pallet::storage]
#[pallet::getter(fn requests_ids)]
pub type RequestsIds<T: Config> =
......@@ -114,12 +143,12 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event {
/// Filled randomness
/// A request for randomness was fulfilled.
FilledRandomness {
request_id: RequestId,
randomness: H256,
},
/// Requested randomness
/// A request for randomness was made.
RequestedRandomness {
request_id: RequestId,
salt: H256,
......@@ -131,16 +160,17 @@ pub mod pallet {
#[pallet::error]
pub enum Error<T> {
/// The queue is full, pleasy retry later
FullQueue,
/// Request randomness queue is full.
QueueFull,
}
// CALLS //
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Request a randomness
#[pallet::weight(500_000_000)]
/// Request randomness.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::request())]
pub fn request(
origin: OriginFor<T>,
randomness_type: RandomnessType,
......@@ -164,12 +194,12 @@ pub mod pallet {
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_: T::BlockNumber) -> Weight {
let mut total_weight = 0;
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
// Overhead to process an empty request
let mut total_weight = T::WeightInfo::on_initialize(0);
total_weight += 100_000;
for Request { request_id, salt } in RequestsReadyAtNextBlock::<T>::take() {
let randomness = T::CurrentBlockRandomness::random(salt.as_ref())
let randomness = T::ParentBlockRandomness::random(salt.as_ref())
.0
.unwrap_or_default();
RequestsIds::<T>::remove(request_id);
......@@ -178,14 +208,15 @@ pub mod pallet {
request_id,
randomness,
});
total_weight += 100_000;
// Weight to process on request
total_weight +=
T::WeightInfo::on_initialize(2).saturating_sub(T::WeightInfo::on_initialize(1));
}
let next_epoch_hook_in = NexEpochHookIn::<T>::mutate(|next_in| {
core::mem::replace(next_in, next_in.saturating_sub(1))
});
if next_epoch_hook_in == 1 {
total_weight += 100_000;
for Request { request_id, salt } in
RequestsReadyAtEpoch::<T>::take(T::GetCurrentEpochIndex::get())
{
......@@ -197,7 +228,9 @@ pub mod pallet {
request_id,
randomness,
});
total_weight += 100_000;
// Weight to process on request
total_weight += T::WeightInfo::on_initialize_epoch(2)
.saturating_sub(T::WeightInfo::on_initialize_epoch(1));
}
}
......@@ -208,6 +241,7 @@ pub mod pallet {
// PUBLIC FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Initiates a randomness request with specified parameters.
pub fn do_request(
requestor: &T::AccountId,
randomness_type: RandomnessType,
......@@ -216,7 +250,7 @@ pub mod pallet {
// Verify phase
ensure!(
RequestsIds::<T>::count() < T::MaxRequests::get(),
Error::<T>::FullQueue
Error::<T>::QueueFull
);
Self::pay_request(requestor)?;
......@@ -224,9 +258,13 @@ pub mod pallet {
// Apply phase
Ok(Self::apply_request(randomness_type, salt))
}
/// Forcefully initiates a randomness request using the specified parameters.
pub fn force_request(randomness_type: RandomnessType, salt: H256) -> RequestId {
Self::apply_request(randomness_type, salt)
}
/// Set the next epoch hook value to 5.
pub fn on_new_epoch() {
NexEpochHookIn::<T>::put(5)
}
......@@ -235,16 +273,20 @@ pub mod pallet {
// INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Withdraw funds from the requestor's account to pay for a request.
fn pay_request(requestor: &T::AccountId) -> DispatchResult {
let imbalance = T::Currency::withdraw(
requestor,
T::RequestPrice::get(),
WithdrawReasons::FEE,
ExistenceRequirement::KeepAlive,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)?;
T::OnUnbalanced::on_unbalanced(imbalance);
Ok(())
}
/// Apply a randomness request with the specified type and salt.
fn apply_request(randomness_type: RandomnessType, salt: H256) -> RequestId {
let request_id = RequestIdProvider::<T>::mutate(|next_request_id| {
core::mem::replace(next_request_id, next_request_id.saturating_add(1))
......
// Copyright 2021 Axiom-Team
//
// This file is part of Substrate-Libre-Currency.
// This file is part of Duniter-v2S.
//
// Substrate-Libre-Currency is free software: you can redistribute it and/or modify
// 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.
//
// Substrate-Libre-Currency is distributed in the hope that it will be useful,
// 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! Various basic types for use in pallet provide randomness
use super::RequestId;
use codec::{Decode, Encode};
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::pallet_prelude::*;
use scale_info::TypeInfo;
use sp_core::H256;
#[derive(Clone, Copy, Decode, Encode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
/// The type of randomness source.
#[derive(
Clone, DecodeWithMemTracking, Copy, Decode, Encode, Eq, PartialEq, RuntimeDebug, TypeInfo,
)]
pub enum RandomnessType {
/// Randomness derived from the previous block.
RandomnessFromPreviousBlock,
/// Randomness derived from one epoch ago.
RandomnessFromOneEpochAgo,
/// Randomness derived from two epochs ago.
RandomnessFromTwoEpochsAgo,
}
/// Represents a randomness request.
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct Request {
/// Request ID.
pub request_id: RequestId,
/// Salt used for the request.
pub salt: H256,
}