Skip to content
Snippets Groups Projects

Compare revisions

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

Source

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

Target

Select target project
  • nodes/rust/duniter-v2s
  • llaq/lc-core-substrate
  • pini-gh/duniter-v2s
  • vincentux/duniter-v2s
  • mildred/duniter-v2s
  • d0p1/duniter-v2s
  • bgallois/duniter-v2s
  • Nicolas80/duniter-v2s
8 results
Select Git revision
  • archive_upgrade_polkadot_v0.9.42
  • david-wot-scenarios-cucumber
  • distance
  • elois-ci-binary-release
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-fix-85
  • elois-fix-idty-post-genesis
  • elois-fix-sufficients-change-owner-key
  • elois-opti-cert
  • elois-remove-renewable-period
  • elois-revoc-with-old-key
  • elois-rework-certs
  • elois-smish-members-cant-change-or-rem-idty
  • elois-smoldot
  • elois-substrate-v0.9.23
  • elois-technical-commitee
  • hugo-gtest
  • hugo-remove-duniter-account
  • hugo-rework-genesis
  • hugo-tmp
  • jrx/workspace_tomls
  • master
  • no-bootnodes
  • pallet-benchmark
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • test-gen-new-owner-key-msg
  • ts-types
  • ud-time-64
  • upgrade_polkadot_v0.9.42
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
52 results
Show changes
Showing
with 3170 additions and 1422 deletions
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use crate::Pallet;
#[benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn unlink_identity() {
let account = account("Alice", 1, 1);
#[extrinsic_call]
_(RawOrigin::Signed(account));
}
#[benchmark]
fn on_revoke_identity() {
let idty: IdtyIdOf<T> = 1u32.into();
#[block]
{
<Pallet<T> as pallet_identity::traits::OnRemoveIdty<T>>::on_revoked(&idty);
}
}
}
// Copyright 2021 Axiom-Team
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
......@@ -14,30 +14,72 @@
// 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 Account Pallet
//!
//! Duniter customizes the `AccountData` of the `Balances` Substrate pallet to include additional fields
//! such as `linked_idty`.
//!
//! ## Sufficiency
//!
//! DuniterAccount adjusts the Substrate `AccountInfo` to accommodate identity-linked accounts without requiring
//! an existential deposit. This flexibility helps reduce barriers to account creation.
//!
//! ## Linked Identity
//!
//! Duniter allows accounts to be linked to identities using the `linked_idty` field. This linkage facilitates
//! transaction fee refunds through the `OnChargeTransaction` mechanism.
#![cfg_attr(not(feature = "std"), no_std)]
pub mod weights;
mod benchmarking;
mod runtime_api;
mod types;
pub use pallet::*;
pub use runtime_api::*;
pub use types::*;
pub use weights::WeightInfo;
use frame_support::pallet_prelude::*;
use frame_support::traits::{OnUnbalanced, StoredMap};
use core::cmp;
use duniter_primitives::GetSigner;
#[cfg(feature = "runtime-benchmarks")]
use frame_support::traits::tokens::fungible::Mutate;
use frame_support::{
dispatch::{DispatchInfo, GetDispatchInfo},
pallet_prelude::*,
traits::{
fungible,
fungible::{Credit, Inspect},
tokens::WithdrawConsequence,
IsSubType, StorageVersion, StoredMap,
},
};
use frame_system::pallet_prelude::*;
use pallet_provide_randomness::RequestId;
use sp_core::H256;
use sp_runtime::traits::{Convert, Saturating, Zero};
use pallet_quota::RefundFee;
use pallet_transaction_payment::OnChargeTransaction;
use scale_info::prelude::{
collections::{BTreeMap, BTreeSet},
fmt::Debug,
};
use sp_runtime::traits::{
DispatchInfoOf, Dispatchable, ExtrinsicLike, PostDispatchInfoOf, Saturating,
};
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::traits::{Currency, ExistenceRequirement, StorageVersion};
pub type IdtyIdOf<T> = <T as pallet_identity::Config>::IdtyIndex;
pub type CurrencyOf<T> = pallet_balances::Pallet<T>;
type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub type BalanceOf<T> = <CurrencyOf<T> 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>(_);
......@@ -46,81 +88,79 @@ pub mod pallet {
#[pallet::config]
pub trait Config:
frame_system::Config<AccountData = AccountData<Self::Balance>>
frame_system::Config<AccountData = AccountData<Self::Balance, IdtyIdOf<Self>>>
+ pallet_balances::Config
+ pallet_provide_randomness::Config<Currency = pallet_balances::Pallet<Self>>
+ pallet_transaction_payment::Config
+ pallet_treasury::Config<Currency = pallet_balances::Pallet<Self>>
+ pallet_quota::Config
{
type AccountIdToSalt: Convert<Self::AccountId, [u8; 32]>;
#[pallet::constant]
type MaxNewAccountsPerBlock: Get<u32>;
#[pallet::constant]
type NewAccountPrice: Get<Self::Balance>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
// STORAGE //
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
#[pallet::storage]
#[pallet::getter(fn pending_random_id_assignments)]
pub type PendingRandomIdAssignments<T: Config> =
StorageMap<_, Twox64Concat, RequestId, T::AccountId, OptionQuery>;
/// A wrapped type that handles the charging of transaction fees.
type InnerOnChargeTransaction: OnChargeTransaction<Self>;
#[pallet::storage]
#[pallet::getter(fn pending_new_accounts)]
pub type PendingNewAccounts<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, (), OptionQuery>;
/// A type that implements the refund behavior for transaction fees.
type Refund: pallet_quota::RefundFee<Self>;
}
// GENESIS STUFF //
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub accounts:
sp_std::collections::btree_map::BTreeMap<T::AccountId, GenesisAccountData<T::Balance>>,
pub accounts: BTreeMap<T::AccountId, GenesisAccountData<T::Balance, IdtyIdOf<T>>>,
pub treasury_balance: T::Balance,
}
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
accounts: Default::default(),
treasury_balance: T::ExistentialDeposit::get(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// Treasury
frame_system::Account::<T>::mutate(
pallet_treasury::Pallet::<T>::account_id(),
|account| {
account.data.random_id = None;
account.data.free = T::ExistentialDeposit::get();
account.data.free = self.treasury_balance;
account.providers = 1;
},
);
// ensure no duplicate
let endowed_accounts = self.accounts.keys().cloned().collect::<BTreeSet<_>>();
assert!(
endowed_accounts.len() == self.accounts.len(),
"duplicate balances in genesis."
);
// Classic accounts
for (
account_id,
GenesisAccountData {
random_id,
balance,
is_identity,
},
) in &self.accounts
{
assert!(!balance.is_zero() || *is_identity);
for (account_id, GenesisAccountData { balance, idty_id }) in &self.accounts {
// if the balance is below existential deposit, the account must be an identity
assert!(balance >= &T::ExistentialDeposit::get() || idty_id.is_some());
// mutate account
frame_system::Account::<T>::mutate(account_id, |account| {
account.data.random_id = Some(*random_id);
if !balance.is_zero() {
account.data.free = *balance;
account.providers = 1;
if idty_id.is_some() {
account.data.linked_idty = *idty_id;
}
if *is_identity {
account.sufficients = 1;
if balance >= &T::ExistentialDeposit::get() {
// accounts above existential deposit self-provide
account.providers = 1;
}
// WARN (disabled) all genesis accounts provide for themselves whether they have existential deposit or not
// this is needed to migrate Ğ1 data where identities with zero Ğ1 can exist
// account.providers = 1;
});
}
}
......@@ -131,122 +171,143 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Force the destruction of an account because its free balance is insufficient to pay
/// the account creation price.
/// [who, balance]
ForceDestroy {
/// account linked to identity
AccountLinked {
who: T::AccountId,
balance: T::Balance,
identity: IdtyIdOf<T>,
},
/// Random id assigned
/// [account_id, random_id]
RandomIdAssigned { who: T::AccountId, random_id: H256 },
}
// HOOKS //
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_: T::BlockNumber) -> Weight {
let mut total_weight = Weight::zero();
for account_id in PendingNewAccounts::<T>::iter_keys()
.drain()
.take(T::MaxNewAccountsPerBlock::get() as usize)
/// The account was unlinked from its identity.
AccountUnlinked(T::AccountId),
}
// CALLS //
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Unlink the identity associated with the account.
#[pallet::call_index(0)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::unlink_identity())]
pub fn unlink_identity(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Self::do_unlink_identity(who);
Ok(().into())
}
}
// PUBLIC FUNCTIONS //
impl<T: Config> Pallet<T> {
pub fn estimate_cost<Extrinsic>(
unchecked_extrinsic: Extrinsic,
) -> EstimatedCost<BalanceOf<T>>
where
BalanceOf<T>: PartialOrd,
Extrinsic: Encode + ExtrinsicLike + GetDispatchInfo + GetSigner<T::Lookup>,
T::RuntimeCall: Dispatchable<Info = DispatchInfo>,
<T as pallet_transaction_payment::Config>::OnChargeTransaction:
pallet_transaction_payment::OnChargeTransaction<T, Balance = BalanceOf<T>>,
{
if frame_system::Pallet::<T>::sufficients(&account_id) > 0 {
// If the account is self-sufficient, it is exempt from account creation fees
let request_id = pallet_provide_randomness::Pallet::<T>::force_request(
pallet_provide_randomness::RandomnessType::RandomnessFromTwoEpochsAgo,
H256(T::AccountIdToSalt::convert(account_id.clone())),
);
PendingRandomIdAssignments::<T>::insert(request_id, account_id);
total_weight += Weight::from_ref_time(100_000);
let signer = unchecked_extrinsic.get_signer();
let len = unchecked_extrinsic.encoded_size();
let fees: BalanceOf<T> = pallet_transaction_payment::Pallet::<T>::query_info(
unchecked_extrinsic,
len as u32,
)
.partial_fee;
let refund: BalanceOf<T> = if let Some(signer) = signer {
let account_data = frame_system::Pallet::<T>::get(&signer);
if let Some(idty_index) = account_data.linked_idty {
pallet_quota::Pallet::<T>::estimate_quota_refund(idty_index)
} else {
// If the account is not self-sufficient, it must pay the account creation fees
let account_data = frame_system::Pallet::<T>::get(&account_id);
let price = T::NewAccountPrice::get();
if account_data.free >= T::ExistentialDeposit::get() + price {
// The account can pay the new account price, we should:
// 1. Withdraw the "new account price" amount
// 2. Increment consumers to prevent the destruction of the account before
// the random id is assigned
// 3. Manage the funds collected
// 4. Submit random id generation request
// 5. Save the id of the random generation request.
let res = <pallet_balances::Pallet<T> as Currency<T::AccountId>>::withdraw(
&account_id,
price,
frame_support::traits::WithdrawReasons::FEE,
ExistenceRequirement::KeepAlive,
);
debug_assert!(
res.is_ok(),
"Cannot fail because we checked that the free balance was sufficient"
);
if let Ok(imbalance) = res {
let res =
frame_system::Pallet::<T>::inc_consumers_without_limit(&account_id);
debug_assert!(
res.is_ok(),
"Cannot fail because any account with funds should have providers"
);
T::OnUnbalanced::on_unbalanced(imbalance);
let request_id = pallet_provide_randomness::Pallet::<T>::force_request(
pallet_provide_randomness::RandomnessType::RandomnessFromTwoEpochsAgo,
H256(T::AccountIdToSalt::convert(account_id.clone())),
);
PendingRandomIdAssignments::<T>::insert(request_id, account_id);
total_weight += Weight::from_ref_time(200_000);
Zero::zero()
}
} else {
// The charges could not be deducted, we must destroy the account
let balance_to_suppr =
account_data.free.saturating_add(account_data.reserved);
// Force account data supression
frame_system::Account::<T>::remove(&account_id);
Self::deposit_event(Event::ForceDestroy {
who: account_id,
balance: balance_to_suppr,
Zero::zero()
};
EstimatedCost {
cost: if fees > refund {
fees - refund
} else {
Zero::zero()
},
fees,
refund,
}
}
}
// INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Unlink the account from its associated identity.
pub fn do_unlink_identity(account_id: T::AccountId) {
// no-op if account already linked to nothing
frame_system::Account::<T>::mutate(&account_id, |account| {
if account.data.linked_idty.is_some() {
Self::deposit_event(Event::AccountUnlinked(account_id.clone()));
}
account.data.linked_idty = None;
})
}
/// Link an account to an identity.
pub fn do_link_identity(account_id: &T::AccountId, idty_id: IdtyIdOf<T>) {
// no-op if identity does not change
if frame_system::Account::<T>::get(account_id).data.linked_idty != Some(idty_id) {
frame_system::Account::<T>::mutate(account_id, |account| {
account.data.linked_idty = Some(idty_id);
Self::deposit_event(Event::AccountLinked {
who: account_id.clone(),
identity: idty_id,
});
T::OnUnbalanced::on_unbalanced(pallet_balances::NegativeImbalance::new(
balance_to_suppr,
));
total_weight += Weight::from_ref_time(300_000);
})
};
}
}
}
/// Implementing identity removal event handling for the pallet.
impl<T: Config> pallet_identity::traits::OnRemoveIdty<T> for Pallet<T> {
fn on_removed(_idty_index: &IdtyIdOf<T>) -> Weight {
Weight::zero()
}
total_weight
/// This implementation unlinks account associated with the identity.
fn on_revoked(idty_index: &IdtyIdOf<T>) -> Weight {
if let Some(account) = <pallet_identity::Pallet<T> as duniter_primitives::Idty<
IdtyIdOf<T>,
T::AccountId,
>>::owner_key(*idty_index)
{
Self::do_unlink_identity(account);
}
<T as pallet::Config>::WeightInfo::on_revoke_identity()
}
}
impl<T> pallet_provide_randomness::OnFilledRandomness for Pallet<T>
// implement account linker
impl<T> pallet_identity::traits::LinkIdty<T::AccountId, IdtyIdOf<T>> for Pallet<T>
where
T: Config,
{
fn on_filled_randomness(request_id: RequestId, randomness: H256) -> Weight {
if let Some(account_id) = PendingRandomIdAssignments::<T>::take(request_id) {
frame_system::Account::<T>::mutate(&account_id, |account| {
account.consumers = account.consumers.saturating_sub(1);
account.data.random_id = Some(randomness);
});
Self::deposit_event(Event::RandomIdAssigned {
who: account_id,
random_id: randomness,
});
Weight::from_ref_time(200_000)
} else {
Weight::from_ref_time(100_000)
}
fn link_identity(account_id: &T::AccountId, idty_id: IdtyIdOf<T>) -> Result<(), DispatchError> {
// Check that account exist
ensure!(
(frame_system::Account::<T>::get(account_id).providers >= 1)
|| (frame_system::Account::<T>::get(account_id).sufficients >= 1),
pallet_identity::Error::<T>::AccountNotExist
);
Self::do_link_identity(account_id, idty_id);
Ok(())
}
}
// implement accountdata storedmap
impl<T, AccountId, Balance>
frame_support::traits::StoredMap<AccountId, pallet_balances::AccountData<Balance>> for Pallet<T>
where
AccountId: Parameter
+ Member
+ MaybeSerializeDeserialize
+ core::fmt::Debug
+ Debug
+ sp_runtime::traits::MaybeDisplay
+ Ord
+ Into<[u8; 32]>
......@@ -258,13 +319,12 @@ where
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ core::fmt::Debug
+ Debug
+ codec::MaxEncodedLen
+ scale_info::TypeInfo,
T: Config
+ frame_system::Config<AccountId = AccountId, AccountData = AccountData<Balance>>
+ pallet_balances::Config<Balance = Balance>
+ pallet_provide_randomness::Config,
+ frame_system::Config<AccountId = AccountId, AccountData = AccountData<Balance, IdtyIdOf<T>>>
+ pallet_balances::Config<Balance = Balance>,
{
fn get(k: &AccountId) -> pallet_balances::AccountData<Balance> {
frame_system::Account::<T>::get(k).data.into()
......@@ -275,7 +335,7 @@ where
f: impl FnOnce(&mut Option<pallet_balances::AccountData<Balance>>) -> Result<R, E>,
) -> Result<R, E> {
let account = frame_system::Account::<T>::get(account_id);
let was_providing = account.data.was_providing();
let was_providing = !account.data.free.is_zero() || !account.data.reserved.is_zero();
let mut some_data = if was_providing {
Some(account.data.into())
} else {
......@@ -283,22 +343,134 @@ where
};
let result = f(&mut some_data)?;
let is_providing = some_data.is_some();
if !was_providing && is_providing {
match (was_providing, is_providing) {
// the account has just been created, increment its provider
(false, true) => {
frame_system::Pallet::<T>::inc_providers(account_id);
PendingNewAccounts::<T>::insert(account_id, ());
} else if was_providing && !is_providing {
}
// the account was existing but is not anymore, decrement the provider
(true, false) => {
match frame_system::Pallet::<T>::dec_providers(account_id)? {
frame_system::DecRefStatus::Reaped => return Ok(result),
frame_system::DecRefStatus::Exists => {
// Update value as normal...
// Update value as normal
}
}
}
} else if !was_providing && !is_providing {
// mutation on unprovided account
(false, false) => {
return Ok(result);
}
// mutation on provided account
(true, true) => {
// Update value as normal
}
}
// do mutate the account by setting the balances
frame_system::Account::<T>::mutate(account_id, |a| {
a.data.set_balances(some_data.unwrap_or_default())
});
Ok(result)
}
}
// ------
// allows pay fees with quota instead of currency if available
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> {
// does not change the withdraw fee step (still fallback to currency adapter or oneshot account)
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> {
// in any case, the default behavior is applied
T::InnerOnChargeTransaction::correct_and_deposit_fee(
who,
dispatch_info,
post_info,
corrected_fee,
tip,
already_withdrawn,
)?;
// if account can be exonerated, add it to a refund queue
let account_data = frame_system::Pallet::<T>::get(who);
if let Some(idty_index) = account_data.linked_idty {
T::Refund::request_refund(who.clone(), idty_index, corrected_fee.saturating_sub(tip));
}
Ok(())
}
#[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()
}
}
/// Implementation of the CheckAccountWorthiness trait for the Pallet.
/// This trait is used to verify the worthiness of an account in terms
/// of existence and sufficient balance to handle identity creation.
impl<AccountId, T: Config> pallet_identity::traits::CheckAccountWorthiness<T> for Pallet<T>
where
T: frame_system::Config<AccountId = AccountId>,
AccountId: cmp::Eq,
{
/// Checks that the account exists and has the balance to handle the
/// identity creation process.
fn check_account_worthiness(account: &AccountId) -> Result<(), DispatchError> {
ensure!(
frame_system::Pallet::<T>::providers(account) > 0,
pallet_identity::Error::<T>::AccountNotExist
);
// This check verifies that the account can withdraw at least twice the minimum balance.
ensure!(
T::Currency::can_withdraw(account, T::Currency::minimum_balance() * 2u32.into())
== WithdrawConsequence::Success,
pallet_identity::Error::<T>::InsufficientBalance
);
Ok(())
}
#[cfg(feature = "runtime-benchmarks")]
fn set_worthy(account: &AccountId) {
T::Currency::set_balance(account, T::Currency::minimum_balance() * 4u32.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::{Codec, Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
sp_api::decl_runtime_apis! {
/// Runtime API for duniter account pallet
pub trait DuniterAccountApi<Balance>
where
EstimatedCost<Balance>: Codec,
{
/// Simulate the maximum cost of an extrinsic
///
/// Returns an object with two fields:
/// - `max_cost`: estimated effective cost for the user (fees - refund)
/// - `max_fees`: estimated amount of fees for the extrinsic
/// - `min_refund`: estimated amount of refund from quota
fn estimate_cost(
uxt: Block::Extrinsic,
) -> EstimatedCost<Balance>;
}
}
/// Account total balance information
#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, RuntimeDebug)]
pub struct EstimatedCost<Balance> {
/// The estimated effective cost for the user (fees - refund)
pub cost: Balance,
/// The estimated amount of fees for the extrinsic
pub fees: Balance,
/// The estimated amount of refund from quota
pub refund: Balance,
}
......@@ -17,49 +17,73 @@
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::pallet_prelude::*;
use scale_info::TypeInfo;
use sp_core::H256;
use sp_runtime::traits::Zero;
// see `struct AccountData` for details in substrate code
#[derive(Clone, Decode, Default, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)]
pub struct AccountData<Balance> {
/// A random identifier that can not be chosen by the user
// this intends to be used as a robust identification system
pub(super) random_id: Option<H256>,
// see Substrate AccountData
/// Account data structure.
///
/// For details, refer to `struct AccountData` in Substrate code.
#[derive(Clone, Decode, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] // Default,
pub struct AccountData<Balance, IdtyId> {
/// Free balance of the account.
pub(super) free: Balance,
// see Substrate AccountData
/// Reserved balance of the account.
pub(super) reserved: Balance,
// see Substrate AccountData
/// Frozen fee balance of the account.
fee_frozen: Balance,
/// Optional pointer to an identity used to determine if this account is linked to a member and in the quota system for fee refunds.
pub linked_idty: Option<IdtyId>,
}
impl<Balance: Zero> AccountData<Balance> {
// explicit implementation of default trait (can not be derived)
impl<Balance: Zero, IdtyId> Default for AccountData<Balance, IdtyId> {
fn default() -> Self {
Self {
linked_idty: None,
free: Balance::zero(),
reserved: Balance::zero(),
fee_frozen: Balance::zero(),
}
}
}
impl<Balance: Zero, IdtyId> AccountData<Balance, IdtyId> {
pub fn set_balances(&mut self, new_balances: pallet_balances::AccountData<Balance>) {
self.free = new_balances.free;
self.reserved = new_balances.reserved;
self.fee_frozen = new_balances.fee_frozen;
}
pub fn was_providing(&self) -> bool {
!self.free.is_zero() || !self.reserved.is_zero()
self.fee_frozen = new_balances.frozen;
}
}
impl<Balance: Zero> From<AccountData<Balance>> for pallet_balances::AccountData<Balance> {
fn from(account_data: AccountData<Balance>) -> Self {
// convert Duniter AccountData to Balances AccountData
// needed for trait implementation
impl<Balance: Zero, IdtyId> From<AccountData<Balance, IdtyId>>
for pallet_balances::AccountData<Balance>
{
fn from(account_data: AccountData<Balance, IdtyId>) -> Self {
Self {
free: account_data.free,
reserved: account_data.reserved,
misc_frozen: Zero::zero(),
fee_frozen: account_data.fee_frozen,
frozen: account_data.fee_frozen,
flags: Default::default(), // default flags since not used
}
}
}
#[derive(Clone, Decode, Default, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
pub struct GenesisAccountData<Balance> {
pub random_id: H256,
#[derive(
Clone,
Decode,
Default,
Encode,
Eq,
MaxEncodedLen,
PartialEq,
RuntimeDebug,
TypeInfo,
serde::Serialize,
serde::Deserialize,
)]
#[serde(deny_unknown_fields)]
pub struct GenesisAccountData<Balance, IdtyId> {
pub balance: Balance,
pub is_identity: bool,
pub idty_id: Option<IdtyId>,
}
// 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 unlink_identity() -> Weight;
fn on_revoke_identity() -> Weight;
}
// Insecure weights implementation, use it for tests only!
impl WeightInfo for () {
/// Storage: System Account (r:1 w:0)
/// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen)
fn unlink_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `3591`
// Minimum execution time: 95_130_000 picoseconds.
Weight::from_parts(110_501_000, 0)
.saturating_add(Weight::from_parts(0, 3591))
.saturating_add(RocksDbWeight::get().reads(1))
}
fn on_revoke_identity() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `3591`
// Minimum execution time: 95_130_000 picoseconds.
Weight::from_parts(110_501_000, 0)
.saturating_add(Weight::from_parts(0, 3591))
.saturating_add(RocksDbWeight::get().reads(1))
}
}
[package]
authors = ['librelois <c@elo.tf>']
description = 'Duniter test parameters.'
edition = "2021"
homepage = 'https://duniter.org'
license = 'AGPL-3.0'
name = 'pallet-duniter-test-parameters'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
authors.workspace = true
description = "duniter pallet test parameters"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-duniter-test-parameters"
repository.workspace = true
version.workspace = true
[features]
default = ['std']
runtime-benchmarks = ['frame-benchmarking']
default = ["std"]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
std = [
'codec/std',
'frame-support/std',
'frame-system/std',
'frame-benchmarking/std',
'serde',
"codec/std",
"frame-support/std",
"frame-system/std",
"scale-info/std",
"serde/std",
"sp-core/std",
"sp-io/std",
"sp-std/std",
"sp-runtime/std",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
]
try-runtime = ['frame-support/try-runtime']
[dependencies]
pallet-duniter-test-parameters-macro = { path = "macro" }
serde = { version = "1.0.101", features = ["derive"], optional = true }
# substrate
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
[dependencies.codec]
default-features = false
features = ['derive']
package = 'parity-scale-codec'
version = "3.1.5"
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/duniter/substrate'
optional = true
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-system]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-io]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
[package.metadata.docs.rs]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
### DOC ###
targets = ["x86_64-unknown-linux-gnu"]
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
[dependencies]
codec = { workspace = true, features = ["derive"] }
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-duniter-test-parameters-macro = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
# Duniter test parameters
This pallet allows ĞDev runtime to tweak parameter values instead of having it runtime constants.
\ No newline at end of file
[package]
authors = ['librelois <c@elo.tf>']
description = 'Duniter test parameters macro.'
edition = "2021"
homepage = 'https://duniter.org'
license = 'AGPL-3.0'
name = 'pallet-duniter-test-parameters-macro'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
authors.workspace = true
description = "duniter test parameters macro"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-duniter-test-parameters-macro"
repository.workspace = true
version.workspace = true
[lib]
proc-macro = true
[dependencies]
num_enum = { version = "0.5.3", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = [ "extra-traits", "fold", "full", "visit" ] }
proc-macro2 = { workspace = true, default-features = false }
quote = { workspace = true, default-features = false }
syn = { workspace = true, features = ["extra-traits", "fold", "full", "visit"] }
......@@ -14,6 +14,10 @@
// 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 Test Parameters Pallet
//!
//! This pallet allows ĞDev runtime to tweak parameter values instead of having it as runtime constants.
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
......@@ -24,17 +28,23 @@ pub mod types {
use codec::{Decode, Encode};
use frame_support::pallet_prelude::*;
use pallet_duniter_test_parameters_macro::generate_fields_getters;
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
#[generate_fields_getters]
#[cfg_attr(feature = "std", derive(Deserialize, Serialize))]
#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
#[derive(
Default,
Encode,
Decode,
Clone,
PartialEq,
serde::Serialize,
serde::Deserialize,
scale_info::TypeInfo,
)]
pub struct Parameters<
BlockNumber: Default + Parameter,
CertCount: Default + Parameter,
PeriodCount: Default + Parameter,
SessionCount: Default + Parameter,
> {
pub babe_epoch_duration: PeriodCount,
pub cert_period: BlockNumber,
......@@ -44,34 +54,28 @@ pub mod types {
pub idty_confirm_period: BlockNumber,
pub idty_creation_period: BlockNumber,
pub membership_period: BlockNumber,
pub pending_membership_period: BlockNumber,
pub ud_creation_period: BlockNumber,
pub ud_reeval_period: BlockNumber,
pub smith_cert_period: BlockNumber,
pub membership_renewal_period: BlockNumber,
pub ud_creation_period: PeriodCount,
pub ud_reeval_period: PeriodCount,
pub smith_cert_max_by_issuer: CertCount,
pub smith_cert_min_received_cert_to_issue_cert: CertCount,
pub smith_cert_validity_period: BlockNumber,
pub smith_membership_period: BlockNumber,
pub smith_pending_membership_period: BlockNumber,
pub smiths_wot_first_cert_issuable_on: BlockNumber,
pub smiths_wot_min_cert_for_membership: CertCount,
pub smith_wot_min_cert_for_membership: CertCount,
pub smith_inactivity_max_duration: SessionCount,
pub wot_first_cert_issuable_on: BlockNumber,
pub wot_min_cert_for_create_idty_right: CertCount,
pub wot_min_cert_for_membership: CertCount,
}
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::traits::StorageVersion;
use frame_support::{pallet_prelude::*, traits::StorageVersion};
/// 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>(_);
......@@ -80,25 +84,29 @@ pub mod pallet {
#[pallet::config]
pub trait Config: frame_system::Config {
type BlockNumber: Default + MaybeSerializeDeserialize + Parameter;
type CertCount: Default + MaybeSerializeDeserialize + Parameter;
type PeriodCount: Default + MaybeSerializeDeserialize + Parameter;
type SessionCount: Default + MaybeSerializeDeserialize + Parameter;
}
// STORAGE //
#[pallet::storage]
#[pallet::getter(fn parameters)]
pub type ParametersStorage<T: Config> =
StorageValue<_, Parameters<T::BlockNumber, T::CertCount, T::PeriodCount>, ValueQuery>;
pub type ParametersStorage<T: Config> = StorageValue<
_,
Parameters<T::BlockNumber, T::CertCount, T::PeriodCount, T::SessionCount>,
ValueQuery,
>;
// GENESIS
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub parameters: Parameters<T::BlockNumber, T::CertCount, T::PeriodCount>,
pub parameters: Parameters<T::BlockNumber, T::CertCount, T::PeriodCount, T::SessionCount>,
}
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
......@@ -108,7 +116,7 @@ pub mod pallet {
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
<ParametersStorage<T>>::put(self.parameters.clone());
}
......
[package]
authors = ['librelois <c@elo.tf>']
description = 'FRAME pallet duniter wot.'
edition = "2021"
homepage = 'https://duniter.org'
license = 'AGPL-3.0'
name = 'pallet-duniter-wot'
readme = 'README.md'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
authors.workspace = true
description = "duniter pallet for web of trust"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-duniter-wot"
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-certification/runtime-benchmarks",
"pallet-distance/runtime-benchmarks",
"pallet-identity/runtime-benchmarks",
"pallet-membership/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
std = [
'codec/std',
'frame-support/std',
'frame-system/std',
'frame-benchmarking/std',
'pallet-certification/std',
'pallet-identity/std',
'pallet-membership/std',
'serde',
'sp-core/std',
'sp-io/std',
'sp-membership/std',
'sp-runtime/std',
'sp-std/std',
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"pallet-certification/std",
"pallet-distance/std",
"pallet-identity/std",
"pallet-membership/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-membership/std",
"sp-runtime/std",
"sp-state-machine/std",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-certification/try-runtime",
"pallet-distance/try-runtime",
"pallet-identity/try-runtime",
"pallet-membership/try-runtime",
"sp-membership/try-runtime",
"sp-runtime/try-runtime",
]
try-runtime = ['frame-support/try-runtime']
[dependencies]
pallet-certification = { path = "../certification", default-features = false }
pallet-identity = { path = "../identity", default-features = false }
pallet-membership = { path = "../membership", default-features = false }
sp-membership = { path = "../../primitives/membership", default-features = false }
# substrate
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
[dependencies.codec]
default-features = false
features = ['derive']
package = 'parity-scale-codec'
version = "3.1.5"
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/duniter/substrate'
optional = true
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-system]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.serde]
version = "1.0.101"
optional = true
features = ["derive"]
[dependencies.sp-core]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-io]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
### DOC ###
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
[dev-dependencies.serde]
version = '1.0.119'
### DEV ###
default-features = false
targets = ["x86_64-unknown-linux-gnu"]
[dev-dependencies.sp-io]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies]
codec = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-certification = { workspace = true }
pallet-distance = { workspace = true }
pallet-identity = { workspace = true }
pallet-membership = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-membership = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
sp-state-machine = { workspace = true, default-features = true }
# Duniter Web of Trust pallet
Duniter WoT is at the core of its identity system and is a big improvement compared to PGP WoT. It is a dynamic directed graph whose nodes are [identities](../identity/) and edges [certifications](../certification/).
There are two instances:
- the main WoT, for every human
- the smith sub-WoT, for authorities
It has both static and dynamic rules, controlling the condition to join and remain [member](../membership/).
- static rules
- minimum number of received certifications (min indegree)
- maximum number of emited certifications (max outdegree)
- distance criterion (see distance pallet)
- dynamic rules
- time interval between two certifications
- certification duration (see certification pallet)
- membership renewal (see membership pallet)
This pallet's main role is to check the Web of Trust rules.
\ No newline at end of file
......@@ -14,6 +14,35 @@
// 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 Web of Trust Pallet
//!
//! Duniter Web of Trust (WoT) lies at the heart of its identity system, representing a significant improvement over PGP Web of Trust. It functions as a dynamic directed graph where nodes are [identities](../identity/) and edges are [certifications](../certification/).
//!
//! ## Instances
//!
//! Duniter WoT consists of two distinct instances:
//!
//! - **Main WoT**: Designed for every human participant in the Duniter network.
//! - **Smith Sub-WoT**: Intended for authorities.
//!
//! ## Rules
//!
//! The Duniter WoT operates under a set of static and dynamic rules that govern membership conditions.
//!
//! ### Static Rules
//!
//! - **Minimum Received Certifications (Min Indegree)**: Specifies the minimum number of certifications an identity must receive to join the WoT.
//! - **Maximum Emitted Certifications (Max Outdegree)**: Limits the maximum number of certifications an identity can issue.
//! - **Distance Criterion**: Governed by the distance pallet, it defines the permissible distance between identities within the WoT graph.
//!
//! ### Dynamic Rules
//!
//! - **Time Interval Between Certifications**: Sets the minimum time interval required between two consecutive certifications issued by the same identity.
//! - **Certification Duration**: Managed by the certification pallet, it determines the validity duration of a certification.
//! - **Membership Renewal**: Regulates the frequency and conditions under which an identity must renew its membership within the WoT.
//!
//! This pallet is responsible for enforcing and validating the rules of the Duniter Web of Trust. It ensures compliance with both static prerequisites for joining and dynamic conditions for ongoing participation.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::type_complexity)]
......@@ -23,20 +52,16 @@ mod mock;
#[cfg(test)]
mod tests;
/*#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;*/
pub use pallet::*;
use frame_support::dispatch::UnfilteredDispatchable;
use frame_support::pallet_prelude::*;
use frame_system::RawOrigin;
use pallet_certification::traits::SetNextIssuableOn;
use pallet_identity::{IdtyEvent, IdtyStatus};
use sp_runtime::traits::IsMember;
use pallet_identity::IdtyStatus;
use pallet_membership::MembershipRemovalReason;
type IdtyIndex = u32;
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
......@@ -46,330 +71,370 @@ pub mod pallet {
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 = ()>:
pub trait Config:
frame_system::Config
+ pallet_certification::Config<I, IdtyIndex = IdtyIndex>
+ pallet_certification::Config<IdtyIndex = IdtyIndex>
+ pallet_identity::Config<IdtyIndex = IdtyIndex>
+ pallet_membership::Config<I, IdtyId = IdtyIndex>
+ pallet_membership::Config<IdtyId = IdtyIndex>
{
/// The block number from which the first certification can be issued.
#[pallet::constant]
type FirstIssuableOn: Get<Self::BlockNumber>;
#[pallet::constant]
type IsSubWot: Get<bool>;
type FirstIssuableOn: Get<frame_system::pallet_prelude::BlockNumberFor<Self>>;
/// The minimum number of certifications required for membership eligibility.
#[pallet::constant]
type MinCertForMembership: Get<u32>;
/// The minimum number of certifications required to create an identity.
#[pallet::constant]
type MinCertForCreateIdtyRight: Get<u32>;
}
// INTERNAL FUNCTIONS //
impl<T: Config<I>, I: 'static> Pallet<T, I> {
impl<T: Config> Pallet<T> {
pub(super) fn do_apply_first_issuable_on(idty_index: IdtyIndex) {
let block_number = frame_system::pallet::Pallet::<T>::block_number();
pallet_certification::Pallet::<T, I>::set_next_issuable_on(
pallet_certification::Pallet::<T>::set_next_issuable_on(
idty_index,
block_number + T::FirstIssuableOn::get(),
);
}
pub(super) fn dispath_idty_call(idty_call: pallet_identity::Call<T>) -> bool {
if !T::IsSubWot::get() {
if let Err(e) = idty_call.dispatch_bypass_filter(RawOrigin::Root.into()) {
sp_std::if_std! {
println!("fail to dispatch idty call: {:?}", e)
}
return false;
}
}
true
}
}
// ERRORS //
#[pallet::error]
pub enum Error<T, I = ()> {
/// Identity not allowed to claim membership
IdtyNotAllowedToClaimMembership,
/// Identity not allowed to request membership
IdtyNotAllowedToRequestMembership,
/// Identity not allowed to renew membership
IdtyNotAllowedToRenewMembership,
/// Identity creation period not respected
pub enum Error<T> {
/// Insufficient certifications received.
NotEnoughCerts,
/// Target status is incompatible with this operation.
// - Membership can not be added/renewed with this status
// - Certification can not be added to identity with this status
TargetStatusInvalid,
/// Identity creation period not respected.
IdtyCreationPeriodNotRespected,
/// Not enough received certifications to create identity
/// Insufficient received certifications to create identity.
NotEnoughReceivedCertsToCreateIdty,
/// Max number of emitted certs reached
/// Maximum number of emitted certifications reached.
MaxEmittedCertsReached,
/// Not allowed to change identity address
NotAllowedToChangeIdtyAddress,
/// Not allowed to remove identity
NotAllowedToRemoveIdty,
/// Issuer can not emit cert because it is not validated
IssuerCanNotEmitCert,
/// Can not issue cert to unconfirmed identity
CertToUnconfirmedIdty,
/// Issuer or receiver not found
/// Issuer cannot emit a certification because it is not member.
IssuerNotMember,
/// Issuer or receiver not found.
IdtyNotFound,
/// Membership can only be renewed after an antispam delay.
MembershipRenewalPeriodNotRespected,
}
}
impl<AccountId, T: Config<I>, I: 'static> pallet_identity::traits::CheckIdtyCallAllowed<T>
for Pallet<T, I>
/// Implementing identity call allowance check for the pallet.
impl<AccountId, T: Config> pallet_identity::traits::CheckIdtyCallAllowed<T> for Pallet<T>
where
T: frame_system::Config<AccountId = AccountId> + pallet_membership::Config<I>,
T: frame_system::Config<AccountId = AccountId> + pallet_membership::Config,
{
/// Checks if identity creation is allowed.
/// This implementation checks the following:
///
/// - Whether the identity has the right to create an identity.
/// - Whether the issuer can emit a certification.
/// - Whether the issuer respect creation period.
fn check_create_identity(creator: IdtyIndex) -> Result<(), DispatchError> {
if !T::IsSubWot::get() {
let cert_meta = pallet_certification::Pallet::<T, I>::idty_cert_meta(creator);
// perform all checks
let cert_meta = pallet_certification::Pallet::<T>::idty_cert_meta(creator);
// 1. Check that the identity has the right to create an identity
// Identity can be a member with 5 certifications and still not reach the identity creation threshold, which could be higher (6, 7...)
ensure!(
cert_meta.received_count >= T::MinCertForCreateIdtyRight::get(),
Error::<T, I>::NotEnoughReceivedCertsToCreateIdty
Error::<T>::NotEnoughReceivedCertsToCreateIdty
);
// 2. Check that the issuer can emit one more certification (partial check)
ensure!(
cert_meta.issued_count < T::MaxByIssuer::get(),
Error::<T, I>::MaxEmittedCertsReached
Error::<T>::MaxEmittedCertsReached
);
// 3. Check that the issuer respects certification creation period
ensure!(
cert_meta.next_issuable_on <= frame_system::pallet::Pallet::<T>::block_number(),
Error::<T, I>::IdtyCreationPeriodNotRespected
);
}
Ok(())
}
fn check_confirm_identity(idty_index: IdtyIndex) -> Result<(), DispatchError> {
if !T::IsSubWot::get() {
pallet_membership::Pallet::<T, I>::force_request_membership(
RawOrigin::Root.into(),
idty_index,
Default::default(),
)
.map_err(|e| e.error)?;
}
Ok(())
}
fn check_validate_identity(idty_index: IdtyIndex) -> Result<(), DispatchError> {
if !T::IsSubWot::get() {
// TODO replace this code by the commented one for distance feature
/*let idty_cert_meta = pallet_certification::Pallet::<T, I>::idty_cert_meta(idty_index);
idty_cert_meta.received_count >= T::MinCertForMembership::get() as u32*/
pallet_membership::Pallet::<T, I>::claim_membership(
RawOrigin::Root.into(),
Some(idty_index),
)
.map_err(|e| e.error)?;
}
Ok(())
}
fn check_change_identity_address(idty_index: IdtyIndex) -> Result<(), DispatchError> {
if T::IsSubWot::get() {
ensure!(
!pallet_membership::Pallet::<T, I>::is_member(&idty_index),
Error::<T, I>::NotAllowedToChangeIdtyAddress
Error::<T>::IdtyCreationPeriodNotRespected
);
}
Ok(())
}
fn check_remove_identity(idty_index: IdtyIndex) -> Result<(), DispatchError> {
if T::IsSubWot::get() {
ensure!(
!pallet_membership::Pallet::<T, I>::is_member(&idty_index),
Error::<T, I>::NotAllowedToRemoveIdty
);
}
Ok(())
}
}
impl<T: Config<I>, I: 'static> pallet_certification::traits::CheckCertAllowed<IdtyIndex>
for Pallet<T, I>
{
/// Implementing certification allowance check for the pallet.
impl<T: Config> pallet_certification::traits::CheckCertAllowed<IdtyIndex> for Pallet<T> {
/// Checks if certification is allowed.
/// This implementation checks the following:
///
/// - Whether the issuer has an identity.
/// - Whether the issuer's identity is a member.
/// - Whether the receiver has an identity.
/// - Whether the receiver's identity is confirmed and not revoked.
fn check_cert_allowed(issuer: IdtyIndex, receiver: IdtyIndex) -> Result<(), DispatchError> {
if let Some(issuer_data) = pallet_identity::Pallet::<T>::identity(issuer) {
// Issuer checks
// Ensure issuer is a member
let issuer_data =
pallet_identity::Pallet::<T>::identity(issuer).ok_or(Error::<T>::IdtyNotFound)?;
ensure!(
issuer_data.status == IdtyStatus::Validated,
Error::<T, I>::IssuerCanNotEmitCert
issuer_data.status == IdtyStatus::Member,
Error::<T>::IssuerNotMember
);
} else {
return Err(Error::<T, I>::IdtyNotFound.into());
}
if let Some(receiver_data) = pallet_identity::Pallet::<T>::identity(receiver) {
match receiver_data.status {
IdtyStatus::ConfirmedByOwner | IdtyStatus::Validated => {} // able to receive cert
IdtyStatus::Created => return Err(Error::<T, I>::CertToUnconfirmedIdty.into()),
};
} else {
return Err(Error::<T, I>::IdtyNotFound.into());
}
Ok(())
}
}
impl<T: Config<I>, I: 'static> sp_membership::traits::CheckCallAllowed<IdtyIndex> for Pallet<T, I> {
fn check_idty_allowed_to_claim_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> {
let idty_cert_meta = pallet_certification::Pallet::<T, I>::idty_cert_meta(idty_index);
// Receiver checks
// Ensure receiver identity is confirmed and not revoked
let receiver_data =
pallet_identity::Pallet::<T>::identity(receiver).ok_or(Error::<T>::IdtyNotFound)?;
ensure!(
idty_cert_meta.received_count >= T::MinCertForMembership::get(),
Error::<T, I>::IdtyNotAllowedToClaimMembership
receiver_data.status == IdtyStatus::Unvalidated
|| receiver_data.status == IdtyStatus::Member
|| receiver_data.status == IdtyStatus::NotMember,
Error::<T>::TargetStatusInvalid
);
Ok(())
}
}
fn check_idty_allowed_to_renew_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> {
if let Some(idty_value) = pallet_identity::Pallet::<T>::identity(idty_index) {
/// Implementing membership operation checks for the pallet.
impl<T: Config> sp_membership::traits::CheckMembershipOpAllowed<IdtyIndex> for Pallet<T> {
/// This implementation checks the following:
///
/// - Whether the identity's status is unvalidated or not a member.
/// - The count of certifications associated with the identity.
fn check_add_membership(idty_index: IdtyIndex) -> Result<(), DispatchError> {
// Check identity status
let idty_value =
pallet_identity::Pallet::<T>::identity(idty_index).ok_or(Error::<T>::IdtyNotFound)?;
ensure!(
idty_value.status == IdtyStatus::Validated,
Error::<T, I>::IdtyNotAllowedToRenewMembership
idty_value.status == IdtyStatus::Unvalidated
|| idty_value.status == IdtyStatus::NotMember,
Error::<T>::TargetStatusInvalid
);
} else {
return Err(Error::<T, I>::IdtyNotFound.into());
}
// Check certificate count
check_cert_count::<T>(idty_index)?;
Ok(())
}
fn check_idty_allowed_to_request_membership(
idty_index: &IdtyIndex,
) -> Result<(), DispatchError> {
if let Some(idty_value) = pallet_identity::Pallet::<T>::identity(idty_index) {
/// This implementation checks the following:
///
/// - Whether the identity's status is member.
///
/// Note: There is no need to check certification count since losing certifications makes membership expire.
/// Membership renewal is only possible when identity is member.
fn check_renew_membership(idty_index: IdtyIndex) -> Result<(), DispatchError> {
let idty_value =
pallet_identity::Pallet::<T>::identity(idty_index).ok_or(Error::<T>::IdtyNotFound)?;
ensure!(
T::IsSubWot::get() && idty_value.status == IdtyStatus::Validated,
Error::<T, I>::IdtyNotAllowedToRequestMembership
idty_value.status == IdtyStatus::Member,
Error::<T>::TargetStatusInvalid
);
} else {
return Err(Error::<T, I>::IdtyNotFound.into());
}
Ok(())
}
}
impl<T: Config<I>, I: 'static, MetaData> sp_membership::traits::OnEvent<IdtyIndex, MetaData>
for Pallet<T, I>
/// Implementing membership event handling for the pallet.
impl<T: Config> sp_membership::traits::OnNewMembership<IdtyIndex> for Pallet<T>
where
T: pallet_membership::Config<I, MetaData = MetaData>,
T: pallet_membership::Config,
{
fn on_event(membership_event: &sp_membership::Event<IdtyIndex, MetaData>) -> Weight {
match membership_event {
sp_membership::Event::<IdtyIndex, MetaData>::MembershipAcquired(_, _) => {}
// Membership expiration cases:
// Triggered by the membership pallet: we should remove the identity only for the main
// wot
sp_membership::Event::<IdtyIndex, MetaData>::MembershipExpired(idty_index) => {
if !T::IsSubWot::get() {
Self::dispath_idty_call(pallet_identity::Call::remove_identity {
idty_index: *idty_index,
idty_name: None,
});
}
}
// Membership revocation cases:
// - Triggered by main identity removal: the underlying identity will be removed by the
// caller.
// - Triggered by the membership pallet: it's only possible for a sub-wot, so we
// should not remove the underlying identity
// So, in any case, we must do nothing
sp_membership::Event::<IdtyIndex, MetaData>::MembershipRevoked(_) => {}
sp_membership::Event::<IdtyIndex, MetaData>::MembershipRenewed(_) => {}
sp_membership::Event::<IdtyIndex, MetaData>::MembershipRequested(_) => {}
sp_membership::Event::<IdtyIndex, MetaData>::PendingMembershipExpired(idty_index) => {
Self::dispath_idty_call(pallet_identity::Call::remove_identity {
idty_index: *idty_index,
idty_name: None,
});
}
}
Weight::zero()
}
}
impl<T: Config<I>, I: 'static> pallet_identity::traits::OnIdtyChange<T> for Pallet<T, I> {
fn on_idty_change(idty_index: IdtyIndex, idty_event: &IdtyEvent<T>) -> Weight {
match idty_event {
IdtyEvent::Created { creator } => {
if let Err(e) = <pallet_certification::Pallet<T, I>>::force_add_cert(
frame_system::Origin::<T>::Root.into(),
*creator,
idty_index,
true,
) {
sp_std::if_std! {
println!("fail to force add cert: {:?}", e)
/// This implementation notifies the identity pallet when a main membership is acquired.
/// It is only used on the first membership acquisition.
fn on_created(idty_index: &IdtyIndex) {
pallet_identity::Pallet::<T>::membership_added(*idty_index);
}
fn on_renewed(_idty_index: &IdtyIndex) {}
}
/// Implementing membership removal event handling for the pallet.
impl<T: Config> sp_membership::traits::OnRemoveMembership<IdtyIndex> for Pallet<T>
where
T: pallet_membership::Config,
{
/// This implementation notifies the identity pallet when a main membership is lost.
fn on_removed(idty_index: &IdtyIndex) -> Weight {
pallet_identity::Pallet::<T>::membership_removed(*idty_index)
}
IdtyEvent::Removed { status } => {
if *status != IdtyStatus::Validated {
}
/// Implementing the identity event handler for the pallet.
impl<T: Config> pallet_identity::traits::OnNewIdty<T> for Pallet<T> {
/// This implementation adds a certificate when a new identity is created.
fn on_created(idty_index: &IdtyIndex, creator: &IdtyIndex) {
if let Err(e) =
<pallet_certification::Pallet<T, I>>::remove_all_certs_received_by(
frame_system::Origin::<T>::Root.into(),
idty_index,
)
<pallet_certification::Pallet<T>>::do_add_cert_checked(*creator, *idty_index, true)
{
sp_std::if_std! {
println!("fail to remove certs received by some idty: {:?}", e)
}
#[cfg(feature = "std")]
println!("fail to force add cert: {:?}", e)
}
}
}
IdtyEvent::Confirmed | IdtyEvent::Validated | IdtyEvent::ChangedOwnerKey { .. } => {}
/// Implementing identity removal event handling for the pallet.
impl<T: Config> pallet_identity::traits::OnRemoveIdty<T> for Pallet<T> {
/// This implementation removes both membership and certificates associated with the identity.
fn on_removed(idty_index: &IdtyIndex) -> Weight {
let mut weight = Self::on_revoked(idty_index);
weight = weight.saturating_add(
<pallet_certification::Pallet<T>>::do_remove_all_certs_received_by(*idty_index),
);
weight
}
Weight::zero()
/// This implementation removes membership only.
fn on_revoked(idty_index: &IdtyIndex) -> Weight {
let mut weight = Weight::zero();
weight = weight.saturating_add(<pallet_membership::Pallet<T>>::do_remove_membership(
*idty_index,
MembershipRemovalReason::Revoked,
));
weight
}
}
impl<T: Config<I>, I: 'static> pallet_certification::traits::OnNewcert<IdtyIndex> for Pallet<T, I> {
/// Implementing the certification event handler for the pallet.
impl<T: Config> pallet_certification::traits::OnNewcert<IdtyIndex> for Pallet<T> {
/// This implementation checks if the receiver has received enough certificates to be able to issue certificates,
/// and applies the first issuable if the condition is met.
fn on_new_cert(
_issuer: IdtyIndex,
_issuer_issued_count: u32,
receiver: IdtyIndex,
receiver_received_count: u32,
) -> Weight {
) {
if receiver_received_count == T::MinReceivedCertToBeAbleToIssueCert::get() {
Self::do_apply_first_issuable_on(receiver);
}
Weight::zero()
}
}
impl<T: Config<I>, I: 'static> pallet_certification::traits::OnRemovedCert<IdtyIndex>
for Pallet<T, I>
{
/// Implementing the certification removal event handler for the pallet.
impl<T: Config> pallet_certification::traits::OnRemovedCert<IdtyIndex> for Pallet<T> {
/// This implementation checks if the receiver has received fewer certificates than required for membership,
/// and if so, and the receiver is a member, it expires the receiver's membership.
fn on_removed_cert(
_issuer: IdtyIndex,
_issuer_issued_count: u32,
receiver: IdtyIndex,
receiver_received_count: u32,
_expiration: bool,
) -> Weight {
) {
if receiver_received_count < T::MinCertForMembership::get()
&& pallet_membership::Pallet::<T, I>::is_member(&receiver)
&& pallet_membership::Pallet::<T>::is_member(&receiver)
{
if T::IsSubWot::get() {
// Revoke receiver membership
let call = pallet_membership::Call::<T, I>::revoke_membership {
maybe_idty_id: Some(receiver),
};
if let Err(e) = call.dispatch_bypass_filter(RawOrigin::Root.into()) {
sp_std::if_std! {
println!("fail to dispatch membership call: {:?}", e)
// Expire receiver membership
<pallet_membership::Pallet<T>>::do_remove_membership(
receiver,
MembershipRemovalReason::NotEnoughCerts,
);
}
}
}
/// Implementing the valid distance status event handler for the pallet.
impl<T: Config + pallet_distance::Config> pallet_distance::traits::OnValidDistanceStatus<T>
for Pallet<T>
{
/// This implementation handles different scenarios based on the identity's status:
///
/// - For `Unconfirmed` or `Revoked` identities, no action is taken.
/// - For `Unvalidated` or `NotMember` identities, an attempt is made to add membership.
/// - For `Member` identities, an attempt is made to renew membership.
fn on_valid_distance_status(idty_index: IdtyIndex) {
if let Some(identity) = pallet_identity::Identities::<T>::get(idty_index) {
match identity.status {
IdtyStatus::Unconfirmed | IdtyStatus::Revoked => {
// IdtyStatus::Unconfirmed
// distance evaluation request should never happen for unconfirmed identity
// IdtyStatus::Revoked
// the identity can have been revoked during distance evaluation by the oracle
}
IdtyStatus::Unvalidated | IdtyStatus::NotMember => {
// IdtyStatus::Unvalidated
// normal scenario for first entry
// IdtyStatus::NotMember
// normal scenario for re-entry
// the following can fail if a certification expired during distance evaluation
// otherwise it should succeed
let _ = pallet_membership::Pallet::<T>::try_add_membership(idty_index);
// sp_std::if_std! {
// if let Err(e) = r {
// print!("failed to claim identity when distance status was found ok: ");
// println!("{:?}", idty_index);
// println!("reason: {:?}", e);
// }
// }
}
IdtyStatus::Member => {
// IdtyStatus::Member
// normal scenario for renewal
// should succeed
let _ = pallet_membership::Pallet::<T>::try_renew_membership(idty_index);
// sp_std::if_std! {
// if let Err(e) = r {
// print!("failed to renew identity when distance status was found ok: ");
// println!("{:?}", idty_index);
// println!("reason: {:?}", e);
// }
// }
}
}
} else {
// Revoke receiver membership and disable his identity
Self::dispath_idty_call(pallet_identity::Call::remove_identity {
idty_index: receiver,
idty_name: None,
});
// identity was removed before distance status was found
// so it's ok to do nothing
#[cfg(feature = "std")]
println!(
"identity was removed before distance status was found: {:?}",
idty_index
);
}
}
Weight::zero()
}
/// Implementing the request distance evaluation check for the pallet.
impl<T: Config + pallet_distance::Config> pallet_distance::traits::CheckRequestDistanceEvaluation<T>
for Pallet<T>
{
/// This implementation performs the following checks:
///
/// - Membership renewal anti-spam check: Ensures that membership renewal requests respect the anti-spam period.
/// - Certificate count check: Ensures that the identity has a sufficient number of certificates.
fn check_request_distance_evaluation(idty_index: IdtyIndex) -> Result<(), DispatchError> {
// Check membership renewal anti-spam
let maybe_membership_data = pallet_membership::Pallet::<T>::membership(idty_index);
if let Some(membership_data) = maybe_membership_data {
// If membership data exists, this is for a renewal, apply anti-spam
ensure!(
// current_block > expiration block - membership period + renewal period
membership_data.expire_on
+ <T as pallet_membership::Config>::MembershipRenewalPeriod::get()
< frame_system::Pallet::<T>::block_number()
+ <T as pallet_membership::Config>::MembershipPeriod::get(),
Error::<T>::MembershipRenewalPeriodNotRespected
);
};
// Check certificate count
check_cert_count::<T>(idty_index)?;
Ok(())
}
}
/// Checks the certificate count for an identity.
fn check_cert_count<T: Config>(idty_index: IdtyIndex) -> Result<(), DispatchError> {
let idty_cert_meta = pallet_certification::Pallet::<T>::idty_cert_meta(idty_index);
ensure!(
idty_cert_meta.received_count >= T::MinCertForMembership::get(),
Error::<T>::NotEnoughCerts
);
Ok(())
}
......@@ -16,43 +16,77 @@
use super::*;
use crate::{self as pallet_duniter_wot};
use frame_support::{parameter_types, traits::Everything};
use frame_support::{derive_impl, parameter_types, traits::Everything};
use frame_system as system;
use sp_core::H256;
use sp_runtime::{
testing::{Header, TestSignature, UintAuthorityId},
testing::{TestSignature as SubtrateTestSignature, UintAuthorityId},
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};
use sp_state_machine::BasicExternalities;
use std::collections::BTreeMap;
type AccountId = u64;
type Block = frame_system::mocking::MockBlock<Test>;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
pub struct IdentityIndexOf<T: pallet_identity::Config>(PhantomData<T>);
impl<T: pallet_identity::Config> sp_runtime::traits::Convert<T::AccountId, Option<T::IdtyIndex>>
for IdentityIndexOf<T>
{
fn convert(account_id: T::AccountId) -> Option<T::IdtyIndex> {
pallet_identity::Pallet::<T>::identity_index_of(account_id)
pub struct AccountId32Mock(u64);
impl From<AccountId32Mock> for u64 {
fn from(account: AccountId32Mock) -> u64 {
account.0
}
}
impl From<[u8; 32]> for AccountId32Mock {
fn from(bytes: [u8; 32]) -> Self {
Self(u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]))
}
}
/// Test signature that impl From<ed25519::Signature> (required to compile pallet identity)
#[derive(
Clone,
codec::DecodeWithMemTracking,
codec::Decode,
Debug,
Eq,
codec::Encode,
PartialEq,
scale_info::TypeInfo,
)]
pub struct TestSignature(SubtrateTestSignature);
impl TestSignature {
pub fn new(signer: u64, message: Vec<u8>) -> Self {
Self(SubtrateTestSignature(signer, message))
}
}
impl From<sp_core::ed25519::Signature> for TestSignature {
fn from(_: sp_core::ed25519::Signature) -> Self {
// Implementation here only to satisfy traits bounds at compilation
// This convertion should not be used inside pallet distance tests
unimplemented!()
}
}
impl sp_runtime::traits::Verify for TestSignature {
type Signer = UintAuthorityId;
fn verify<L: sp_runtime::traits::Lazy<[u8]>>(&self, msg: L, signer: &u64) -> bool {
<SubtrateTestSignature as sp_runtime::traits::Verify>::verify::<L>(&self.0, msg, signer)
}
}
// 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>},
DuniterWot: pallet_duniter_wot::<Instance1>::{Pallet},
Identity: pallet_identity::{Pallet, Call, Config<T>, Storage, Event<T>},
Membership: pallet_membership::<Instance1>::{Pallet, Call, Config<T>, Storage, Event<T>},
Cert: pallet_certification::<Instance1>::{Pallet, Call, Config<T>, Storage, Event<T>},
SmithsSubWot: pallet_duniter_wot::<Instance2>::{Pallet},
SmithsMembership: pallet_membership::<Instance2>::{Pallet, Call, Config<T>, Storage, Event<T>},
SmithsCert: pallet_certification::<Instance2>::{Pallet, Call, Config<T>, Storage, Event<T>},
pub enum Test {
System: frame_system,
DuniterWot: pallet_duniter_wot,
Identity: pallet_identity,
Membership: pallet_membership,
Cert: pallet_certification,
}
);
......@@ -62,31 +96,22 @@ 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 RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Index = u64;
type BlockNumber = u64;
type Block = Block;
type BlockHashCount = BlockHashCount;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type RuntimeEvent = RuntimeEvent;
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>;
}
// DuniterWot
......@@ -96,19 +121,20 @@ parameter_types! {
pub const FirstIssuableOn: u64 = 2;
}
impl pallet_duniter_wot::Config<Instance1> for Test {
type IsSubWot = frame_support::traits::ConstBool<false>;
type MinCertForMembership = MinCertForMembership;
type MinCertForCreateIdtyRight = MinCertForCreateIdtyRight;
impl pallet_duniter_wot::Config for Test {
type FirstIssuableOn = FirstIssuableOn;
type MinCertForCreateIdtyRight = MinCertForCreateIdtyRight;
type MinCertForMembership = MinCertForMembership;
}
// Identity
parameter_types! {
pub const ChangeOwnerKeyPeriod: u64 = 10;
pub const ConfirmPeriod: u64 = 2;
pub const ValidationPeriod: u64 = 5;
pub const AutorevocationPeriod: u64 = 6;
pub const DeletionPeriod: u64 = 7;
pub const IdtyCreationPeriod: u64 = 3;
pub const ValidationPeriod: u64 = 2;
}
pub struct IdtyNameValidatorTestImpl;
......@@ -119,37 +145,46 @@ impl pallet_identity::traits::IdtyNameValidator for IdtyNameValidatorTestImpl {
}
impl pallet_identity::Config for Test {
type AccountId32 = AccountId32Mock;
type AccountLinker = ();
type AutorevocationPeriod = AutorevocationPeriod;
type ChangeOwnerKeyPeriod = ChangeOwnerKeyPeriod;
type CheckAccountWorthiness = ();
type CheckIdtyCallAllowed = DuniterWot;
type ConfirmPeriod = ConfirmPeriod;
type CheckIdtyCallAllowed = (DuniterWot, SmithsSubWot);
type DeletionPeriod = DeletionPeriod;
type IdtyCreationPeriod = IdtyCreationPeriod;
type IdtyData = ();
type IdtyNameValidator = IdtyNameValidatorTestImpl;
type IdtyIndex = IdtyIndex;
type NewOwnerKeySigner = UintAuthorityId;
type NewOwnerKeySignature = TestSignature;
type OnIdtyChange = DuniterWot;
type RemoveIdentityConsumers = ();
type RevocationSigner = UintAuthorityId;
type RevocationSignature = TestSignature;
type IdtyNameValidator = IdtyNameValidatorTestImpl;
type OnKeyChange = ();
type OnNewIdty = DuniterWot;
type OnRemoveIdty = DuniterWot;
type RuntimeEvent = RuntimeEvent;
type Signature = TestSignature;
type Signer = UintAuthorityId;
type ValidationPeriod = ValidationPeriod;
type WeightInfo = ();
}
// Membership
parameter_types! {
pub const MembershipPeriod: u64 = 8;
pub const PendingMembershipPeriod: u64 = 3;
pub const MembershipRenewalPeriod: u64 = 2;
}
impl pallet_membership::Config<Instance1> for Test {
type CheckCallAllowed = DuniterWot;
impl pallet_membership::Config for Test {
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkSetupHandler = ();
type CheckMembershipOpAllowed = DuniterWot;
type IdtyAttr = Identity;
type IdtyId = IdtyIndex;
type IdtyIdOf = IdentityIndexOf<Self>;
type MembershipPeriod = MembershipPeriod;
type MetaData = ();
type OnEvent = DuniterWot;
type MembershipRenewalPeriod = MembershipRenewalPeriod;
type OnNewMembership = DuniterWot;
type OnRemoveMembership = DuniterWot;
type RuntimeEvent = RuntimeEvent;
type PendingMembershipPeriod = PendingMembershipPeriod;
type WeightInfo = ();
}
// Cert
......@@ -160,80 +195,26 @@ parameter_types! {
pub const ValidityPeriod: u64 = 20;
}
impl pallet_certification::Config<Instance1> for Test {
impl pallet_certification::Config for Test {
type CertPeriod = CertPeriod;
type IdtyIndex = IdtyIndex;
type OwnerKeyOf = Identity;
type CheckCertAllowed = DuniterWot;
type IdtyAttr = Identity;
type IdtyIndex = IdtyIndex;
type MaxByIssuer = MaxByIssuer;
type MinReceivedCertToBeAbleToIssueCert = MinReceivedCertToBeAbleToIssueCert;
type OnNewcert = DuniterWot;
type OnRemovedCert = DuniterWot;
type RuntimeEvent = RuntimeEvent;
type ValidityPeriod = ValidityPeriod;
}
// SMITHS SUB-WOT //
parameter_types! {
pub const SmithsMinCertForMembership: u32 = 2;
pub const SmithsFirstIssuableOn: u64 = 2;
}
impl pallet_duniter_wot::Config<Instance2> for Test {
type IsSubWot = frame_support::traits::ConstBool<true>;
type MinCertForMembership = SmithsMinCertForMembership;
type MinCertForCreateIdtyRight = frame_support::traits::ConstU32<0>;
type FirstIssuableOn = SmithsFirstIssuableOn;
}
// SmithsMembership
parameter_types! {
pub const SmithsMembershipPeriod: u64 = 20;
pub const SmithsPendingMembershipPeriod: u64 = 3;
}
impl pallet_membership::Config<Instance2> for Test {
type CheckCallAllowed = SmithsSubWot;
type IdtyId = IdtyIndex;
type IdtyIdOf = IdentityIndexOf<Self>;
type MembershipPeriod = SmithsMembershipPeriod;
type MetaData = ();
type OnEvent = SmithsSubWot;
type PendingMembershipPeriod = SmithsPendingMembershipPeriod;
type RuntimeEvent = RuntimeEvent;
}
// SmithsCert
parameter_types! {
pub const SmithsMaxByIssuer: u8 = 8;
pub const SmithsMinReceivedCertToBeAbleToIssueCert: u32 = 2;
pub const SmithsCertPeriod: u64 = 2;
pub const SmithsValidityPeriod: u64 = 10;
}
impl pallet_certification::Config<Instance2> for Test {
type CertPeriod = SmithsCertPeriod;
type IdtyIndex = IdtyIndex;
type OwnerKeyOf = Identity;
type CheckCertAllowed = SmithsSubWot;
type MaxByIssuer = SmithsMaxByIssuer;
type MinReceivedCertToBeAbleToIssueCert = SmithsMinReceivedCertToBeAbleToIssueCert;
type OnNewcert = SmithsSubWot;
type OnRemovedCert = SmithsSubWot;
type RuntimeEvent = RuntimeEvent;
type ValidityPeriod = SmithsValidityPeriod;
type WeightInfo = ();
}
pub const NAMES: [&str; 6] = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie"];
// Build genesis storage according to the mock runtime.
pub fn new_test_ext(
initial_identities_len: usize,
initial_smiths_len: usize,
) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default()
.build_storage::<Test>()
pub fn new_test_ext(initial_identities_len: usize) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
pallet_identity::GenesisConfig::<Test> {
......@@ -246,8 +227,8 @@ pub fn new_test_ext(
next_creatable_identity_on: 0,
owner_key: i as u64,
old_owner_key: None,
removable_on: 0,
status: pallet_identity::IdtyStatus::Validated,
next_scheduled: 0,
status: pallet_identity::IdtyStatus::Member,
},
})
.collect(),
......@@ -255,7 +236,7 @@ pub fn new_test_ext(
.assimilate_storage(&mut t)
.unwrap();
pallet_membership::GenesisConfig::<Test, Instance1> {
pallet_membership::GenesisConfig::<Test> {
memberships: (1..=initial_identities_len)
.map(|i| {
(
......@@ -270,38 +251,16 @@ pub fn new_test_ext(
.assimilate_storage(&mut t)
.unwrap();
pallet_certification::GenesisConfig::<Test, Instance1> {
pallet_certification::GenesisConfig::<Test> {
apply_cert_period_at_genesis: true,
certs_by_receiver: clique_wot(initial_identities_len, ValidityPeriod::get()),
}
.assimilate_storage(&mut t)
.unwrap();
pallet_membership::GenesisConfig::<Test, Instance2> {
memberships: (1..=initial_smiths_len)
.map(|i| {
(
i as u32,
sp_membership::MembershipData {
expire_on: SmithsMembershipPeriod::get(),
},
)
})
.collect(),
}
.assimilate_storage(&mut t)
.unwrap();
pallet_certification::GenesisConfig::<Test, Instance2> {
apply_cert_period_at_genesis: true,
certs_by_receiver: clique_wot(initial_smiths_len, SmithsValidityPeriod::get()),
}
.assimilate_storage(&mut t)
.unwrap();
frame_support::BasicExternalities::execute_with_storage(&mut t, || {
BasicExternalities::execute_with_storage(&mut t, || {
// manually increment genesis identities sufficient counter
// In real world, this should be handle manually by genesis creator
// In real world, this is done by pallet-identity
for i in 1..=initial_identities_len {
frame_system::Pallet::<Test>::inc_sufficients(&(i as u64));
}
......@@ -315,24 +274,21 @@ pub fn new_test_ext(
pub fn run_to_block(n: u64) {
while System::block_number() < n {
// finalize previous block
DuniterWot::on_finalize(System::block_number());
Identity::on_finalize(System::block_number());
Membership::on_finalize(System::block_number());
Cert::on_finalize(System::block_number());
SmithsSubWot::on_finalize(System::block_number());
SmithsMembership::on_finalize(System::block_number());
SmithsCert::on_finalize(System::block_number());
System::on_finalize(System::block_number());
// reset events and change block number
System::reset_events();
System::set_block_number(System::block_number() + 1);
// initialize next block
System::on_initialize(System::block_number());
DuniterWot::on_initialize(System::block_number());
Identity::on_initialize(System::block_number());
Membership::on_initialize(System::block_number());
Cert::on_initialize(System::block_number());
SmithsSubWot::on_initialize(System::block_number());
SmithsMembership::on_initialize(System::block_number());
SmithsCert::on_initialize(System::block_number());
}
}
......
......@@ -14,168 +14,92 @@
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::mock::*;
use crate::mock::{Identity, System};
use crate::pallet as pallet_duniter_wot;
use crate::{mock::*, pallet as pallet_duniter_wot};
use codec::Encode;
use frame_support::instances::{Instance1, Instance2};
use frame_support::{assert_noop, assert_ok};
use pallet_identity::{
IdtyName, IdtyStatus, NewOwnerKeyPayload, RevocationPayload, NEW_OWNER_KEY_PAYLOAD_PREFIX,
REVOCATION_PAYLOAD_PREFIX,
IdtyName, IdtyStatus, RevocationPayload, RevocationReason, REVOCATION_PAYLOAD_PREFIX,
};
use sp_runtime::testing::TestSignature;
use pallet_membership::MembershipRemovalReason;
/// test that genesis builder creates the good number of identities
/// and good identity and certification metadate
#[test]
fn test_genesis_build() {
new_test_ext(3, 2).execute_with(|| {
new_test_ext(3).execute_with(|| {
run_to_block(1);
// Verify state
assert_eq!(Identity::identities_count(), 3);
assert_eq!(Identity::identity(1).unwrap().next_creatable_identity_on, 0);
assert_eq!(
pallet_certification::Pallet::<Test, Instance1>::idty_cert_meta(1).next_issuable_on,
pallet_certification::Pallet::<Test>::idty_cert_meta(1).next_issuable_on,
2
);
});
}
/// test that Alice is not able to create an identity when she received too few certs (2 of 4)
#[test]
fn test_creator_not_allowed_to_create_idty() {
new_test_ext(3, 2).execute_with(|| {
new_test_ext(3).execute_with(|| {
run_to_block(1);
// Alice should not be able to create an identity before block #2
// because Alice.next_issuable_on = 2
// but the true reason is that alice did not receive enough certs
// Alice did not receive enough certs
// (anyway Alice should not be able to create an identity before block #2
// because Alice.next_issuable_on = 2)
assert_noop!(
Identity::create_identity(RuntimeOrigin::signed(1), 4),
pallet_duniter_wot::Error::<Test, Instance1>::NotEnoughReceivedCertsToCreateIdty
pallet_duniter_wot::Error::<Test>::NotEnoughReceivedCertsToCreateIdty
);
});
}
/// test that Alice is able to create an identity when she received enough certs (4)
#[test]
fn test_join_smiths() {
new_test_ext(5, 3).execute_with(|| {
run_to_block(2);
// Dave shoud be able to request smith membership
assert_ok!(SmithsMembership::request_membership(
RuntimeOrigin::signed(4),
()
));
System::assert_has_event(RuntimeEvent::SmithsMembership(
pallet_membership::Event::MembershipRequested(4),
));
// Then, Alice should be able to send a smith cert to Dave
run_to_block(3);
assert_ok!(SmithsCert::add_cert(RuntimeOrigin::signed(1), 1, 4));
// Then, Bob should be able to send a smith cert to Dave
run_to_block(4);
assert_ok!(SmithsCert::add_cert(RuntimeOrigin::signed(2), 2, 4));
// Then, Dave should be able to claim his membership
run_to_block(4);
assert_ok!(SmithsMembership::claim_membership(
RuntimeOrigin::signed(4),
Some(4)
));
System::assert_has_event(RuntimeEvent::SmithsMembership(
pallet_membership::Event::MembershipAcquired(4),
));
});
}
#[test]
fn test_smith_certs_expirations_should_revoke_smith_membership() {
new_test_ext(5, 3).execute_with(|| {
// After block #10, alice membership should be revoked due to smith certs expiration
run_to_block(10);
System::assert_has_event(RuntimeEvent::SmithsMembership(
pallet_membership::Event::MembershipRevoked(1),
));
});
}
#[test]
fn test_smith_member_cant_change_its_idty_address() {
new_test_ext(5, 3).execute_with(|| {
fn test_creator_allowed_to_create_idty() {
new_test_ext(5).execute_with(|| {
run_to_block(2);
let genesis_hash = System::block_hash(0);
let new_key_payload = NewOwnerKeyPayload {
genesis_hash: &genesis_hash,
idty_index: 3u32,
old_owner_key: &3u64,
};
// Identity 3 can't change it's address
assert_noop!(
Identity::change_owner_key(
RuntimeOrigin::signed(3),
13,
TestSignature(13, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
),
pallet_duniter_wot::Error::<Test, Instance2>::NotAllowedToChangeIdtyAddress
);
});
}
#[test]
fn test_smith_member_cant_revoke_its_idty() {
new_test_ext(5, 3).execute_with(|| {
run_to_block(2);
let revocation_payload = RevocationPayload {
idty_index: 3u32,
genesis_hash: System::block_hash(0),
};
// Identity 3 can't change it's address
assert_noop!(
Identity::revoke_identity(
RuntimeOrigin::signed(3),
3,
3,
TestSignature(3, (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode())
),
pallet_duniter_wot::Error::<Test, Instance2>::NotAllowedToRemoveIdty
// Alice should be able to create an identity
assert_ok!(
Identity::create_identity(RuntimeOrigin::signed(1), 6),
// pallet_duniter_wot::Error::<Test>::NotEnoughReceivedCertsToCreateIdty
);
});
}
/// test identity creation and that a first cert is emitted
#[test]
fn test_create_idty_ok() {
new_test_ext(5, 2).execute_with(|| {
new_test_ext(5).execute_with(|| {
run_to_block(2);
// Alice should be able to create an identity at block #2
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 6));
// 2 events should have occurred: IdtyCreated and NewCert
// 2 events should have occurred: IdtyCreated and CertAdded
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyCreated {
idty_index: 6,
owner_key: 6,
},
));
System::assert_has_event(RuntimeEvent::Cert(pallet_certification::Event::NewCert {
System::assert_has_event(RuntimeEvent::Cert(pallet_certification::Event::CertAdded {
issuer: 1,
issuer_issued_count: 5,
receiver: 6,
receiver_received_count: 1,
}));
assert_eq!(Identity::identity(6).unwrap().status, IdtyStatus::Created);
assert_eq!(Identity::identity(6).unwrap().removable_on, 4);
assert_eq!(
Identity::identity(6).unwrap().status,
IdtyStatus::Unconfirmed
);
assert_eq!(Identity::identity(6).unwrap().next_scheduled, 2 + 2);
});
}
/// test identity validation
#[test]
fn test_new_idty_validation() {
new_test_ext(5, 2).execute_with(|| {
new_test_ext(5).execute_with(|| {
// Alice creates Ferdie identity
run_to_block(2);
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 6));
......@@ -189,19 +113,20 @@ fn test_new_idty_validation() {
// Bob should be able to certify Ferdie
run_to_block(4);
assert_ok!(Cert::add_cert(RuntimeOrigin::signed(2), 2, 6));
System::assert_has_event(RuntimeEvent::Cert(pallet_certification::Event::NewCert {
assert_ok!(Cert::add_cert(RuntimeOrigin::signed(2), 6));
System::assert_has_event(RuntimeEvent::Cert(pallet_certification::Event::CertAdded {
issuer: 2,
issuer_issued_count: 5,
receiver: 6,
receiver_received_count: 2,
}));
// Anyone should be able to validate Ferdie identity
// Ferdie should be able to claim membership
run_to_block(5);
assert_ok!(Identity::validate_identity(RuntimeOrigin::signed(42), 6));
assert_ok!(Membership::try_add_membership(6));
System::assert_has_event(RuntimeEvent::Membership(
pallet_membership::Event::MembershipAcquired(6),
pallet_membership::Event::MembershipAdded {
member: 6,
expire_on: 5 + <Test as pallet_membership::Config>::MembershipPeriod::get(),
},
));
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyValidated { idty_index: 6 },
......@@ -216,16 +141,17 @@ fn test_new_idty_validation() {
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: 6,
removable_on: 0,
status: IdtyStatus::Validated,
next_scheduled: 0,
status: IdtyStatus::Member,
})
);
});
}
/// test that Ferdie can confirm an identity created for him by Alice
#[test]
fn test_confirm_idty_ok() {
new_test_ext(5, 2).execute_with(|| {
new_test_ext(5).execute_with(|| {
run_to_block(2);
// Alice creates Ferdie identity
......@@ -238,54 +164,159 @@ fn test_confirm_idty_ok() {
RuntimeOrigin::signed(6),
IdtyName::from("Ferdie"),
));
System::assert_has_event(RuntimeEvent::Membership(
pallet_membership::Event::MembershipRequested(6),
));
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyConfirmed {
idty_index: 6,
owner_key: 6,
name: IdtyName::from("Ferdie"),
},
));
});
}
/// test identity revocation
/// - anyone can submit a revocation certificate signed by bob
#[test]
fn test_idty_membership_expire_them_requested() {
new_test_ext(3, 2).execute_with(|| {
fn test_revoke_idty() {
new_test_ext(5).execute_with(|| {
run_to_block(2);
// Alice identity can be revoked
assert_ok!(Identity::revoke_identity(
RuntimeOrigin::signed(1),
1,
1,
TestSignature::new(
1,
(
REVOCATION_PAYLOAD_PREFIX,
RevocationPayload {
idty_index: 1u32,
genesis_hash: System::block_hash(0),
}
)
.encode()
)
));
// her membership should be removed
System::assert_has_event(RuntimeEvent::Membership(
pallet_membership::Event::MembershipRemoved {
member: 1,
reason: pallet_membership::MembershipRemovalReason::Revoked,
},
));
// Anyone should be able to submit Bob revocation certificate
assert_ok!(Identity::revoke_identity(
RuntimeOrigin::signed(42),
2,
2,
TestSignature::new(
2,
(
REVOCATION_PAYLOAD_PREFIX,
RevocationPayload {
idty_index: 2u32,
genesis_hash: System::block_hash(0),
}
)
.encode()
)
));
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyRevoked {
idty_index: 2,
reason: pallet_identity::RevocationReason::User,
},
));
});
}
/// test that expired membership lose the identity after a delay
#[test]
fn test_idty_membership_expire() {
new_test_ext(3).execute_with(|| {
run_to_block(4);
// Alice renews her membership
assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(1), None));
assert_ok!(Membership::try_renew_membership(1));
// Bob renews his membership
assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2), None));
assert_ok!(Membership::try_renew_membership(2));
run_to_block(5);
// renew certifications so that Alice can still issue cert at block 22
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(2), 1));
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(3), 1));
// Charlie's membership should expire at block #8
run_to_block(8);
assert_ok!(Membership::try_renew_membership(1));
assert!(Membership::membership(3).is_none());
System::assert_has_event(RuntimeEvent::Membership(
pallet_membership::Event::MembershipExpired(3),
pallet_membership::Event::MembershipRemoved {
member: 3,
reason: MembershipRemovalReason::Expired,
},
));
assert_eq!(
Identity::identity(3),
Some(pallet_identity::IdtyValue {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: 3,
next_scheduled: 14, // = 8 (membership removal block) + 6 (auto revocation period)
status: IdtyStatus::NotMember,
})
);
// check that identity is added to auto-revoke list (currently IdentityChangeSchedule)
assert_eq!(Identity::next_scheduled(14), vec!(3));
run_to_block(14);
assert_ok!(Membership::try_renew_membership(1));
// Charlie's identity should be auto-revoked at block #11 (8 + 3)
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyRemoved { idty_index: 3 },
pallet_identity::Event::IdtyRevoked {
idty_index: 3,
reason: pallet_identity::RevocationReason::Expired,
},
));
assert_eq!(
Identity::identity(3),
Some(pallet_identity::IdtyValue {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: 3,
next_scheduled: 21, // = 14 (revocation block) + 7 (deletion period)
status: IdtyStatus::Revoked,
})
);
// Alice can't certify revoked identity
assert_noop!(
Cert::add_cert(RuntimeOrigin::signed(1), 3),
pallet_duniter_wot::Error::<Test>::TargetStatusInvalid
);
// Charlie's identity should be removed at block #8
assert!(Identity::identity(3).is_none());
// Alice can't renew her cert to Charlie
run_to_block(21);
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyRemoved {
idty_index: 3,
reason: pallet_identity::RemovalReason::Revoked,
},
));
// Alice can't certify removed identity
assert_noop!(
Cert::add_cert(RuntimeOrigin::signed(1), 1, 3),
pallet_duniter_wot::Error::<Test, Instance1>::IdtyNotFound
Cert::add_cert(RuntimeOrigin::signed(1), 3),
pallet_duniter_wot::Error::<Test>::IdtyNotFound
);
});
}
/// when an identity is confirmed and not validated, the certification received should be removed
#[test]
fn test_unvalidated_idty_certs_removal() {
new_test_ext(5, 2).execute_with(|| {
new_test_ext(5).execute_with(|| {
// Alice creates Ferdie identity
run_to_block(2);
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 6));
......@@ -297,10 +328,152 @@ fn test_unvalidated_idty_certs_removal() {
IdtyName::from("Ferdie"),
));
// After PendingMembershipPeriod, Ferdie identity should expire
// and his received certifications should be removed
assert_eq!(Cert::certs_by_receiver(6).len(), 1);
run_to_block(6);
// After ValidationPeriod, Ferdie identity should be automatically removed
// and his received certifications should be removed
run_to_block(8);
assert_eq!(Cert::certs_by_receiver(6).len(), 0);
});
}
/// test what happens when certification expire
#[test]
fn test_certification_expire() {
new_test_ext(3).execute_with(|| {
// smith cert Bob → Alice not renewed
// cert Bob → Alice not renewed
// --- BLOCK 2 ---
run_to_block(2);
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(1), 2));
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(2), 3));
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(3), 1));
// --- BLOCK 4 ---
run_to_block(4);
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(1), 3));
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(3), 2));
// --- BLOCK 7 ---
run_to_block(7);
assert_ok!(Membership::try_renew_membership(1));
assert_ok!(Membership::try_renew_membership(2));
assert_ok!(Membership::try_renew_membership(3));
// --- BLOCK 14 ---
run_to_block(14);
assert_ok!(Membership::try_renew_membership(1));
assert_ok!(Membership::try_renew_membership(2));
assert_ok!(Membership::try_renew_membership(3));
// normal cert Bob → Alice expires at block 20
run_to_block(20);
// println!("{:?}", System::events());
System::assert_has_event(RuntimeEvent::Cert(
pallet_certification::Event::CertRemoved {
issuer: 2, // Bob
receiver: 1, // Alice
expiration: true,
},
));
// in consequence, since Alice has only 1/2 normal certification remaining, she looses normal membership
System::assert_has_event(RuntimeEvent::Membership(
pallet_membership::Event::MembershipRemoved {
member: 1,
reason: MembershipRemovalReason::NotEnoughCerts,
},
));
// --- BLOCK 21 ---
// Bob and Charlie can renew their membership
run_to_block(21);
assert_ok!(Membership::try_renew_membership(2));
assert_ok!(Membership::try_renew_membership(3));
// Alice can not renew her membership which does not exist
assert_noop!(
Membership::try_renew_membership(1),
pallet_membership::Error::<Test>::MembershipNotFound
);
// Alice can not claim her membership because she does not have enough certifications
assert_noop!(
Membership::try_add_membership(1),
pallet_duniter_wot::Error::<Test>::NotEnoughCerts
);
// --- BLOCK 23 ---
run_to_block(26);
// println!("{:?}", System::events());
// after a delay, the non member identity is automatically revoked
System::assert_has_event(RuntimeEvent::Identity(
pallet_identity::Event::IdtyRevoked {
idty_index: 1,
reason: RevocationReason::Expired,
},
));
})
}
/// test some cases where identity should not be able to issue cert
// - when source or target is not member (sub wot)
// - when source or target membership is pending (both wot)
#[test]
fn test_cert_can_not_be_issued() {
new_test_ext(4).execute_with(|| {
// smith cert Bob → Alice not renewed
// cert Bob → Alice not renewed
// --- BLOCK 2 ---
run_to_block(2);
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(1), 2)); // +20
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(2), 3)); // +20
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(3), 4)); // +20
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(4), 1)); // +20
// --- BLOCK 4 ---
run_to_block(4);
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(2), 4)); // +20
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(3), 2)); // +20
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(4), 3)); // +20
// --- BLOCK 7 ---
run_to_block(7);
assert_ok!(Membership::try_renew_membership(1)); // + 8
assert_ok!(Membership::try_renew_membership(2)); // + 8
assert_ok!(Membership::try_renew_membership(3)); // + 8
assert_ok!(Membership::try_renew_membership(4)); // + 8
run_to_block(14);
assert_ok!(Membership::try_renew_membership(1)); // + 8
assert_ok!(Membership::try_renew_membership(2)); // + 8
assert_ok!(Membership::try_renew_membership(3)); // + 8
assert_ok!(Membership::try_renew_membership(4)); // + 8
run_to_block(20);
// println!("{:?}", System::events());
System::assert_has_event(RuntimeEvent::Cert(
pallet_certification::Event::CertRemoved {
issuer: 2, // Bob
receiver: 1, // Alice
expiration: true,
},
));
// other certifications expire, but not Dave → Alice
// in consequence, since Alice has only 1/2 certification remaining, she looses membership
System::assert_has_event(RuntimeEvent::Membership(
pallet_membership::Event::MembershipRemoved {
member: 1,
reason: MembershipRemovalReason::NotEnoughCerts,
}, // pending membership expires at 23
));
run_to_block(21);
// println!("{:?}", System::events());
// Charlie certifies Alice so she again has enough certs
assert_ok!(Cert::add_cert(RuntimeOrigin::signed(3), 1));
assert_ok!(Cert::renew_cert(RuntimeOrigin::signed(4), 1));
// renew
// Alice did not claim membership, she is not member
// but her cert delay has been reset (→ 23)
assert_eq!(Membership::membership(1), None);
// run_to_block(23);
// if identity of alice was not removed because pending for too long
// she would have been able to emit a cert without being member
})
}
[package]
authors = ['librelois <c@elo.tf>']
description = 'FRAME pallet identity.'
edition = "2021"
homepage = 'https://duniter.org'
license = 'AGPL-3.0'
name = 'pallet-identity'
readme = 'README.md'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
authors.workspace = true
description = "duniter pallet identity"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-identity"
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",
"sp-runtime/runtime-benchmarks",
]
std = [
'codec/std',
'frame-support/std',
'frame-system/std',
'frame-benchmarking/std',
'serde',
'sp-core/std',
'sp-runtime/std',
'sp-std/std',
"codec/std",
"duniter-primitives/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"scale-info/std",
"serde/std",
"sp-core/std",
"sp-io/std",
"sp-keystore/std",
"sp-runtime/std",
"sp-state-machine/std",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
]
try-runtime = ['frame-support/try-runtime']
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
[dependencies]
# crates.io
codec = { package = 'parity-scale-codec', version = "3.1.5", features = ['derive'], default-features = false }
impl-trait-for-tuples = "0.2.1"
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
serde = { version = "1.0.101", features = ["derive"], optional = true }
# substrate
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/duniter/substrate'
optional = true
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
targets = ["x86_64-unknown-linux-gnu"]
[dependencies.frame-system]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-core]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies]
base64 = { workspace = true }
bs58 = { workspace = true }
codec = { workspace = true, features = ["derive"] }
duniter-primitives = { workspace = true }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
log = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
serde = '1.0.119'
sp-io = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32' }
sp-keystore = { workspace = true, default-features = true }
sp-state-machine = { workspace = true, default-features = true }
# Duniter identity pallet
Duniter has a builtin identity system that does not work with external registrar compared to [parity identity pallet](https://github.com/paritytech/substrate/tree/master/frame/identity).
## Duniter identity
A Duniter identity contains:
- its **owner key** (that can change)
- an optional **old owner key** with the date of the key change
- a **status** that can be
- created (by an existing identity)
- confirmed (by owner, comes with a name)
- validated (that has become member in the allowed timeframe)
It also contains:
- the block number at which it can emit its **next certification**
- the block number at which it can be **removed from storage**
It also contains attached data defined by the runtime that can be for example
- the number of the first UD it is eligible to
### Name
Each identity is declared with a name emited on confirmation event. Duniter keeps a list of identity names hash to ensure unicity.
### Owner key
The idea of the owner key is to allow the user to keep a fixed identity while changing the keys for security reasons. For example when a device with the keys might have been compromised. There is a limit to the frequency of owner key change and the old owner key can still revoke the identity for a given period.
### Status / removable date
The status is a temporary value allowing to prune identities before they become member. When an identity is not valiated (not member of the WoT for instance), it can be removed when the date is reached. The remove date of a validated identity is block zero.
### Next certification
The next certification is a rate limit to the emission of certification (and then identity creation).
### Revokation
Revoking an identity basically means deleting it.
\ No newline at end of file
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use codec::Encode;
use frame_benchmarking::{account, v2::*};
use frame_support::traits::OnInitialize;
use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
use sp_core::{crypto::Ss58Codec, Get};
use sp_io::crypto::{sr25519_generate, sr25519_sign};
use sp_runtime::{AccountId32, MultiSigner};
use crate::Pallet;
#[benchmarks(
where
T::Signature: From<sp_core::sr25519::Signature>,
T::AccountId: From<AccountId32>,
T::IdtyIndex: From<u32>,
)]
mod benchmarks {
use super::*;
fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_has_event(generic_event.into());
}
struct Account<T: Config> {
key: T::AccountId,
index: T::IdtyIndex,
origin: <T as frame_system::Config>::RuntimeOrigin,
}
// Create and confirm one account using Alice authorized account.
// key, origin, name and index are returned.
// Alice next_creatable_identity_on is reinitialized at the end so several account can be
// created in a row.
fn create_one_identity<T: Config>(owner_key: T::AccountId) -> Result<Account<T>, &'static str> {
// get Alice account to create identity
let caller: T::AccountId = Identities::<T>::get(T::IdtyIndex::from(1u32))
.unwrap()
.owner_key;
let caller_origin: <T as frame_system::Config>::RuntimeOrigin =
RawOrigin::Signed(caller.clone()).into();
let owner_key_origin: <T as frame_system::Config>::RuntimeOrigin =
RawOrigin::Signed(owner_key.clone()).into();
T::CheckAccountWorthiness::set_worthy(&owner_key);
Pallet::<T>::create_identity(caller_origin.clone(), owner_key.clone())?;
let name = IdtyName("new_identity".into());
Pallet::<T>::confirm_identity(owner_key_origin.clone(), name.clone())?;
let idty_index = IdentityIndexOf::<T>::get(&owner_key).unwrap();
// make identity member
<Identities<T>>::mutate_exists(idty_index, |idty_val_opt| {
if let Some(ref mut idty_val) = idty_val_opt {
idty_val.status = IdtyStatus::Member;
}
});
// Reset next_creatable_identity_on to add more identities with Alice
<Identities<T>>::mutate_exists(T::IdtyIndex::from(1u32), |idty_val_opt| {
if let Some(ref mut idty_val) = idty_val_opt {
idty_val.next_creatable_identity_on = BlockNumberFor::<T>::zero();
}
});
Ok(Account {
key: owner_key,
index: idty_index,
origin: owner_key_origin,
// name: name,
})
}
// Create a dummy identity bypassing all the checks.
fn create_dummy_identity<T: Config>(i: u32) -> Result<(), &'static str> {
let idty_index: T::IdtyIndex = i.into();
let owner_key: T::AccountId = account("Bob", i, 1);
let next_scheduled = BlockNumberFor::<T>::zero();
let value = IdtyValue {
data: Default::default(),
next_creatable_identity_on: BlockNumberFor::<T>::zero(),
old_owner_key: None,
owner_key: owner_key.clone(),
next_scheduled,
status: IdtyStatus::Unvalidated,
};
let name = i.to_le_bytes();
let idty_name = IdtyName(name.into());
frame_system::Pallet::<T>::inc_sufficients(&owner_key);
<Identities<T>>::insert(idty_index, value);
IdentityChangeSchedule::<T>::append(next_scheduled, idty_index);
IdentityIndexOf::<T>::insert(owner_key.clone(), idty_index);
<IdentitiesNames<T>>::insert(idty_name.clone(), idty_index);
Ok(())
}
// Add `i` dummy identities.
fn create_identities<T: Config>(i: u32) -> Result<(), &'static str> {
let identities_count = Pallet::<T>::identities_count();
for j in 0..i {
create_dummy_identity::<T>(j + identities_count + 1)?;
}
assert!(
identities_count + i == Pallet::<T>::identities_count(),
"Identities not created"
);
Ok(())
}
#[benchmark]
fn create_identity() {
let caller: T::AccountId = Identities::<T>::get(T::IdtyIndex::one()).unwrap().owner_key; // Alice
let owner_key: T::AccountId = account("new_identity", 2, 1);
T::CheckAccountWorthiness::set_worthy(&owner_key);
#[extrinsic_call]
_(RawOrigin::Signed(caller), owner_key.clone());
let idty_index = IdentityIndexOf::<T>::get(&owner_key);
assert!(idty_index.is_some(), "Identity not added");
assert_has_event::<T>(
Event::<T>::IdtyCreated {
idty_index: idty_index.unwrap(),
owner_key,
}
.into(),
);
}
#[benchmark]
fn confirm_identity() -> Result<(), BenchmarkError> {
let caller: T::AccountId = Identities::<T>::get(T::IdtyIndex::one()).unwrap().owner_key;
let caller_origin: <T as frame_system::Config>::RuntimeOrigin =
RawOrigin::Signed(caller.clone()).into();
let owner_key: T::AccountId = account("new_identity", 2, 1);
T::CheckAccountWorthiness::set_worthy(&owner_key);
Pallet::<T>::create_identity(caller_origin.clone(), owner_key.clone())?;
#[extrinsic_call]
_(
RawOrigin::Signed(owner_key.clone()),
IdtyName("new_identity".into()),
);
let idty_index = IdentityIndexOf::<T>::get(&owner_key);
assert_has_event::<T>(
Event::<T>::IdtyConfirmed {
idty_index: idty_index.unwrap(),
name: IdtyName("new_identity".into()),
}
.into(),
);
Ok(())
}
#[benchmark]
fn change_owner_key() -> Result<(), BenchmarkError> {
let old_key: T::AccountId = account("new_identity", 2, 1);
let account: Account<T> = create_one_identity(old_key.clone())?;
// Change key a first time to add an old-old key
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: account.index,
old_owner_key: &account.key,
};
let message = (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode();
let caller_public = sr25519_generate(0.into(), None);
let caller: T::AccountId = MultiSigner::Sr25519(caller_public).into_account().into();
let signature = sr25519_sign(0.into(), &caller_public, &message)
.unwrap()
.into();
Pallet::<T>::change_owner_key(account.origin.clone(), caller.clone(), signature)?;
// Change key a second time to benchmark
// The sufficients for the old_old key will drop to 0 during benchmark
let caller_origin = RawOrigin::Signed(caller.clone());
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: account.index,
old_owner_key: &caller_public,
};
let message = (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode();
let caller_public = sr25519_generate(0.into(), None);
let caller: T::AccountId = MultiSigner::Sr25519(caller_public).into_account().into();
let signature = sr25519_sign(0.into(), &caller_public, &message)
.unwrap()
.into();
<frame_system::Pallet<T>>::set_block_number(
<frame_system::Pallet<T>>::block_number() + T::ChangeOwnerKeyPeriod::get(),
);
#[extrinsic_call]
_(caller_origin, caller.clone(), signature);
assert_has_event::<T>(
Event::<T>::IdtyChangedOwnerKey {
idty_index: account.index,
new_owner_key: caller.clone(),
}
.into(),
);
assert!(
IdentityIndexOf::<T>::get(&caller).unwrap() == account.index,
"Owner key not changed"
);
Ok(())
}
#[benchmark]
fn revoke_identity() -> Result<(), BenchmarkError> {
let old_key: T::AccountId = account("new_identity", 2, 1);
let account: Account<T> = create_one_identity(old_key.clone())?;
// Change key
// The sufficients for the old key will drop to 0 during benchmark (not for revoke, only for remove)
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: account.index,
old_owner_key: &account.key,
};
let message = (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode();
let caller_public = sr25519_generate(0.into(), None);
let caller: T::AccountId = MultiSigner::Sr25519(caller_public).into_account().into();
let signature = sr25519_sign(0.into(), &caller_public, &message)
.unwrap()
.into();
Pallet::<T>::change_owner_key(account.origin.clone(), caller.clone(), signature)?;
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let revocation_payload = RevocationPayload {
genesis_hash: &genesis_hash,
idty_index: account.index,
};
let message = (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode();
let signature = sr25519_sign(0.into(), &caller_public, &message)
.unwrap()
.into();
#[extrinsic_call]
_(
RawOrigin::Signed(account.key),
account.index,
caller.clone(),
signature,
);
assert_has_event::<T>(
Event::<T>::IdtyRevoked {
idty_index: account.index,
reason: RevocationReason::User,
}
.into(),
);
Ok(())
}
#[benchmark]
fn prune_item_identities_names(i: Linear<2, 1_000>) -> Result<(), BenchmarkError> {
// The complexity depends on the number of identities to prune
// Populate identities
let identities_count = Pallet::<T>::identities_count();
create_identities::<T>(i)?;
let mut names = Vec::<IdtyName>::new();
for k in 1..i {
let name: IdtyName = IdtyName((k + identities_count).to_le_bytes().into());
assert!(
IdentitiesNames::<T>::contains_key(&name),
"Name not existing"
);
names.push(name);
}
#[extrinsic_call]
_(RawOrigin::Root, names.clone());
for name in names {
assert!(!IdentitiesNames::<T>::contains_key(&name), "Name existing");
}
Ok(())
}
#[benchmark]
fn fix_sufficients() -> Result<(), BenchmarkError> {
let new_identity: T::AccountId = account("Bob", 2, 1);
let account: Account<T> = create_one_identity(new_identity)?;
let sufficient = frame_system::Pallet::<T>::sufficients(&account.key);
#[extrinsic_call]
_(RawOrigin::Root, account.key.clone(), true);
assert!(
sufficient < frame_system::Pallet::<T>::sufficients(&account.key),
"Sufficient not incremented"
);
Ok(())
}
#[benchmark]
fn link_account() -> Result<(), BenchmarkError> {
let alice_origin =
RawOrigin::Signed(Identities::<T>::get(T::IdtyIndex::one()).unwrap().owner_key);
let bob_public = sr25519_generate(0.into(), None);
let bob: T::AccountId = MultiSigner::Sr25519(bob_public).into_account().into();
frame_system::Pallet::<T>::inc_providers(&bob);
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let payload = (
LINK_IDTY_PAYLOAD_PREFIX,
genesis_hash,
T::IdtyIndex::one(),
bob.clone(),
)
.encode();
let signature = sr25519_sign(0.into(), &bob_public, &payload)
.unwrap()
.into();
#[extrinsic_call]
_(alice_origin, bob, signature);
Ok(())
}
#[benchmark]
fn on_initialize() {
// Base weight of an empty initialize
#[block]
{
Pallet::<T>::on_initialize(BlockNumberFor::<T>::zero());
}
}
#[benchmark]
fn do_revoke_identity_noop() {
let idty_index: T::IdtyIndex = 0u32.into();
assert!(Identities::<T>::get(idty_index).is_none());
#[block]
{
Pallet::<T>::do_revoke_identity(idty_index, RevocationReason::Root);
}
}
#[benchmark]
fn do_revoke_identity() {
let idty_index: T::IdtyIndex = 1u32.into();
let new_identity: T::AccountId = account("Bob", 2, 1);
assert!(Identities::<T>::get(idty_index).is_some());
Identities::<T>::mutate(idty_index, |id| {
if let Some(id) = id {
id.old_owner_key = Some((new_identity, BlockNumberFor::<T>::zero()));
}
});
assert!(Identities::<T>::get(idty_index)
.unwrap()
.old_owner_key
.is_some());
#[block]
{
Pallet::<T>::do_revoke_identity(idty_index, RevocationReason::Root);
}
assert_has_event::<T>(
Event::<T>::IdtyRevoked {
idty_index,
reason: RevocationReason::Root,
}
.into(),
);
}
#[benchmark]
fn do_remove_identity_noop() {
let idty_index: T::IdtyIndex = 0u32.into();
assert!(Identities::<T>::get(idty_index).is_none());
#[block]
{
Pallet::<T>::do_remove_identity(idty_index, RemovalReason::Revoked);
}
}
#[benchmark]
fn do_remove_identity() {
let idty_index: T::IdtyIndex = 1u32.into();
let new_identity: T::AccountId = account("Bob", 2, 1);
assert!(Identities::<T>::get(idty_index).is_some());
frame_system::Pallet::<T>::inc_sufficients(&new_identity);
Identities::<T>::mutate(idty_index, |id| {
if let Some(id) = id {
id.old_owner_key = Some((new_identity, BlockNumberFor::<T>::zero()));
}
});
assert!(Identities::<T>::get(idty_index)
.unwrap()
.old_owner_key
.is_some());
#[block]
{
Pallet::<T>::do_remove_identity(idty_index, RemovalReason::Revoked);
}
assert_has_event::<T>(
Event::<T>::IdtyRemoved {
idty_index,
reason: RemovalReason::Revoked,
}
.into(),
);
}
#[benchmark]
fn do_remove_identity_handler() {
let idty_index: T::IdtyIndex = 1u32.into();
let new_identity: T::AccountId = account("Bob", 2, 1);
assert!(Identities::<T>::get(idty_index).is_some());
frame_system::Pallet::<T>::inc_sufficients(&new_identity);
Identities::<T>::mutate(idty_index, |id| {
if let Some(id) = id {
id.old_owner_key = Some((new_identity, BlockNumberFor::<T>::zero()));
}
});
assert!(Identities::<T>::get(idty_index)
.unwrap()
.old_owner_key
.is_some());
#[block]
{
T::OnRemoveIdty::on_removed(&idty_index);
}
}
#[benchmark]
fn membership_removed() -> Result<(), BenchmarkError> {
let key: T::AccountId = account("new_identity", 2, 1);
let account: Account<T> = create_one_identity(key)?;
assert_eq!(
Identities::<T>::get(account.index).unwrap().status,
IdtyStatus::Member
);
#[block]
{
Pallet::<T>::membership_removed(account.index);
}
assert_eq!(
Identities::<T>::get(account.index).unwrap().status,
IdtyStatus::NotMember
);
Ok(())
}
#[benchmark]
fn prune_identities_noop() {
assert!(IdentityChangeSchedule::<T>::try_get(BlockNumberFor::<T>::zero()).is_err());
#[block]
{
Pallet::<T>::prune_identities(BlockNumberFor::<T>::zero());
}
}
#[benchmark]
fn prune_identities_none() {
let idty_index: T::IdtyIndex = 100u32.into();
IdentityChangeSchedule::<T>::append(BlockNumberFor::<T>::zero(), idty_index);
assert!(IdentityChangeSchedule::<T>::try_get(BlockNumberFor::<T>::zero()).is_ok());
assert!(<Identities<T>>::try_get(idty_index).is_err());
#[block]
{
Pallet::<T>::prune_identities(BlockNumberFor::<T>::zero());
}
}
#[benchmark]
fn prune_identities_err() -> Result<(), BenchmarkError> {
let idty_index: T::IdtyIndex = 100u32.into();
create_dummy_identity::<T>(100u32)?;
IdentityChangeSchedule::<T>::append(BlockNumberFor::<T>::zero(), idty_index);
#[block]
{
Pallet::<T>::prune_identities(BlockNumberFor::<T>::zero());
}
Ok(())
}
#[benchmark]
fn revoke_identity_legacy() -> Result<(), BenchmarkError> {
let caller_index = T::IdtyIndex::from(1u32);
let caller: T::AccountId = Identities::<T>::get(caller_index).unwrap().owner_key;
let idty_index: T::IdtyIndex = 102.into();
let owner_key: T::AccountId =
AccountId32::from_ss58check("5H2nLXGku46iztpqdRwsCAiP6vHZbShhKmSV4yyufQgEUFvV")
.unwrap()
.into();
let next_scheduled = BlockNumberFor::<T>::zero();
let value = IdtyValue {
data: Default::default(),
next_creatable_identity_on: BlockNumberFor::<T>::zero(),
old_owner_key: None,
owner_key: owner_key.clone(),
next_scheduled,
status: IdtyStatus::Member,
};
let name = "Charlie";
let idty_name = IdtyName(name.into());
frame_system::Pallet::<T>::inc_sufficients(&owner_key);
<Identities<T>>::insert(idty_index, value);
IdentityChangeSchedule::<T>::append(next_scheduled, idty_index);
IdentityIndexOf::<T>::insert(owner_key.clone(), idty_index);
<IdentitiesNames<T>>::insert(idty_name.clone(), idty_index);
let document = r"Version: 10
Type: Revocation
Currency: g1
Issuer: Fnf2xaxYdQpB4kU45DMLQ9Ey4bd6DtoebKJajRkLBUXm
IdtyUniqueID: Charlie
IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
IdtySignature: 7KUagcMiQw05rwbkBsRrnNqPRHu/Y5ukCLoAEpb/1tXAQsSNf2gRi1h5PWIGs9y/vHnFXvF5epKsOjA6X75vDg==
CfiG4xhcWS+/DgxY0xFIyOA9TVr4Im3XEXcCApNgXC+Ns9jy2yrNoC3NF8MCD63cZ8QTRfrr4Iv6n3leYCCcDQ==
";
#[extrinsic_call]
_(RawOrigin::Signed(caller), document.into());
assert_has_event::<T>(
Event::<T>::IdtyRevoked {
idty_index,
reason: RevocationReason::User,
}
.into(),
);
Ok(())
}
impl_benchmark_test_suite!(
Pallet,
// Create genesis identity Alice to test benchmark in mock
crate::mock::new_test_ext(crate::mock::IdentityConfig {
identities: vec![
crate::GenesisIdty {
index: 1,
name: crate::IdtyName::from("Alice"),
value: crate::IdtyValue {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: frame_benchmarking::account("Alice", 1, 1),
next_scheduled: 0,
status: crate::IdtyStatus::Member,
},
},
crate::GenesisIdty {
index: 2,
name: crate::IdtyName::from("Bob"),
value: crate::IdtyValue {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: frame_benchmarking::account("Bob", 1, 1),
next_scheduled: 0,
status: crate::IdtyStatus::Unconfirmed,
},
},
]
}),
crate::mock::Test,
);
}
// Copyright 2021 Axiom-Team
// Copyright 2021-2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
......@@ -14,11 +14,49 @@
// 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 Identity Pallet
//!
//! Duniter features a built-in identity system that does not rely on external registrars, unlike the [Parity Identity Pallet](https://github.com/paritytech/substrate/tree/master/frame/identity).
//!
//! ## Duniter Identity Structure
//!
//! A Duniter identity comprises several key components:
//!
//! ### Name
//!
//! Each identity is declared with a name emitted during the confirmation event. Duniter maintains a hashed list of identity names to ensure uniqueness.
//!
//! ### Owner Key
//!
//! The owner key allows users to maintain a fixed identity while changing keys for security reasons, such as when a device with the keys might have been compromised. Changes are subject to frequency limits, and the old owner key can still revoke the identity for a given period.
//!
//! ### Status / Removable Date
//!
//! The status is a temporary value that allows pruning of identities before they become full members:
//! - **Unconfirmed**: Created by a member identity but not yet confirmed by the owner.
//! - **Unvalidated**: Confirmed by the owner, including assignment of a name.
//! - **Member**: Part of the main Web of Trust (WoT).
//! - **NotMember**: Not part of the main WoT.
//! - **Revoked**: Automatically or manually revoked.
//!
//! An identity that is not yet validated (e.g., not a member of the WoT) can be removed when its removable date is reached. The removable date of a validated identity is set to block zero.
//!
//! ### Next Certification
//!
//! The next certification specifies the block number from which the identity can issue its next certification, acting as a rate limit for certification issuance and identity creation.
//!
//! ### Revocation
//!
//! Revoking an identity essentially means deleting it from the system.
//!
//! Additional runtime-defined data may also be attached to identities, such the number of the first Universal Dividends (UD) it is eligible to.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::type_complexity)]
pub mod traits;
mod types;
pub mod weights;
#[cfg(test)]
mod mock;
......@@ -26,34 +64,37 @@ mod mock;
#[cfg(test)]
mod tests;
/*#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;*/
pub mod benchmarking;
pub use pallet::*;
pub use types::*;
pub use weights::WeightInfo;
use crate::traits::*;
use codec::Codec;
use frame_support::dispatch::Weight;
use frame_support::pallet_prelude::Weight;
use scale_info::prelude::{collections::BTreeSet, fmt::Debug, vec::Vec};
use sp_runtime::traits::{AtLeast32BitUnsigned, IdentifyAccount, One, Saturating, Verify, Zero};
use sp_std::fmt::Debug;
use sp_std::prelude::*;
// icok = identity change owner key
pub const NEW_OWNER_KEY_PAYLOAD_PREFIX: [u8; 4] = [b'i', b'c', b'o', b'k'];
// revo = revocation
pub const REVOCATION_PAYLOAD_PREFIX: [u8; 4] = [b'r', b'e', b'v', b'o'];
// link = link (identity with account)
pub const LINK_IDTY_PAYLOAD_PREFIX: [u8; 4] = [b'l', b'i', b'n', b'k'];
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::traits::StorageVersion;
use base64::Engine;
use frame_support::{pallet_prelude::*, traits::StorageVersion};
use frame_system::pallet_prelude::*;
/// 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>(_);
......@@ -62,19 +103,44 @@ pub mod pallet {
#[pallet::config]
pub trait Config: frame_system::Config {
/// The way to deserialize accound id from 32 raw bytes
type AccountId32: From<[u8; 32]> + Into<Self::AccountId>;
/// The period during which the owner can confirm the new identity.
#[pallet::constant]
/// Period during which the owner can confirm the new identity.
type ConfirmPeriod: Get<Self::BlockNumber>;
type ConfirmPeriod: Get<BlockNumberFor<Self>>;
/// The period during which the identity has to be validated to become a member.
#[pallet::constant]
/// Minimum duration between two owner key changes
type ChangeOwnerKeyPeriod: Get<Self::BlockNumber>;
/// Management of the authorizations of the different calls.
/// The default implementation allows everything.
type CheckIdtyCallAllowed: CheckIdtyCallAllowed<Self>;
type ValidationPeriod: Get<BlockNumberFor<Self>>;
/// The period before which an identity that lost membership is automatically revoked.
#[pallet::constant]
type AutorevocationPeriod: Get<BlockNumberFor<Self>>;
/// The period after which a revoked identity is removed and the keys are freed.
#[pallet::constant]
/// Minimum duration between the creation of 2 identities by the same creator
type IdtyCreationPeriod: Get<Self::BlockNumber>;
/// Custom data to store in each identity
type DeletionPeriod: Get<BlockNumberFor<Self>>;
/// The minimum duration between two owner key changes to prevent identity theft.
#[pallet::constant]
type ChangeOwnerKeyPeriod: Get<BlockNumberFor<Self>>;
/// The minimum duration between the creation of two identities by the same creator.
/// Should be greater than or equal to the certification period defined in the certification pallet.
#[pallet::constant]
type IdtyCreationPeriod: Get<BlockNumberFor<Self>>;
/// Management of the authorizations of the different calls related to identity.
type CheckIdtyCallAllowed: CheckIdtyCallAllowed<Self>;
/// The type used to check account worthiness.
type CheckAccountWorthiness: CheckAccountWorthiness<Self>;
/// A handler called when an identity's owner key change.
type OnKeyChange: KeyChange<Self>;
/// Custom data to store in each identity.
type IdtyData: Clone
+ Codec
+ Default
......@@ -82,7 +148,8 @@ pub mod pallet {
+ TypeInfo
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
/// A short identity index.
/// A short identity index type.
type IdtyIndex: Parameter
+ Member
+ AtLeast32BitUnsigned
......@@ -92,33 +159,42 @@ pub mod pallet {
+ MaybeSerializeDeserialize
+ Debug
+ MaxEncodedLen;
/// Handle logic to validate an identity name
/// A type for linking account data to identity.
type AccountLinker: LinkIdty<Self::AccountId, Self::IdtyIndex>;
/// Handle logic to validate an identity name.
type IdtyNameValidator: IdtyNameValidator;
/// On identity confirmed by its owner
type OnIdtyChange: OnIdtyChange<Self>;
/// Signing key of new owner key payload
type NewOwnerKeySigner: IdentifyAccount<AccountId = Self::AccountId>;
/// Signature of new owner key payload
type NewOwnerKeySignature: Parameter + Verify<Signer = Self::NewOwnerKeySigner>;
/// Handle the logic that removes all identity consumers.
/// "identity consumers" meaning all things that rely on the existence of the identity.
type RemoveIdentityConsumers: RemoveIdentityConsumers<Self::IdtyIndex>;
/// Signing key of revocation payload
type RevocationSigner: IdentifyAccount<AccountId = Self::AccountId>;
/// Signature of revocation payload
type RevocationSignature: Parameter + Verify<Signer = Self::RevocationSigner>;
/// Because this pallet emits events, it depends on the runtime's definition of an event.
/// Handler called when a new identity is created.
type OnNewIdty: OnNewIdty<Self>;
/// Handler called when an identity is removed.
type OnRemoveIdty: OnRemoveIdty<Self>;
/// Signing key type used for payload signatures.
type Signer: IdentifyAccount<AccountId = Self::AccountId>;
/// Signature type for payload verification.
type Signature: Parameter
+ Verify<Signer = Self::Signer>
+ From<sp_core::ed25519::Signature>;
/// 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;
}
// GENESIS STUFF //
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
#[derive(Encode, Decode, Clone, PartialEq, Eq)]
#[derive(Encode, Decode, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))]
pub struct GenesisIdty<T: Config> {
pub index: T::IdtyIndex,
pub name: IdtyName,
pub value: IdtyValue<T::BlockNumber, T::AccountId, T::IdtyData>,
pub value: IdtyValue<BlockNumberFor<T>, T::AccountId, T::IdtyData>,
}
#[pallet::genesis_config]
......@@ -126,7 +202,6 @@ pub mod pallet {
pub identities: Vec<GenesisIdty<T>>,
}
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
......@@ -136,16 +211,15 @@ pub mod pallet {
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
let mut names = sp_std::collections::btree_set::BTreeSet::new();
let mut names = BTreeSet::new();
for idty in &self.identities {
assert!(
!names.contains(&idty.name),
"Idty name {:?} is present twice",
&idty.name
);
assert!(idty.value.removable_on == T::BlockNumber::zero());
names.insert(idty.name.clone());
}
......@@ -153,59 +227,69 @@ pub mod pallet {
identities.sort_unstable_by(|a, b| a.index.cmp(&b.index));
for idty in identities.into_iter() {
let idty_index = Pallet::<T>::get_next_idty_index();
if idty.value.removable_on > T::BlockNumber::zero() {
<IdentitiesRemovableOn<T>>::append(
idty.value.removable_on,
(idty_index, idty.value.status),
)
// if get_next_idty_index() is not called
// then NextIdtyIndex is not incremented
let _ = Pallet::<T>::get_next_idty_index();
// use instead custom provided index
let idty_index = idty.index;
if idty.value.next_scheduled > BlockNumberFor::<T>::zero() {
<IdentityChangeSchedule<T>>::append(idty.value.next_scheduled, idty_index)
}
<Identities<T>>::insert(idty_index, idty.value.clone());
IdentitiesNames::<T>::insert(idty.name.clone(), ());
IdentityIndexOf::<T>::insert(idty.value.owner_key, idty_index);
IdentitiesNames::<T>::insert(idty.name.clone(), idty_index);
IdentityIndexOf::<T>::insert(&idty.value.owner_key, idty_index);
frame_system::Pallet::<T>::inc_sufficients(&idty.value.owner_key);
if let Some((old_owner_key, _last_change)) = idty.value.old_owner_key {
frame_system::Pallet::<T>::inc_sufficients(&old_owner_key);
}
}
}
}
// STORAGE //
/// The identity value for each identity.
#[pallet::storage]
#[pallet::getter(fn identity)]
pub type Identities<T: Config> = CountedStorageMap<
_,
Twox64Concat,
T::IdtyIndex,
IdtyValue<T::BlockNumber, T::AccountId, T::IdtyData>,
IdtyValue<BlockNumberFor<T>, T::AccountId, T::IdtyData>,
OptionQuery,
>;
/// The identity associated with each account.
#[pallet::storage]
#[pallet::getter(fn identity_index_of)]
pub type IdentityIndexOf<T: Config> =
StorageMap<_, Blake2_128, T::AccountId, T::IdtyIndex, OptionQuery>;
StorageMap<_, Blake2_128Concat, T::AccountId, T::IdtyIndex, OptionQuery>;
/// The name associated with each identity.
#[pallet::storage]
#[pallet::getter(fn identity_by_did)]
pub type IdentitiesNames<T: Config> = StorageMap<_, Blake2_128, IdtyName, (), OptionQuery>;
pub type IdentitiesNames<T: Config> =
StorageMap<_, Blake2_128Concat, IdtyName, T::IdtyIndex, OptionQuery>;
/// The identity index to assign to the next created identity.
#[pallet::storage]
pub(super) type NextIdtyIndex<T: Config> = StorageValue<_, T::IdtyIndex, ValueQuery>;
/// Identities by removed block
/// The identities to remove at a given block.
#[pallet::storage]
#[pallet::getter(fn removable_on)]
pub type IdentitiesRemovableOn<T: Config> =
StorageMap<_, Twox64Concat, T::BlockNumber, Vec<(T::IdtyIndex, IdtyStatus)>, ValueQuery>;
#[pallet::getter(fn next_scheduled)]
pub type IdentityChangeSchedule<T: Config> =
StorageMap<_, Twox64Concat, BlockNumberFor<T>, Vec<T::IdtyIndex>, ValueQuery>;
// HOOKS //
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: T::BlockNumber) -> Weight {
if n > T::BlockNumber::zero() {
Self::prune_identities(n)
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
if n > BlockNumberFor::<T>::zero() {
Self::prune_identities(n).saturating_add(T::WeightInfo::on_initialize())
} else {
Weight::zero()
T::WeightInfo::on_initialize()
}
}
}
......@@ -217,29 +301,32 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A new identity has been created
/// [idty_index, owner_key]
/// A new identity has been created.
IdtyCreated {
idty_index: T::IdtyIndex,
owner_key: T::AccountId,
},
/// An identity has been confirmed by its owner
/// [idty_index, owner_key, name]
/// An identity has been confirmed by its owner.
IdtyConfirmed {
idty_index: T::IdtyIndex,
owner_key: T::AccountId,
name: IdtyName,
},
/// An identity has been validated
/// [idty_index]
/// An identity has been validated.
IdtyValidated { idty_index: T::IdtyIndex },
IdtyChangedOwnerKey {
idty_index: T::IdtyIndex,
new_owner_key: T::AccountId,
},
/// An identity has been removed
/// [idty_index]
IdtyRemoved { idty_index: T::IdtyIndex },
/// An identity has been revoked.
IdtyRevoked {
idty_index: T::IdtyIndex,
reason: RevocationReason,
},
/// An identity has been removed.
IdtyRemoved {
idty_index: T::IdtyIndex,
reason: RemovalReason,
},
}
// CALLS //
......@@ -254,7 +341,8 @@ pub mod pallet {
/// - `owner_key`: the public key corresponding to the identity to be created
///
/// The origin must be allowed to create an identity.
#[pallet::weight(1_000_000_000)]
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::create_identity())]
pub fn create_identity(
origin: OriginFor<T>,
owner_key: T::AccountId,
......@@ -262,62 +350,51 @@ pub mod pallet {
// Verification phase //
let who = ensure_signed(origin)?;
let creator =
IdentityIndexOf::<T>::try_get(&who).map_err(|_| Error::<T>::IdtyIndexNotFound)?;
let creator_idty_val =
Identities::<T>::try_get(creator).map_err(|_| Error::<T>::IdtyNotFound)?;
if IdentityIndexOf::<T>::contains_key(&owner_key) {
return Err(Error::<T>::IdtyAlreadyCreated.into());
}
// run checks for identity creation
T::CheckIdtyCallAllowed::check_create_identity(creator)?;
let block_number = frame_system::pallet::Pallet::<T>::block_number();
if creator_idty_val.next_creatable_identity_on > block_number {
return Err(Error::<T>::NotRespectIdtyCreationPeriod.into());
}
let creator_index = Self::check_create_identity(&who, &owner_key, block_number)?;
// Apply phase //
frame_system::Pallet::<T>::inc_sufficients(&owner_key);
<Identities<T>>::mutate_exists(creator, |idty_val_opt| {
<Identities<T>>::mutate_exists(creator_index, |idty_val_opt| {
if let Some(ref mut idty_val) = idty_val_opt {
idty_val.next_creatable_identity_on =
block_number + T::IdtyCreationPeriod::get();
}
});
let removable_on = block_number + T::ConfirmPeriod::get();
let idty_index = Self::get_next_idty_index();
<Identities<T>>::insert(
idty_index,
IdtyValue {
data: Default::default(),
next_creatable_identity_on: T::BlockNumber::zero(),
next_creatable_identity_on: BlockNumberFor::<T>::zero(),
old_owner_key: None,
owner_key: owner_key.clone(),
removable_on,
status: IdtyStatus::Created,
next_scheduled: Self::schedule_identity_change(
idty_index,
T::ConfirmPeriod::get(),
),
status: IdtyStatus::Unconfirmed,
},
);
IdentitiesRemovableOn::<T>::append(removable_on, (idty_index, IdtyStatus::Created));
IdentityIndexOf::<T>::insert(owner_key.clone(), idty_index);
Self::deposit_event(Event::IdtyCreated {
idty_index,
owner_key,
owner_key: owner_key.clone(),
});
T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Created { creator });
T::AccountLinker::link_identity(&owner_key, idty_index)?;
T::OnNewIdty::on_created(&idty_index, &creator_index);
Ok(().into())
}
/// Confirm the creation of an identity and give it a name
///
/// - `idty_name`: the name uniquely associated to this identity. Must match the validation rules defined by the runtime.
///
/// The identity must have been created using `create_identity` before it can be confirmed.
#[pallet::weight(1_000_000_000)]
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::confirm_identity())]
pub fn confirm_identity(
origin: OriginFor<T>,
idty_name: IdtyName,
......@@ -328,10 +405,10 @@ pub mod pallet {
let idty_index =
IdentityIndexOf::<T>::try_get(&who).map_err(|_| Error::<T>::IdtyIndexNotFound)?;
let mut idty_value =
let idty_value =
Identities::<T>::try_get(idty_index).map_err(|_| Error::<T>::IdtyNotFound)?;
if idty_value.status != IdtyStatus::Created {
if idty_value.status != IdtyStatus::Unconfirmed {
return Err(Error::<T>::IdtyAlreadyConfirmed.into());
}
if !T::IdtyNameValidator::validate(&idty_name) {
......@@ -340,63 +417,36 @@ pub mod pallet {
if <IdentitiesNames<T>>::contains_key(&idty_name) {
return Err(Error::<T>::IdtyNameAlreadyExist.into());
}
T::CheckIdtyCallAllowed::check_confirm_identity(idty_index)?;
// Apply phase //
idty_value.status = IdtyStatus::ConfirmedByOwner;
Self::update_identity_status(
idty_index,
idty_value,
IdtyStatus::Unvalidated,
T::ValidationPeriod::get(),
);
<Identities<T>>::insert(idty_index, idty_value);
<IdentitiesNames<T>>::insert(idty_name.clone(), ());
<IdentitiesNames<T>>::insert(idty_name.clone(), idty_index);
Self::deposit_event(Event::IdtyConfirmed {
idty_index,
owner_key: who,
name: idty_name,
});
T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Confirmed);
Ok(().into())
}
#[pallet::weight(1_000_000_000)]
pub fn validate_identity(
origin: OriginFor<T>,
idty_index: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
// Verification phase //
let _ = ensure_signed(origin)?;
let mut idty_value =
Identities::<T>::try_get(idty_index).map_err(|_| Error::<T>::IdtyNotFound)?;
match idty_value.status {
IdtyStatus::Created => return Err(Error::<T>::IdtyNotConfirmedByOwner.into()),
IdtyStatus::ConfirmedByOwner => {
T::CheckIdtyCallAllowed::check_validate_identity(idty_index)?;
}
IdtyStatus::Validated => return Err(Error::<T>::IdtyAlreadyValidated.into()),
}
// Apply phase //
idty_value.removable_on = T::BlockNumber::zero();
idty_value.status = IdtyStatus::Validated;
<Identities<T>>::insert(idty_index, idty_value);
Self::deposit_event(Event::IdtyValidated { idty_index });
T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Validated);
Ok(().into())
}
/// Change identity owner key.
///
/// - `new_key`: the new owner key.
/// - `new_key_sig`: the signature of the encoded form of `NewOwnerKeyPayload`.
/// - `new_key_sig`: the signature of the encoded form of `IdtyIndexAccountIdPayload`.
/// Must be signed by `new_key`.
///
/// The origin should be the old identity owner key.
#[pallet::weight(1_000_000_000)]
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::change_owner_key())]
pub fn change_owner_key(
origin: OriginFor<T>,
new_key: T::AccountId,
new_key_sig: T::NewOwnerKeySignature,
new_key_sig: T::Signature,
) -> DispatchResultWithPostInfo {
// verification phase
let who = ensure_signed(origin)?;
......@@ -411,8 +461,6 @@ pub mod pallet {
Error::<T>::OwnerKeyAlreadyUsed
);
T::CheckIdtyCallAllowed::check_change_identity_address(idty_index)?;
let block_number = frame_system::Pallet::<T>::block_number();
let maybe_old_old_owner_key =
if let Some((old_owner_key, last_change)) = idty_value.old_owner_key {
......@@ -429,8 +477,8 @@ pub mod pallet {
None
};
let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
let new_key_payload = NewOwnerKeyPayload {
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index,
old_owner_key: &idty_value.owner_key,
......@@ -439,7 +487,7 @@ pub mod pallet {
ensure!(
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload)
.using_encoded(|bytes| new_key_sig.verify(bytes, &new_key)),
Error::<T>::InvalidNewOwnerKeySig
Error::<T>::InvalidSignature
);
// Apply phase
......@@ -453,16 +501,12 @@ pub mod pallet {
frame_system::Pallet::<T>::inc_sufficients(&idty_value.owner_key);
IdentityIndexOf::<T>::insert(&idty_value.owner_key, idty_index);
Identities::<T>::insert(idty_index, idty_value);
T::AccountLinker::link_identity(&new_key, idty_index)?;
T::OnKeyChange::on_changed(idty_index, new_key.clone())?;
Self::deposit_event(Event::IdtyChangedOwnerKey {
idty_index,
new_owner_key: new_key.clone(),
});
T::OnIdtyChange::on_idty_change(
idty_index,
&IdtyEvent::ChangedOwnerKey {
new_owner_key: new_key,
},
);
});
Ok(().into())
}
......@@ -475,19 +519,29 @@ pub mod pallet {
/// Must be signed by `revocation_key`.
///
/// Any signed origin can execute this call.
#[pallet::weight(1_000_000_000)]
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::revoke_identity())]
pub fn revoke_identity(
origin: OriginFor<T>,
idty_index: T::IdtyIndex,
revocation_key: T::AccountId,
revocation_sig: T::RevocationSignature,
revocation_sig: T::Signature,
) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
let idty_value = Identities::<T>::get(idty_index).ok_or(Error::<T>::IdtyNotFound)?;
match idty_value.status {
IdtyStatus::Unconfirmed => Err(Error::<T>::CanNotRevokeUnconfirmed),
IdtyStatus::Unvalidated => Err(Error::<T>::CanNotRevokeUnvalidated),
IdtyStatus::Member => Ok(()),
IdtyStatus::NotMember => Ok(()),
IdtyStatus::Revoked => Err(Error::<T>::AlreadyRevoked),
}?;
ensure!(
if let Some((ref old_owner_key, last_change)) = idty_value.old_owner_key {
// old owner key can also revoke the identity until the period expired
revocation_key == idty_value.owner_key
|| (&revocation_key == old_owner_key
&& frame_system::Pallet::<T>::block_number()
......@@ -498,9 +552,8 @@ pub mod pallet {
Error::<T>::InvalidRevocationKey
);
T::CheckIdtyCallAllowed::check_remove_identity(idty_index)?;
let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
// then check payload signature
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let revocation_payload = RevocationPayload {
genesis_hash,
idty_index,
......@@ -509,30 +562,134 @@ pub mod pallet {
ensure!(
(REVOCATION_PAYLOAD_PREFIX, revocation_payload)
.using_encoded(|bytes| revocation_sig.verify(bytes, &revocation_key)),
Error::<T>::InvalidRevocationSig
Error::<T>::InvalidSignature
);
Self::do_remove_identity(idty_index);
// finally if all checks pass, remove identity
Self::do_revoke_identity(idty_index, RevocationReason::User);
Ok(().into())
}
#[pallet::weight(1_000_000_000)]
pub fn remove_identity(
/// Revoke an identity using a legacy (DUBP) revocation document
///
/// - `revocation document`: the full-length revocation document, signature included
///
/// Any signed origin can execute this call.
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::revoke_identity_legacy())]
pub fn revoke_identity_legacy(
origin: OriginFor<T>,
idty_index: T::IdtyIndex,
idty_name: Option<IdtyName>,
revocation_document: Vec<u8>,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
let _ = ensure_signed(origin)?;
Self::do_remove_identity(idty_index);
if let Some(idty_name) = idty_name {
<IdentitiesNames<T>>::remove(idty_name);
}
// Strip possible Unicode magic number that is not part of the protocol
let revocation_document = revocation_document
.strip_prefix(b"\xef\xbb\xbf")
.unwrap_or(&revocation_document);
let mut lines = revocation_document.split(|b| *b == b'\n');
ensure!(
lines.next() == Some(b"Version: 10"),
Error::<T>::InvalidLegacyRevocationFormat
);
ensure!(
lines.next() == Some(b"Type: Revocation"),
Error::<T>::InvalidLegacyRevocationFormat
);
ensure!(
lines.next() == Some(b"Currency: g1"),
Error::<T>::InvalidLegacyRevocationFormat
);
let line_issuer = lines
.next()
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
let line_username = lines
.next()
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
let _line_blockstamp = lines
.next()
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
let _line_idty_signature = lines
.next()
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
let line_signature = lines
.next()
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
ensure!(
lines.next() == Some(b""),
Error::<T>::InvalidLegacyRevocationFormat
);
ensure!(
lines.next().is_none(),
Error::<T>::InvalidLegacyRevocationFormat
);
let document = revocation_document
.get(0..revocation_document.len().saturating_sub(89))
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
let mut signature = [0; 64];
base64::prelude::BASE64_STANDARD
.decode_slice(line_signature, &mut signature)
.map_err(|_| Error::<T>::InvalidLegacyRevocationFormat)?;
let issuer = bs58::decode(
line_issuer
.get(8..)
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?,
)
.into_array_const::<32>()
.map_err(|_| Error::<T>::InvalidLegacyRevocationFormat)?;
// Verify signature
let revocation_key = T::AccountId32::from(issuer).into();
let signature = T::Signature::from(sp_core::ed25519::Signature::from(signature));
ensure!(
signature.verify(document, &revocation_key),
Error::<T>::InvalidSignature
);
let username = line_username
.get(14..)
.ok_or(Error::<T>::InvalidLegacyRevocationFormat)?;
let idty_index = <IdentitiesNames<T>>::get(IdtyName(username.into()))
.ok_or(Error::<T>::IdtyNotFound)?;
let idty_value = Identities::<T>::get(idty_index).ok_or(Error::<T>::IdtyNotFound)?;
match idty_value.status {
IdtyStatus::Unconfirmed => Err(Error::<T>::CanNotRevokeUnconfirmed),
IdtyStatus::Unvalidated => Err(Error::<T>::CanNotRevokeUnvalidated),
IdtyStatus::Member => Ok(()),
IdtyStatus::NotMember => Ok(()),
IdtyStatus::Revoked => Err(Error::<T>::AlreadyRevoked),
}?;
ensure!(
if let Some((ref old_owner_key, last_change)) = idty_value.old_owner_key {
// old owner key can also revoke the identity until the period expired
revocation_key == idty_value.owner_key
|| (&revocation_key == old_owner_key
&& frame_system::Pallet::<T>::block_number()
< last_change + T::ChangeOwnerKeyPeriod::get())
} else {
revocation_key == idty_value.owner_key
},
Error::<T>::InvalidRevocationKey
);
// finally if all checks pass, remove identity
Self::do_revoke_identity(idty_index, RevocationReason::User);
Ok(().into())
}
#[pallet::weight(1_000_000_000)]
/// Remove identity names from storage.
///
/// This function allows a privileged root origin to remove multiple identity names from storage
/// in bulk.
///
/// - `origin` - The origin of the call. It must be root.
/// - `names` - A vector containing the identity names to be removed from storage.
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::prune_item_identities_names(names.len() as u32))]
pub fn prune_item_identities_names(
origin: OriginFor<T>,
names: Vec<IdtyName>,
......@@ -546,7 +703,17 @@ pub mod pallet {
Ok(().into())
}
#[pallet::weight(1_000_000_000)]
/// Change sufficient reference count for a given key.
///
/// This function allows a privileged root origin to increment or decrement the sufficient
/// reference count associated with a specified owner key.
///
/// - `origin` - The origin of the call. It must be root.
/// - `owner_key` - The account whose sufficient reference count will be modified.
/// - `inc` - A boolean indicating whether to increment (`true`) or decrement (`false`) the count.
///
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::fix_sufficients())]
pub fn fix_sufficients(
origin: OriginFor<T>,
owner_key: T::AccountId,
......@@ -562,89 +729,188 @@ pub mod pallet {
Ok(().into())
}
/// Link an account to an identity.
///
/// This function links a specified account to an identity, requiring both the account and the
/// identity to sign the operation.
///
/// - `origin` - The origin of the call, which must have an associated identity index.
/// - `account_id` - The account ID to link, which must sign the payload.
/// - `payload_sig` - The signature with the linked identity.
// can be used for quota system
// re-uses new owner key payload for simplicity
// with other custom prefix
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::link_account())]
pub fn link_account(
origin: OriginFor<T>,
account_id: T::AccountId,
payload_sig: T::Signature,
) -> DispatchResultWithPostInfo {
// verif
let who = ensure_signed(origin)?;
let idty_index =
IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::IdtyIndexNotFound)?;
let genesis_hash = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
let payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index,
old_owner_key: &account_id,
};
ensure!(
(LINK_IDTY_PAYLOAD_PREFIX, payload)
.using_encoded(|bytes| payload_sig.verify(bytes, &account_id)),
Error::<T>::InvalidSignature
);
// apply
T::AccountLinker::link_identity(&account_id, idty_index)?;
Ok(().into())
}
}
// ERRORS //
#[pallet::error]
pub enum Error<T> {
/// Identity already confirmed
/// Identity already confirmed.
IdtyAlreadyConfirmed,
/// Identity already created
/// Identity already created.
IdtyAlreadyCreated,
/// Identity already validated
IdtyAlreadyValidated,
/// You are not allowed to create a new identity now
IdtyCreationNotAllowed,
/// Identity index not found
/// Identity index not found.
IdtyIndexNotFound,
/// Identity name already exists
/// Identity name already exists.
IdtyNameAlreadyExist,
/// Invalid identity name
/// Invalid identity name.
IdtyNameInvalid,
/// Identity not confirmed by its owner
IdtyNotConfirmedByOwner,
/// Identity not found
/// Identity not found.
IdtyNotFound,
/// Identity not member
IdtyNotMember,
/// Identity not validated
IdtyNotValidated,
/// Identity not yet renewable
IdtyNotYetRenewable,
/// New owner key payload signature is invalid
InvalidNewOwnerKeySig,
/// Revocation key is invalid
/// Invalid payload signature.
InvalidSignature,
/// Key used as validator.
OwnerKeyUsedAsValidator,
/// Key in bound period.
OwnerKeyInBound,
/// Invalid revocation key.
InvalidRevocationKey,
/// Revocation payload signature is invalid
InvalidRevocationSig,
/// Identity creation period is not respected
/// Issuer is not member and can not perform this action.
IssuerNotMember,
/// Identity creation period is not respected.
NotRespectIdtyCreationPeriod,
/// Not the same identity name
NotSameIdtyName,
/// Owner key already recently changed
/// Owner key already changed recently.
OwnerKeyAlreadyRecentlyChanged,
/// Owner key already used
/// Owner key already used.
OwnerKeyAlreadyUsed,
/// Prohibited to revert to an old key
/// Reverting to an old key is prohibited.
ProhibitedToRevertToAnOldKey,
/// Right already added
RightAlreadyAdded,
/// Right does not exist
RightNotExist,
/// Already revoked.
AlreadyRevoked,
/// Can not revoke identity that never was member.
CanNotRevokeUnconfirmed,
/// Can not revoke identity that never was member.
CanNotRevokeUnvalidated,
/// Cannot link to an inexisting account.
AccountNotExist,
/// Insufficient balance to create an identity.
InsufficientBalance,
/// Legacy revocation document format is invalid
InvalidLegacyRevocationFormat,
}
// PUBLIC FUNCTIONS //
// INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> {
/// Get the number of identities.
pub fn identities_count() -> u32 {
Identities::<T>::count()
}
/// Handle the addition of membership to an identity.
///
/// This function is called when an identity transitions to a member status. It updates
/// the identity's status, unschedules any pending identity change actions, and resets
/// the identity's next scheduled action to zero.
pub fn membership_added(idty_index: T::IdtyIndex) {
if let Some(mut idty_value) = Identities::<T>::get(idty_index) {
Self::unschedule_identity_change(idty_index, idty_value.next_scheduled);
idty_value.next_scheduled = BlockNumberFor::<T>::zero();
if idty_value.status == IdtyStatus::Unvalidated {
// only submit event first time, after that, only membership events are relevant
Self::deposit_event(Event::IdtyValidated { idty_index });
};
idty_value.status = IdtyStatus::Member;
<Identities<T>>::insert(idty_index, idty_value);
}
// else should not happen
}
// INTERNAL FUNCTIONS //
/// Handle the removal of membership from an identity.
///
/// This function is called when membership is revoked from an identity. It checks
/// if the identity is currently a member, and if so, updates its status to `NotMember`.
/// If the identity is already revoked, this function does nothing.
pub fn membership_removed(idty_index: T::IdtyIndex) -> Weight {
if let Some(idty_value) = Identities::<T>::get(idty_index) {
if idty_value.status == IdtyStatus::Member {
Self::update_identity_status(
idty_index,
idty_value,
IdtyStatus::NotMember,
T::AutorevocationPeriod::get(),
);
}
}
T::WeightInfo::membership_removed()
// else should not happen
}
impl<T: Config> Pallet<T> {
pub(super) fn do_remove_identity(idty_index: T::IdtyIndex) -> Weight {
if let Some(idty_val) = Identities::<T>::get(idty_index) {
let _ = T::RemoveIdentityConsumers::remove_idty_consumers(idty_index);
IdentityIndexOf::<T>::remove(&idty_val.owner_key);
/// Perform the removal of an identity.
///
/// This function acts as a garbage collector for identities. It should not be called
/// while the identity is still a member; otherwise, there will still be a membership
/// in storage, but no more identity.
pub fn do_remove_identity(idty_index: T::IdtyIndex, reason: RemovalReason) -> Weight {
if let Some(idty_value) = Identities::<T>::get(idty_index) {
// this line allows the owner key to be used after that
IdentityIndexOf::<T>::remove(&idty_value.owner_key);
// Identity should be removed after the consumers of the identity
Identities::<T>::remove(idty_index);
frame_system::Pallet::<T>::dec_sufficients(&idty_val.owner_key);
if let Some((old_owner_key, _last_change)) = idty_val.old_owner_key {
frame_system::Pallet::<T>::dec_sufficients(&idty_value.owner_key);
if let Some((old_owner_key, _last_change)) = idty_value.old_owner_key {
frame_system::Pallet::<T>::dec_sufficients(&old_owner_key);
}
Self::deposit_event(Event::IdtyRemoved { idty_index });
T::OnIdtyChange::on_idty_change(
Self::deposit_event(Event::IdtyRemoved { idty_index, reason });
let weight = T::OnRemoveIdty::on_removed(&idty_index);
return weight.saturating_add(
T::WeightInfo::do_remove_identity()
.saturating_sub(T::WeightInfo::do_remove_identity_handler()),
);
}
T::WeightInfo::do_remove_identity_noop()
}
/// Revoke an identity.
///
/// This function revokes an identity, updating its status to `Revoked` and scheduling
/// it for removal after the specified deletion period.
pub fn do_revoke_identity(idty_index: T::IdtyIndex, reason: RevocationReason) -> Weight {
if let Some(idty_value) = Identities::<T>::get(idty_index) {
Self::update_identity_status(
idty_index,
&IdtyEvent::Removed {
status: idty_val.status,
},
idty_value,
IdtyStatus::Revoked,
T::DeletionPeriod::get(),
);
Self::deposit_event(Event::IdtyRevoked { idty_index, reason });
T::OnRemoveIdty::on_revoked(&idty_index);
return T::WeightInfo::do_revoke_identity();
}
Weight::zero()
T::WeightInfo::do_revoke_identity_noop()
}
/// incremental counter for identity index
fn get_next_idty_index() -> T::IdtyIndex {
if let Ok(next_index) = <NextIdtyIndex<T>>::try_get() {
<NextIdtyIndex<T>>::put(next_index.saturating_add(T::IdtyIndex::one()));
......@@ -654,32 +920,158 @@ pub mod pallet {
T::IdtyIndex::one()
}
}
fn prune_identities(block_number: T::BlockNumber) -> Weight {
/// Prune identities planned for removal at the given block number.
pub fn prune_identities(block_number: BlockNumberFor<T>) -> Weight {
let mut total_weight = Weight::zero();
for (idty_index, idty_status) in IdentitiesRemovableOn::<T>::take(block_number) {
for idty_index in IdentityChangeSchedule::<T>::take(block_number) {
if let Ok(idty_val) = <Identities<T>>::try_get(idty_index) {
if idty_val.removable_on == block_number && idty_val.status == idty_status {
total_weight += Self::do_remove_identity(idty_index)
if idty_val.next_scheduled == block_number {
match idty_val.status {
IdtyStatus::Unconfirmed => {
total_weight =
total_weight.saturating_add(Self::do_remove_identity(
idty_index,
RemovalReason::Unconfirmed,
));
}
IdtyStatus::Unvalidated => {
total_weight =
total_weight.saturating_add(Self::do_remove_identity(
idty_index,
RemovalReason::Unvalidated,
));
}
IdtyStatus::Revoked => {
total_weight = total_weight.saturating_add(
Self::do_remove_identity(idty_index, RemovalReason::Revoked),
);
}
IdtyStatus::NotMember => {
total_weight = total_weight.saturating_add(
Self::do_revoke_identity(idty_index, RevocationReason::Expired),
);
}
IdtyStatus::Member => { // do not touch identities of member accounts
// this should not happen
}
}
} else {
total_weight = total_weight.saturating_add(
T::WeightInfo::prune_identities_err()
.saturating_sub(T::WeightInfo::prune_identities_none()),
);
}
} else {
total_weight = total_weight.saturating_add(
T::WeightInfo::prune_identities_none()
.saturating_sub(T::WeightInfo::prune_identities_noop()),
);
}
}
total_weight.saturating_add(T::WeightInfo::prune_identities_noop())
}
total_weight
/// Change the identity status and reschedule the next action accordingly.
fn update_identity_status(
idty_index: T::IdtyIndex,
mut idty_value: IdtyValue<BlockNumberFor<T>, T::AccountId, T::IdtyData>,
new_status: IdtyStatus,
period: BlockNumberFor<T>,
) {
Self::unschedule_identity_change(idty_index, idty_value.next_scheduled);
idty_value.next_scheduled = Self::schedule_identity_change(idty_index, period);
idty_value.status = new_status;
<Identities<T>>::insert(idty_index, idty_value);
}
/// Unschedules the change related to an identity.
fn unschedule_identity_change(idty_id: T::IdtyIndex, block_number: BlockNumberFor<T>) {
let mut scheduled = IdentityChangeSchedule::<T>::get(block_number);
if let Some(pos) = scheduled.iter().position(|x| *x == idty_id) {
scheduled.swap_remove(pos);
IdentityChangeSchedule::<T>::set(block_number, scheduled);
}
}
/// Schedule an identity change after a specified period.
fn schedule_identity_change(
idty_id: T::IdtyIndex,
period: BlockNumberFor<T>,
) -> BlockNumberFor<T> {
let block_number = frame_system::pallet::Pallet::<T>::block_number();
let next_scheduled = block_number + period;
IdentityChangeSchedule::<T>::append(next_scheduled, idty_id);
next_scheduled
}
/// Check if creating an identity is allowed.
// first internal checks
// then other pallet checks trough trait
fn check_create_identity(
issuer_key: &T::AccountId,
receiver_key: &T::AccountId,
block_number: BlockNumberFor<T>,
) -> Result<T::IdtyIndex, DispatchError> {
// first get issuer details
let creator_index = IdentityIndexOf::<T>::try_get(issuer_key)
.map_err(|_| Error::<T>::IdtyIndexNotFound)?;
let creator_idty_val =
Identities::<T>::try_get(creator_index).map_err(|_| Error::<T>::IdtyNotFound)?;
// --- some checks can be done internally
// 1. issuer is member
ensure!(
creator_idty_val.status == IdtyStatus::Member,
Error::<T>::IssuerNotMember
);
// 2. issuer respects identity creation period
ensure!(
creator_idty_val.next_creatable_identity_on <= block_number,
Error::<T>::NotRespectIdtyCreationPeriod
);
// 3. receiver key is not already used by another identity
ensure!(
!IdentityIndexOf::<T>::contains_key(receiver_key),
Error::<T>::IdtyAlreadyCreated
);
// --- other checks depend on other pallets
// run checks for identity creation
T::CheckIdtyCallAllowed::check_create_identity(creator_index)?;
T::CheckAccountWorthiness::check_account_worthiness(receiver_key)?;
Ok(creator_index)
}
}
}
// implement getting owner key of identity index
impl<T: Config> sp_runtime::traits::Convert<T::IdtyIndex, Option<T::AccountId>> for Pallet<T> {
fn convert(idty_index: T::IdtyIndex) -> Option<T::AccountId> {
Identities::<T>::get(idty_index).map(|idty_val| idty_val.owner_key)
}
}
// implement Idty trait
impl<T: Config> duniter_primitives::Idty<T::IdtyIndex, T::AccountId> for Pallet<T> {
fn idty_index(owner_key: T::AccountId) -> Option<T::IdtyIndex> {
IdentityIndexOf::<T>::get(owner_key)
}
fn owner_key(idty_index: T::IdtyIndex) -> Option<T::AccountId> {
Identities::<T>::get(idty_index).map(|idty_val| idty_val.owner_key)
}
}
// implement StoredMap trait for this pallet
impl<T> frame_support::traits::StoredMap<T::AccountId, T::IdtyData> for Pallet<T>
where
T: Config,
{
/// Get identity data for an account.
fn get(key: &T::AccountId) -> T::IdtyData {
if let Some(idty_index) = Self::identity_index_of(key) {
if let Some(idty_val) = Identities::<T>::get(idty_index) {
......@@ -691,6 +1083,8 @@ where
Default::default()
}
}
/// Mutate an account in function of its data.
fn try_mutate_exists<R, E: From<sp_runtime::DispatchError>>(
key: &T::AccountId,
f: impl FnOnce(&mut Option<T::IdtyData>) -> Result<R, E>,
......
......@@ -17,29 +17,35 @@
use super::*;
use crate::{self as pallet_identity};
use frame_support::{
parameter_types,
traits::{Everything, GenesisBuild, OnFinalize, OnInitialize},
derive_impl, parameter_types,
traits::{Everything, OnFinalize, OnInitialize},
};
use frame_system as system;
use sp_core::H256;
use sp_core::{Pair, H256};
use sp_keystore::{testing::MemoryKeystore, KeystoreExt};
use sp_runtime::{
testing::{Header, TestSignature, UintAuthorityId},
traits::{BlakeTwo256, IdentityLookup},
BuildStorage, MultiSignature, MultiSigner,
};
use sp_state_machine::BasicExternalities;
use std::sync::Arc;
type AccountId = u64;
type Block = frame_system::mocking::MockBlock<Test>;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
pub type Signature = MultiSignature;
pub type AccountPublic = <Signature as Verify>::Signer;
pub type AccountId = <AccountPublic as IdentifyAccount>::AccountId;
fn account(id: u8) -> AccountId {
let pair = sp_core::sr25519::Pair::from_seed(&[id; 32]);
MultiSigner::Sr25519(pair.public()).into_account()
}
// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
pub enum Test
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Identity: pallet_identity::{Pallet, Call, Storage, Config<T>, Event<T>},
System: frame_system,
Identity: pallet_identity,
}
);
......@@ -48,39 +54,31 @@ 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 RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Index = u64;
type BlockNumber = u64;
type Block = Block;
type BlockHashCount = BlockHashCount;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type RuntimeEvent = RuntimeEvent;
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 ChangeOwnerKeyPeriod: u64 = 10;
pub const ConfirmPeriod: u64 = 2;
pub const ValidationPeriod: u64 = 3;
pub const AutorevocationPeriod: u64 = 5;
pub const DeletionPeriod: u64 = 7;
pub const IdtyCreationPeriod: u64 = 3;
pub const MaxInactivityPeriod: u64 = 5;
pub const ValidationPeriod: u64 = 2;
}
pub struct IdtyNameValidatorTestImpl;
......@@ -91,38 +89,46 @@ impl pallet_identity::traits::IdtyNameValidator for IdtyNameValidatorTestImpl {
}
impl pallet_identity::Config for Test {
type AccountId32 = AccountId;
type AccountLinker = ();
type AutorevocationPeriod = AutorevocationPeriod;
type ChangeOwnerKeyPeriod = ChangeOwnerKeyPeriod;
type ConfirmPeriod = ConfirmPeriod;
type CheckAccountWorthiness = ();
type CheckIdtyCallAllowed = ();
type ConfirmPeriod = ConfirmPeriod;
type DeletionPeriod = DeletionPeriod;
type IdtyCreationPeriod = IdtyCreationPeriod;
type IdtyData = ();
type IdtyNameValidator = IdtyNameValidatorTestImpl;
type IdtyIndex = u64;
type NewOwnerKeySigner = UintAuthorityId;
type NewOwnerKeySignature = TestSignature;
type OnIdtyChange = ();
type RemoveIdentityConsumers = ();
type RevocationSigner = UintAuthorityId;
type RevocationSignature = TestSignature;
type IdtyNameValidator = IdtyNameValidatorTestImpl;
type OnKeyChange = ();
type OnNewIdty = ();
type OnRemoveIdty = ();
type RuntimeEvent = RuntimeEvent;
type Signature = Signature;
type Signer = AccountPublic;
type ValidationPeriod = ValidationPeriod;
type WeightInfo = ();
}
// Build genesis storage according to the mock runtime.
pub fn new_test_ext(gen_conf: pallet_identity::GenesisConfig<Test>) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default()
.build_storage::<Test>()
let mut t = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
gen_conf.assimilate_storage(&mut t).unwrap();
frame_support::BasicExternalities::execute_with_storage(&mut t, || {
// Some dedicated test account
frame_system::Pallet::<Test>::inc_sufficients(&1);
frame_system::Pallet::<Test>::inc_providers(&2);
frame_system::Pallet::<Test>::inc_providers(&3);
BasicExternalities::execute_with_storage(&mut t, || {
frame_system::Pallet::<Test>::inc_providers(&account(2));
frame_system::Pallet::<Test>::inc_providers(&account(3));
});
sp_io::TestExternalities::new(t)
let keystore = MemoryKeystore::new();
let mut ext = sp_io::TestExternalities::new(t);
ext.register_extension(KeystoreExt(Arc::new(keystore)));
ext.execute_with(|| System::set_block_number(1));
ext
}
pub fn run_to_block(n: u64) {
......
......@@ -14,16 +14,35 @@
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::mock::*;
use crate::{
Error, GenesisIdty, IdtyName, IdtyValue, NewOwnerKeyPayload, RevocationPayload,
NEW_OWNER_KEY_PAYLOAD_PREFIX, REVOCATION_PAYLOAD_PREFIX,
};
use core::str::FromStr;
use crate::{mock::*, *};
use codec::Encode;
use frame_support::{assert_noop, assert_ok};
use sp_runtime::testing::TestSignature;
use frame_support::{assert_noop, assert_ok, dispatch::DispatchResultWithPostInfo};
use sp_core::{sr25519::Pair as KeyPair, Pair};
use sp_runtime::{AccountId32, MultiSignature, MultiSigner};
type IdtyVal = IdtyValue<u64, AccountId, ()>;
type IdtyVal = IdtyValue<u64, u64, ()>;
// Store the account id and the key pair to sign payload
struct Account {
id: AccountId,
signer: KeyPair,
}
// Create an Account given a u8
fn account(id: u8) -> Account {
let pair = sp_core::sr25519::Pair::from_seed(&[id; 32]);
Account {
id: MultiSigner::Sr25519(pair.public()).into_account(),
signer: pair,
}
}
// Sign a payload using a key pair
fn test_signature(key_pair: KeyPair, payload: Vec<u8>) -> MultiSignature {
MultiSignature::Sr25519(key_pair.sign(&payload))
}
fn alice() -> GenesisIdty<Test> {
GenesisIdty {
......@@ -33,9 +52,9 @@ fn alice() -> GenesisIdty<Test> {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: 1,
removable_on: 0,
status: crate::IdtyStatus::Validated,
owner_key: account(1).id,
next_scheduled: 0,
status: crate::IdtyStatus::Member,
},
}
}
......@@ -48,9 +67,41 @@ fn bob() -> GenesisIdty<Test> {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: 2,
removable_on: 0,
status: crate::IdtyStatus::Validated,
owner_key: account(2).id,
next_scheduled: 0,
status: crate::IdtyStatus::Member,
},
}
}
fn inactive_bob() -> GenesisIdty<Test> {
GenesisIdty {
index: 2,
name: IdtyName::from("Bob"),
value: IdtyVal {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: account(2).id,
next_scheduled: 2,
status: crate::IdtyStatus::NotMember,
},
}
}
// From legacy credentials Charlie:Charlie
fn legacy_charlie() -> GenesisIdty<Test> {
GenesisIdty {
index: 102,
name: IdtyName::from("Charlie"),
value: IdtyVal {
data: (),
next_creatable_identity_on: 0,
old_owner_key: None,
owner_key: AccountId32::from_str("5H2nLXGku46iztpqdRwsCAiP6vHZbShhKmSV4yyufQgEUFvV")
.unwrap(),
next_scheduled: 0,
status: crate::IdtyStatus::Member,
},
}
}
......@@ -65,6 +116,19 @@ fn test_no_identity() {
});
}
/// test that next identity index is correctly initialized and incremented
#[test]
fn test_identity_index() {
new_test_ext(IdentityConfig {
identities: vec![alice(), bob()],
})
.execute_with(|| {
assert_eq!(Identity::identities_count(), 2);
// assert_eq!(NextIdtyIndex, 3); // TODO check how to test that
// ... create identity and check it was incremented
});
}
#[test]
fn test_create_identity_ok() {
new_test_ext(IdentityConfig {
......@@ -75,11 +139,14 @@ fn test_create_identity_ok() {
run_to_block(1);
// Alice should be able to create an identity
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 2));
assert_ok!(Identity::create_identity(
RuntimeOrigin::signed(account(1).id),
account(2).id
));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyCreated {
idty_index: 2,
owner_key: 2,
owner_key: account(2).id,
}));
});
}
......@@ -94,22 +161,29 @@ fn test_create_identity_but_not_confirm_it() {
run_to_block(1);
// Alice should be able to create an identity
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 2));
assert_ok!(Identity::create_identity(
RuntimeOrigin::signed(account(1).id),
account(2).id
));
// The identity shoud expire in blocs #3
// The identity should expire in blocs #3
run_to_block(3);
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved {
idty_index: 2,
reason: RemovalReason::Unconfirmed,
}));
// We shoud be able to recreate the identity
run_to_block(4);
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 2));
assert_ok!(Identity::create_identity(
RuntimeOrigin::signed(account(1).id),
account(2).id
));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyCreated {
idty_index: 3,
owner_key: 2,
owner_key: account(2).id,
}));
});
}
......@@ -124,11 +198,14 @@ fn test_idty_creation_period() {
run_to_block(1);
// Alice should be able to create an identity
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 2));
assert_ok!(Identity::create_identity(
RuntimeOrigin::signed(account(1).id),
account(2).id
));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyCreated {
idty_index: 2,
owner_key: 2,
owner_key: account(2).id,
}));
assert_eq!(Identity::identity(1).unwrap().next_creatable_identity_on, 4);
......@@ -136,21 +213,25 @@ fn test_idty_creation_period() {
// Alice cannot create a new identity before block #4
run_to_block(2);
assert_eq!(
Identity::create_identity(RuntimeOrigin::signed(1), 3),
Identity::create_identity(RuntimeOrigin::signed(account(1).id), account(3).id),
Err(Error::<Test>::NotRespectIdtyCreationPeriod.into())
);
// Alice should be able to create a second identity after block #4
run_to_block(4);
assert_ok!(Identity::create_identity(RuntimeOrigin::signed(1), 3));
assert_ok!(Identity::create_identity(
RuntimeOrigin::signed(account(1).id),
account(3).id
));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyCreated {
idty_index: 3,
owner_key: 3,
owner_key: account(3).id,
}));
});
}
//
#[test]
fn test_change_owner_key() {
new_test_ext(IdentityConfig {
......@@ -158,8 +239,8 @@ fn test_change_owner_key() {
})
.execute_with(|| {
let genesis_hash = System::block_hash(0);
let old_owner_key = 1u64;
let mut new_key_payload = NewOwnerKeyPayload {
let old_owner_key = account(1).id;
let mut new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: 1u64,
old_owner_key: &old_owner_key,
......@@ -169,82 +250,93 @@ fn test_change_owner_key() {
run_to_block(1);
// Verify genesis data
assert_eq!(System::sufficients(&1), 1);
assert_eq!(System::sufficients(&10), 0);
assert_eq!(System::sufficients(&account(1).id), 1);
assert_eq!(System::sufficients(&account(10).id), 0);
// Caller should have an associated identity
assert_noop!(
Identity::change_owner_key(
RuntimeOrigin::signed(42),
10,
TestSignature(10, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
RuntimeOrigin::signed(account(42).id),
account(10).id,
test_signature(
account(10).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
)
),
Error::<Test>::IdtyIndexNotFound
);
// Payload must be signed by the new key
assert_noop!(
Identity::change_owner_key(
RuntimeOrigin::signed(1),
10,
TestSignature(42, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(
account(42).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
)
),
Error::<Test>::InvalidNewOwnerKeySig
Error::<Test>::InvalidSignature
);
// Payload must be prefixed
assert_noop!(
Identity::change_owner_key(
RuntimeOrigin::signed(1),
10,
TestSignature(10, new_key_payload.encode())
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(account(10).signer, new_key_payload.clone().encode())
),
Error::<Test>::InvalidNewOwnerKeySig
Error::<Test>::InvalidSignature
);
// New owner key should not be used by another identity
assert_noop!(
Identity::change_owner_key(
RuntimeOrigin::signed(1),
2,
TestSignature(2, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
RuntimeOrigin::signed(account(1).id),
account(2).id,
test_signature(
account(2).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
)
),
Error::<Test>::OwnerKeyAlreadyUsed
);
// Alice can change her owner key
assert_ok!(Identity::change_owner_key(
RuntimeOrigin::signed(1),
10,
TestSignature(10, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(
account(10).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
)
));
assert_eq!(
Identity::identity(1),
Some(IdtyVal {
data: (),
next_creatable_identity_on: 0,
old_owner_key: Some((1, 1)),
owner_key: 10,
removable_on: 0,
status: crate::IdtyStatus::Validated,
old_owner_key: Some((account(1).id, 1)),
owner_key: account(10).id,
next_scheduled: 0,
status: crate::IdtyStatus::Member,
})
);
// Alice still sufficient
assert_eq!(System::sufficients(&1), 1);
assert_eq!(System::sufficients(&account(1).id), 1);
// New owner key should become a sufficient account
assert_eq!(System::sufficients(&10), 1);
assert_eq!(System::sufficients(&account(10).id), 1);
run_to_block(2);
//
// Alice can't re-change her owner key too early
new_key_payload.old_owner_key = &10;
let old = account(10).id;
new_key_payload.old_owner_key = &old;
assert_noop!(
Identity::change_owner_key(
RuntimeOrigin::signed(10),
100,
TestSignature(
100,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode()
RuntimeOrigin::signed(account(10).id),
account(100).id,
test_signature(
account(100).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
)
),
Error::<Test>::OwnerKeyAlreadyRecentlyChanged
......@@ -253,27 +345,27 @@ fn test_change_owner_key() {
// Alice can re-change her owner key after ChangeOwnerKeyPeriod blocs
run_to_block(2 + <Test as crate::Config>::ChangeOwnerKeyPeriod::get());
assert_ok!(Identity::change_owner_key(
RuntimeOrigin::signed(10),
100,
TestSignature(
100,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode()
RuntimeOrigin::signed(account(10).id),
account(100).id,
test_signature(
account(100).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
)
));
// Old old owner key should not be sufficient anymore
assert_eq!(System::sufficients(&1), 0);
assert_eq!(System::sufficients(&account(1).id), 0);
// Old owner key should still sufficient
assert_eq!(System::sufficients(&10), 1);
assert_eq!(System::sufficients(&account(10).id), 1);
// New owner key should become a sufficient account
assert_eq!(System::sufficients(&100), 1);
assert_eq!(System::sufficients(&account(100).id), 1);
// Revoke identity 1
assert_ok!(Identity::revoke_identity(
RuntimeOrigin::signed(42),
RuntimeOrigin::signed(account(42).id),
1,
100,
TestSignature(
100,
account(100).id,
test_signature(
account(100).signer,
(
REVOCATION_PAYLOAD_PREFIX,
RevocationPayload {
......@@ -284,10 +376,74 @@ fn test_change_owner_key() {
.encode()
)
));
// Old owner key should not be sufficient anymore
assert_eq!(System::sufficients(&10), 0);
// Last owner key should not be sufficient anymore
assert_eq!(System::sufficients(&100), 0);
// Old owner key is still sufficient (identity is revoked but not removed)
assert_eq!(System::sufficients(&account(10).id), 1);
// Last owner key should still be sufficient (identity is revoked but not removed)
assert_eq!(System::sufficients(&account(100).id), 1);
});
}
// test link identity (does nothing because of AccountLinker type)
#[test]
fn test_link_account() {
new_test_ext(IdentityConfig {
identities: vec![alice(), bob()],
})
.execute_with(|| {
let genesis_hash = System::block_hash(0);
let account_id = account(10).id;
let payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: 1u64,
old_owner_key: &account_id,
};
run_to_block(1);
// Caller should have an associated identity
assert_noop!(
Identity::link_account(
RuntimeOrigin::signed(account(42).id),
account(10).id,
test_signature(
account(10).signer,
(LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode()
)
),
Error::<Test>::IdtyIndexNotFound
);
// Payload must be signed by the new key
assert_noop!(
Identity::link_account(
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(
account(42).signer,
(LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode()
)
),
Error::<Test>::InvalidSignature
);
// Payload must be prefixed
assert_noop!(
Identity::link_account(
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(account(10).signer, payload.clone().encode())
),
Error::<Test>::InvalidSignature
);
// Alice can call link_account successfully
assert_ok!(Identity::link_account(
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(
account(10).signer,
(LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode()
)
));
});
}
......@@ -298,10 +454,10 @@ fn test_idty_revocation_with_old_key() {
})
.execute_with(|| {
let genesis_hash = System::block_hash(0);
let new_key_payload = NewOwnerKeyPayload {
let new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: 1u64,
old_owner_key: &1u64,
old_owner_key: &account(1).id,
};
let revocation_payload = RevocationPayload {
idty_index: 1u64,
......@@ -313,22 +469,28 @@ fn test_idty_revocation_with_old_key() {
// Change alice owner key
assert_ok!(Identity::change_owner_key(
RuntimeOrigin::signed(1),
10,
TestSignature(10, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(
account(10).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode()
)
));
assert!(Identity::identity(&1).is_some());
let idty_val = Identity::identity(&1).unwrap();
assert_eq!(idty_val.owner_key, 10);
assert_eq!(idty_val.old_owner_key, Some((1, 1)));
assert!(Identity::identity(1).is_some());
let idty_val = Identity::identity(1).unwrap();
assert_eq!(idty_val.owner_key, account(10).id);
assert_eq!(idty_val.old_owner_key, Some((account(1).id, 1)));
// We should be able to revoke Alice identity with old key
run_to_block(2);
assert_ok!(Identity::revoke_identity(
RuntimeOrigin::signed(42),
RuntimeOrigin::signed(account(42).id),
1,
1,
TestSignature(1, (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode())
account(1).id,
test_signature(
account(1).signer,
(REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode()
)
));
//run_to_block(2 + <Test as crate::Config>::ChangeOwnerKeyPeriod::get());
......@@ -342,10 +504,10 @@ fn test_idty_revocation_with_old_key_after_old_key_expiration() {
})
.execute_with(|| {
let genesis_hash = System::block_hash(0);
let new_key_payload = NewOwnerKeyPayload {
let new_key_payload = IdtyIndexAccountIdPayload {
genesis_hash: &genesis_hash,
idty_index: 1u64,
old_owner_key: &1u64,
old_owner_key: &account(1).id,
};
let revocation_payload = RevocationPayload {
idty_index: 1u64,
......@@ -357,23 +519,29 @@ fn test_idty_revocation_with_old_key_after_old_key_expiration() {
// Change alice owner key
assert_ok!(Identity::change_owner_key(
RuntimeOrigin::signed(1),
10,
TestSignature(10, (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode())
RuntimeOrigin::signed(account(1).id),
account(10).id,
test_signature(
account(10).signer,
(NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload).encode()
)
));
assert!(Identity::identity(&1).is_some());
let idty_val = Identity::identity(&1).unwrap();
assert_eq!(idty_val.owner_key, 10);
assert_eq!(idty_val.old_owner_key, Some((1, 1)));
assert!(Identity::identity(1).is_some());
let idty_val = Identity::identity(1).unwrap();
assert_eq!(idty_val.owner_key, account(10).id);
assert_eq!(idty_val.old_owner_key, Some((account(1).id, 1)));
// We should not be able to revoke Alice identity with old key after ChangeOwnerKeyPeriod
run_to_block(2 + <Test as crate::Config>::ChangeOwnerKeyPeriod::get());
assert_noop!(
Identity::revoke_identity(
RuntimeOrigin::signed(42),
1,
RuntimeOrigin::signed(account(42).id),
1,
TestSignature(1, (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode())
account(1).id,
test_signature(
account(1).signer,
(REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode()
)
),
Error::<Test>::InvalidRevocationKey
);
......@@ -397,10 +565,13 @@ fn test_idty_revocation() {
// Payload must be signed by the right identity
assert_eq!(
Identity::revoke_identity(
RuntimeOrigin::signed(1),
RuntimeOrigin::signed(account(1).id),
1,
42,
TestSignature(42, (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode())
account(42).id,
test_signature(
account(42).signer,
(REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode()
)
),
Err(Error::<Test>::InvalidRevocationKey.into())
);
......@@ -408,40 +579,214 @@ fn test_idty_revocation() {
// Payload must be prefixed
assert_eq!(
Identity::revoke_identity(
RuntimeOrigin::signed(1),
1,
RuntimeOrigin::signed(account(1).id),
1,
TestSignature(1, revocation_payload.encode())
account(1).id,
test_signature(account(1).signer, revocation_payload.encode())
),
Err(Error::<Test>::InvalidRevocationSig.into())
Err(Error::<Test>::InvalidSignature.into())
);
// Anyone can submit a revocation payload
assert_ok!(Identity::revoke_identity(
RuntimeOrigin::signed(42),
RuntimeOrigin::signed(account(42).id),
1,
1,
TestSignature(1, (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode())
account(1).id,
test_signature(
account(1).signer,
(REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode()
)
));
System::assert_has_event(RuntimeEvent::System(frame_system::Event::KilledAccount {
account: 1,
}));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved {
// // account is not killed anymore for revoked identity
// System::assert_has_event(RuntimeEvent::System(frame_system::Event::KilledAccount {
// account: account(1).id,
// }));
// // identity is not removed immediately after revocation
// System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved {
// idty_index: 1,
// reason: RemovalReason::Revoked,
// }));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRevoked {
idty_index: 1,
reason: RevocationReason::User,
}));
run_to_block(2);
// The identity no longer exists
// The identity can not be revoked multiple times
assert_eq!(
Identity::revoke_identity(
RuntimeOrigin::signed(1),
1,
RuntimeOrigin::signed(account(1).id),
1,
TestSignature(1, (REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode())
account(1).id,
test_signature(
account(1).signer,
(REVOCATION_PAYLOAD_PREFIX, revocation_payload).encode()
)
),
Err(Error::<Test>::IdtyNotFound.into())
Err(Error::<Test>::AlreadyRevoked.into())
);
});
}
// # Generate dummy revocation documents in Python.
// # The seed derivation is not the same as sr25519.from_seed so this doesn't work yet.
// ```python
// import duniterpy, substrateinterface
// s = duniterpy.key.SigningKey.from_credentials("Charlie", "Charlie")
// block = duniterpy.documents.BlockID(42, "A"*64)
// idty = duniterpy.documents.Identity(s.pubkey, "Charlie", block, s)
// r = duniterpy.documents.Revocation(idty, s)
// print("SS58 address:", substrateinterface.base.ss58_encode(s.vk))
// print(r.signed_raw())
// ```
#[test]
fn test_idty_revocation_legacy() {
new_test_ext(IdentityConfig {
identities: vec![alice(), legacy_charlie()],
})
.execute_with(|| {
// We need to initialize at least one block before any call
run_to_block(1);
let valid_revocation_document = r"Version: 10
Type: Revocation
Currency: g1
Issuer: Fnf2xaxYdQpB4kU45DMLQ9Ey4bd6DtoebKJajRkLBUXm
IdtyUniqueID: Charlie
IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
IdtySignature: 7KUagcMiQw05rwbkBsRrnNqPRHu/Y5ukCLoAEpb/1tXAQsSNf2gRi1h5PWIGs9y/vHnFXvF5epKsOjA6X75vDg==
CfiG4xhcWS+/DgxY0xFIyOA9TVr4Im3XEXcCApNgXC+Ns9jy2yrNoC3NF8MCD63cZ8QTRfrr4Iv6n3leYCCcDQ==
";
let revocation_document_bad_username = r"Version: 10
Type: Revocation
Currency: g1
Issuer: Fnf2xaxYdQpB4kU45DMLQ9Ey4bd6DtoebKJajRkLBUXm
IdtyUniqueID: Alice
IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
IdtySignature: dqO8nnYWZDDDzadMXpOVwehJXQ9wocE9QTKsVBa88rPONLhz12QA6Ytib2+VtPU+gnewO2mRVOvzdYKXemQPDg==
0q/Dy4jwLTjZGSOu4GWdkfW+SqXRAPHUwwvWQenqiuNuL2eEc0x2hM0MWhIOuSLy2ifNq6PfSH/dBrV5CgYIAw==
";
let revocation_document_bad_signer = r"Version: 10
Type: Revocation
Currency: g1
Issuer: 9cFLFh12MZSL8HHW9KvEDGTEEyKALGjEdHpNP8rqmmw3
IdtyUniqueID: Charlie
IdtyTimestamp: 42-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
IdtySignature: 8P2vjDHZf4tHaGpZYTuTXJ9Xe+3qQ0FAM6fypvwl2mYqLs1ZfE07gp4mqRpNY90rC9+CIIi7eHvv2uAlFVpfCQ==
iWOssQ1y2svWeUD4byjJx6n+/Xgf0pgMe1FDhnR9oN76Ri9B8SQfP+hFD3GCth7sZRD162sR83g3UvYpFHJLBQ==
";
assert_eq!(
Identity::revoke_identity_legacy(
RuntimeOrigin::signed(account(1).id),
revocation_document_bad_username.into()
),
Err(Error::<Test>::InvalidRevocationKey.into())
);
assert_eq!(
Identity::revoke_identity_legacy(
RuntimeOrigin::signed(account(1).id),
revocation_document_bad_signer.into()
),
Err(Error::<Test>::InvalidRevocationKey.into())
);
// Anyone can submit a revocation payload
assert_ok!(Identity::revoke_identity_legacy(
RuntimeOrigin::signed(account(42).id),
valid_revocation_document.into()
));
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRevoked {
idty_index: 102,
reason: RevocationReason::User,
}));
});
}
#[test]
fn test_inactive_genesis_members() {
new_test_ext(IdentityConfig {
identities: vec![alice(), inactive_bob()],
})
.execute_with(|| {
let alice = alice();
let bob = inactive_bob();
assert!(pallet::Identities::<Test>::get(alice.index).is_some());
assert!(pallet::Identities::<Test>::get(bob.index).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&alice.value.owner_key).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&bob.value.owner_key).is_some());
run_to_block(2);
// alice identity remains untouched
assert!(pallet::Identities::<Test>::get(alice.index).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&alice.value.owner_key).is_some());
// but bob identity has been revoked
assert!(pallet::Identities::<Test>::get(bob.index).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&bob.value.owner_key).is_some());
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRevoked {
idty_index: bob.index,
reason: RevocationReason::Expired,
}));
});
}
#[test]
fn test_revocation_of_genesis_member() {
let alice = alice();
let bob = inactive_bob();
new_test_ext(IdentityConfig {
identities: vec![alice.clone(), bob.clone()],
})
.execute_with(|| {
assert!(pallet::Identities::<Test>::get(alice.index).is_some());
assert!(pallet::Identities::<Test>::get(bob.index).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&alice.value.owner_key).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&bob.value.owner_key).is_some());
// Necessary to go to block#1 to allow extrinsics consumption
run_to_block(1);
assert_ok!(revoke_self_identity(bob.clone()));
// alice identity remains untouched
assert!(pallet::Identities::<Test>::get(alice.index).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&alice.value.owner_key).is_some());
// but bob identity has been revoked
assert!(pallet::Identities::<Test>::get(bob.index).is_some());
assert!(pallet::IdentityIndexOf::<Test>::get(&bob.value.owner_key).is_some());
System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRevoked {
idty_index: bob.index,
reason: RevocationReason::User, // because called manually by revoke_self_identity
}));
});
}
fn revoke_self_identity(idty: GenesisIdty<Test>) -> DispatchResultWithPostInfo {
Identity::revoke_identity(
RuntimeOrigin::signed(account(idty.index as u8).id),
idty.index,
account(idty.index as u8).id,
test_signature(
account(idty.index as u8).signer,
(
REVOCATION_PAYLOAD_PREFIX,
RevocationPayload {
idty_index: idty.index,
genesis_hash: System::block_hash(0),
},
)
.encode(),
),
)
}
// TODO add tests for all periods (confirmation, validation, autorevocation, deletion)