From 68285b5e8cae00491b87c6b2b527b88a21b1f544 Mon Sep 17 00:00:00 2001 From: Hugo Trentesaux <hugo@trentesaux.fr> Date: Mon, 18 Dec 2023 12:35:21 +0100 Subject: [PATCH] automatic membership after distance eval add more details to idty creation test cargo check ok add integration test and fix behavior fix benchmarks for renaming cargo check ok cargo test ok add certification count check update benchmarks update distance setup handler fix weight info update metadata and generated doc fix cucumber distance pallet unit test stub update metadata and generate doc after rebase make membership claim no-op an error update metadata with new error wip add comments add membership renewal antispam and clean up pending membership parameters test it not compatible with keeping distance result change renewal antispam metadata fix param for other runtimes fix e2e test remove membership calls adjust integration tests accordingly update metadata fix cucumber clean up benchmarks and other tests wip --- docs/api/runtime-calls.md | 90 ++---- docs/api/runtime-errors.md | 63 +++- docs/api/runtime-events.md | 28 +- .../identity_creation.feature | 2 - end2end-tests/cucumber-genesis/default.json | 14 +- end2end-tests/cucumber-genesis/wot.json | 14 +- end2end-tests/tests/common/membership.rs | 25 +- end2end-tests/tests/cucumber_tests.rs | 35 --- node/src/chain_spec/gdev.rs | 5 +- node/src/chain_spec/gen_genesis_data.rs | 21 +- node/src/chain_spec/gtest.rs | 2 +- pallets/distance/src/benchmarking.rs | 54 ++-- pallets/distance/src/lib.rs | 292 ++++++++++-------- pallets/distance/src/mock.rs | 16 +- pallets/distance/src/tests.rs | 41 +++ pallets/distance/src/traits.rs | 24 +- pallets/distance/src/weights.rs | 116 +++---- pallets/duniter-test-parameters/src/lib.rs | 2 +- pallets/duniter-wot/src/lib.rs | 123 +++++++- pallets/duniter-wot/src/mock.rs | 5 +- pallets/duniter-wot/src/tests.rs | 46 +-- pallets/duniter-wot/src/traits.rs | 14 - pallets/membership/src/benchmarking.rs | 44 +-- pallets/membership/src/lib.rs | 145 +++++---- pallets/membership/src/mock.rs | 11 +- pallets/membership/src/tests.rs | 72 ++--- pallets/universal-dividend/src/lib.rs | 6 + resources/gdev.yaml | 7 +- resources/metadata.scale | Bin 129557 -> 129125 bytes runtime/common/src/pallets_config.rs | 5 +- runtime/common/src/providers.rs | 36 +-- runtime/common/src/weights/pallet_distance.rs | 122 +++++--- runtime/g1/src/lib.rs | 5 +- runtime/g1/src/parameters.rs | 2 +- runtime/gdev/src/lib.rs | 10 +- runtime/gdev/src/parameters.rs | 5 + runtime/gdev/tests/common/mod.rs | 2 +- runtime/gdev/tests/integration_tests.rs | 264 ++++++++++++---- runtime/gdev/tests/xt_tests.rs | 1 + runtime/gtest/src/lib.rs | 2 +- runtime/gtest/src/parameters.rs | 2 +- 41 files changed, 1028 insertions(+), 745 deletions(-) create mode 100644 pallets/distance/src/tests.rs diff --git a/docs/api/runtime-calls.md b/docs/api/runtime-calls.md index 033203a40..3c96e8358 100644 --- a/docs/api/runtime-calls.md +++ b/docs/api/runtime-calls.md @@ -13,7 +13,7 @@ through on-chain governance mechanisms. ## User calls -There are **79** user calls from **22** pallets. +There are **77** user calls from **21** pallets. ### Account - 1 @@ -831,51 +831,6 @@ payload_sig: T::Signature Link an account to an identity -### Membership - 42 - -#### claim_membership - 1 - -<details><summary><code>claim_membership()</code></summary> - -Taking 0.0213 % of a block. - -```rust -``` -</details> - - -claim membership -it must fullfill the requirements (certs, distance) -TODO #159 for main wot claim_membership is called automatically when distance is evaluated positively -for smith wot, it means joining the authority members - -#### renew_membership - 2 - -<details><summary><code>renew_membership()</code></summary> - -Taking 0.0164 % of a block. - -```rust -``` -</details> - - -extend the validity period of an active membership - -#### revoke_membership - 3 - -<details><summary><code>revoke_membership()</code></summary> - -Taking 0.0586 % of a block. - -```rust -``` -</details> - - -revoke an active membership -(only available for sub wot, automatic for main wot) - ### Certification - 43 #### add_cert - 0 @@ -932,20 +887,37 @@ remove all certifications received by an identity (only root) <details><summary><code>request_distance_evaluation()</code></summary> -Taking 0.0187 % of a block. +Taking 0.06 % of a block. + +```rust +``` +</details> + + +Request caller identity to be evaluated +positive evaluation will result in claim/renew membership +negative evaluation will result in slash for caller + +#### request_distance_evaluation_for - 4 + +<details><summary><code>request_distance_evaluation_for(target)</code></summary> + +Taking 0.0805 % of a block. ```rust +target: T::IdtyIndex ``` </details> -Request an identity to be evaluated +Request target identity to be evaluated +only possible for unvalidated identity #### update_evaluation - 1 <details><summary><code>update_evaluation(computation_result)</code></summary> -Taking 0.0195 % of a block. +Taking 0.0914 % of a block. ```rust computation_result: ComputationResult @@ -954,12 +926,13 @@ computation_result: ComputationResult (Inherent) Push an evaluation result to the pool +this is called internally by validators (= inherent) #### force_update_evaluation - 2 <details><summary><code>force_update_evaluation(evaluator, computation_result)</code></summary> -Taking 0.0122 % of a block. +Taking 0.0759 % of a block. ```rust evaluator: <T as frame_system::Config>::AccountId @@ -968,28 +941,21 @@ computation_result: ComputationResult </details> -Push an evaluation result to the pool +Force push an evaluation result to the pool -#### force_set_distance_status - 3 +#### force_valid_distance_status - 3 -<details><summary><code>force_set_distance_status(identity, status)</code></summary> +<details><summary><code>force_valid_distance_status(identity)</code></summary> -Taking 0.011 % of a block. +Taking 0.074 % of a block. ```rust identity: <T as pallet_identity::Config>::IdtyIndex -status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)> ``` </details> -Set the distance evaluation status of an identity - -Removes the status if `status` is `None`. - -* `status.0` is the account for whom the price will be unreserved or slashed - when the evaluation completes. -* `status.1` is the status of the evaluation. +Force set the distance evaluation status of an identity ### AtomicSwap - 50 diff --git a/docs/api/runtime-errors.md b/docs/api/runtime-errors.md index ebb10eb1a..03b5c31fb 100644 --- a/docs/api/runtime-errors.md +++ b/docs/api/runtime-errors.md @@ -1,6 +1,6 @@ # Runtime errors -There are **178** errors from **35** pallets. +There are **185** errors from **35** pallets. <ul> <li>System - 0 @@ -775,8 +775,8 @@ Distance evaluation has not been requested <li> <details> <summary> -<code>IdtyNotAllowedToRequestMembership</code> - 5</summary> -Identity is not allowed to request membership. +<code>IdtyNotAllowedToClaimMembership</code> - 5</summary> +Identity is not allowed to claim membership. </details> </li> <li> @@ -849,6 +849,20 @@ Cannot issue a certification to a revoked identity Issuer or receiver not found. </details> </li> +<li> +<details> +<summary> +<code>NotEnoughCertsReceivedToRequestDistanceEvaluation</code> - 16</summary> +Not enough certs received to request distance evaluation. +</details> +</li> +<li> +<details> +<summary> +<code>MembershipRenewalPeriodNotRespected</code> - 17</summary> +Membership can only be renewed after an antispam delay +</details> +</li> </ul> </li> <li>Identity - 41 @@ -1011,6 +1025,13 @@ Membership already acquired. Membership not found. </details> </li> +<li> +<details> +<summary> +<code>AlreadyMember</code> - 3</summary> +Already member, can not claim membership +</details> +</li> </ul> </li> <li>Certification - 43 @@ -1085,31 +1106,59 @@ No author for this block. <li> <details> <summary> -<code>NoIdentity</code> - 4</summary> +<code>CallerHasNoIdentity</code> - 4</summary> Caller has no identity. </details> </li> <li> <details> <summary> -<code>QueueFull</code> - 5</summary> +<code>CallerIdentityNotFound</code> - 5</summary> +Caller identity not found. +</details> +</li> +<li> +<details> +<summary> +<code>CallerNotMember</code> - 6</summary> +Caller not member. +</details> +</li> +<li> +<details> +<summary> +<code>TargetIdentityNotFound</code> - 7</summary> +Target identity not found. +</details> +</li> +<li> +<details> +<summary> +<code>QueueFull</code> - 8</summary> Evaluation queue is full. </details> </li> <li> <details> <summary> -<code>TooManyEvaluators</code> - 6</summary> +<code>TooManyEvaluators</code> - 9</summary> Too many evaluators in the current evaluation pool. </details> </li> <li> <details> <summary> -<code>WrongResultLength</code> - 7</summary> +<code>WrongResultLength</code> - 10</summary> Evaluation result has a wrong length. </details> </li> +<li> +<details> +<summary> +<code>DistanceRequestOnlyAllowedForUnvalidated</code> - 11</summary> +Targeted distance evaluation request is only possible for an unvalidated identity +</details> +</li> </ul> </li> <li>AtomicSwap - 50 diff --git a/docs/api/runtime-events.md b/docs/api/runtime-events.md index 9aa809263..c495670a1 100644 --- a/docs/api/runtime-events.md +++ b/docs/api/runtime-events.md @@ -1,6 +1,6 @@ # Runtime events -There are **127** events from **35** pallets. +There are **128** events from **35** pallets. <ul> <li>System - 0 @@ -1286,7 +1286,20 @@ expire_on: BlockNumberFor<T> <li> <details> <summary> -<code>MembershipRemoved(member, reason)</code> - 1</summary> +<code>MembershipRenewed(member, expire_on)</code> - 1</summary> +A membership was renewed. + +```rust +member: T::IdtyId +expire_on: BlockNumberFor<T> +``` + +</details> +</li> +<li> +<details> +<summary> +<code>MembershipRemoved(member, reason)</code> - 2</summary> A membership was removed. ```rust @@ -1360,11 +1373,11 @@ who: T::AccountId <li> <details> <summary> -<code>EvaluationUpdated(evaluator)</code> - 1</summary> -A distance evaluation was updated. +<code>EvaluatedValid(idty_index)</code> - 1</summary> +Distance rule was found valid ```rust -evaluator: T::AccountId +idty_index: T::IdtyIndex ``` </details> @@ -1372,12 +1385,11 @@ evaluator: T::AccountId <li> <details> <summary> -<code>EvaluationStatusForced(idty_index, status)</code> - 2</summary> -A distance status was forced. +<code>EvaluatedInvalid(idty_index)</code> - 2</summary> +Distance rule was found invalid ```rust idty_index: T::IdtyIndex -status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)> ``` </details> diff --git a/end2end-tests/cucumber-features/identity_creation.feature b/end2end-tests/cucumber-features/identity_creation.feature index f8bffeacc..7de97f182 100644 --- a/end2end-tests/cucumber-features/identity_creation.feature +++ b/end2end-tests/cucumber-features/identity_creation.feature @@ -29,6 +29,4 @@ Feature: Identity creation Then dave should have distance result in 1 session When alice runs distance oracle When 30 blocks later - Then dave should have distance ok - When dave claims membership Then dave identity should be member diff --git a/end2end-tests/cucumber-genesis/default.json b/end2end-tests/cucumber-genesis/default.json index 245e91e31..cfd66ba93 100644 --- a/end2end-tests/cucumber-genesis/default.json +++ b/end2end-tests/cucumber-genesis/default.json @@ -53,7 +53,7 @@ "idty_confirm_period": 40, "idty_creation_period": 50, "membership_period": 1000, - "pending_membership_period": 500, + "membership_renewal_period": 500, "ud_creation_period": 60000, "ud_reeval_period": 600000, "smith_cert_max_by_issuer": 8, @@ -64,9 +64,15 @@ "wot_min_cert_for_membership": 2 }, "clique_smiths": [ - { "name": "Alice" }, - { "name": "Bob" }, - { "name": "Charlie" } + { + "name": "Alice" + }, + { + "name": "Bob" + }, + { + "name": "Charlie" + } ], "sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", "technical_committee": [ diff --git a/end2end-tests/cucumber-genesis/wot.json b/end2end-tests/cucumber-genesis/wot.json index 6b1fad670..813fc6fc1 100644 --- a/end2end-tests/cucumber-genesis/wot.json +++ b/end2end-tests/cucumber-genesis/wot.json @@ -62,7 +62,7 @@ "idty_confirm_period": 40, "idty_creation_period": 50, "membership_period": 1000, - "pending_membership_period": 500, + "membership_renewal_period": 500, "ud_creation_period": 60000, "ud_reeval_period": 600000, "smith_cert_max_by_issuer": 8, @@ -73,9 +73,15 @@ "wot_min_cert_for_membership": 2 }, "clique_smiths": [ - { "name": "Alice" }, - { "name": "Bob" }, - { "name": "Charlie" } + { + "name": "Alice" + }, + { + "name": "Bob" + }, + { + "name": "Charlie" + } ], "sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", "technical_committee": [ diff --git a/end2end-tests/tests/common/membership.rs b/end2end-tests/tests/common/membership.rs index d800f3387..c7b3fa56c 100644 --- a/end2end-tests/tests/common/membership.rs +++ b/end2end-tests/tests/common/membership.rs @@ -14,27 +14,4 @@ // You should have received a copy of the GNU Affero General Public License // along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. -use super::*; -use sp_keyring::AccountKeyring; -use subxt::tx::PairSigner; - -pub async fn claim_membership(client: &Client, from: AccountKeyring) -> Result<()> { - let from = PairSigner::new(from.pair()); - - let _events = create_block_with_extrinsic( - client, - client - .tx() - .create_signed( - &gdev::tx().membership().claim_membership(), - &from, - BaseExtrinsicParamsBuilder::new(), - ) - .await - .unwrap(), - ) - .await - .unwrap(); - - Ok(()) -} +// no more membership calls diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index 7832a4a3d..48d7c30be 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -333,14 +333,6 @@ async fn confirm_identity(world: &mut DuniterWorld, from: String, pseudo: String common::identity::confirm_identity(world.client(), from, pseudo).await } -#[allow(clippy::needless_pass_by_ref_mut)] -#[when(regex = r#"([a-zA-Z]+) claims membership"#)] -async fn claim_membership(world: &mut DuniterWorld, from: String) -> Result<()> { - // input names to keyrings - let from = AccountKeyring::from_str(&from).expect("unknown from"); - common::membership::claim_membership(world.client(), from).await -} - #[allow(clippy::needless_pass_by_ref_mut)] #[when(regex = r#"([a-zA-Z]+) requests distance evaluation"#)] async fn request_distance_evaluation(world: &mut DuniterWorld, who: String) -> Result<()> { @@ -541,33 +533,6 @@ async fn should_have_distance_result_in_sessions( Err(anyhow::anyhow!("no evaluation in given pool").into()) } -#[allow(clippy::needless_pass_by_ref_mut)] -#[then(regex = r"([a-zA-Z]+) should have distance ok")] -async fn should_have_distance_ok(world: &mut DuniterWorld, who: String) -> Result<()> { - let who = AccountKeyring::from_str(&who).unwrap().to_account_id(); - - let idty_id = world - .read(&gdev::storage().identity().identity_index_of(&who.into())) - .await - .await? - .unwrap(); - - match world - .read(&gdev::storage().distance().identity_distance_status(idty_id)) - .await - .await? - { - Some((_, gdev::runtime_types::pallet_distance::types::DistanceStatus::Valid)) => Ok(()), - Some((_, gdev::runtime_types::pallet_distance::types::DistanceStatus::Invalid)) => { - Err(anyhow::anyhow!("invalid distance status").into()) - } - Some((_, gdev::runtime_types::pallet_distance::types::DistanceStatus::Pending)) => { - Err(anyhow::anyhow!("pending distance status").into()) - } - None => Err(anyhow::anyhow!("no distance status").into()), - } -} - use gdev::runtime_types::pallet_identity::types::IdtyStatus; // status from string diff --git a/node/src/chain_spec/gdev.rs b/node/src/chain_spec/gdev.rs index e927d483b..a66be70d2 100644 --- a/node/src/chain_spec/gdev.rs +++ b/node/src/chain_spec/gdev.rs @@ -87,7 +87,7 @@ fn get_parameters(parameters_from_file: &Option<GenesisParameters>) -> CommonPar identity_change_owner_key_period: parameters::ChangeOwnerKeyPeriod::get(), identity_idty_creation_period: parameters_from_file.idty_creation_period, membership_membership_period: parameters_from_file.membership_period, - membership_pending_membership_period: parameters_from_file.pending_membership_period, + membership_membership_renewal_period: parameters_from_file.membership_renewal_period, cert_max_by_issuer: parameters_from_file.cert_max_by_issuer, cert_min_received_cert_to_be_able_to_issue_cert: parameters_from_file .cert_min_received_cert_to_issue_cert, @@ -373,6 +373,7 @@ fn get_local_chain_parameters() -> Option<GenesisParameters> { let babe_epoch_duration = get_env("DUNITER_BABE_EPOCH_DURATION", 30) as u64; let cert_validity_period = get_env("DUNITER_CERT_VALIDITY_PERIOD", 1_000); let membership_period = get_env("DUNITER_MEMBERSHIP_PERIOD", 1_000); + let membership_renewal_period = get_env("DUNITER_MEMBERSHIP_RENEWAL_PERIOD", 1_000); let ud_creation_period = get_env("DUNITER_UD_CREATION_PERIOD", 60_000); let ud_reeval_period = get_env("DUNITER_UD_REEEVAL_PERIOD", 1_200_000); Some(GenesisParameters { @@ -384,7 +385,7 @@ fn get_local_chain_parameters() -> Option<GenesisParameters> { idty_confirm_period: 40, idty_creation_period: 50, membership_period, - pending_membership_period: 500, + membership_renewal_period, ud_creation_period, ud_reeval_period, smith_cert_max_by_issuer: 8, diff --git a/node/src/chain_spec/gen_genesis_data.rs b/node/src/chain_spec/gen_genesis_data.rs index 56481245e..2565f3d4a 100644 --- a/node/src/chain_spec/gen_genesis_data.rs +++ b/node/src/chain_spec/gen_genesis_data.rs @@ -553,6 +553,15 @@ where G1_DUNITER_V1_MSVALIDITY as f32 / DUNITER_V1_DAYS as f32 ) } + if common_parameters.membership_membership_renewal_period / DAYS + != G1_DUNITER_V1_MSVALIDITY / DUNITER_V1_DAYS + { + warn!( + "parameter `membership_renewal_period` ({} days) is different from Ğ1's ({} days)", + common_parameters.membership_membership_renewal_period as f32 / DAYS as f32, + G1_DUNITER_V1_MSVALIDITY as f32 / DUNITER_V1_DAYS as f32 + ) + } if common_parameters.cert_cert_period / DAYS != G1_DUNITER_V1_SIGPERIOD / DUNITER_V1_DAYS { warn!( "parameter `cert_period` ({} days) is different from Ğ1's ({} days)", @@ -807,8 +816,8 @@ fn dump_genesis_info(info: GenesisInfo) { get_best_unit_and_diviser_for_blocks(p.identity_idty_creation_period); let (membership_membership_period, membership_membership_period_unit) = get_best_unit_and_diviser_for_blocks(p.membership_membership_period); - let (membership_pending_membership_period, membership_pending_membership_period_unit) = - get_best_unit_and_diviser_for_blocks(p.membership_pending_membership_period); + let (membership_membership_renewal_period, membership_membership_renewal_period_unit) = + get_best_unit_and_diviser_for_blocks(p.membership_membership_renewal_period); let (cert_cert_period, cert_cert_period_unit) = get_best_unit_and_diviser_for_blocks(p.cert_cert_period); let (cert_max_by_issuer, cert_max_by_issuer_unit) = @@ -857,7 +866,7 @@ fn dump_genesis_info(info: GenesisInfo) { - identity.change_owner_key_period: {} {} - identity.idty_creation_period: {} {} - membership.membership_period: {} {} - - membership.pending_membership_period: {} {} + - membership.membership_renewal_period: {} {} - cert.cert_period: {} {} - cert.max_by_issuer: {} {} - cert.min_received_cert_to_be_able_to_issue_cert: {} {} @@ -907,8 +916,8 @@ fn dump_genesis_info(info: GenesisInfo) { identity_idty_creation_period_unit, membership_membership_period, membership_membership_period_unit, - membership_pending_membership_period, - membership_pending_membership_period_unit, + membership_membership_renewal_period, + membership_membership_renewal_period_unit, cert_cert_period, cert_cert_period_unit, cert_max_by_issuer, @@ -1980,7 +1989,7 @@ pub struct CommonParameters { pub identity_change_owner_key_period: u32, pub identity_idty_creation_period: u32, pub membership_membership_period: u32, - pub membership_pending_membership_period: u32, + pub membership_membership_renewal_period: u32, pub cert_cert_period: u32, pub cert_max_by_issuer: u32, pub cert_min_received_cert_to_be_able_to_issue_cert: u32, diff --git a/node/src/chain_spec/gtest.rs b/node/src/chain_spec/gtest.rs index d01ee0cce..e7814f6d0 100644 --- a/node/src/chain_spec/gtest.rs +++ b/node/src/chain_spec/gtest.rs @@ -91,7 +91,7 @@ fn get_parameters(_: &Option<GenesisParameters>) -> CommonParameters { identity_change_owner_key_period: parameters::ChangeOwnerKeyPeriod::get(), identity_idty_creation_period: parameters::IdtyCreationPeriod::get(), membership_membership_period: parameters::MembershipPeriod::get(), - membership_pending_membership_period: parameters::PendingMembershipPeriod::get(), + membership_membership_renewal_period: parameters::MembershipRenewalPeriod::get(), cert_max_by_issuer: parameters::MaxByIssuer::get(), cert_min_received_cert_to_be_able_to_issue_cert: parameters::MinReceivedCertToBeAbleToIssueCert::get(), diff --git a/pallets/distance/src/benchmarking.rs b/pallets/distance/src/benchmarking.rs index 33ba9419b..c762bde19 100644 --- a/pallets/distance/src/benchmarking.rs +++ b/pallets/distance/src/benchmarking.rs @@ -50,16 +50,36 @@ benchmarks! { T: pallet_balances::Config, T::Balance: From<u64>, T::BlockNumber: From<u32>, } + + // request distance evaluation request_distance_evaluation { - let idty = T::IdtyIndex::one(); - let caller: T::AccountId = pallet_identity::Identities::<T>::get(idty).unwrap().owner_key; - let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); - let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); + let idty = T::IdtyIndex::one(); + let caller: T::AccountId = pallet_identity::Identities::<T>::get(idty).unwrap().owner_key; + let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); }: _<T::RuntimeOrigin>(caller_origin.clone()) verify { - assert!(IdentityDistanceStatus::<T>::get(idty) == Some((caller.clone(), DistanceStatus::Pending)), "Request not added"); + assert!(PendingEvaluationRequest::<T>::get(idty) == Some(caller.clone()), "Request not added"); assert_has_event::<T>(Event::<T>::EvaluationRequested { idty_index: idty, who: caller }.into()); } + + // request distance evaluation for + request_distance_evaluation_for { + let idty = T::IdtyIndex::one(); + let caller: T::AccountId = pallet_identity::Identities::<T>::get(idty).unwrap().owner_key; + let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); + let target = 2u32; + // set target status since targeted distance evaluation only allowed for unvalidated + pallet_identity::Identities::<T>::mutate(target, + |idty_val| idty_val.as_mut().unwrap().status = pallet_identity::IdtyStatus::Unvalidated); + }: _<T::RuntimeOrigin>(caller_origin.clone(), target) + verify { + assert!(PendingEvaluationRequest::<T>::get(target) == Some(caller.clone()), "Request not added"); + assert_has_event::<T>(Event::<T>::EvaluationRequested { idty_index: target, who: caller }.into()); + } + + // update evaluation update_evaluation { let digest_data = sp_consensus_babe::digests::PreDigest::SecondaryPlain( sp_consensus_babe::digests::SecondaryPlainPreDigest { authority_index: 0u32, slot: Default::default() }); @@ -71,27 +91,25 @@ benchmarks! { let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); let i in 1 .. MAX_EVALUATIONS_PER_SESSION => populate_pool::<T>(i)?; }: _<T::RuntimeOrigin>(RawOrigin::None.into(), ComputationResult{distances: vec![Perbill::one(); i as usize]}) - verify { - assert_has_event::<T>(Event::<T>::EvaluationUpdated { evaluator: caller }.into()); - } + + // force update evaluation force_update_evaluation { let idty = T::IdtyIndex::one(); let caller: T::AccountId = pallet_identity::Identities::<T>::get(idty).unwrap().owner_key; let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); let i in 1 .. MAX_EVALUATIONS_PER_SESSION => populate_pool::<T>(i)?; }: _<T::RuntimeOrigin>(RawOrigin::Root.into(), caller.clone(), ComputationResult{distances: vec![Perbill::one(); i as usize]}) + + // force valid distance status + force_valid_distance_status { + let idty = T::IdtyIndex::one(); + let caller: T::AccountId = pallet_identity::Identities::<T>::get(idty).unwrap().owner_key; + }: _<T::RuntimeOrigin>(RawOrigin::Root.into(), idty) verify { - assert_has_event::<T>(Event::<T>::EvaluationUpdated { evaluator: caller }.into()); - } - force_set_distance_status { - let idty = T::IdtyIndex::one(); - let caller: T::AccountId = pallet_identity::Identities::<T>::get(idty).unwrap().owner_key; - let status = Some((caller.clone(), DistanceStatus::Valid)); - }: _<T::RuntimeOrigin>(RawOrigin::Root.into(), idty, status.clone()) - verify { - assert!(IdentityDistanceStatus::<T>::get(idty) == Some((caller, DistanceStatus::Valid)), "Status not set"); - assert_has_event::<T>(Event::<T>::EvaluationStatusForced { idty_index: idty, status }.into()); + assert_has_event::<T>(Event::<T>::EvaluatedValid { idty_index: idty }.into()); } + + // on finalize on_finalize { DidUpdate::<T>::set(true); }: { Pallet::<T>::on_finalize(Default::default()); } diff --git a/pallets/distance/src/lib.rs b/pallets/distance/src/lib.rs index d7d131cd8..b7ee0e214 100644 --- a/pallets/distance/src/lib.rs +++ b/pallets/distance/src/lib.rs @@ -17,7 +17,7 @@ #![cfg_attr(not(feature = "std"), no_std)] mod median; -mod traits; +pub mod traits; mod types; mod weights; @@ -26,6 +26,8 @@ pub mod benchmarking; #[cfg(test)] mod mock; +#[cfg(test)] +mod tests; pub use pallet::*; pub use traits::*; @@ -37,6 +39,7 @@ use pallet_authority_members::SessionIndex; use sp_distance::{InherentError, INHERENT_IDENTIFIER}; use sp_inherents::{InherentData, InherentIdentifier}; use sp_std::convert::TryInto; +use sp_std::prelude::*; type IdtyIndex = u32; @@ -66,6 +69,7 @@ pub mod pallet { + pallet_identity::Config<IdtyIndex = IdtyIndex> + pallet_session::Config { + /// Currency type used in this pallet (used for reserve/slash) type Currency: ReservableCurrency<Self::AccountId>; /// Amount reserved during evaluation #[pallet::constant] @@ -79,12 +83,14 @@ pub mod pallet { /// Minimum ratio of accessible referees #[pallet::constant] type MinAccessibleReferees: Get<Perbill>; - /// Number of session to keep a positive evaluation result - type ResultExpiration: Get<u32>; /// 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; + /// Handler for successful distance evaluation + type OnValidDistanceStatus: OnValidDistanceStatus<Self>; + /// Trait to check that distance evaluation request is allowed + type CheckRequestDistanceEvaluation: CheckRequestDistanceEvaluation<Self>; } // STORAGE // @@ -128,35 +134,20 @@ pub mod pallet { pub type EvaluationBlock<T: Config> = StorageValue<_, <T as frame_system::Config>::Hash, ValueQuery>; - /// Distance evaluation status by identity + /// Pending evaluation requesters /// - /// * `.0` is the account who requested an evaluation and reserved the price, + /// account who requested an evaluation and reserved the price, /// for whom the price will be unreserved or slashed when the evaluation completes. - /// * `.1` is the status of the evaluation. #[pallet::storage] - #[pallet::getter(fn identity_distance_status)] - pub type IdentityDistanceStatus<T: Config> = StorageMap< + #[pallet::getter(fn pending_evaluation_request)] + pub type PendingEvaluationRequest<T: Config> = StorageMap< _, Twox64Concat, <T as pallet_identity::Config>::IdtyIndex, - (<T as frame_system::Config>::AccountId, DistanceStatus), + <T as frame_system::Config>::AccountId, OptionQuery, >; - /// Identities by distance status expiration session index - #[pallet::storage] - #[pallet::getter(fn distance_status_expire_on)] - pub type DistanceStatusExpireOn<T: Config> = StorageMap< - _, - Twox64Concat, - u32, - BoundedVec< - <T as pallet_identity::Config>::IdtyIndex, - ConstU32<MAX_EVALUATIONS_PER_SESSION>, - >, - ValueQuery, - >; - /// Did evaluation get updated in this block? #[pallet::storage] pub(super) type DidUpdate<T: Config> = StorageValue<_, bool, ValueQuery>; @@ -176,13 +167,10 @@ pub mod pallet { idty_index: T::IdtyIndex, who: T::AccountId, }, - /// A distance evaluation was updated. - EvaluationUpdated { evaluator: T::AccountId }, - /// A distance status was forced. - EvaluationStatusForced { - idty_index: T::IdtyIndex, - status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>, - }, + /// Distance rule was found valid + EvaluatedValid { idty_index: T::IdtyIndex }, + /// Distance rule was found invalid + EvaluatedInvalid { idty_index: T::IdtyIndex }, } // ERRORS // @@ -198,13 +186,21 @@ pub mod pallet { /// No author for this block. NoAuthor, /// Caller has no identity. - NoIdentity, + CallerHasNoIdentity, + /// Caller identity not found. + CallerIdentityNotFound, + /// Caller not member. + CallerNotMember, + /// Target identity not found. + TargetIdentityNotFound, /// Evaluation queue is full. QueueFull, /// Too many evaluators in the current evaluation pool. TooManyEvaluators, /// Evaluation result has a wrong length. WrongResultLength, + /// Targeted distance evaluation request is only possible for an unvalidated identity + DistanceRequestOnlyAllowedForUnvalidated, } #[pallet::hooks] @@ -228,32 +224,45 @@ pub mod pallet { #[pallet::call] impl<T: Config> Pallet<T> { - /// Request an identity to be evaluated + /// Request caller identity to be evaluated + /// positive evaluation will result in claim/renew membership + /// negative evaluation will result in slash for caller #[pallet::call_index(0)] #[pallet::weight(<T as pallet::Config>::WeightInfo::request_distance_evaluation())] pub fn request_distance_evaluation(origin: OriginFor<T>) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let idty = - pallet_identity::IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::NoIdentity)?; + let idty = Self::check_request_distance_evaluation_self(&who)?; - ensure!( - IdentityDistanceStatus::<T>::get(idty) - != Some((who.clone(), DistanceStatus::Pending)), - Error::<T>::AlreadyInEvaluation - ); + Pallet::<T>::do_request_distance_evaluation(&who, idty)?; + Ok(().into()) + } - Pallet::<T>::do_request_distance_evaluation(who, idty)?; + /// Request target identity to be evaluated + /// only possible for unvalidated identity + #[pallet::call_index(4)] + #[pallet::weight(<T as pallet::Config>::WeightInfo::request_distance_evaluation_for())] + pub fn request_distance_evaluation_for( + origin: OriginFor<T>, + target: T::IdtyIndex, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + Self::check_request_distance_evaluation_for(&who, target)?; + + Pallet::<T>::do_request_distance_evaluation(&who, target)?; Ok(().into()) } /// (Inherent) Push an evaluation result to the pool + /// this is called internally by validators (= inherent) #[pallet::call_index(1)] #[pallet::weight(<T as pallet::Config>::WeightInfo::update_evaluation(MAX_EVALUATIONS_PER_SESSION))] pub fn update_evaluation( origin: OriginFor<T>, computation_result: ComputationResult, ) -> DispatchResult { + // no origin = inherent ensure_none(origin)?; ensure!( !DidUpdate::<T>::exists(), @@ -267,7 +276,8 @@ pub mod pallet { Ok(()) } - /// Push an evaluation result to the pool + /// Force push an evaluation result to the pool + // (it is convenient to have this call in end2end tests) #[pallet::call_index(2)] #[pallet::weight(<T as pallet::Config>::WeightInfo::force_update_evaluation(MAX_EVALUATIONS_PER_SESSION))] pub fn force_update_evaluation( @@ -280,60 +290,21 @@ pub mod pallet { Pallet::<T>::do_update_evaluation(evaluator, computation_result) } - /// Set the distance evaluation status of an identity - /// - /// Removes the status if `status` is `None`. - /// - /// * `status.0` is the account for whom the price will be unreserved or slashed - /// when the evaluation completes. - /// * `status.1` is the status of the evaluation. + /// Force set the distance evaluation status of an identity + // (it is convenient to have this in test network) #[pallet::call_index(3)] - #[pallet::weight(<T as pallet::Config>::WeightInfo::force_set_distance_status())] - pub fn force_set_distance_status( + #[pallet::weight(<T as pallet::Config>::WeightInfo::force_valid_distance_status())] + pub fn force_valid_distance_status( origin: OriginFor<T>, identity: <T as pallet_identity::Config>::IdtyIndex, - status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>, ) -> DispatchResult { ensure_root(origin)?; - IdentityDistanceStatus::<T>::set(identity, status.clone()); - DistanceStatusExpireOn::<T>::mutate( - pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(), - move |identities| { - identities - .try_push(identity) - .map_err(|_| Error::<T>::TooManyEvaluationsInBlock) - }, - )?; - Self::deposit_event(Event::EvaluationStatusForced { - idty_index: identity, - status, - }); + Self::do_valid_distance_status(identity); Ok(()) } } - // BENCHMARK FUNCTIONS // - - impl<T: Config> Pallet<T> { - /// Force the distance status using IdtyIndex and AccountId - /// only to prepare identity for benchmarking. - pub fn set_distance_status( - identity: <T as pallet_identity::Config>::IdtyIndex, - status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>, - ) -> DispatchResult { - IdentityDistanceStatus::<T>::set(identity, status); - DistanceStatusExpireOn::<T>::mutate( - pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(), - move |identities| { - identities - .try_push(identity) - .map_err(|_| Error::<T>::TooManyEvaluationsInBlock.into()) - }, - ) - } - } - // INTERNAL FUNCTIONS // impl<T: Config> Pallet<T> { @@ -398,55 +369,110 @@ pub mod pallet { } } + /// check that request distance evaluation is allowed + fn check_request_distance_evaluation_self( + who: &T::AccountId, + ) -> Result<<T as pallet_identity::Config>::IdtyIndex, DispatchError> { + // caller has an identity + let idty_index = pallet_identity::IdentityIndexOf::<T>::get(who) + .ok_or(Error::<T>::CallerHasNoIdentity)?; + Self::check_request_distance_evaluation_common(idty_index)?; + Ok(idty_index) + } + + /// check that targeted request distance evaluation is allowed + fn check_request_distance_evaluation_for( + who: &T::AccountId, + target: <T as pallet_identity::Config>::IdtyIndex, + ) -> Result<(), DispatchError> { + // caller has an identity + let caller_idty_index = pallet_identity::IdentityIndexOf::<T>::get(who) + .ok_or(Error::<T>::CallerHasNoIdentity)?; + let caller_idty = pallet_identity::Identities::<T>::get(caller_idty_index) + .ok_or(Error::<T>::CallerIdentityNotFound)?; + // caller is member + ensure!( + caller_idty.status == pallet_identity::IdtyStatus::Member, + Error::<T>::CallerNotMember + ); + // target has an identity + let target_idty = pallet_identity::Identities::<T>::get(target) + .ok_or(Error::<T>::TargetIdentityNotFound)?; + // target is unvalidated + ensure!( + target_idty.status == pallet_identity::IdtyStatus::Unvalidated, + Error::<T>::DistanceRequestOnlyAllowedForUnvalidated + ); + Self::check_request_distance_evaluation_common(target)?; + Ok(()) + } + + // common checks between check_request_distance_evaluation _self and _for + fn check_request_distance_evaluation_common( + target: <T as pallet_identity::Config>::IdtyIndex, + ) -> Result<(), DispatchError> { + // no pending evaluation request + ensure!( + PendingEvaluationRequest::<T>::get(target).is_none(), + Error::<T>::AlreadyInEvaluation + ); + // external validation + // target has received enough certifications + T::CheckRequestDistanceEvaluation::check_request_distance_evaluation(target) + } + + /// request distance evaluation in current pool fn do_request_distance_evaluation( - who: T::AccountId, + who: &T::AccountId, idty_index: <T as pallet_identity::Config>::IdtyIndex, ) -> Result<(), DispatchError> { Pallet::<T>::mutate_current_pool( - pallet_session::CurrentIndex::<T>::get(), + pallet_session::CurrentIndex::<T>::get(), // TODO look |current_pool| { + // TODO not needed if called in extrinsics only + // since extrinsics are transactional by default ensure!( current_pool.evaluations.len() < (MAX_EVALUATIONS_PER_SESSION as usize), Error::<T>::QueueFull ); - T::Currency::reserve(&who, <T as Config>::EvaluationPrice::get())?; + T::Currency::reserve(who, <T as Config>::EvaluationPrice::get())?; current_pool .evaluations .try_push((idty_index, median::MedianAcc::new())) .map_err(|_| Error::<T>::QueueFull)?; - IdentityDistanceStatus::<T>::insert( - idty_index, - (&who, DistanceStatus::Pending), - ); + PendingEvaluationRequest::<T>::insert(idty_index, who); - DistanceStatusExpireOn::<T>::mutate( - pallet_session::CurrentIndex::<T>::get() + T::ResultExpiration::get(), - move |identities| identities.try_push(idty_index).ok(), - ); - Self::deposit_event(Event::EvaluationRequested { idty_index, who }); + Self::deposit_event(Event::EvaluationRequested { + idty_index, + who: who.clone(), + }); Ok(()) }, ) } + /// update distance evaluation in next pool fn do_update_evaluation( evaluator: <T as frame_system::Config>::AccountId, computation_result: ComputationResult, ) -> DispatchResult { Pallet::<T>::mutate_next_pool(pallet_session::CurrentIndex::<T>::get(), |result_pool| { + // evaluation must be provided for all identities (no more, no less) ensure!( computation_result.distances.len() == result_pool.evaluations.len(), Error::<T>::WrongResultLength ); + // insert the evaluator if not already there if result_pool .evaluators .try_insert(evaluator.clone()) .map_err(|_| Error::<T>::TooManyEvaluators)? { + // update the median accumulator with the new result for (distance_value, (_identity, median_acc)) in computation_result .distances .into_iter() @@ -454,23 +480,28 @@ pub mod pallet { { median_acc.push(distance_value); } - - Self::deposit_event(Event::EvaluationUpdated { evaluator }); Ok(()) } else { + // one author can only submit one evaluation Err(Error::<T>::TooManyEvaluationsByAuthor.into()) } }) } + + /// Set the distance status using IdtyIndex and AccountId + pub fn do_valid_distance_status(idty: <T as pallet_identity::Config>::IdtyIndex) { + // callback + T::OnValidDistanceStatus::on_valid_distance_status(idty); + // deposit event + Self::deposit_event(Event::EvaluatedValid { idty_index: idty }); + } } impl<T: Config> pallet_authority_members::OnNewSession for Pallet<T> { fn on_new_session(index: SessionIndex) { + // set evaluation block EvaluationBlock::<T>::set(frame_system::Pallet::<T>::parent_hash()); - // Make results expire - DistanceStatusExpireOn::<T>::remove(index); - // Apply the results from the current pool (which was previous session's result pool) // We take the results so the pool is left empty for the new session. #[allow(clippy::type_complexity)] @@ -479,35 +510,48 @@ pub mod pallet { <T as pallet_identity::Config>::IdtyIndex, > = Pallet::<T>::take_current_pool(index); for (idty, median_acc) in current_pool.evaluations.into_iter() { + // distance result + let mut distance_result: Option<bool> = None; + + // get result of the computation if let Some(median_result) = median_acc.get_median() { let median = match median_result { MedianResult::One(m) => m, MedianResult::Two(m1, m2) => m1 + (m2 - m1) / 2, // Avoid overflow (since max is 1) }; - IdentityDistanceStatus::<T>::mutate(idty, |entry| { - if let Some((account_id, status)) = entry.as_mut() { - if median >= T::MinAccessibleReferees::get() { - T::Currency::unreserve( - account_id, - <T as Config>::EvaluationPrice::get(), - ); - *status = DistanceStatus::Valid; - } else { - T::Currency::slash_reserved( - account_id, - <T as Config>::EvaluationPrice::get(), - ); - *status = DistanceStatus::Invalid; - } + // update distance result + distance_result = Some(median >= T::MinAccessibleReferees::get()); + } + + // take requester and perform unreserve or slash + if let Some(requester) = PendingEvaluationRequest::<T>::take(idty) { + match distance_result { + None => { + // no result, unreserve + T::Currency::unreserve( + &requester, + <T as Config>::EvaluationPrice::get(), + ); } - }); - } else if let Some((account_id, _status)) = IdentityDistanceStatus::<T>::take(idty) - { - <T as Config>::Currency::unreserve( - &account_id, - <T as Config>::EvaluationPrice::get(), - ); + Some(true) => { + // positive result, unreserve and apply + T::Currency::unreserve( + &requester, + <T as Config>::EvaluationPrice::get(), + ); + Self::do_valid_distance_status(idty); + } + Some(false) => { + // negative result, slash and deposit event + T::Currency::slash_reserved( + &requester, + <T as Config>::EvaluationPrice::get(), + ); + Self::deposit_event(Event::EvaluatedInvalid { idty_index: idty }); + } + } } + // if evaluation happened without request, it's ok to do nothing } } } diff --git a/pallets/distance/src/mock.rs b/pallets/distance/src/mock.rs index ef477a8c8..6861b3c43 100644 --- a/pallets/distance/src/mock.rs +++ b/pallets/distance/src/mock.rs @@ -19,7 +19,7 @@ use crate::{self as pallet_distance}; use core::marker::PhantomData; use frame_support::{ parameter_types, - traits::{Everything, GenesisBuild}, + traits::{Everything, GenesisBuild, OnFinalize, OnInitialize}, }; use frame_system as system; use pallet_balances::AccountData; @@ -256,9 +256,10 @@ impl pallet_distance::Config for Test { type EvaluationPrice = frame_support::traits::ConstU64<1000>; type MaxRefereeDistance = frame_support::traits::ConstU32<5>; type MinAccessibleReferees = MinAccessibleReferees; - type ResultExpiration = frame_support::traits::ConstU32<720>; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); + type OnValidDistanceStatus = (); + type CheckRequestDistanceEvaluation = (); } // Build genesis storage according to the mock runtime. @@ -290,3 +291,14 @@ pub fn new_test_ext() -> sp_io::TestExternalities { sp_io::TestExternalities::new(t) } + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + Session::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Session::on_initialize(System::block_number()); + } +} diff --git a/pallets/distance/src/tests.rs b/pallets/distance/src/tests.rs new file mode 100644 index 000000000..b1be3c04a --- /dev/null +++ b/pallets/distance/src/tests.rs @@ -0,0 +1,41 @@ +// Copyright 2023 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +use crate::mock::*; +use crate::*; +use frame_support::assert_ok; +use frame_support::traits::Currency; + +#[test] +fn test_request_distance_evaluation() { + new_test_ext().execute_with(|| { + run_to_block(1); + // give enough for reserve + Balances::make_free_balance_be(&1, 10_000); + + // call request + assert_ok!(Distance::request_distance_evaluation( + RuntimeOrigin::signed(1) + )); + System::assert_has_event(RuntimeEvent::Distance(Event::EvaluationRequested { + idty_index: 1, + who: 1, + })); + + // currency was reserved + assert_eq!(Balances::reserved_balance(1), 1000); + }); +} diff --git a/pallets/distance/src/traits.rs b/pallets/distance/src/traits.rs index de9d532d6..6849d9c25 100644 --- a/pallets/distance/src/traits.rs +++ b/pallets/distance/src/traits.rs @@ -14,8 +14,24 @@ // 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/>. -pub trait HandleNegativeEvaluation<T: crate::Config> { - /// Do something with the reserved amount on the account, - /// when distance evaluation result is negative. - fn handle_negative_evaluation(account_id: T::AccountId); +use crate::*; +use frame_support::pallet_prelude::*; + +pub trait OnValidDistanceStatus<T: Config> { + /// Handler for valid distance evaluation + fn on_valid_distance_status(idty_index: T::IdtyIndex); +} + +impl<T: Config> OnValidDistanceStatus<T> for () { + fn on_valid_distance_status(_idty_index: T::IdtyIndex) {} +} + +pub trait CheckRequestDistanceEvaluation<T: Config> { + fn check_request_distance_evaluation(idty_index: T::IdtyIndex) -> Result<(), DispatchError>; +} + +impl<T: Config> CheckRequestDistanceEvaluation<T> for () { + fn check_request_distance_evaluation(_idty_index: T::IdtyIndex) -> Result<(), DispatchError> { + Ok(()) + } } diff --git a/pallets/distance/src/weights.rs b/pallets/distance/src/weights.rs index b0808361a..0e8f34c45 100644 --- a/pallets/distance/src/weights.rs +++ b/pallets/distance/src/weights.rs @@ -20,102 +20,82 @@ use frame_support::weights::{constants::RocksDbWeight, Weight}; pub trait WeightInfo { fn request_distance_evaluation() -> Weight; + fn request_distance_evaluation_for() -> Weight; fn update_evaluation(i: u32) -> Weight; fn force_update_evaluation(i: u32) -> Weight; - fn force_set_distance_status() -> Weight; + fn force_valid_distance_status() -> Weight; fn on_finalize() -> Weight; } // Insecure weights implementation, use it for tests only! impl WeightInfo for () { - /// Storage: Identity IdentityIndexOf (r:1 w:0) - /// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured) - /// Storage: Distance IdentityDistanceStatus (r:1 w:1) - /// Proof Skipped: Distance IdentityDistanceStatus (max_values: None, max_size: None, mode: Measured) - /// Storage: Session CurrentIndex (r:1 w:0) - /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Distance EvaluationPool2 (r:1 w:1) - /// Proof Skipped: Distance EvaluationPool2 (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) - /// Storage: Distance DistanceStatusExpireOn (r:1 w:1) - /// Proof Skipped: Distance DistanceStatusExpireOn (max_values: None, max_size: None, mode: Measured) fn request_distance_evaluation() -> Weight { // Proof Size summary in bytes: - // Measured: `935` - // Estimated: `4400` - // Minimum execution time: 28_469_000 picoseconds. - Weight::from_parts(30_905_000, 0) - .saturating_add(Weight::from_parts(0, 4400)) - .saturating_add(RocksDbWeight::get().reads(6)) - .saturating_add(RocksDbWeight::get().writes(4)) + // Measured: `1280` + // Estimated: `4745` + // Minimum execution time: 876_053_000 picoseconds. + Weight::from_parts(898_445_000, 0) + .saturating_add(Weight::from_parts(0, 4745)) + .saturating_add(RocksDbWeight::get().reads(8)) + .saturating_add(RocksDbWeight::get().writes(3)) + } + + fn request_distance_evaluation_for() -> Weight { + // Proof Size summary in bytes: + // Measured: `1485` + // Estimated: `7425` + // Minimum execution time: 1_118_982_000 picoseconds. + Weight::from_parts(1_292_782_000, 0) + .saturating_add(Weight::from_parts(0, 7425)) + .saturating_add(RocksDbWeight::get().reads(10)) + .saturating_add(RocksDbWeight::get().writes(3)) } - /// Storage: Distance DidUpdate (r:1 w:1) - /// Proof Skipped: Distance DidUpdate (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Authorship Author (r:1 w:1) - /// Proof: Authorship Author (max_values: Some(1), max_size: Some(32), added: 527, mode: MaxEncodedLen) - /// Storage: System Digest (r:1 w:0) - /// Proof Skipped: System Digest (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Session CurrentIndex (r:1 w:0) - /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Distance EvaluationPool0 (r:1 w:1) - /// Proof Skipped: Distance EvaluationPool0 (max_values: Some(1), max_size: None, mode: Measured) - /// The range of component `i` is `[1, 600]`. + fn update_evaluation(i: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `744 + i * (10 ±0)` - // Estimated: `2228 + i * (10 ±0)` - // Minimum execution time: 13_870_000 picoseconds. - Weight::from_parts(17_116_748, 0) - .saturating_add(Weight::from_parts(0, 2228)) - // Standard Error: 684 - .saturating_add(Weight::from_parts(128_989, 0).saturating_mul(i.into())) - .saturating_add(RocksDbWeight::get().reads(5)) + // Measured: `773 + i * (10 ±0)` + // Estimated: `2256 + i * (10 ±0)` + // Minimum execution time: 463_878_000 picoseconds. + Weight::from_parts(743_823_548, 0) + .saturating_add(Weight::from_parts(0, 2256)) + // Standard Error: 292_144 + .saturating_add(Weight::from_parts(1_326_639, 0).saturating_mul(i.into())) + .saturating_add(RocksDbWeight::get().reads(6)) .saturating_add(RocksDbWeight::get().writes(3)) .saturating_add(Weight::from_parts(0, 10).saturating_mul(i.into())) } - /// Storage: Session CurrentIndex (r:1 w:0) - /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Distance EvaluationPool0 (r:1 w:1) - /// Proof Skipped: Distance EvaluationPool0 (max_values: Some(1), max_size: None, mode: Measured) - /// The range of component `i` is `[1, 600]`. + fn force_update_evaluation(i: u32) -> Weight { // Proof Size summary in bytes: // Measured: `612 + i * (10 ±0)` - // Estimated: `2096 + i * (10 ±0)` - // Minimum execution time: 8_392_000 picoseconds. - Weight::from_parts(10_825_908, 0) - .saturating_add(Weight::from_parts(0, 2096)) - // Standard Error: 326 - .saturating_add(Weight::from_parts(123_200, 0).saturating_mul(i.into())) + // Estimated: `2095 + i * (10 ±0)` + // Minimum execution time: 208_812_000 picoseconds. + Weight::from_parts(257_150_521, 0) + .saturating_add(Weight::from_parts(0, 2095)) + // Standard Error: 53_366 + .saturating_add(Weight::from_parts(1_841_329, 0).saturating_mul(i.into())) .saturating_add(RocksDbWeight::get().reads(2)) .saturating_add(RocksDbWeight::get().writes(1)) .saturating_add(Weight::from_parts(0, 10).saturating_mul(i.into())) } - /// Storage: Session CurrentIndex (r:1 w:0) - /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Distance DistanceStatusExpireOn (r:1 w:1) - /// Proof Skipped: Distance DistanceStatusExpireOn (max_values: None, max_size: None, mode: Measured) - /// Storage: Distance IdentityDistanceStatus (r:0 w:1) - /// Proof Skipped: Distance IdentityDistanceStatus (max_values: None, max_size: None, mode: Measured) - fn force_set_distance_status() -> Weight { + + fn force_valid_distance_status() -> Weight { // Proof Size summary in bytes: - // Measured: `586` - // Estimated: `4051` - // Minimum execution time: 8_099_000 picoseconds. - Weight::from_parts(8_786_000, 0) - .saturating_add(Weight::from_parts(0, 4051)) - .saturating_add(RocksDbWeight::get().reads(2)) - .saturating_add(RocksDbWeight::get().writes(2)) + // Measured: `1181` + // Estimated: `7121` + // Minimum execution time: 873_892_000 picoseconds. + Weight::from_parts(1_081_510_000, 0) + .saturating_add(Weight::from_parts(0, 7121)) + .saturating_add(RocksDbWeight::get().reads(7)) + .saturating_add(RocksDbWeight::get().writes(5)) } - /// Storage: Distance DidUpdate (r:1 w:1) - /// Proof Skipped: Distance DidUpdate (max_values: Some(1), max_size: None, mode: Measured) + fn on_finalize() -> Weight { // Proof Size summary in bytes: // Measured: `170` // Estimated: `1655` - // Minimum execution time: 3_904_000 picoseconds. - Weight::from_parts(4_132_000, 0) + // Minimum execution time: 93_595_000 picoseconds. + Weight::from_parts(109_467_000, 0) .saturating_add(Weight::from_parts(0, 1655)) .saturating_add(RocksDbWeight::get().reads(1)) .saturating_add(RocksDbWeight::get().writes(1)) diff --git a/pallets/duniter-test-parameters/src/lib.rs b/pallets/duniter-test-parameters/src/lib.rs index 4da26974b..55ca1c612 100644 --- a/pallets/duniter-test-parameters/src/lib.rs +++ b/pallets/duniter-test-parameters/src/lib.rs @@ -45,7 +45,7 @@ pub mod types { pub idty_confirm_period: BlockNumber, pub idty_creation_period: BlockNumber, pub membership_period: BlockNumber, - pub pending_membership_period: BlockNumber, + pub membership_renewal_period: BlockNumber, pub ud_creation_period: PeriodCount, pub ud_reeval_period: PeriodCount, pub smith_cert_max_by_issuer: CertCount, diff --git a/pallets/duniter-wot/src/lib.rs b/pallets/duniter-wot/src/lib.rs index eb127c0cc..079467a3f 100644 --- a/pallets/duniter-wot/src/lib.rs +++ b/pallets/duniter-wot/src/lib.rs @@ -30,8 +30,6 @@ mod benchmarking;*/ pub use pallet::*; -use traits::*; - use frame_support::pallet_prelude::*; use pallet_certification::traits::SetNextIssuableOn; use pallet_identity::{IdtyEvent, IdtyStatus}; @@ -61,8 +59,6 @@ pub mod pallet { + pallet_identity::Config<IdtyIndex = IdtyIndex> + pallet_membership::Config<IdtyId = IdtyIndex> { - /// Distance evaluation provider - type IsDistanceOk: IsDistanceOk<IdtyIndex>; #[pallet::constant] type FirstIssuableOn: Get<Self::BlockNumber>; #[pallet::constant] @@ -97,8 +93,8 @@ pub mod pallet { DistanceEvaluationPending, /// Distance evaluation has not been requested DistanceEvaluationNotRequested, - /// Identity is not allowed to request membership. - IdtyNotAllowedToRequestMembership, + /// Identity is not allowed to claim membership. + IdtyNotAllowedToClaimMembership, /// Identity not allowed to renew membership. IdtyNotAllowedToRenewMembership, /// Identity creation period not respected. @@ -119,6 +115,10 @@ pub mod pallet { CertToRevoked, /// Issuer or receiver not found. IdtyNotFound, + /// Not enough certs received to request distance evaluation. + NotEnoughCertsReceivedToRequestDistanceEvaluation, + /// Membership can only be renewed after an antispam delay + MembershipRenewalPeriodNotRespected, } } @@ -191,26 +191,34 @@ impl<T: Config> pallet_certification::traits::CheckCertAllowed<IdtyIndex> for Pa impl<T: Config> sp_membership::traits::CheckMembershipCallAllowed<IdtyIndex> for Pallet<T> { // membership claim is only possible when enough certs are received (both wots) and distance is ok fn check_idty_allowed_to_claim_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::Unvalidated + || idty_value.status == IdtyStatus::NotMember, + Error::<T>::IdtyNotAllowedToClaimMembership + ); + // check received certifications + // TODO deduplicate let idty_cert_meta = pallet_certification::Pallet::<T>::idty_cert_meta(idty_index); ensure!( idty_cert_meta.received_count >= T::MinCertForMembership::get(), Error::<T>::NotEnoughCertsToClaimMembership ); - T::IsDistanceOk::is_distance_ok(idty_index)?; Ok(()) } // membership renewal is only possible when identity is member (otherwise it should claim again) fn check_idty_allowed_to_renew_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> { - if let Some(idty_value) = pallet_identity::Pallet::<T>::identity(idty_index) { - ensure!( - idty_value.status == IdtyStatus::Member, - Error::<T>::IdtyNotAllowedToRenewMembership - ); - T::IsDistanceOk::is_distance_ok(idty_index)?; - } else { - return Err(Error::<T>::IdtyNotFound.into()); - } + // check identity status + let idty_value = + pallet_identity::Pallet::<T>::identity(idty_index).ok_or(Error::<T>::IdtyNotFound)?; + ensure!( + idty_value.status == IdtyStatus::Member, + Error::<T>::IdtyNotAllowedToRenewMembership + ); + // no need to check certification count since loosing certifications make membership expire Ok(()) } } @@ -321,3 +329,86 @@ impl<T: Config> pallet_certification::traits::OnRemovedCert<IdtyIndex> for Palle } } } + +/// valid distance status handler +impl<T: Config + pallet_distance::Config> pallet_distance::traits::OnValidDistanceStatus<T> + for Pallet<T> +{ + 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 { + // identity was removed before distance status was found + // so it's ok to do nothing + sp_std::if_std! { + println!("identity was removed before distance status was found: {:?}", idty_index); + } + } + } +} + +/// distance evaluation request allowed check +impl<T: Config + pallet_distance::Config> pallet_distance::traits::CheckRequestDistanceEvaluation<T> + for Pallet<T> +{ + fn check_request_distance_evaluation(idty_index: IdtyIndex) -> Result<(), DispatchError> { + // check membership renewal antispam + 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 antispam + 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 cert count + // TODO deduplicate + let idty_cert_meta = pallet_certification::Pallet::<T>::idty_cert_meta(idty_index); + ensure!( + idty_cert_meta.received_count >= T::MinCertForMembership::get(), + Error::<T>::NotEnoughCertsReceivedToRequestDistanceEvaluation + ); + Ok(()) + } +} diff --git a/pallets/duniter-wot/src/mock.rs b/pallets/duniter-wot/src/mock.rs index c043ef764..d2679e1b9 100644 --- a/pallets/duniter-wot/src/mock.rs +++ b/pallets/duniter-wot/src/mock.rs @@ -48,7 +48,7 @@ frame_support::construct_runtime!( System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, DuniterWot: pallet_duniter_wot::{Pallet}, Identity: pallet_identity::{Pallet, Call, Config<T>, Storage, Event<T>}, - Membership: pallet_membership::{Pallet, Call, Config<T>, Storage, Event<T>}, + Membership: pallet_membership::{Pallet, Config<T>, Storage, Event<T>}, Cert: pallet_certification::{Pallet, Call, Config<T>, Storage, Event<T>}, } ); @@ -97,7 +97,6 @@ impl pallet_duniter_wot::Config for Test { type MinCertForMembership = MinCertForMembership; type MinCertForCreateIdtyRight = MinCertForCreateIdtyRight; type FirstIssuableOn = FirstIssuableOn; - type IsDistanceOk = crate::traits::DistanceAlwaysOk; } // Identity @@ -139,6 +138,7 @@ impl pallet_identity::Config for Test { // Membership parameter_types! { pub const MembershipPeriod: u64 = 8; + pub const MembershipRenewalPeriod: u64 = 2; } impl pallet_membership::Config for Test { @@ -147,6 +147,7 @@ impl pallet_membership::Config for Test { type IdtyIdOf = IdentityIndexOf<Self>; type AccountIdOf = (); type MembershipPeriod = MembershipPeriod; + type MembershipRenewalPeriod = MembershipRenewalPeriod; type OnEvent = DuniterWot; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); diff --git a/pallets/duniter-wot/src/tests.rs b/pallets/duniter-wot/src/tests.rs index 2b86ee97f..fc4f4df0e 100644 --- a/pallets/duniter-wot/src/tests.rs +++ b/pallets/duniter-wot/src/tests.rs @@ -123,7 +123,7 @@ fn test_new_idty_validation() { // Ferdie should be able to claim membership run_to_block(5); - assert_ok!(Membership::claim_membership(RuntimeOrigin::signed(6)),); + assert_ok!(Membership::try_add_membership(6)); System::assert_has_event(RuntimeEvent::Membership( pallet_membership::Event::MembershipAdded { member: 6, @@ -242,9 +242,9 @@ fn test_idty_membership_expire() { run_to_block(4); // Alice renews her membership - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(1))); + assert_ok!(Membership::try_renew_membership(1)); // Bob renews his membership - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2))); + assert_ok!(Membership::try_renew_membership(2)); run_to_block(5); // renew certifications so that Alice can still issue cert at block 22 @@ -253,7 +253,7 @@ fn test_idty_membership_expire() { // Charlie's membership should expire at block #8 run_to_block(8); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(1))); + assert_ok!(Membership::try_renew_membership(1)); assert!(Membership::membership(3).is_none()); System::assert_has_event(RuntimeEvent::Membership( @@ -276,7 +276,7 @@ fn test_idty_membership_expire() { // 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::renew_membership(RuntimeOrigin::signed(1))); + 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::IdtyRevoked { @@ -356,15 +356,15 @@ fn test_certification_expire() { assert_ok!(Cert::add_cert(RuntimeOrigin::signed(3), 3, 2)); // --- BLOCK 7 --- run_to_block(7); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(1))); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2))); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(3))); + 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::renew_membership(RuntimeOrigin::signed(1))); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2))); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(3))); + 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); @@ -387,18 +387,18 @@ fn test_certification_expire() { // --- BLOCK 21 --- // Bob and Charlie can renew their membership run_to_block(21); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2))); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(3))); + 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::renew_membership(RuntimeOrigin::signed(1)), + 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::claim_membership(RuntimeOrigin::signed(1)), + Membership::try_add_membership(1), pallet_duniter_wot::Error::<Test>::NotEnoughCertsToClaimMembership ); @@ -436,16 +436,16 @@ fn test_cert_can_not_be_issued() { assert_ok!(Cert::add_cert(RuntimeOrigin::signed(4), 4, 3)); // +20 // --- BLOCK 7 --- run_to_block(7); - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(1))); // + 8 - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2))); // + 8 - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(3))); // + 8 - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(4))); // + 8 + 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::renew_membership(RuntimeOrigin::signed(1))); // + 8 - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(2))); // + 8 - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(3))); // + 8 - assert_ok!(Membership::renew_membership(RuntimeOrigin::signed(4))); // + 8 + 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()); diff --git a/pallets/duniter-wot/src/traits.rs b/pallets/duniter-wot/src/traits.rs index 6934ce454..f3474822b 100644 --- a/pallets/duniter-wot/src/traits.rs +++ b/pallets/duniter-wot/src/traits.rs @@ -13,17 +13,3 @@ // // 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::DispatchError; - -pub trait IsDistanceOk<IdtyId> { - fn is_distance_ok(idty_id: &IdtyId) -> Result<(), DispatchError>; -} - -pub struct DistanceAlwaysOk; - -impl<IdtyId> IsDistanceOk<IdtyId> for DistanceAlwaysOk { - fn is_distance_ok(_idty_id: &IdtyId) -> Result<(), DispatchError> { - Ok(()) - } -} diff --git a/pallets/membership/src/benchmarking.rs b/pallets/membership/src/benchmarking.rs index 1bdf5edf6..84216da89 100644 --- a/pallets/membership/src/benchmarking.rs +++ b/pallets/membership/src/benchmarking.rs @@ -20,17 +20,15 @@ use super::*; use frame_benchmarking::benchmarks; use frame_system::pallet_prelude::BlockNumberFor; -use frame_system::RawOrigin; -use sp_runtime::traits::{Convert, One}; #[cfg(test)] use maplit::btreemap; use crate::Pallet; -fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) { - frame_system::Pallet::<T>::assert_has_event(generic_event.into()); -} +// fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) { +// frame_system::Pallet::<T>::assert_has_event(generic_event.into()); +// } benchmarks! { where_clause { @@ -39,39 +37,7 @@ benchmarks! { <T as frame_system::Config>::BlockNumber: From<u32>, } - // claim membership - claim_membership { - let idty: T::IdtyId = 3.into(); - Membership::<T>::take(idty); - let caller: T::AccountId = T::AccountIdOf::convert(idty).unwrap(); - let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); - T::BenchmarkSetupHandler::force_status_ok(&idty, &caller); - }: _<T::RuntimeOrigin>(caller_origin) - verify { - assert_has_event::<T>(Event::<T>::MembershipAdded{member: idty, expire_on: BlockNumberFor::<T>::one() + T::MembershipPeriod::get()}.into()); - } - - // renew membership - renew_membership { - let idty: T::IdtyId = 3.into(); - let caller: T::AccountId = T::AccountIdOf::convert(idty).unwrap(); - let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); - T::BenchmarkSetupHandler::force_status_ok(&idty, &caller); - }: _<T::RuntimeOrigin>(caller_origin) - verify { - assert_has_event::<T>(Event::<T>::MembershipAdded{member: idty, expire_on: BlockNumberFor::<T>::one() + T::MembershipPeriod::get()}.into()); - } - - // revoke membership - revoke_membership { - let idty: T::IdtyId = 3.into(); - let caller: T::AccountId = T::AccountIdOf::convert(idty).unwrap(); - let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); - frame_system::pallet::Pallet::<T>::set_block_number(10_000_000.into()); // Arbitrarily high, to be in the worst case of wot instance. - }: _<T::RuntimeOrigin>(caller_origin) - verify { - assert_has_event::<T>(Event::<T>::MembershipRemoved{member: idty, reason: MembershipRemovalReason::Revoked}.into()); - } + // TODO membership add and renewal should be included to distance on_new_session as worst case scenario // Base weight of an empty initialize on_initialize { @@ -98,7 +64,7 @@ benchmarks! { impl_benchmark_test_suite!( Pallet, - crate::mock::new_test_ext(crate::mock::DefaultMembershipConfig { + crate::mock::new_test_ext(crate::mock::MembershipConfig { memberships: btreemap![ 3 => MembershipData { expire_on: 3, diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs index cba39e37e..38f1c491c 100644 --- a/pallets/membership/src/lib.rs +++ b/pallets/membership/src/lib.rs @@ -32,9 +32,7 @@ pub use pallet::*; pub use weights::WeightInfo; use frame_support::dispatch::Weight; -use frame_support::error::BadOrigin; use frame_support::pallet_prelude::*; -use frame_system::RawOrigin; use sp_membership::traits::*; use sp_membership::MembershipData; use sp_runtime::traits::Zero; @@ -44,13 +42,13 @@ use std::collections::BTreeMap; #[cfg(feature = "runtime-benchmarks")] pub trait SetupBenchmark<IdtyId, AccountId> { - fn force_status_ok(idty_index: &IdtyId, account: &AccountId); + fn force_valid_distance_status(idty_index: &IdtyId); fn add_cert(_issuer: &IdtyId, _receiver: &IdtyId); } #[cfg(feature = "runtime-benchmarks")] impl<IdtyId, AccountId> SetupBenchmark<IdtyId, AccountId> for () { - fn force_status_ok(_idty_id: &IdtyId, _account: &AccountId) {} + fn force_valid_distance_status(_idty_id: &IdtyId) {} fn add_cert(_issuer: &IdtyId, _receiver: &IdtyId) {} } @@ -94,8 +92,12 @@ pub mod pallet { /// Something that gives the AccountId of an IdtyId type AccountIdOf: Convert<Self::IdtyId, Option<Self::AccountId>>; #[pallet::constant] - /// Maximum life span of a non-renewable membership (in number of blocks) + /// Maximum life span of a single membership (in number of blocks) + // TODO this could be renamed "validity" or "duration" type MembershipPeriod: Get<Self::BlockNumber>; + /// Minimum delay to wait before renewing membership + // i.e. asking for distance evaluation + type MembershipRenewalPeriod: Get<Self::BlockNumber>; /// On event handler type OnEvent: OnEvent<Self::IdtyId>; /// Because this pallet emits events, it depends on the runtime's definition of an event. @@ -156,6 +158,11 @@ pub mod pallet { member: T::IdtyId, expire_on: BlockNumberFor<T>, }, + /// A membership was renewed. + MembershipRenewed { + member: T::IdtyId, + expire_on: BlockNumberFor<T>, + }, /// A membership was removed. MembershipRemoved { member: T::IdtyId, @@ -173,6 +180,8 @@ pub mod pallet { MembershipAlreadyAcquired, /// Membership not found. MembershipNotFound, + /// Already member, can not claim membership + AlreadyMember, } // HOOKS // @@ -188,61 +197,13 @@ pub mod pallet { } } - // CALLS // - - #[pallet::call] - impl<T: Config> Pallet<T> { - /// claim membership - /// it must fullfill the requirements (certs, distance) - /// TODO #159 for main wot claim_membership is called automatically when distance is evaluated positively - /// for smith wot, it means joining the authority members - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::claim_membership())] - pub fn claim_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo { - // get identity - let idty_id = Self::get_idty_id(origin)?; - - Self::check_allowed_to_claim(idty_id)?; - Self::do_add_membership(idty_id); - Ok(().into()) - } - - /// extend the validity period of an active membership - #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::renew_membership())] - pub fn renew_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo { - // Verify phase - let idty_id = Self::get_idty_id(origin)?; - let membership_data = - Membership::<T>::get(idty_id).ok_or(Error::<T>::MembershipNotFound)?; - - T::CheckMembershipCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?; - - // apply phase - Self::unschedule_membership_expiry(idty_id, membership_data.expire_on); - Self::insert_membership_and_schedule_expiry(idty_id); - T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id)); - - Ok(().into()) - } - - /// revoke an active membership - /// (only available for sub wot, automatic for main wot) - #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::revoke_membership())] - pub fn revoke_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo { - // Verify phase - let idty_id = Self::get_idty_id(origin)?; - - // Apply phase - Self::do_remove_membership(idty_id, MembershipRemovalReason::Revoked); - - Ok(().into()) - } - } + // // CALLS // + // #[pallet::call] + // impl<T: Config> Pallet<T> { + // // no calls for membership pallet + // } // INTERNAL FUNCTIONS // - impl<T: Config> Pallet<T> { /// unschedule membership expiry fn unschedule_membership_expiry(idty_id: T::IdtyId, block_number: T::BlockNumber) { @@ -254,31 +215,78 @@ pub mod pallet { } } /// schedule membership expiry - fn insert_membership_and_schedule_expiry(idty_id: T::IdtyId) { + fn insert_membership_and_schedule_expiry(idty_id: T::IdtyId) -> T::BlockNumber { let block_number = frame_system::pallet::Pallet::<T>::block_number(); let expire_on = block_number + T::MembershipPeriod::get(); Membership::<T>::insert(idty_id, MembershipData { expire_on }); MembershipsExpireOn::<T>::append(expire_on, idty_id); - Self::deposit_event(Event::MembershipAdded { - member: idty_id, - expire_on, - }); + expire_on } /// check that membership can be claimed pub fn check_allowed_to_claim(idty_id: T::IdtyId) -> Result<(), DispatchError> { + // no-op is error + ensure!( + Membership::<T>::get(idty_id).is_none(), + Error::<T>::AlreadyMember + ); + // enough certifications and distance rule for example T::CheckMembershipCallAllowed::check_idty_allowed_to_claim_membership(&idty_id)?; Ok(()) } + /// check that membership can be renewed + pub fn check_allowed_to_renew( + idty_id: T::IdtyId, + ) -> Result<MembershipData<T::BlockNumber>, DispatchError> { + let membership_data = + Membership::<T>::get(idty_id).ok_or(Error::<T>::MembershipNotFound)?; + + // enough certifications and distance rule for example + T::CheckMembershipCallAllowed::check_idty_allowed_to_renew_membership(&idty_id)?; + Ok(membership_data) + } + + /// try claim membership + pub fn try_add_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> { + Self::check_allowed_to_claim(idty_id)?; + Self::do_add_membership(idty_id); + Ok(()) + } + + /// try renew membership + pub fn try_renew_membership(idty_id: T::IdtyId) -> Result<(), DispatchError> { + let membership_data = Self::check_allowed_to_renew(idty_id)?; + Self::do_renew_membership(idty_id, membership_data); + Ok(()) + } + /// perform membership addition fn do_add_membership(idty_id: T::IdtyId) { - Self::insert_membership_and_schedule_expiry(idty_id); + let expire_on = Self::insert_membership_and_schedule_expiry(idty_id); + Self::deposit_event(Event::MembershipAdded { + member: idty_id, + expire_on, + }); T::OnEvent::on_event(&sp_membership::Event::MembershipAdded(idty_id)); } + /// perform membership renewal + fn do_renew_membership( + idty_id: T::IdtyId, + membership_data: MembershipData<T::BlockNumber>, + ) { + Self::unschedule_membership_expiry(idty_id, membership_data.expire_on); + let expire_on = Self::insert_membership_and_schedule_expiry(idty_id); + Self::deposit_event(Event::MembershipRenewed { + member: idty_id, + expire_on, + }); + T::OnEvent::on_event(&sp_membership::Event::MembershipRenewed(idty_id)); + } + /// perform membership removal pub fn do_remove_membership(idty_id: T::IdtyId, reason: MembershipRemovalReason) { if let Some(membership_data) = Membership::<T>::take(idty_id) { @@ -291,15 +299,6 @@ pub mod pallet { } } - /// check the origin and get identity id if valid - fn get_idty_id(origin: OriginFor<T>) -> Result<T::IdtyId, DispatchError> { - if let Ok(RawOrigin::Signed(account_id)) = origin.into() { - T::IdtyIdOf::convert(account_id).ok_or_else(|| Error::<T>::IdtyIdNotFound.into()) - } else { - Err(BadOrigin.into()) - } - } - /// perform the membership expiry scheduled at given block pub fn expire_memberships(block_number: T::BlockNumber) -> Weight { let mut expired_idty_count = 0u32; diff --git a/pallets/membership/src/mock.rs b/pallets/membership/src/mock.rs index 5173ff716..a13325a0c 100644 --- a/pallets/membership/src/mock.rs +++ b/pallets/membership/src/mock.rs @@ -41,7 +41,7 @@ frame_support::construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, - DefaultMembership: pallet_membership::{Pallet, Call, Event<T>, Storage, Config<T>}, + Membership: pallet_membership::{Pallet, Event<T>, Storage, Config<T>}, } ); @@ -79,7 +79,7 @@ impl system::Config for Test { parameter_types! { pub const MembershipPeriod: BlockNumber = 5; - pub const PendingMembershipPeriod: BlockNumber = 3; + pub const MembershipRenewalPeriod: BlockNumber = 2; } impl pallet_membership::Config for Test { @@ -88,6 +88,7 @@ impl pallet_membership::Config for Test { type IdtyIdOf = ConvertInto; type AccountIdOf = ConvertInto; type MembershipPeriod = MembershipPeriod; + type MembershipRenewalPeriod = MembershipRenewalPeriod; type OnEvent = (); type RuntimeEvent = RuntimeEvent; type WeightInfo = (); @@ -99,7 +100,7 @@ impl pallet_membership::Config for Test { pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::TestExternalities { GenesisConfig { system: SystemConfig::default(), - default_membership: gen_conf, + membership: gen_conf, } .build_storage() .unwrap() @@ -108,11 +109,11 @@ pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io:: pub fn run_to_block(n: u64) { while System::block_number() < n { - DefaultMembership::on_finalize(System::block_number()); + Membership::on_finalize(System::block_number()); System::on_finalize(System::block_number()); System::reset_events(); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); - DefaultMembership::on_initialize(System::block_number()); + Membership::on_initialize(System::block_number()); } } diff --git a/pallets/membership/src/tests.rs b/pallets/membership/src/tests.rs index 53da60336..4f22c3213 100644 --- a/pallets/membership/src/tests.rs +++ b/pallets/membership/src/tests.rs @@ -22,11 +22,8 @@ use maplit::btreemap; use sp_membership::traits::*; use sp_membership::MembershipData; -// alias -type RtEvent = RuntimeEvent; - -fn default_gen_conf() -> DefaultMembershipConfig { - DefaultMembershipConfig { +fn default_gen_conf() -> MembershipConfig { + MembershipConfig { memberships: btreemap![ 0 => MembershipData { expire_on: 3, @@ -41,10 +38,10 @@ fn test_genesis_build() { run_to_block(1); // Verify state assert_eq!( - DefaultMembership::membership(0), + Membership::membership(0), Some(MembershipData { expire_on: 3 }) ); - assert_eq!(DefaultMembership::members_count(), 1); + assert_eq!(Membership::members_count(), 1); }); } @@ -55,42 +52,39 @@ fn test_membership_expiration() { new_test_ext(default_gen_conf()).execute_with(|| { // Membership 0 should not expired on block #2 run_to_block(2); - assert!(DefaultMembership::is_member(&0)); + assert!(Membership::is_member(&0)); // Membership 0 should expire on block #3 run_to_block(3); - assert!(!DefaultMembership::is_member(&0)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipRemoved { + assert!(!Membership::is_member(&0)); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved { member: 0, reason: MembershipRemovalReason::Expired, })); }); } -/// test membership renewal -// there is no limit for membership renewal outside wot rules (number of certs, distance rule) +/// test membership renewal (triggered automatically after distance evaluation) #[test] fn test_membership_renewal() { new_test_ext(default_gen_conf()).execute_with(|| { // membership still valid at block 2 run_to_block(2); - assert!(DefaultMembership::is_member(&0)); + assert!(Membership::is_member(&0)); // Membership 0 can be renewed - assert_ok!(DefaultMembership::renew_membership(RuntimeOrigin::signed( - 0 - ),)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipAdded { + assert_ok!(Membership::try_renew_membership(0)); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRenewed { member: 0, expire_on: 2 + <Test as crate::Config>::MembershipPeriod::get(), })); // membership should not expire at block 3 to 6 because it has been renewed run_to_block(3); - assert!(DefaultMembership::is_member(&0)); + assert!(Membership::is_member(&0)); run_to_block(6); - assert!(DefaultMembership::is_member(&0)); + assert!(Membership::is_member(&0)); // membership should expire at block 7 (2+5) run_to_block(7); - assert!(!DefaultMembership::is_member(&0)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipRemoved { + assert!(!Membership::is_member(&0)); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved { member: 0, reason: MembershipRemovalReason::Expired, })); @@ -102,14 +96,14 @@ fn test_membership_renewal() { fn test_membership_renewal_nope() { new_test_ext(default_gen_conf()).execute_with(|| { run_to_block(2); - assert!(!DefaultMembership::is_member(&1)); + assert!(!Membership::is_member(&1)); // Membership 1 can not be renewed assert_noop!( - DefaultMembership::renew_membership(RuntimeOrigin::signed(1)), + Membership::try_renew_membership(1), Error::<Test>::MembershipNotFound, ); run_to_block(3); - assert!(!DefaultMembership::is_member(&1)); + assert!(!Membership::is_member(&1)); }); } @@ -119,21 +113,17 @@ fn test_membership_revocation() { new_test_ext(default_gen_conf()).execute_with(|| { run_to_block(1); // Membership 0 can be revocable on block #1 - assert_ok!(DefaultMembership::revoke_membership(RuntimeOrigin::signed( - 0 - ),)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipRemoved { + Membership::do_remove_membership(0, MembershipRemovalReason::Revoked); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved { member: 0, reason: MembershipRemovalReason::Revoked, })); - assert_eq!(DefaultMembership::membership(0), None); + assert_eq!(Membership::membership(0), None); // Membership 0 can re-claim membership run_to_block(5); - assert_ok!(DefaultMembership::claim_membership(RuntimeOrigin::signed( - 0 - ),)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipAdded { + assert_ok!(Membership::try_add_membership(0)); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipAdded { member: 0, expire_on: 5 + <Test as crate::Config>::MembershipPeriod::get(), })); @@ -149,28 +139,24 @@ fn test_membership_workflow() { new_test_ext(Default::default()).execute_with(|| { // - Then, idty 0 claim membership run_to_block(2); - assert_ok!(DefaultMembership::claim_membership(RuntimeOrigin::signed( - 0 - ),)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipAdded { + assert_ok!(Membership::try_add_membership(0)); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipAdded { member: 0, expire_on: 2 + <Test as crate::Config>::MembershipPeriod::get(), })); // - Then, idty 0 claim renewal, should success run_to_block(2); - assert_ok!(DefaultMembership::renew_membership(RuntimeOrigin::signed( - 0 - ),)); + assert_ok!(Membership::try_renew_membership(0)); // idty 0 should still be member until membership period ended run_to_block(6); // 2 + 5 - 1 - assert!(DefaultMembership::is_member(&0)); + assert!(Membership::is_member(&0)); // - Then, idty 0 should expire after membership period run_to_block(7); // 2 + 5 - assert!(!DefaultMembership::is_member(&0)); - System::assert_has_event(RtEvent::DefaultMembership(Event::MembershipRemoved { + assert!(!Membership::is_member(&0)); + System::assert_has_event(RuntimeEvent::Membership(Event::MembershipRemoved { member: 0, reason: MembershipRemovalReason::Expired, })); diff --git a/pallets/universal-dividend/src/lib.rs b/pallets/universal-dividend/src/lib.rs index 25de8fb02..be744570e 100644 --- a/pallets/universal-dividend/src/lib.rs +++ b/pallets/universal-dividend/src/lib.rs @@ -99,6 +99,8 @@ pub mod pallet { #[pallet::type_value] pub fn DefaultForCurrentUdIndex() -> UdIndex { + // FIXME seems off by 1 + // or rename to "next" ud index instead of "current" ud index 1 } @@ -110,6 +112,9 @@ pub mod pallet { #[cfg(test)] #[pallet::storage] + // UD should be linked to idtyid instead of accountid + // if it is convenient in test, why not have it in runtime also? + // storing it in idty_value.data is strange pub type TestMembers<T: Config> = StorageMap< _, Blake2_128Concat, @@ -263,6 +268,7 @@ pub mod pallet { // Increment ud index let ud_index = CurrentUdIndex::<T>::mutate(|next_ud_index| { + // FIXME seems off by 1 due to default to 1 core::mem::replace(next_ud_index, next_ud_index.saturating_add(1)) }); diff --git a/resources/gdev.yaml b/resources/gdev.yaml index a90145753..fcfd98034 100644 --- a/resources/gdev.yaml +++ b/resources/gdev.yaml @@ -27,8 +27,8 @@ parameters: cert_validity_period: 2102400 # Validity duration of a membership. 1051200 blocks = 73 days. membership_period: 1051200 - # Validity duration of a pending membership. 172800 blocks = 12 days. - pending_membership_period: 172800 + # Period to wait before membership renewal. 1051200 blocks = 1 days. + membership_renewal_period: 14400 # Delay a new member must observe before being able to emit a certification wot_first_cert_issuable_on: 0 # Number of required received certs to become a member @@ -68,4 +68,5 @@ sudo_key: "5CfodrEFe64MJtWvfhTHYBuUySr4WXLv2B41mZFucTXSGMFA" treasury_funder_address: "5E6q47RRGZU15LjUiBTm2DZjpqFKAjRNafYS8YV8AzTQZtLG" # The technical committee members, to act as sudo -technical_committee: ["Pini", "moul", "HugoTrentesaux", "tuxmain", "1000i100", "vit", "cgeek"] \ No newline at end of file +technical_committee: + ["Pini", "moul", "HugoTrentesaux", "tuxmain", "1000i100", "vit", "cgeek"] diff --git a/resources/metadata.scale b/resources/metadata.scale index 15ad56874e147a9b94c30468250afd196b8504ec..5908806517d61badcdd60a81e867552b7a21ea01 100644 GIT binary patch delta 8034 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 Or!Y#g3bOXFvH}25ss%d$ delta 8275 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 diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs index 6bae43eb5..d0ce866c0 100644 --- a/runtime/common/src/pallets_config.rs +++ b/runtime/common/src/pallets_config.rs @@ -453,7 +453,6 @@ macro_rules! pallets_config { impl pallet_duniter_wot::Config for Runtime { type FirstIssuableOn = WotFirstCertIssuableOn; - type IsDistanceOk = common_runtime::providers::MainWotIsDistanceOk<Runtime>; type MinCertForMembership = WotMinCertForMembership; type MinCertForCreateIdtyRight = WotMinCertForCreateIdtyRight; } @@ -488,6 +487,7 @@ macro_rules! pallets_config { type IdtyIdOf = common_runtime::providers::IdentityIndexOf<Self>; type AccountIdOf = common_runtime::providers::IdentityAccountIdProvider<Self>; type MembershipPeriod = MembershipPeriod; + type MembershipRenewalPeriod = MembershipRenewalPeriod; type OnEvent = (OnMembershipEventHandler<Wot, Runtime>, Wot); type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_membership::WeightInfo<Runtime>; @@ -516,9 +516,10 @@ macro_rules! pallets_config { type EvaluationPrice = frame_support::traits::ConstU64<1000>; type MaxRefereeDistance = frame_support::traits::ConstU32<5>; type MinAccessibleReferees = MinAccessibleReferees; - type ResultExpiration = frame_support::traits::ConstU32<720>; type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_distance::WeightInfo<Runtime>; + type OnValidDistanceStatus = Wot; + type CheckRequestDistanceEvaluation = Wot; } // SMITH-MEMBERS diff --git a/runtime/common/src/providers.rs b/runtime/common/src/providers.rs index ca631988e..504cf92f8 100644 --- a/runtime/common/src/providers.rs +++ b/runtime/common/src/providers.rs @@ -17,7 +17,6 @@ use crate::{entities::IdtyData, AccountId, IdtyIndex}; use core::marker::PhantomData; use pallet_universal_dividend::FirstEligibleUd; -use sp_runtime::DispatchError; pub struct IdentityAccountIdProvider<Runtime>(PhantomData<Runtime>); @@ -71,31 +70,6 @@ where } } -pub struct MainWotIsDistanceOk<T>(PhantomData<T>); - -impl<T> pallet_duniter_wot::traits::IsDistanceOk<<T as pallet_identity::Config>::IdtyIndex> - for MainWotIsDistanceOk<T> -where - T: pallet_distance::Config + pallet_duniter_wot::Config, -{ - fn is_distance_ok( - idty_id: &<T as pallet_identity::Config>::IdtyIndex, - ) -> Result<(), DispatchError> { - match pallet_distance::Pallet::<T>::identity_distance_status(idty_id) { - Some((_, status)) => match status { - pallet_distance::DistanceStatus::Valid => Ok(()), - pallet_distance::DistanceStatus::Invalid => { - Err(pallet_duniter_wot::Error::<T>::DistanceIsInvalid.into()) - } - pallet_distance::DistanceStatus::Pending => { - Err(pallet_duniter_wot::Error::<T>::DistanceEvaluationPending.into()) - } - }, - None => Err(pallet_duniter_wot::Error::<T>::DistanceEvaluationNotRequested.into()), - } - } -} - pub struct IsWoTMemberProvider<T>(PhantomData<T>); impl<T: pallet_smith_members::Config> sp_runtime::traits::IsMember<<T as pallet_membership::Config>::IdtyId> @@ -121,14 +95,8 @@ macro_rules! impl_benchmark_setup_handler { T: pallet_certification::Config, <T as pallet_certification::Config>::IdtyIndex: From<u32>, { - fn force_status_ok( - idty_id: &IdtyIndex, - account: &<T as frame_system::Config>::AccountId, - ) -> () { - let _ = pallet_distance::Pallet::<T>::set_distance_status( - *idty_id, - Some((account.clone(), pallet_distance::DistanceStatus::Valid)), - ); + fn force_valid_distance_status(idty_id: &IdtyIndex) -> () { + let _ = pallet_distance::Pallet::<T>::do_valid_distance_status(*idty_id); } fn add_cert(issuer: &IdtyIndex, receiver: &IdtyIndex) { let _ = pallet_certification::Pallet::<T>::do_add_cert_checked( diff --git a/runtime/common/src/weights/pallet_distance.rs b/runtime/common/src/weights/pallet_distance.rs index 27aa94475..c1c3c8c55 100644 --- a/runtime/common/src/weights/pallet_distance.rs +++ b/runtime/common/src/weights/pallet_distance.rs @@ -17,19 +17,19 @@ //! Autogenerated weights for `pallet_distance` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2024-01-15, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-12-20, STEPS: `8`, REPEAT: `4`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `bgallois-ms7d43`, CPU: `12th Gen Intel(R) Core(TM) i3-12100F` +//! HOSTNAME: `squirrel`, CPU: `Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// target/release/duniter +// ./target/debug/duniter // benchmark // pallet // --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=* +// --steps=8 +// --repeat=4 +// --pallet=pallet-distance // --extrinsic=* // --execution=wasm // --wasm-execution=compiled @@ -50,25 +50,57 @@ pub struct WeightInfo<T>(PhantomData<T>); impl<T: frame_system::Config> pallet_distance::WeightInfo for WeightInfo<T> { /// Storage: Identity IdentityIndexOf (r:1 w:0) /// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured) - /// Storage: Distance IdentityDistanceStatus (r:1 w:1) - /// Proof Skipped: Distance IdentityDistanceStatus (max_values: None, max_size: None, mode: Measured) + /// Storage: Distance PendingEvaluationRequest (r:1 w:1) + /// Proof Skipped: Distance PendingEvaluationRequest (max_values: None, max_size: None, mode: Measured) + /// Storage: Distance ValidEvaluationResult (r:1 w:0) + /// Proof Skipped: Distance ValidEvaluationResult (max_values: None, max_size: None, mode: Measured) + /// Storage: Cert StorageIdtyCertMeta (r:1 w:0) + /// Proof Skipped: Cert StorageIdtyCertMeta (max_values: None, max_size: None, mode: Measured) + /// Storage: Parameters ParametersStorage (r:1 w:0) + /// Proof Skipped: Parameters ParametersStorage (max_values: Some(1), max_size: None, mode: Measured) /// Storage: Session CurrentIndex (r:1 w:0) /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) /// Storage: Distance EvaluationPool2 (r:1 w:1) /// Proof Skipped: Distance EvaluationPool2 (max_values: Some(1), max_size: None, mode: Measured) /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) - /// Storage: Distance DistanceStatusExpireOn (r:1 w:1) - /// Proof Skipped: Distance DistanceStatusExpireOn (max_values: None, max_size: None, mode: Measured) fn request_distance_evaluation() -> Weight { // Proof Size summary in bytes: - // Measured: `939` - // Estimated: `4404` - // Minimum execution time: 33_043_000 picoseconds. - Weight::from_parts(33_977_000, 0) - .saturating_add(Weight::from_parts(0, 4404)) - .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().writes(4)) + // Measured: `1280` + // Estimated: `4745` + // Minimum execution time: 876_053_000 picoseconds. + Weight::from_parts(898_445_000, 0) + .saturating_add(Weight::from_parts(0, 4745)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(3)) + } + /// Storage: Identity IdentityIndexOf (r:1 w:0) + /// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured) + /// Storage: Identity Identities (r:2 w:0) + /// Proof Skipped: Identity Identities (max_values: None, max_size: None, mode: Measured) + /// Storage: Distance PendingEvaluationRequest (r:1 w:1) + /// Proof Skipped: Distance PendingEvaluationRequest (max_values: None, max_size: None, mode: Measured) + /// Storage: Distance ValidEvaluationResult (r:1 w:0) + /// Proof Skipped: Distance ValidEvaluationResult (max_values: None, max_size: None, mode: Measured) + /// Storage: Cert StorageIdtyCertMeta (r:1 w:0) + /// Proof Skipped: Cert StorageIdtyCertMeta (max_values: None, max_size: None, mode: Measured) + /// Storage: Parameters ParametersStorage (r:1 w:0) + /// Proof Skipped: Parameters ParametersStorage (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Session CurrentIndex (r:1 w:0) + /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Distance EvaluationPool2 (r:1 w:1) + /// Proof Skipped: Distance EvaluationPool2 (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) + fn request_distance_evaluation_for() -> Weight { + // Proof Size summary in bytes: + // Measured: `1485` + // Estimated: `7425` + // Minimum execution time: 1_118_982_000 picoseconds. + Weight::from_parts(1_292_782_000, 0) + .saturating_add(Weight::from_parts(0, 7425)) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: Distance DidUpdate (r:1 w:1) /// Proof Skipped: Distance DidUpdate (max_values: Some(1), max_size: None, mode: Measured) @@ -85,13 +117,13 @@ impl<T: frame_system::Config> pallet_distance::WeightInfo for WeightInfo<T> { /// The range of component `i` is `[1, 600]`. fn update_evaluation(i: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `772 + i * (10 ±0)` + // Measured: `773 + i * (10 ±0)` // Estimated: `2256 + i * (10 ±0)` - // Minimum execution time: 22_135_000 picoseconds. - Weight::from_parts(27_043_883, 0) + // Minimum execution time: 463_878_000 picoseconds. + Weight::from_parts(743_823_548, 0) .saturating_add(Weight::from_parts(0, 2256)) - // Standard Error: 1_421 - .saturating_add(Weight::from_parts(125_435, 0).saturating_mul(i.into())) + // Standard Error: 292_144 + .saturating_add(Weight::from_parts(1_326_639, 0).saturating_mul(i.into())) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(3)) .saturating_add(Weight::from_parts(0, 10).saturating_mul(i.into())) @@ -103,32 +135,40 @@ impl<T: frame_system::Config> pallet_distance::WeightInfo for WeightInfo<T> { /// The range of component `i` is `[1, 600]`. fn force_update_evaluation(i: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `611 + i * (10 ±0)` + // Measured: `612 + i * (10 ±0)` // Estimated: `2095 + i * (10 ±0)` - // Minimum execution time: 13_033_000 picoseconds. - Weight::from_parts(15_741_933, 0) + // Minimum execution time: 208_812_000 picoseconds. + Weight::from_parts(257_150_521, 0) .saturating_add(Weight::from_parts(0, 2095)) - // Standard Error: 693 - .saturating_add(Weight::from_parts(121_989, 0).saturating_mul(i.into())) + // Standard Error: 53_366 + .saturating_add(Weight::from_parts(1_841_329, 0).saturating_mul(i.into())) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) .saturating_add(Weight::from_parts(0, 10).saturating_mul(i.into())) } /// Storage: Session CurrentIndex (r:1 w:0) /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Distance DistanceStatusExpireOn (r:1 w:1) - /// Proof Skipped: Distance DistanceStatusExpireOn (max_values: None, max_size: None, mode: Measured) - /// Storage: Distance IdentityDistanceStatus (r:0 w:1) - /// Proof Skipped: Distance IdentityDistanceStatus (max_values: None, max_size: None, mode: Measured) - fn force_set_distance_status() -> Weight { + /// Storage: Distance ValidEvaluationExpireOn (r:1 w:1) + /// Proof Skipped: Distance ValidEvaluationExpireOn (max_values: None, max_size: None, mode: Measured) + /// Storage: Identity Identities (r:1 w:0) + /// Proof Skipped: Identity Identities (max_values: None, max_size: None, mode: Measured) + /// Storage: Membership Membership (r:1 w:1) + /// Proof Skipped: Membership Membership (max_values: None, max_size: None, mode: Measured) + /// Storage: Membership MembershipsExpireOn (r:2 w:2) + /// Proof Skipped: Membership MembershipsExpireOn (max_values: None, max_size: None, mode: Measured) + /// Storage: Parameters ParametersStorage (r:1 w:0) + /// Proof Skipped: Parameters ParametersStorage (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Distance ValidEvaluationResult (r:0 w:1) + /// Proof Skipped: Distance ValidEvaluationResult (max_values: None, max_size: None, mode: Measured) + fn force_valid_distance_status() -> Weight { // Proof Size summary in bytes: - // Measured: `585` - // Estimated: `4050` - // Minimum execution time: 12_886_000 picoseconds. - Weight::from_parts(13_465_000, 0) - .saturating_add(Weight::from_parts(0, 4050)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `1181` + // Estimated: `7121` + // Minimum execution time: 873_892_000 picoseconds. + Weight::from_parts(1_081_510_000, 0) + .saturating_add(Weight::from_parts(0, 7121)) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(5)) } /// Storage: Distance DidUpdate (r:1 w:1) /// Proof Skipped: Distance DidUpdate (max_values: Some(1), max_size: None, mode: Measured) @@ -136,8 +176,8 @@ impl<T: frame_system::Config> pallet_distance::WeightInfo for WeightInfo<T> { // Proof Size summary in bytes: // Measured: `170` // Estimated: `1655` - // Minimum execution time: 3_830_000 picoseconds. - Weight::from_parts(4_065_000, 0) + // Minimum execution time: 93_595_000 picoseconds. + Weight::from_parts(109_467_000, 0) .saturating_add(Weight::from_parts(0, 1655)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) diff --git a/runtime/g1/src/lib.rs b/runtime/g1/src/lib.rs index 40a3025ac..8c9bca67c 100644 --- a/runtime/g1/src/lib.rs +++ b/runtime/g1/src/lib.rs @@ -174,9 +174,6 @@ impl Contains<RuntimeCall> for BaseCallFilter { call, RuntimeCall::System( frame_system::Call::remark { .. } | frame_system::Call::remark_with_event { .. } - ) | RuntimeCall::Membership( - pallet_membership::Call::claim_membership { .. } - | pallet_membership::Call::revoke_membership { .. } ) | RuntimeCall::Session(_) ) } @@ -287,7 +284,7 @@ construct_runtime!( // Web Of Trust Wot: pallet_duniter_wot::{Pallet} = 40, Identity: pallet_identity::{Pallet, Call, Config<T>, Storage, Event<T>} = 41, - Membership: pallet_membership::{Pallet, Call, Config<T>, Storage, Event<T>} = 42, + Membership: pallet_membership::{Pallet, Config<T>, Storage, Event<T>} = 42, Certification: pallet_certification::{Pallet, Call, Config<T>, Storage, Event<T>} = 43, Distance: pallet_distance::{Pallet, Call, Storage, Inherent, Event<T>} = 44, diff --git a/runtime/g1/src/parameters.rs b/runtime/g1/src/parameters.rs index 22ef4efa4..adfc0183a 100644 --- a/runtime/g1/src/parameters.rs +++ b/runtime/g1/src/parameters.rs @@ -105,7 +105,7 @@ parameter_types! { // Membership parameter_types! { pub const MembershipPeriod: BlockNumber = YEARS; - pub const PendingMembershipPeriod: BlockNumber = 2 * MONTHS; + pub const MembershipRenewalPeriod: BlockNumber = 2 * MONTHS; } // Certification diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs index 04366d5db..d64153d64 100644 --- a/runtime/gdev/src/lib.rs +++ b/runtime/gdev/src/lib.rs @@ -177,10 +177,9 @@ impl Contains<RuntimeCall> for BaseCallFilter { fn contains(call: &RuntimeCall) -> bool { !matches!( call, - // in main web of trust, membership request and revoke are handeled through identity pallet - // the user can not call them directly - RuntimeCall::Membership(pallet_membership::Call::revoke_membership { .. }) - | RuntimeCall::Session(_) + // session calls can not be called directly + // it should be done through authority-members pallet + RuntimeCall::Session(_) ) } } @@ -257,6 +256,7 @@ common_runtime::pallets_config! { pub type ConfirmPeriod = pallet_duniter_test_parameters::IdtyConfirmPeriod<Runtime>; pub type IdtyCreationPeriod = pallet_duniter_test_parameters::IdtyCreationPeriod<Runtime>; pub type MembershipPeriod = pallet_duniter_test_parameters::MembershipPeriod<Runtime>; + pub type MembershipRenewalPeriod = pallet_duniter_test_parameters::MembershipRenewalPeriod<Runtime>; pub type UdCreationPeriod = pallet_duniter_test_parameters::UdCreationPeriod<Runtime>; pub type UdReevalPeriod = pallet_duniter_test_parameters::UdReevalPeriod<Runtime>; pub type WotFirstCertIssuableOn = pallet_duniter_test_parameters::WotFirstCertIssuableOn<Runtime>; @@ -329,7 +329,7 @@ construct_runtime!( // Web Of Trust Wot: pallet_duniter_wot::{Pallet} = 40, Identity: pallet_identity::{Pallet, Call, Config<T>, Storage, Event<T>} = 41, - Membership: pallet_membership::{Pallet, Call, Config<T>, Storage, Event<T>} = 42, + Membership: pallet_membership::{Pallet, Config<T>, Storage, Event<T>} = 42, Certification: pallet_certification::{Pallet, Call, Config<T>, Storage, Event<T>} = 43, Distance: pallet_distance::{Pallet, Call, Storage, Inherent, Event<T>} = 44, diff --git a/runtime/gdev/src/parameters.rs b/runtime/gdev/src/parameters.rs index 7dc4e8177..abd164fe6 100644 --- a/runtime/gdev/src/parameters.rs +++ b/runtime/gdev/src/parameters.rs @@ -90,6 +90,11 @@ frame_support::parameter_types! { pub const ChangeOwnerKeyPeriod: BlockNumber = 7 * DAYS; } +// Membership +frame_support::parameter_types! { + pub const SmithMembershipRenewalPeriod: BlockNumber = MONTHS; +} + /*************/ /* UTILITIES */ /*************/ diff --git a/runtime/gdev/tests/common/mod.rs b/runtime/gdev/tests/common/mod.rs index 261755f75..0ca8aa74c 100644 --- a/runtime/gdev/tests/common/mod.rs +++ b/runtime/gdev/tests/common/mod.rs @@ -105,7 +105,7 @@ impl ExtBuilder { idty_confirm_period: 40, idty_creation_period: 50, membership_period: 100, - pending_membership_period: 500, + membership_renewal_period: 10, ud_creation_period: 60_000, ud_reeval_period: 60_000 * 20, smith_cert_max_by_issuer: 8, diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs index 49755e0c4..6f1dbc89d 100644 --- a/runtime/gdev/tests/integration_tests.rs +++ b/runtime/gdev/tests/integration_tests.rs @@ -311,7 +311,7 @@ fn test_remove_identity() { }); } -/// test identity is validated when membership is claimed +/// test identity is "validated" (= membership is claimed) when distance is evaluated positively #[test] fn test_validate_identity_when_claim() { ExtBuilder::new(1, 3, 4) @@ -346,11 +346,14 @@ fn test_validate_identity_when_claim() { 5 )); + // eve request distance evaluation for herself assert_ok!(Distance::request_distance_evaluation( frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), )); - run_to_block(51); // Pass 2 sessions + // Pass 2 sessions + run_to_block(51); + // simulate an evaluation published by smith Alice assert_ok!(Distance::force_update_evaluation( frame_system::RawOrigin::Root.into(), AccountKeyring::Alice.to_account_id(), @@ -358,20 +361,142 @@ fn test_validate_identity_when_claim() { distances: vec![Perbill::one()], } )); - run_to_block(76); // Pass 1 session + run_to_block(75); // Pass 1 session + System::assert_has_event(RuntimeEvent::Distance( + pallet_distance::Event::EvaluatedValid { idty_index: 5 }, + )); + + // eve can not claim her membership manually because it is done automatically + // the following call does not exist anymore + // assert_noop!( + // Membership::claim_membership( + // frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + // ), + // pallet_membership::Error::<Runtime>::AlreadyMember + // ); + + // println!("{:?}", System::events()); + System::assert_has_event(RuntimeEvent::Membership( + pallet_membership::Event::MembershipAdded { + member: 5, + expire_on: 75 + <Runtime as pallet_membership::Config>::MembershipPeriod::get(), + }, + )); + }); +} - // eve can claim her membership - assert_ok!(Membership::claim_membership( +/// test identity creation workflow +// with distance requested by last certifier +#[test] +fn test_identity_creation_workflow() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Charlie.to_account_id(), 10_000), // necessary for evalation distance reserve + (AccountKeyring::Eve.to_account_id(), 2_000), + (AccountKeyring::Ferdie.to_account_id(), 1_000), + ]) + .build() + .execute_with(|| { + run_to_block(1); + // alice create identity for Eve + assert_ok!(Identity::create_identity( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + AccountKeyring::Eve.to_account_id(), + )); + assert_eq!( + Identity::identity(5), + Some(pallet_identity::IdtyValue { + data: Default::default(), + next_creatable_identity_on: 0u32, + old_owner_key: None, + owner_key: AccountKeyring::Eve.to_account_id(), + next_scheduled: 1 + 40, + status: pallet_identity::IdtyStatus::Unconfirmed, + }) + ); + run_to_block(2); + // eve confirms her identity + assert_ok!(Identity::confirm_identity( frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + "Eeeeeveeeee".into(), + )); + assert_eq!( + Identity::identity(5), + Some(pallet_identity::IdtyValue { + data: Default::default(), + next_creatable_identity_on: 0u32, + old_owner_key: None, + owner_key: AccountKeyring::Eve.to_account_id(), + next_scheduled: 2 + 876600, + status: pallet_identity::IdtyStatus::Unvalidated, + }) + ); + run_to_block(3); + // eve gets certified by bob and charlie + assert_ok!(Certification::add_cert( + frame_system::RawOrigin::Signed(AccountKeyring::Bob.to_account_id()).into(), + 2, + 5 )); + assert_ok!(Certification::add_cert( + frame_system::RawOrigin::Signed(AccountKeyring::Charlie.to_account_id()).into(), + 3, + 5 + )); + // charlie also request distance evaluation for eve + // (could be done in batch) + assert_ok!(Distance::request_distance_evaluation_for( + frame_system::RawOrigin::Signed(AccountKeyring::Charlie.to_account_id()).into(), + 5 + )); + // then the evaluation is pending + assert_eq!( + Distance::pending_evaluation_request(5), + Some(AccountKeyring::Charlie.to_account_id(),) + ); + // Pass 2 sessions + run_to_block(51); + // simulate evaluation published by smith Alice + assert_ok!(Distance::force_update_evaluation( + frame_system::RawOrigin::Root.into(), + AccountKeyring::Alice.to_account_id(), + pallet_distance::ComputationResult { + distances: vec![Perbill::one()], + } + )); + // Pass 1 session + run_to_block(75); + + // eve should not even have to claim her membership System::assert_has_event(RuntimeEvent::Membership( pallet_membership::Event::MembershipAdded { member: 5, - expire_on: 76 + <Runtime as pallet_membership::Config>::MembershipPeriod::get(), + expire_on: 75 + <Runtime as pallet_membership::Config>::MembershipPeriod::get(), }, )); - // not possible anymore to validate identity of someone else + + // test state coherence + assert_eq!( + Identity::identity(5), + Some(pallet_identity::IdtyValue { + data: IdtyData { + // ud creation period is 60_000 ms ~ 10 blocks + // block time is 6_000 ms + // first ud is at 24_000 ms ~ 4 blocks + // at block 75 this should be the UD number 7, so next is 8 not 9 + // FIXME wrong UD count + first_eligible_ud: pallet_universal_dividend::FirstEligibleUd(Some( + sp_std::num::NonZeroU16::new(9).unwrap() + )) + }, + next_creatable_identity_on: 0u32, + old_owner_key: None, + owner_key: AccountKeyring::Eve.to_account_id(), + next_scheduled: 0, + status: pallet_identity::IdtyStatus::Member, + }) + ); }); } @@ -424,6 +549,16 @@ fn test_membership_renewal() { .with_initial_balances(vec![(AccountKeyring::Alice.to_account_id(), 2000)]) .build() .execute_with(|| { + // can not renew membership immediately + assert_noop!( + Distance::request_distance_evaluation( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + ), + pallet_duniter_wot::Error::<Runtime>::MembershipRenewalPeriodNotRespected, + ); + + // but ok after waiting 10 blocks delay + run_to_block(11); assert_ok!(Distance::request_distance_evaluation( frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), )); @@ -436,33 +571,28 @@ fn test_membership_renewal() { distances: vec![Perbill::one()], } )); - run_to_block(76); // Pass 1 session - - // renew at block 76 - assert_ok!(Membership::renew_membership( - frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), - )); + // Pass 1 session, membership is renewed automatically + run_to_block(75); System::assert_has_event(RuntimeEvent::Membership( - pallet_membership::Event::MembershipAdded { + pallet_membership::Event::MembershipRenewed { member: 1, - expire_on: 76 + <Runtime as pallet_membership::Config>::MembershipPeriod::get(), + expire_on: 75 + <Runtime as pallet_membership::Config>::MembershipPeriod::get(), }, )); - // renew at block 77 - run_to_block(77); - assert_ok!(Membership::renew_membership( - frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), - )); - System::assert_has_event(RuntimeEvent::Membership( - pallet_membership::Event::MembershipAdded { - member: 1, - expire_on: 77 + <Runtime as pallet_membership::Config>::MembershipPeriod::get(), - }, - )); + run_to_block(76); + // not possible to renew manually + // can not ask renewal when period is not respected + // TODO check that it is possible again within the right delay + assert_noop!( + Distance::request_distance_evaluation( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + ), + pallet_duniter_wot::Error::<Runtime>::MembershipRenewalPeriodNotRespected, + ); - // should expire at block 177 = 77+100 - run_to_block(177); + // should expire at block 175 = 75+100 + run_to_block(175); System::assert_has_event(RuntimeEvent::Membership( pallet_membership::Event::MembershipRemoved { member: 1, @@ -583,18 +713,19 @@ fn test_ud_claimed_membership_on_and_off() { }, )); - // alice claims back her membership - assert_ok!(Distance::force_set_distance_status( + // alice claims back her membership through distance evaluation + assert_ok!(Distance::force_valid_distance_status( frame_system::RawOrigin::Root.into(), 1, - Some(( - AccountKeyring::Alice.to_account_id(), - pallet_distance::DistanceStatus::Valid - )) - )); - assert_ok!(Membership::claim_membership( - frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into() )); + // it can not be done manually + // because the call does not exist anymore + // assert_noop!( + // Membership::claim_membership( + // frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + // ), + // pallet_membership::Error::<Runtime>::AlreadyMember + // ); System::assert_has_event(RuntimeEvent::Membership( pallet_membership::Event::MembershipAdded { member: 1, @@ -963,12 +1094,22 @@ fn test_create_new_idty_without_founds() { .build() .execute_with(|| { run_to_block(2); + assert_eq!( + Balances::free_balance(AccountKeyring::Eve.to_account_id()), + 0 + ); // Should be able to create an identity without founds assert_ok!(Identity::create_identity( frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), AccountKeyring::Eve.to_account_id(), )); + System::assert_has_event(RuntimeEvent::Identity( + pallet_identity::Event::IdtyCreated { + idty_index: 5, + owner_key: AccountKeyring::Eve.to_account_id(), + }, + )); // At next block, nothing should be preleved run_to_block(3); @@ -1029,24 +1170,26 @@ fn test_validate_new_idty_after_few_uds() { pallet_identity::IdtyName::from("Eve"), )); - // At next block, Bob should be able to certify and validate the new identity + // At next block, Bob should be able to certify the new identity run_to_block(23); assert_ok!(Certification::add_cert( frame_system::RawOrigin::Signed(AccountKeyring::Bob.to_account_id()).into(), 2, 5, )); - assert_ok!(Distance::force_set_distance_status( + // valid distance status should trigger identity validation + assert_ok!(Distance::force_valid_distance_status( frame_system::RawOrigin::Root.into(), 5, - Some(( - AccountKeyring::Bob.to_account_id(), - pallet_distance::DistanceStatus::Valid - )) - )); - assert_ok!(Membership::claim_membership( - frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), )); + // and it is not possible to call it manually + // because the call does not exist anymore + // assert_noop!( + // Membership::claim_membership( + // frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + // ), + // pallet_membership::Error::<Runtime>::AlreadyMember + // ); // The new member should have first_eligible_ud equal to three assert!(Identity::identity(5).is_some()); @@ -1095,18 +1238,19 @@ fn test_claim_memberhsip_after_few_uds() { 5, )); - // eve should be able to claim her membership - assert_ok!(Distance::force_set_distance_status( + // eve membership should be able to be claimed through distance evaluation + assert_ok!(Distance::force_valid_distance_status( frame_system::RawOrigin::Root.into(), 5, - Some(( - AccountKeyring::Eve.to_account_id(), - pallet_distance::DistanceStatus::Valid - )) - )); - assert_ok!(Membership::claim_membership( - frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), )); + // but not manually + // because the call does not exist + // assert_noop!( + // Membership::claim_membership( + // frame_system::RawOrigin::Signed(AccountKeyring::Eve.to_account_id()).into(), + // ), + // pallet_membership::Error::<Runtime>::AlreadyMember + // ); // The new member should have first_eligible_ud equal to three assert!(Identity::identity(5).is_some()); @@ -1451,16 +1595,6 @@ fn test_new_account_linked() { ); }) } -#[test] -#[ignore = "what was this test supposed to do?"] -fn smith_data_problem() { - ExtBuilder::new(1, 3, 4) - .change_parameters(|_parameters| {}) - .build() - .execute_with(|| { - run_to_block(4); - }); -} /// test killed account // The only way to kill an account is to kill the identity diff --git a/runtime/gdev/tests/xt_tests.rs b/runtime/gdev/tests/xt_tests.rs index 1663d6221..5caf3a87e 100644 --- a/runtime/gdev/tests/xt_tests.rs +++ b/runtime/gdev/tests/xt_tests.rs @@ -15,6 +15,7 @@ // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // these integration tests aim to test fees and extrinsic-related externalities +// they do not work with runtim-benchmark because it has a different fees model mod common; diff --git a/runtime/gtest/src/lib.rs b/runtime/gtest/src/lib.rs index a4b6e010a..7fe0cfeba 100644 --- a/runtime/gtest/src/lib.rs +++ b/runtime/gtest/src/lib.rs @@ -292,7 +292,7 @@ construct_runtime!( // Web Of Trust Wot: pallet_duniter_wot::{Pallet} = 40, Identity: pallet_identity::{Pallet, Call, Config<T>, Storage, Event<T>} = 41, - Membership: pallet_membership::{Pallet, Call, Config<T>, Storage, Event<T>} = 42, + Membership: pallet_membership::{Pallet, Config<T>, Storage, Event<T>} = 42, Certification: pallet_certification::{Pallet, Call, Config<T>, Storage, Event<T>} = 43, Distance: pallet_distance::{Pallet, Call, Storage, Inherent, Event<T>} = 44, diff --git a/runtime/gtest/src/parameters.rs b/runtime/gtest/src/parameters.rs index 252ae211a..c7731b0a9 100644 --- a/runtime/gtest/src/parameters.rs +++ b/runtime/gtest/src/parameters.rs @@ -107,7 +107,7 @@ parameter_types! { // Membership parameter_types! { pub const MembershipPeriod: BlockNumber = 73 * DAYS; - pub const PendingMembershipPeriod: BlockNumber = 12 * DAYS; + pub const MembershipRenewalPeriod: BlockNumber = 56 * DAYS; } // Certification -- GitLab