// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::{ extension::{AsPerson, AsPersonInfo}, mock::*, pezpallet::{AccountToPersonalId, Origin as PeopleOrigin}, WeightInfo, *, }; use pezframe_support::{ assert_noop, assert_ok, dispatch::Pays, traits::{Get, OnIdle, OnPoll}, weights::RuntimeDbWeight, BoundedVec, }; use pezsp_runtime::{ transaction_validity::InvalidTransaction::{self, BadSigner}, DispatchResult, Weight, }; use verifiable::demo_impls::Simple; fn generate_people_with_index( start: u8, end: u8, ) -> Vec<(PersonalId, MemberOf, SecretOf)> { let mut people = Vec::new(); for i in start..=end { let person = PeoplePallet::reserve_new_id(); let secret = Simple::new_secret([i; 32]); let public = Simple::member_from_secret(&secret); PeoplePallet::recognize_personhood(person, Some(public)).unwrap(); people.push((person, public, secret)); } people } fn suspended_indices_list(ring_index: u32) -> BoundedVec::MaxRingSize> { let suspended_indices = PendingSuspensions::::get(ring_index); assert!(&suspended_indices[..].windows(2).all(|pair| pair[0] < pair[1])); suspended_indices } fn build_ring(ring_index: RingIndex, limit: Option) -> DispatchResult { let to_include = PeoplePallet::should_build_ring( ring_index, limit.unwrap_or(::MaxRingSize::get()), ) .ok_or(Error::::StillFresh)?; PeoplePallet::build_ring(ring_index, to_include) } #[test] fn build_ring_works() { TestExt::new().execute_with(|| { PeoplePallet::set_onboarding_size(RuntimeOrigin::root(), 5).unwrap(); // No one to onboard. assert_noop!(build_ring(RI_ZERO, None), Error::::StillFresh); // Not enough for a queue generate_people_with_index(0, 3); // People are recognized but not onboarded yet, the ring has 0 members, same as initial // value. assert_eq!(Keys::::count(), 4); assert_noop!(build_ring(RI_ZERO, None), Error::::StillFresh); // Onboard people assert_noop!(PeoplePallet::onboard_people(), Error::::Incomplete); // Now we have enough to build one. generate_people_with_index(4, 4); assert_ok!(PeoplePallet::onboard_people()); // There isn't a root yet. assert!(!Root::::contains_key(0)); assert_eq!(RingKeysStatus::::get(0), RingStatus { total: 5, included: 0 }); // Build the root. assert_ok!(build_ring(RI_ZERO, None)); assert!(Root::::contains_key(0)); // We can add 5 more people generate_people_with_index(5, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // We can add 26 more people generate_people_with_index(10, 35); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(1, None)); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(2, None)); // Can't build 3, because then there are only 4 spots left which is less than onboarding // size of 5 assert_noop!(PeoplePallet::onboard_people(), Error::::Incomplete); }); } #[test] fn recognize_person_with_duplicate_key() { TestExt::new().execute_with(|| { // Recognize person A with a key. let person_a = PeoplePallet::reserve_new_id(); let secret_a = Simple::new_secret([1; 32]); let key_a = Simple::member_from_secret(&secret_a); PeoplePallet::recognize_personhood(person_a, Some(key_a)).unwrap(); // Recognize person B with the same key. let person_b = PeoplePallet::reserve_new_id(); assert_noop!( PeoplePallet::recognize_personhood(person_b, Some(key_a)), Error::::KeyAlreadyInUse ); }); } #[test] fn recognize_same_person_2_times() { TestExt::new().execute_with(|| { let person_a = PeoplePallet::reserve_new_id(); let secret_a = Simple::new_secret([1; 32]); let key_a = Simple::member_from_secret(&secret_a); assert_ok!(PeoplePallet::recognize_personhood(person_a, Some(key_a))); assert!(People::::get(person_a).is_some()); assert_noop!( PeoplePallet::recognize_personhood(person_a, Some(key_a)), Error::::KeyAlreadyInUse, ); assert_noop!( PeoplePallet::renew_id_reservation(person_a), Error::::PersonalIdReservationCannotRenew, ); }); } #[test] fn recognize_person_with_duplicate_key_after_suspend() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); OnboardingSize::::set(1); let person_a = PeoplePallet::reserve_new_id(); let person_b = PeoplePallet::reserve_new_id(); let person_c = PeoplePallet::reserve_new_id(); let secret_a = Simple::new_secret([1; 32]); let secret_b = Simple::new_secret([2; 32]); let secret_c = Simple::new_secret([3; 32]); let key_a = Simple::member_from_secret(&secret_a); let key_b = Simple::member_from_secret(&secret_b); let key_c = Simple::member_from_secret(&secret_c); // Recognize person A and B assert_ok!(PeoplePallet::recognize_personhood(person_a, Some(key_a))); // Onboard A so that they become part of a ring. assert_ok!(PeoplePallet::onboard_people()); // B will be part of the onboarding queue. assert_ok!(PeoplePallet::recognize_personhood(person_b, Some(key_b))); assert_eq!( People::::get(person_a).unwrap().position, RingPosition::Included { ring_index: 0, ring_position: 0, scheduled_for_removal: false } ); assert_eq!( People::::get(person_b).unwrap().position, RingPosition::Onboarding { queue_page: 0 } ); // Start suspensions. assert_ok!(PeoplePallet::start_people_set_mutation_session()); // Suspend person A and B assert_ok!(PeoplePallet::suspend_personhood(&[person_a, person_b])); // End suspensions. assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); PeoplePallet::remove_suspended_keys(RI_ZERO); // Make sure both A and B are suspended. assert_eq!(People::::get(person_a).unwrap().position, RingPosition::Suspended); assert_eq!(People::::get(person_b).unwrap().position, RingPosition::Suspended); // Recognize person C with same key as A assert_noop!( PeoplePallet::recognize_personhood(person_c, Some(key_a)), Error::::KeyAlreadyInUse ); // Recognize person C with a different key assert_ok!(PeoplePallet::recognize_personhood(person_c, Some(key_c))); // Resume personhood for A and B. assert_ok!(PeoplePallet::recognize_personhood(person_a, None)); assert_ok!(PeoplePallet::recognize_personhood(person_b, None)); // Both A and B kept their keys. assert_eq!(Keys::::get(key_a), Some(person_a)); assert_eq!(Keys::::get(key_b), Some(person_b)); }); } #[test] fn id_reservation_works() { TestExt::new().execute_with(|| { // Initially, no personal IDs are reserved or recognized. assert_eq!(NextPersonalId::::get(), 0); assert!(!ReservedPersonalId::::contains_key(0)); assert!(People::::get(0).is_none()); assert_noop!( PeoplePallet::renew_id_reservation(0), Error::::PersonalIdReservationCannotRenew, ); // Reserve a new ID. This should create a reservation at ID=0. assert_eq!(PeoplePallet::reserve_new_id(), 0); assert_eq!(NextPersonalId::::get(), 1); assert!(ReservedPersonalId::::contains_key(0)); assert!(People::::get(0).is_none()); assert_noop!( PeoplePallet::renew_id_reservation(0), Error::::PersonalIdReservationCannotRenew, ); // Reserve another new ID. This should create a reservation at ID=1. assert_eq!(PeoplePallet::reserve_new_id(), 1); assert_eq!(NextPersonalId::::get(), 2); assert!(ReservedPersonalId::::contains_key(1)); assert!(People::::get(1).is_none()); // Cancel the reservation for ID=0. assert_ok!(PeoplePallet::cancel_id_reservation(0)); assert!(!ReservedPersonalId::::contains_key(0)); assert!(People::::get(0).is_none()); // Reserve a new ID again. This should create a reservation at ID=2. assert_eq!(PeoplePallet::reserve_new_id(), 2); assert_eq!(NextPersonalId::::get(), 3); assert!(ReservedPersonalId::::contains_key(2)); assert!(People::::get(2).is_none()); // Renew the reservation for ID=0. assert_ok!(PeoplePallet::renew_id_reservation(0)); assert!(ReservedPersonalId::::contains_key(0)); assert!(People::::get(0).is_none()); assert_noop!( PeoplePallet::renew_id_reservation(0), Error::::PersonalIdReservationCannotRenew, ); // Recognize personhood for ID=0 with a dummy key. assert_ok!(PeoplePallet::recognize_personhood(0, Some([0; 32]))); assert!(People::::get(0).is_some()); assert!(!ReservedPersonalId::::contains_key(0)); assert_noop!( PeoplePallet::renew_id_reservation(0), Error::::PersonalIdReservationCannotRenew, ); }); } #[test] fn force_recognize_personhood_works() { TestExt::new().execute_with(|| { use verifiable::demo_impls::Simple; // We'll create 5 new people to recognize. let num_people = 5; let mut keys = Vec::new(); for i in 0..num_people { let secret = Simple::new_secret([i as u8; 32]); let public_key = Simple::member_from_secret(&secret); keys.push(public_key); } // Initially, no one is recognized. for id in 0..num_people { assert!(People::::get(id).is_none()); assert!(!ReservedPersonalId::::contains_key(id)); } assert_eq!(NextPersonalId::::get(), 0); // Using the root origin, force recognize these people. assert_ok!(PeoplePallet::force_recognize_personhood(RuntimeOrigin::root(), keys.clone())); // After recognition, each person should now exist in storage. for (i, key) in keys.clone().into_iter().enumerate() { let who = i as PersonalId; let record = People::::get(who).expect("Person should be recognized"); assert_eq!(record.key, key); assert!(!ReservedPersonalId::::contains_key(who)); } // NextPersonalId should now point to the next free ID after recognizing `num_people`. assert_eq!(NextPersonalId::::get(), num_people); // Any further IDs not used yet should be empty. assert!(People::::get(num_people).is_none()); // Fails for non-root origin. assert_noop!( PeoplePallet::force_recognize_personhood(RuntimeOrigin::signed(0), keys.clone()), pezsp_runtime::DispatchError::BadOrigin ); // Fails for duplicate keys. let another_key = { let secret = Simple::new_secret([233; 32]); Simple::member_from_secret(&secret) }; assert_noop!( PeoplePallet::force_recognize_personhood( RuntimeOrigin::root(), vec![another_key, another_key] ), Error::::KeyAlreadyInUse ); }); } #[test] fn cannot_renew_future_id() { TestExt::new().execute_with(|| { // Initially, NextPersonalId should be 0. assert_eq!(NextPersonalId::::get(), 0); // Id 0 is not reserved, can't renew. assert_noop!( PeoplePallet::renew_id_reservation(0), Error::::PersonalIdReservationCannotRenew ); // Id 1 is not reserved, can't renew. assert_noop!( PeoplePallet::renew_id_reservation(1), Error::::PersonalIdReservationCannotRenew ); // Reserve a new personal ID. This will be ID 0, and NextPersonalId should now become 1. let first_id = PeoplePallet::reserve_new_id(); assert_eq!(first_id, 0); assert_eq!(NextPersonalId::::get(), 1); // Id 0 is reserved, can't renew. assert_noop!( PeoplePallet::renew_id_reservation(0), Error::::PersonalIdReservationCannotRenew ); // Id 1 is future, can't renew. assert_noop!( PeoplePallet::renew_id_reservation(1), Error::::PersonalIdReservationCannotRenew ); // Cancel the reservation for ID=0. assert_ok!(PeoplePallet::cancel_id_reservation(0)); // Id 0 is not reserved, can renew. assert_ok!(PeoplePallet::renew_id_reservation(0)); // Id 1 is future, can't renew. assert_noop!( PeoplePallet::renew_id_reservation(1), Error::::PersonalIdReservationCannotRenew ); }); } #[test] fn test_set_personal_id_account() { TestExt::new().execute_with(|| { generate_people_with_index(0, 3); // (In our test, we treat PersonalId as a simple u64.) // Verify that there are no mappings for personal id 1 and account 42. assert!(AccountToPersonalId::::get(42).is_none()); assert!(People::::get(1).unwrap().account.is_none()); // Create an origin that represents a personal identity. // (Recall that your pezpallet’s Origin enum has a variant PersonalIdentity.) let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(1)); // Call the extrinsic to set personal id account. assert_ok!(PeoplePallet::set_personal_id_account(origin, 42, 0), Pays::No.into()); // Check that the mapping is now present. assert_eq!(AccountToPersonalId::::get(42), Some(1)); assert_eq!(People::::get(1).unwrap().account, Some(42)); // Now update the mapping by calling the extrinsic again. let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(1)); // Here we change the account to 43. assert_ok!(PeoplePallet::set_personal_id_account(origin, 43, 0), Pays::Yes.into()); // The old mapping for account 42 should be removed. assert!(AccountToPersonalId::::get(42).is_none()); assert_eq!(AccountToPersonalId::::get(43), Some(1)); assert_eq!(People::::get(1).unwrap().account, Some(43)); // Test that a non-personal identity origin (for example, a Signed origin) // does not work (the call should error with BadOrigin). let origin = RuntimeOrigin::signed(44); assert_noop!( PeoplePallet::set_personal_id_account(origin, 44, 0), pezsp_runtime::DispatchError::BadOrigin ); // Test that trying to use an account that is already in use fails. // First, set a mapping for personal id 2 using account 45. let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(2)); assert_ok!(PeoplePallet::set_personal_id_account(origin, 45, 0), Pays::No.into()); // Then try to set personal id 3 to use the same account 45. let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_noop!( PeoplePallet::set_personal_id_account(origin, 45, 0), Error::::AccountInUse ); }); } #[test] fn test_unset_personal_id_account() { TestExt::new().execute_with(|| { generate_people_with_index(0, 1); // First, set a mapping for personal id 1 to account 50. let id_origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(1)); assert_ok!( PeoplePallet::set_personal_id_account(id_origin.clone(), 50, 0), Pays::No.into() ); assert_eq!(AccountToPersonalId::::get(50), Some(1)); // Now call the unset extrinsic. assert_ok!(PeoplePallet::unset_personal_id_account(id_origin.clone()), Pays::Yes.into()); // Verify that the mappings have been removed. assert!(AccountToPersonalId::::get(50).is_none()); assert!(People::::get(1).unwrap().account.is_none()); // Calling unset again on the same account should fail. assert_noop!( PeoplePallet::unset_personal_id_account(id_origin.clone()), Error::::InvalidAccount ); }); } #[test] fn test_as_personal_identity_with_account_check_and_nonce() { // Use our test externalities. new_test_ext().execute_with(|| { let dummy_call = pezframe_system::Call::::remark { remark: vec![] }; let account: u64 = 42; // 0: transaction fails because there signer is wrong, no associated personal id. let nonce: u64 = 0; let tx_ext = ( AsPerson::::new(Some(AsPersonInfo::AsPersonalIdentityWithAccount(nonce))), pezframe_system::CheckNonce::::from(nonce), ); assert_noop!( exec_tx(Some(account), tx_ext, dummy_call.clone()), InvalidTransaction::BadSigner ); // Add a person and an associated account --- let personal_id = generate_people_with_index(0, 0).pop().unwrap().0; AccountToPersonalId::::insert(account, personal_id); System::inc_sufficients(&account); // 1: a successful transaction let nonce: u64 = 0; let tx_ext = ( AsPerson::::new(Some(AsPersonInfo::AsPersonalIdentityWithAccount(nonce))), pezframe_system::CheckNonce::::from(nonce), ); assert_ok!(exec_tx(Some(account), tx_ext, dummy_call.clone())); assert_eq!(pezframe_system::Pezpallet::::account_nonce(account), 1); // 2: another successful transaction let nonce: u64 = 1; let tx_ext = ( AsPerson::::new(Some(AsPersonInfo::AsPersonalIdentityWithAccount(nonce))), pezframe_system::CheckNonce::::from(nonce), ); assert_ok!(exec_tx(Some(account), tx_ext, dummy_call.clone())); assert_eq!(pezframe_system::Pezpallet::::account_nonce(account), 2); // 3: transaction fails because the nonce is wrong let nonce: u64 = 1; let tx_ext = ( AsPerson::::new(Some(AsPersonInfo::AsPersonalIdentityWithAccount(nonce))), pezframe_system::CheckNonce::::from(nonce), ); assert_noop!(exec_tx(Some(account), tx_ext, dummy_call), InvalidTransaction::Stale); assert_eq!(pezframe_system::Pezpallet::::account_nonce(account), 2); }); } mod on_idle { use super::*; use pezframe_support::assert_storage_noop; #[test] fn one_ring_and_no_suspensions() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { // A ring exists but no-one in it is suspended generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_storage_noop!(PeoplePallet::on_idle(0, Weight::MAX)); }); }); } #[test] fn no_rings_and_empty_queue() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { assert_storage_noop!(PeoplePallet::on_idle(0, Weight::MAX)); }); }); } #[test] fn no_rings_and_not_enough_people_in_the_queue() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { // Several people are awaiting in the queue generate_people_with_index(0, 3); assert_storage_noop!(PeoplePallet::on_idle(0, Weight::MAX)); }); }); } #[test] fn no_rings_and_some_people_awaiting_onboarding() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { // Several people are awaiting in the queue generate_people_with_index(0, 3); // Mutation session is ongoing assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_storage_noop!(PeoplePallet::on_idle(0, Weight::MAX)); }); }); } mod suspensions { use super::*; #[test] fn one_ring_with_suspensions() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { // A ring exists with some suspensions generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); // Close the key migration process. let mut meter = WeightMeter::new(); PeoplePallet::on_poll(0, &mut meter); assert!(RingsState::::get().append_only()); PeoplePallet::on_idle(0, Weight::MAX); assert_eq!(RingKeys::::get(RI_ZERO).len(), 9); assert_eq!( RingKeysStatus::::get(RI_ZERO), RingStatus { total: 9, included: 9 } ); }); }); } #[test] fn multiple_rings_with_suspensions() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { // // Two rings exist with some suspensions generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 11]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); // The keys are removed let mut meter = WeightMeter::new(); PeoplePallet::on_poll(0, &mut meter); assert!(RingsState::::get().append_only()); // Ring should be built here PeoplePallet::on_idle(0, Weight::MAX); // Rings were built assert!(PeoplePallet::should_build_ring( RI_ZERO, ::MaxRingSize::get() ) .is_none()); assert_eq!( RingKeysStatus::::get(RI_ZERO), RingStatus { total: 9, included: 9 } ); assert!(PeoplePallet::should_build_ring( 1, ::MaxRingSize::get() ) .is_none()); assert_eq!( RingKeysStatus::::get(1), RingStatus { total: 9, included: 9 } ); }); }); } } mod onboarding { use super::*; #[test] fn onboarding_all_conditions_met() { TestExt::new().execute_with(|| { let mut ext = new_test_ext(); ext.execute_with(|| { PeoplePallet::set_onboarding_size(RuntimeOrigin::root(), 3).unwrap(); OnboardingSize::::set(3); // Several people are awaiting in the queue generate_people_with_index(0, 3); assert_storage_noop!(PeoplePallet::on_idle( 0, ::WeightInfo::on_idle_base(), )); assert_storage_noop!(PeoplePallet::on_idle( 0, ::WeightInfo::on_idle_base().saturating_mul(2), )); let expected_weight = ::WeightInfo::on_idle_base() .saturating_add(::WeightInfo::onboard_people()); assert_eq!( PeoplePallet::on_idle( 0, ::WeightInfo::on_idle_base() .saturating_add(::WeightInfo::onboard_people()) .saturating_mul(2), ), expected_weight ); // People were onboarded but not included assert_eq!( RingKeysStatus::::get(RI_ZERO), RingStatus { total: 4, included: 0 } ); }); }); } } } mod manual_tasks { use super::*; use pezframe_support::BoundedVec; #[test] fn works_for_build_ring() { TestExt::new().execute_with(|| { // Set-up to make the checks validating the need of ring build pass OnboardingSize::::set(1); let member_key = Simple::member_from_secret(&Simple::new_secret([0u8; 32])); let keys: BoundedVec, ::MaxRingSize> = BoundedVec::try_from(vec![member_key]).expect("failed to init members"); let ring_status = RingStatus { total: keys.len().saturated_into(), included: 0 }; RingKeys::::insert(RI_ZERO, keys); RingKeysStatus::::insert(RI_ZERO, ring_status); // build_ring call should succeed assert_ok!(PeoplePallet::build_ring_manual( RuntimeOrigin::signed(0), 0, Some(OnboardingSize::::get()) )); }); } #[test] fn works_for_onboard_people() { TestExt::new().execute_with(|| { // Set-up needed to make the call pass generate_people_with_index(0, 19); // onboard_people_manual call should succeed assert_ok!(PeoplePallet::onboard_people_manual(RuntimeOrigin::signed(0))); }); } #[test] fn checks_the_need_to_build_the_ring() { TestExt::new().execute_with(|| { OnboardingSize::::set(5); assert!(PeoplePallet::should_build_ring(0, OnboardingSize::::get()).is_none()); // Set-up some people but not enough generate_people_with_index(0, 3); assert!(PeoplePallet::should_build_ring(0, OnboardingSize::::get()).is_none()); // Set-up needed to make the call pass generate_people_with_index(4, 4); assert_ok!(PeoplePallet::onboard_people()); assert_eq!(PeoplePallet::should_build_ring(0, OnboardingSize::::get()), Some(5)); }); } } mod chunks { use super::*; use pezframe_support::traits::Get; use pezsp_runtime::BoundedVec; #[test] #[should_panic] fn no_chunks_first_page() { new_test_ext().execute_with(|| { Chunks::::remove(0); assert!(PeoplePallet::fetch_chunks(0..5).is_err()); }) } #[test] #[should_panic] fn no_chunks_second_page() { new_test_ext().execute_with(|| { let chunk_page_size: u32 = ::ChunkPageSize::get(); Chunks::::remove(1); assert!(PeoplePallet::fetch_chunks( (chunk_page_size - 1) as usize..(chunk_page_size * 2) as usize ) .is_err()); }) } #[test] #[should_panic] fn insufficient_chunks() { new_test_ext().execute_with(|| { let drain = Chunks::::drain(); for _ in drain {} let chunk_page_size: u32 = ::ChunkPageSize::get(); let chunks: BoundedVec<(), _> = [(); 4096] .into_iter() .take(chunk_page_size as usize - 1) .collect::>() .try_into() .unwrap(); Chunks::::insert(0, chunks); assert!(PeoplePallet::fetch_chunks(0..(chunk_page_size as usize + 1)).is_err()); }) } #[test] fn single_page_chunks() { new_test_ext().execute_with(|| { let chunk_page_size: u32 = ::ChunkPageSize::get(); assert_eq!( PeoplePallet::fetch_chunks(0..(chunk_page_size - 1) as usize).unwrap().len(), chunk_page_size as usize - 1 ); assert_eq!( PeoplePallet::fetch_chunks(1..chunk_page_size as usize).unwrap().len(), chunk_page_size as usize - 1 ); assert_eq!( PeoplePallet::fetch_chunks(1..(chunk_page_size - 1) as usize).unwrap().len(), chunk_page_size as usize - 2 ); assert_eq!( PeoplePallet::fetch_chunks(0..chunk_page_size as usize).unwrap().len(), chunk_page_size as usize ); }) } #[test] fn multi_page_chunks() { new_test_ext().execute_with(|| { let chunk_page_size: u32 = ::ChunkPageSize::get(); let chunks: BoundedVec<(), _> = [(); 4096] .into_iter() .take(chunk_page_size as usize) .collect::>() .try_into() .unwrap(); Chunks::::insert(0, &chunks); Chunks::::insert(1, chunks); assert_eq!( PeoplePallet::fetch_chunks(0..(2 * chunk_page_size - 1) as usize).unwrap().len(), 2 * chunk_page_size as usize - 1 ); assert_eq!( PeoplePallet::fetch_chunks(1..2 * chunk_page_size as usize).unwrap().len(), 2 * chunk_page_size as usize - 1 ); assert_eq!( PeoplePallet::fetch_chunks(1..(2 * chunk_page_size - 1) as usize).unwrap().len(), 2 * chunk_page_size as usize - 2 ); assert_eq!( PeoplePallet::fetch_chunks(0..2 * chunk_page_size as usize).unwrap().len(), 2 * chunk_page_size as usize ); }); } } mod merge_rings { use super::*; #[test] fn fails_if_suspension_session_is_in_progress() { TestExt::new().execute_with(|| { RingsState::::set(RingMembersState::Mutating(1)); assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 1, 2), Error::::SuspensionSessionInProgress ); }); } #[test] fn fails_given_rings_with_the_same_id() { TestExt::new().execute_with(|| { assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 1, 1), Error::::InvalidRing ); }); } #[test] fn fails_given_current_ring_id() { TestExt::new().execute_with(|| { CurrentRingIndex::::set(14); assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 1, 14), Error::::InvalidRing ); assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 14, 1), Error::::InvalidRing ); }); } #[test] fn fails_when_one_of_rings_is_half_full() { TestExt::new().execute_with(|| { // Two rings exist generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); // The current ring has a higher index than the ones being merged CurrentRingIndex::::set(14); assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 0, 1), Error::::RingAboveMergeThreshold ); assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 1, 0), Error::::RingAboveMergeThreshold ); }); } #[test] fn fails_if_one_of_rings_has_pending_suspensions() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // Two rings exist generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); // Suspend and remove more than half of the people in the first ring assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 2, 3, 4, 5, 6]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5, 6]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); PeoplePallet::remove_suspended_keys(RI_ZERO); // Suspend a few more people in the first ring assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[7, 8]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // The current ring has a higher index than the ones being merged CurrentRingIndex::::set(14); assert_noop!( PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 0, 1), Error::::SuspensionsPending ); }); } #[test] fn moves_all_keys_from_target_ring_to_base_ring() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // Two rings exist generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); // Suspend and remove more than half of the people in both rings assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 2, 3, 4, 5, 6, 11, 12, 13, 14, 15, 16]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5, 6]); assert_eq!(suspended_indices_list(1).into_inner(), vec![1, 2, 3, 4, 5, 6]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); PeoplePallet::remove_suspended_keys(RI_ZERO); PeoplePallet::remove_suspended_keys(1); // The current ring has a higher index than the ones being merged CurrentRingIndex::::set(14); assert_ok!(PeoplePallet::merge_rings(RuntimeOrigin::signed(0), 0, 1)); assert_eq!(RingKeys::::get(RI_ZERO).len(), 8); assert_eq!(RingKeysStatus::::get(RI_ZERO).total, 8); assert!(Root::::get(RI_ZERO).is_some()); assert!(Root::::get(1).is_none()); }); } } mod suspensions { use super::*; #[test] fn suspending_personhood_fails_if_no_session_started() { TestExt::new().execute_with(|| { let suspensions: &[PersonalId] = &[1]; assert_noop!( PeoplePallet::suspend_personhood(suspensions), Error::::NoMutationSession ); }); } #[test] fn suspending_personhood_fails_if_id_not_in_ring() { TestExt::new().execute_with(|| { // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // Attempt to suspend a person fails assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[14]; assert_noop!( PeoplePallet::suspend_personhood(suspensions), Error::::InvalidSuspensions ); }); } #[test] fn suspending_personhood_marks_people_as_suspended() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // Attempt to suspend a person assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Makes the person's record suspended let personal_record = People::::get(1); assert!(personal_record.is_some()); assert_eq!(personal_record.unwrap().position, RingPosition::Suspended); // Pending suspensions for the ring are incremented assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1]); }); } #[test] fn suspended_people_removal_fails_if_session_started() { TestExt::new().execute_with(|| { assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert!(!PeoplePallet::should_remove_suspended_keys(RI_ZERO, true)); }); } #[test] fn suspended_people_removal_fails_if_keys_are_migrated() { TestExt::new().execute_with(|| { assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::end_people_set_mutation_session()); assert!(!PeoplePallet::should_remove_suspended_keys(RI_ZERO, true)); }); } #[test] fn suspended_people_removal_fails_if_no_suspensions_recorded_in_ring() { TestExt::new().execute_with(|| { assert!(!PeoplePallet::should_remove_suspended_keys(RI_ZERO, true)); }); } #[test] fn suspended_people_removal_modifies_ring_data() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_root = Root::::get(RI_ZERO).unwrap(); // One person becomes suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Attempt to remove suspended people succeeds assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1]); PeoplePallet::remove_suspended_keys(RI_ZERO); // Ring data becomes modified assert_eq!(RingKeysStatus::::get(RI_ZERO), RingStatus { included: 0, total: 9 }); assert_eq!(RingKeys::::get(RI_ZERO).len(), 9); assert_ne!(Root::::get(RI_ZERO).unwrap().intermediate, initial_root.intermediate); // Pending suspensions are cleared for the ring assert!(suspended_indices_list(RI_ZERO).is_empty()); }); } #[test] fn suspending_in_multiple_sessions() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with multiple people generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // First session: some people become suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::suspend_personhood(&[1, 2])); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Those people are then removed assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2]); PeoplePallet::remove_suspended_keys(RI_ZERO); // Second session: some more people become suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::suspend_personhood(&[3, 4])); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Pending suspensions are tracked correctly assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3, 4]); // Those extra people are removed too PeoplePallet::remove_suspended_keys(RI_ZERO); // Final ring state is correct assert_eq!(RingKeys::::get(RI_ZERO).len(), 6); }); } #[test] fn suspending_personhood_then_resume() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // Attempt to suspend a person assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Makes the person's record suspended let personal_record = People::::get(1); assert!(personal_record.is_some()); assert_eq!(personal_record.unwrap().position, RingPosition::Suspended); // Pending suspensions for the ring are incremented assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1]); assert_ok!(PeoplePallet::recognize_personhood(1, None)); // Still needs to remove a key. assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1]); PeoplePallet::remove_suspended_keys(RI_ZERO); assert!(suspended_indices_list(RI_ZERO).is_empty()); assert_ok!(build_ring(RI_ZERO, None)); }); } #[test] fn suspending_person_removes_associated_data() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with multiple people generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // A person associated with an account let person_id = 0; let account_id = 42; let id_origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(person_id)); assert_ok!(PeoplePallet::set_personal_id_account(id_origin, account_id, 0)); // The association exists assert_eq!(AccountToPersonalId::::get(account_id), Some(person_id)); // The person becomes suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::suspend_personhood(&[person_id])); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Account to personal id is removed assert!(AccountToPersonalId::::get(account_id).is_none()); // The person is removed assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![person_id as u32]); PeoplePallet::remove_suspended_keys(RI_ZERO); // Account to personal id stays removed assert!(AccountToPersonalId::::get(account_id).is_none()); // Using the account for authentication fails let nonce = 0; let tx_ext = ( AsPerson::::new(Some(AsPersonInfo::AsPersonalIdentityWithAccount(nonce))), pezframe_system::CheckNonce::::from(nonce), ); let dummy_call = Call::::unset_alias_account {}; assert_noop!( exec_tx(Some(account_id), tx_ext, dummy_call), InvalidTransaction::BadSigner ); }); } } mod key_migration { use super::*; #[test] fn migrate_key_fails_without_old_key() { TestExt::new().execute_with(|| { let secret = Simple::new_secret([0; 32]); let public = Simple::member_from_secret(&secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_noop!( PeoplePallet::migrate_included_key(origin, public), Error::::NotPerson ); }); } #[test] fn migrate_key_fails_for_existing_key() { TestExt::new().execute_with(|| { // A ring with people exists generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let record3 = People::::get(3).unwrap(); let record13 = People::::get(13).unwrap(); assert!(matches!( record3.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); assert!(matches!(record13.position, RingPosition::Onboarding { queue_page: 0 })); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); // The key is already used by person 3 who is included assert_noop!( PeoplePallet::migrate_included_key(origin.clone(), record3.key), Error::::KeyAlreadyInUse ); // The key is already used by person 13 who is onboarding assert_noop!( PeoplePallet::migrate_included_key(origin, record13.key), Error::::KeyAlreadyInUse ); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(13)); // The key is already used by person 3 who is included assert_noop!( PeoplePallet::migrate_onboarding_key(origin.clone(), record3.key), Error::::KeyAlreadyInUse ); // The key is already used by person 13 who is onboarding assert_noop!( PeoplePallet::migrate_onboarding_key(origin, record13.key), Error::::KeyAlreadyInUse ); }); } #[test] fn migrate_key_fails_for_suspended_person() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // One person becomes suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[3]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // Attempt to remove suspended people succeeds assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); PeoplePallet::remove_suspended_keys(RI_ZERO); assert!(matches!(People::::get(3).unwrap().position, RingPosition::Suspended)); let new_secret = Simple::new_secret([100; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_noop!( PeoplePallet::migrate_included_key(origin, new_public), Error::::Suspended ); }); } #[test] fn migrate_key_enqueues_included_key_for_removal() { TestExt::new().execute_with(|| { // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_record = People::::get(3).unwrap(); assert!(matches!( initial_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); let temp_secret = Simple::new_secret([100; 32]); let temp_public = Simple::member_from_secret(&temp_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin, temp_public)); assert_eq!(KeyMigrationQueue::::get(3).unwrap(), temp_public); // Makes the person's record scheduled for removal let temp_record = People::::get(3).unwrap(); assert_eq!(temp_record.key, initial_record.key); assert_eq!( temp_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: true } ); assert_eq!(Keys::::get(temp_public).unwrap(), 3); assert_eq!(Keys::::get(initial_record.key).unwrap(), 3); let new_secret = Simple::new_secret([101; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin, new_public)); assert_eq!(KeyMigrationQueue::::get(3).unwrap(), new_public); let new_record = People::::get(3).unwrap(); assert_eq!( new_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: true } ); assert_eq!(Keys::::get(new_public).unwrap(), 3); assert_eq!(Keys::::get(initial_record.key).unwrap(), 3); assert!(!Keys::::contains_key(temp_public)); }); } #[test] fn migrate_key_replaces_onboarding_key() { TestExt::new().execute_with(|| { // A ring with people exists generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_record = People::::get(13).unwrap(); assert!(matches!(initial_record.position, RingPosition::Onboarding { queue_page: 0 })); let initial_pos = OnboardingQueue::::get(0) .iter() .position(|k| *k == initial_record.key) .unwrap(); let new_secret = Simple::new_secret([100; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(13)); assert_ok!(PeoplePallet::migrate_onboarding_key(origin, new_public)); // The person's record is the same but the key that they onboard with changed. let personal_record = People::::get(13).unwrap(); assert_eq!(personal_record.position, RingPosition::Onboarding { queue_page: 0 }); assert_eq!(OnboardingQueue::::get(0)[initial_pos], new_public); assert_eq!(Keys::::get(new_public).unwrap(), 13); assert!(!Keys::::contains_key(initial_record.key)); // Calling it again with a different key replaces the previous one. let new_secret = Simple::new_secret([101; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(13)); let temp_record = personal_record; assert_ok!(PeoplePallet::migrate_onboarding_key(origin, new_public)); assert_eq!(Keys::::get(new_public).unwrap(), 13); assert_eq!(OnboardingQueue::::get(0)[initial_pos], new_public); assert!(!Keys::::contains_key(temp_record.key)); }); } #[test] fn migrate_key_invalid_state() { TestExt::new().execute_with(|| { // A ring with people exists generate_people_with_index(0, 19); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let new_secret = Simple::new_secret([100; 32]); let new_public = Simple::member_from_secret(&new_secret); let initial_record = People::::get(13).unwrap(); assert!(matches!(initial_record.position, RingPosition::Onboarding { queue_page: 0 })); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(13)); assert_noop!( PeoplePallet::migrate_included_key(origin, new_public), Error::::InvalidKeyMigration ); let initial_record = People::::get(3).unwrap(); assert!(matches!( initial_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_noop!( PeoplePallet::migrate_onboarding_key(origin, new_public), Error::::InvalidKeyMigration ); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin, new_public)); let new_secret = Simple::new_secret([101; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(13)); assert_ok!(PeoplePallet::migrate_onboarding_key(origin, new_public)); }); } #[test] fn migrate_keys_mutates_record() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_record = People::::get(3).unwrap(); assert!(matches!( initial_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); let secret1 = Simple::new_secret([100; 32]); let public1 = Simple::member_from_secret(&secret1); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin, public1)); assert_eq!(KeyMigrationQueue::::get(3).unwrap(), public1); // Handle migrations assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!(Keys::::get(public1).unwrap(), 3); let new_record = People::::get(3).unwrap(); assert_eq!(new_record.key, public1); assert!(matches!(new_record.position, RingPosition::Onboarding { queue_page: 0 })); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); let secret2 = Simple::new_secret([101; 32]); let public2 = Simple::member_from_secret(&secret2); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(4)); assert_ok!(PeoplePallet::migrate_included_key(origin, public2)); assert_eq!(KeyMigrationQueue::::get(4).unwrap(), public2); // Handle migrations assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!(Keys::::get(public2).unwrap(), 4); let new_record = People::::get(4).unwrap(); assert_eq!(new_record.key, public2); assert!(matches!(new_record.position, RingPosition::Onboarding { queue_page: 0 })); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3, 4]); }); } #[test] fn migrate_keys_before_suspension() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_record = People::::get(3).unwrap(); assert!(matches!( initial_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); let secret1 = Simple::new_secret([100; 32]); let public1 = Simple::member_from_secret(&secret1); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin.clone(), public1)); assert_eq!(KeyMigrationQueue::::get(3).unwrap(), public1); assert!(suspended_indices_list(RI_ZERO).is_empty()); assert!(!RingsState::::get().key_migration()); // Handle migrations assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert!(!RingsState::::get().key_migration()); assert!(suspended_indices_list(RI_ZERO).is_empty()); assert_ok!(PeoplePallet::suspend_personhood(&[3])); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); // Migrate another key without removing the other key. let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(4)); let secret2 = Simple::new_secret([101; 32]); let public2 = Simple::member_from_secret(&secret2); assert_ok!(PeoplePallet::migrate_included_key(origin, public2)); assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert!(!RingsState::::get().key_migration()); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); assert_ok!(PeoplePallet::suspend_personhood(&[4])); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3, 4]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3, 4]); }); } #[test] fn migrate_keys_after_suspension() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_record = People::::get(3).unwrap(); assert!(matches!( initial_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); let secret1 = Simple::new_secret([100; 32]); let public1 = Simple::member_from_secret(&secret1); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin.clone(), public1)); assert_eq!(KeyMigrationQueue::::get(3).unwrap(), public1); // Handle migrations assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::suspend_personhood(&[3])); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); assert!(!RingsState::::get().key_migration()); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); PeoplePallet::remove_suspended_keys(RI_ZERO); assert!(suspended_indices_list(RI_ZERO).is_empty()); let secret2 = Simple::new_secret([101; 32]); let public2 = Simple::member_from_secret(&secret2); assert_noop!( PeoplePallet::migrate_included_key(origin, public2), Error::::Suspended ); }); } #[test] fn migrate_key_and_remove_it() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let initial_record = People::::get(3).unwrap(); assert!(matches!( initial_record.position, RingPosition::Included { ring_index: 0, ring_position: 3, scheduled_for_removal: false } )); let secret1 = Simple::new_secret([100; 32]); let public1 = Simple::member_from_secret(&secret1); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(3)); assert_ok!(PeoplePallet::migrate_included_key(origin.clone(), public1)); assert_eq!(KeyMigrationQueue::::get(3).unwrap(), public1); // Handle migrations assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert!(!RingsState::::get().key_migration()); assert!(suspended_indices_list(RI_ZERO).is_empty()); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![3]); PeoplePallet::remove_suspended_keys(RI_ZERO); assert!(suspended_indices_list(RI_ZERO).is_empty()); assert_eq!(RingKeys::::get(0).len(), 9); assert!(!RingKeys::::get(0) .into_iter() .any(|k| k == initial_record.key || k == public1)); }); } #[test] fn migrate_and_suspend_keys_then_remove_them() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); for personal_id in 0..10 { assert_eq!( People::::get(personal_id).unwrap().position.ring_index().unwrap(), RI_ZERO ); } let old_keys = RingKeys::::get(RI_ZERO); let new_keys: Vec<_> = (100..110) .map(|x| Simple::new_secret([x; 32])) .map(|s| Simple::member_from_secret(&s)) .collect(); let static_people: Vec = (0..10).filter(|i| i % 3 == 2).collect(); let migrated_people: Vec = (0..10).filter(|i| i % 3 == 0).collect(); for personal_id in &migrated_people { let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(*personal_id)); assert_ok!(PeoplePallet::migrate_included_key( origin.clone(), new_keys[*personal_id as usize] )); } assert!(suspended_indices_list(RI_ZERO).is_empty()); // Handle migrations assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: Vec = (0..10).filter(|i| i % 3 == 1).collect(); assert_ok!(PeoplePallet::suspend_personhood(&suspensions[..])); assert_eq!( suspended_indices_list(RI_ZERO).into_inner(), suspensions.iter().copied().map(|x| x as u32).collect::>() ); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert_eq!( suspended_indices_list(RI_ZERO).len(), migrated_people.len() + suspensions.len() ); let suspended_indices = suspended_indices_list(RI_ZERO); assert!(&suspended_indices[..].windows(2).all(|pair| pair[0] < pair[1])); PeoplePallet::remove_suspended_keys(RI_ZERO); assert!(suspended_indices_list(RI_ZERO).is_empty()); let current_keys = RingKeys::::get(0).into_inner(); assert_eq!(current_keys.len(), static_people.len()); let static_keys: Vec<_> = old_keys .into_iter() .enumerate() .filter(|(i, _)| static_people.contains(&(*i as PersonalId))) .map(|(_, k)| k) .collect(); assert_eq!(current_keys, static_keys); }); } } mod onboard_people { use super::*; #[test] fn fails_if_suspensions_ongoing() { TestExt::new().execute_with(|| { // Several people are awaiting onboarding generate_people_with_index(0, 9); // Mutation session starts assert_ok!(PeoplePallet::start_people_set_mutation_session()); // Attempt to onboard people fails assert_noop!(PeoplePallet::onboard_people(), Error::::Incomplete); }); } #[test] fn fails_if_ring_contains_suspended_members() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); OnboardingSize::::set(5); // A ring with people exists generate_people_with_index(0, 4); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); // Several people become suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 2, 3]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); let db_weight: RuntimeDbWeight = ::DbWeight::get(); let end_migrate_db_weight = db_weight.reads_writes(2, 1); let mut expected_consumed = end_migrate_db_weight; assert_eq!(meter.consumed(), expected_consumed); // and are removed from the ring let suspended_indices = vec![1, 2, 3]; assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), suspended_indices); PeoplePallet::remove_suspended_keys(RI_ZERO); // One more person becomes suspended assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[4]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); expected_consumed += end_migrate_db_weight; assert_eq!(meter.consumed(), expected_consumed); // More people are awaiting onboarding generate_people_with_index(10, 22); // Attempt to build on that same ring fails assert_noop!(PeoplePallet::onboard_people(), Error::::Incomplete); }); } #[test] fn no_keys_in_queue() { TestExt::new().execute_with(|| { assert_noop!(PeoplePallet::onboard_people(), Error::::Incomplete); }); } #[test] fn less_keys_in_queue_than_onboarding_size() { TestExt::new().execute_with(|| { OnboardingSize::::set(5); // Several people are awaiting onboarding generate_people_with_index(0, 3); // Onboarding attempt fails assert_noop!(PeoplePallet::onboard_people(), Error::::Incomplete); }); } #[test] fn current_ring_empty() { TestExt::new().execute_with(|| { // Several people are awaiting onboarding generate_people_with_index(0, 9); // Onboarding attempt succeeds assert_ok!(PeoplePallet::onboard_people()); // Keys are removed from the onboarding queue let queue = OnboardingQueue::::get(RI_ZERO); assert_eq!(queue.len(), 0); // Keys are inserted into ring keys and statuses assert_eq!(RingKeys::::get(RI_ZERO).len(), 10); assert_eq!(RingKeysStatus::::get(RI_ZERO), RingStatus { total: 10, included: 0 }); // Personal records are updated for id in 0..=9 { let record = People::::get(id); assert!(record.is_some()); match record.unwrap().position { RingPosition::Included { ring_index, .. } => { assert_eq!(ring_index, RI_ZERO); }, _ => panic!("Expected RingPosition::Included variant"), } } // Current ring index is incremented assert_eq!(CurrentRingIndex::::get(), RI_ZERO + 1); }); } #[test] fn current_ring_with_keys_in_it() { TestExt::new().execute_with(|| { OnboardingSize::::set(4); // A ring with people exists generate_people_with_index(0, 4); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert!(Root::::get(RI_ZERO).is_some()); // Several people are awaiting onboarding generate_people_with_index(5, 9); // Onboarding attempt succeeds assert_ok!(PeoplePallet::onboard_people()); // Newcomers are included in the same ring assert!(Root::::get(RI_ZERO + 1).is_none()); assert_eq!(RingKeys::::get(RI_ZERO).len(), 10); assert_eq!(RingKeysStatus::::get(RI_ZERO), RingStatus { total: 10, included: 5 }); }); } #[test] fn multiple_queue_pages() { TestExt::new().execute_with(|| { // Note: default onboarding size: 10, default page size: 40 let queue_page_size: u32 = ::OnboardingQueuePageSize::get(); let onboarding_size: u32 = OnboardingSize::::get(); let people_to_onboard = queue_page_size * 4; let expected_pages_to_fill: u32 = people_to_onboard / queue_page_size; let expected_rings_to_build: u32 = people_to_onboard / onboarding_size; // Start with empty onboarding queue assert_eq!(OnboardingQueue::::get(0).len(), 0); // Enough people in the queue to fill multiple pages generate_people_with_index(1, people_to_onboard as u8); // Keys are placed in multiple onboarding queue pages for page_index in 0..=expected_pages_to_fill - 1 { assert_eq!( OnboardingQueue::::get(page_index).len(), queue_page_size as usize ); } // Each call to onboard people should onboard OnboardingSize number of keys // which is equal to the number of rings to build for _ in 0..=expected_rings_to_build - 1 { assert_ok!(PeoplePallet::onboard_people()); } // Keys are removed from the onboarding queue for page_index in 0..=expected_pages_to_fill - 1 { assert_eq!(OnboardingQueue::::get(page_index).len(), 0); } // and are moved forward in the onboarding process let mut personal_id: u32 = 1; for ring_index in 0..=expected_rings_to_build - 1 { assert_eq!(RingKeys::::get(ring_index).len(), 10); assert_eq!( RingKeysStatus::::get(ring_index), RingStatus { total: 10, included: 0 } ); while personal_id < onboarding_size { let record = People::::get(personal_id as u64); assert!(record.is_some()); match record.unwrap().position { RingPosition::Included { ring_index: record_ring_index, .. } => { assert_eq!(record_ring_index, ring_index, "{personal_id}"); }, _ => panic!("Expected RingPosition::Included variant"), } personal_id += 1; } } // When build_ring calls are executed for ring_index in 0..=expected_rings_to_build - 1 { assert_ok!(build_ring(ring_index, None)); // Ring related storage items change assert!(Root::::get(ring_index).is_some()); assert_eq!(RingKeys::::get(ring_index).len(), 10); assert_eq!( RingKeysStatus::::get(ring_index), RingStatus { total: 10, included: 10 } ); // Records stay the same while personal_id < onboarding_size { let record = People::::get(personal_id as u64); assert!(record.is_some()); match record.unwrap().position { RingPosition::Included { ring_index: record_ring_index, .. } => { assert_eq!(record_ring_index, ring_index, "{personal_id}"); }, _ => panic!("Expected RingPosition::Included variant"), } personal_id += 1; } } }); } #[test] fn awaiting_people_in_between_rings_boundaries() { TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); // 3 rings with 9/10 people exists generate_people_with_index(1, 30); for _ in 1..=3 { assert_ok!(PeoplePallet::onboard_people()); } for ring_index in 0..=2 { assert_ok!(build_ring(ring_index, None)); } assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: Vec = vec![1, 11, 21]; assert_ok!(PeoplePallet::suspend_personhood(&suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); for ring_index in 0..=2 { assert_eq!( PeoplePallet::remove_suspended_keys(ring_index), ::WeightInfo::remove_suspended_people(10) ); } for ring_index in 0..=2 { assert_ok!(build_ring(ring_index, None)); assert!(Root::::get(ring_index).is_some()); assert_eq!(RingKeys::::get(ring_index).len(), 9); assert_eq!( RingKeysStatus::::get(ring_index), RingStatus { total: 9, included: 9 } ); } // 10 more people in the queue, that then onboard generate_people_with_index(31, 40); assert_eq!(OnboardingQueue::::get(0).len(), 10); assert_ok!(PeoplePallet::onboard_people()); assert_eq!(OnboardingQueue::::get(0).len(), 0); // Previous rings stay intact for ring_index in 0..=2 { assert!(Root::::get(ring_index).is_some()); assert_eq!(RingKeys::::get(ring_index).len(), 9); assert_eq!( RingKeysStatus::::get(ring_index), RingStatus { total: 9, included: 9 } ); } // New onboarding group is assigned to a new ring assert_eq!(RingKeys::::get(3).len(), 10); assert_eq!(RingKeysStatus::::get(3), RingStatus { total: 10, included: 0 }); }); } #[test] fn onboarding_queue_index_rotates_when_overflown() { TestExt::new().execute_with(|| { // Queue page indices near the maximum u32 value QueuePageIndices::::set((u32::MAX - 2, u32::MAX - 1)); // Enough people in the queue to fill current pages and trigger creation of a new page let queue_page_size: u32 = ::OnboardingQueuePageSize::get(); let people_to_onboard: u8 = (queue_page_size * 3) as u8; generate_people_with_index(1, people_to_onboard); // The tail has overflown and wrapped around // The head should have advanced but not overflown yet let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, u32::MAX - 2); assert_eq!(tail, 0); // Onboarding people should cause the head to overflow let onboarding_size: u32 = OnboardingSize::::get(); let expected_rings_to_build: u32 = people_to_onboard as u32 / onboarding_size; for _ in 0..=expected_rings_to_build - 1 { assert_ok!(PeoplePallet::onboard_people()); } // Head should have overflown and wrapped around let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 0); assert_eq!(tail, 0); // Just to make sure that the queue is still functional by adding more people // and onboarding them after the overflow generate_people_with_index( (queue_page_size * 3) as u8 + 1, (queue_page_size * 6) as u8, ); let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 0); assert_eq!(tail, 2); assert!( !OnboardingQueue::::get(head).is_empty() && !OnboardingQueue::::get(tail).is_empty() ); for _ in 0..=expected_rings_to_build - 1 { assert_ok!(PeoplePallet::onboard_people()); } assert_eq!(head, 0); assert_eq!(tail, 2); }); } #[test] fn multi_paged_queue_left_with_one_page_after_single_onboard_call() { let queue_page_size: u32 = ::OnboardingQueuePageSize::get(); let people_to_onboard = queue_page_size * 3; TestExt::new().max_ring_size(queue_page_size * 2).execute_with(|| { // Make the onboarding size big enough to consume 2 queue pages per single onboard call OnboardingSize::::set(queue_page_size * 2); // Enough people in the queue to fill multiple pages generate_people_with_index(1, people_to_onboard as u8); let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 0); assert_eq!(tail, 2); assert_eq!(OnboardingQueue::::get(0).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(1).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(2).len(), queue_page_size as usize); // Onboard people call succeeds assert_ok!(PeoplePallet::onboard_people()); // Members added assert_eq!(ActiveMembers::::get(), queue_page_size * 2); // Queue indices moved forward let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 2); assert_eq!(tail, 2); // First two pages are empty now assert_eq!(OnboardingQueue::::get(0).len(), 0); assert_eq!(OnboardingQueue::::get(1).len(), 0); // The last one still full assert_eq!(OnboardingQueue::::get(2).len(), queue_page_size as usize); // More people come along to fill in one more page generate_people_with_index( people_to_onboard as u8 + 1, people_to_onboard as u8 + queue_page_size as u8, ); // Previously empty pages stay empty, previously full page stays full, // one more page created and filled in assert_eq!(OnboardingQueue::::get(0).len(), 0); assert_eq!(OnboardingQueue::::get(1).len(), 0); assert_eq!(OnboardingQueue::::get(2).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(3).len(), queue_page_size as usize); // Indices change let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 2); assert_eq!(tail, 3); // Another onboard people call assert_ok!(PeoplePallet::onboard_people()); // Members added assert_eq!(ActiveMembers::::get(), queue_page_size * 4); // Queue indices move forward again let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 3); assert_eq!(tail, 3); // All the pages are empty now for i in 0..3 { assert_eq!(OnboardingQueue::::get(i).len(), 0); } // Again, more people come along to fill in one more page generate_people_with_index( people_to_onboard as u8 + queue_page_size as u8 + 1, people_to_onboard as u8 + (queue_page_size as u8 * 2), ); assert_eq!(OnboardingQueue::::get(3).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(4).len(), 0); let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 3); assert_eq!(tail, 3); }); } #[test] fn multi_paged_queue_left_with_no_queue_page_after_single_onboard_call() { let queue_page_size: u32 = ::OnboardingQueuePageSize::get(); let people_to_onboard = queue_page_size * 4; let onboarding_size = 5; TestExt::new().execute_with(|| { let mut meter = WeightMeter::new(); crate::OnboardingSize::::set(onboarding_size); // Enough people in the queue to fill multiple pages generate_people_with_index(1, people_to_onboard as u8); let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 0); assert_eq!(tail, 3); assert_eq!(OnboardingQueue::::get(0).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(1).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(2).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(3).len(), queue_page_size as usize); // Queue indices are updated let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 0); assert_eq!(tail, 3); assert_ok!(PeoplePallet::start_people_set_mutation_session()); let mut suspensions: Vec = (3u64..queue_page_size as PersonalId).collect(); suspensions .extend(2 + queue_page_size as PersonalId..2u64 * queue_page_size as PersonalId); assert_ok!(PeoplePallet::suspend_personhood(&suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); let db_weight: RuntimeDbWeight = ::DbWeight::get(); let end_migrate_db_weight = db_weight.reads_writes(2, 1); assert_eq!(meter.consumed(), end_migrate_db_weight); assert_eq!(OnboardingQueue::::get(0).len(), 3); assert_eq!(OnboardingQueue::::get(1).len(), 2); assert_eq!(OnboardingQueue::::get(2).len(), queue_page_size as usize); assert_eq!(OnboardingQueue::::get(3).len(), queue_page_size as usize); // Queue indices are the same because we still have the first 2 pages let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 0); assert_eq!(tail, 3); // Onboard people call succeeds assert_ok!(PeoplePallet::onboard_people()); // Members added assert_eq!(ActiveMembers::::get(), onboarding_size); // First two pages are empty now assert_eq!(OnboardingQueue::::get(0).len(), 0); assert_eq!(OnboardingQueue::::get(1).len(), 0); // Queue indices are updated to reflect the discarded pages let (head, tail) = QueuePageIndices::::get(); assert_eq!(head, 2); assert_eq!(tail, 3); // The we still have another 2 queue pages. assert_eq!(OnboardingQueue::::get(2).len(), queue_page_size as usize); }); } } mod merge_queue_pages { use super::*; use pezframe_support::pezpallet_prelude::Get; #[test] fn fails_if_queue_empty() { TestExt::new().execute_with(|| { assert!(matches!(PeoplePallet::should_merge_queue_pages(), QueueMergeAction::NoAction)); }); } #[test] fn fails_if_only_one_page_exists() { TestExt::new().execute_with(|| { generate_people_with_index(0, 8); assert!(matches!(PeoplePallet::should_merge_queue_pages(), QueueMergeAction::NoAction)); }); } #[test] fn fails_if_too_many_elements_per_page() { TestExt::new().execute_with(|| { // Two full pages exist let queue_size: u32 = ::OnboardingQueuePageSize::get(); let mut meter = WeightMeter::new(); generate_people_with_index(1, queue_size as u8 * 2); // Half of the people become suspended in the first page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let first_page_suspensions: Vec = (1..20).collect(); assert_ok!(PeoplePallet::suspend_personhood(&first_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); let db_weight: RuntimeDbWeight = ::DbWeight::get(); let end_migrate_db_weight = db_weight.reads_writes(2, 1); assert_eq!(meter.consumed(), end_migrate_db_weight); // Half of the people minus one become suspended in the second page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let second_page_suspensions: Vec = (40..60).collect(); assert_ok!(PeoplePallet::suspend_personhood(&second_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); // Attempt to merge pages fails assert!(matches!(PeoplePallet::should_merge_queue_pages(), QueueMergeAction::NoAction)); }); } #[test] fn succeeds_if_only_two_pages_exist() { TestExt::new().execute_with(|| { // Two full pages exist let queue_size: u32 = ::OnboardingQueuePageSize::get(); let mut meter = WeightMeter::new(); generate_people_with_index(1, queue_size as u8 * 2); // So the queue has two full pages in it assert_eq!(QueuePageIndices::::get(), (0, 1)); assert!(OnboardingQueue::::get(0).is_full()); assert!(OnboardingQueue::::get(1).is_full()); // Half of the people become suspended in the first page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let first_page_suspensions: Vec = (0..19).collect(); assert_ok!(PeoplePallet::suspend_personhood(&first_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); let db_weight: RuntimeDbWeight = ::DbWeight::get(); let end_migrate_db_weight = db_weight.reads_writes(2, 1); let mut expected_consumed = end_migrate_db_weight; assert_eq!(meter.consumed(), expected_consumed); // More than half of the people become suspended in the second page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let second_page_suspensions: Vec = (40..61).collect(); assert_ok!(PeoplePallet::suspend_personhood(&second_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); expected_consumed += end_migrate_db_weight; assert_eq!(meter.consumed(), expected_consumed); // Attempt to merge pages succeeds let QueueMergeAction::Merge { initial_head, new_head, first_key_page, second_key_page } = PeoplePallet::should_merge_queue_pages() else { panic!("should be mergeable") }; PeoplePallet::merge_queue_pages( initial_head, new_head, first_key_page, second_key_page, ); // The queue pages have changed assert_eq!(QueuePageIndices::::get(), (1, 1)); assert!(OnboardingQueue::::get(0).is_empty()); let page = OnboardingQueue::::get(1); assert!(page.is_full()); // All non-suspended people are still in the queue, in right order, with modified // personal records let first_page_remaining: Vec = (19..40).collect(); let second_page_remaining: Vec = (61..80).collect(); let mut page_iter = page.iter(); for id in first_page_remaining.iter().chain(second_page_remaining.iter()) { let key = page_iter.next(); assert!(key.is_some()); let key = key.unwrap(); let personal_id_from_page = Keys::::get(key).unwrap(); assert_eq!(*id, personal_id_from_page); let personal_record = People::::get(personal_id_from_page).unwrap(); assert_eq!(personal_record.position, RingPosition::Onboarding { queue_page: 1 }); } }); } #[test] fn succeeds_if_multiple_pages_exist() { TestExt::new().execute_with(|| { // Three full pages exist let queue_size: u32 = ::OnboardingQueuePageSize::get(); let mut meter = WeightMeter::new(); generate_people_with_index(1, queue_size as u8 * 3); // So the queue has three full pages in it assert_eq!(QueuePageIndices::::get(), (0, 2)); assert!(OnboardingQueue::::get(0).is_full()); assert!(OnboardingQueue::::get(1).is_full()); assert!(OnboardingQueue::::get(2).is_full()); // Half of the people become suspended in the first page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let first_page_suspensions: Vec = (0..19).collect(); assert_ok!(PeoplePallet::suspend_personhood(&first_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); let db_weight: RuntimeDbWeight = ::DbWeight::get(); let end_migrate_db_weight = db_weight.reads_writes(2, 1); let mut expected_consumed = end_migrate_db_weight; assert_eq!(meter.consumed(), expected_consumed); // More than half of the people become suspended in the second page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let second_page_suspensions: Vec = (40..61).collect(); assert_ok!(PeoplePallet::suspend_personhood(&second_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); expected_consumed += end_migrate_db_weight; assert_eq!(meter.consumed(), expected_consumed); // Attempt to merge pages succeeds let QueueMergeAction::Merge { initial_head, new_head, first_key_page, second_key_page } = PeoplePallet::should_merge_queue_pages() else { panic!("should be mergeable") }; PeoplePallet::merge_queue_pages( initial_head, new_head, first_key_page, second_key_page, ); // The queue pages have changed assert_eq!(QueuePageIndices::::get(), (1, 2)); assert!(OnboardingQueue::::get(0).is_empty()); assert!(OnboardingQueue::::get(2).is_full()); let page = OnboardingQueue::::get(1); assert!(page.is_full()); // All non-suspended people are still in the queue, in right order, with modified // personal records let first_page_remaining: Vec = (19..40).collect(); let second_page_remaining: Vec = (61..80).collect(); let mut page_iter = page.iter(); for id in first_page_remaining.iter().chain(second_page_remaining.iter()) { let key = page_iter.next(); assert!(key.is_some()); let key = key.unwrap(); let personal_id_from_page = Keys::::get(key).unwrap(); assert_eq!(*id, personal_id_from_page); let personal_record = People::::get(personal_id_from_page).unwrap(); assert_eq!(personal_record.position, RingPosition::Onboarding { queue_page: 1 }); } }); } } #[test] fn test_revision_in_tx_ext_as_alias_account() { new_test_ext().execute_with(|| { // Setup crate::OnboardingSize::::set(1); let (_, pk, sk) = generate_people_with_index(0, 0).pop().unwrap(); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(0, None)); let alias_account = 37; setup_alias_account(&pk, &sk, MOCK_CONTEXT, alias_account); // Use alias account successfully let call = pezframe_system::Call::remark { remark: vec![] }; assert_ok!(exec_as_alias_tx(alias_account, call)); // Revise the ring crate::Root::::mutate(0, |root| { root.as_mut().unwrap().revision = 1; }); // Fail to alias account with outdated revision let call = pezframe_system::Call::remark { remark: vec![] }; assert_noop!(exec_as_alias_tx(alias_account, call), BadSigner); }); } mod poll { use super::*; #[test] fn on_poll_migrates_keys() { new_test_ext().execute_with(|| { // A ring with people exists generate_people_with_index(0, 9); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); for i in 0..5 { let new_secret = Simple::new_secret([100 + i; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(i as PersonalId)); assert_ok!(PeoplePallet::migrate_included_key(origin, new_public)); assert_eq!(KeyMigrationQueue::::get(i as PersonalId).unwrap(), new_public); } let db_weight: RuntimeDbWeight = ::DbWeight::get(); let db_weight = db_weight.reads_writes(1, 1); let base_weight = <::WeightInfo as crate::WeightInfo>::on_poll_base(); let step_migration_weight = <::WeightInfo as crate::WeightInfo>::migrate_keys_single_included_key() .saturating_add(db_weight); assert_ok!(PeoplePallet::start_people_set_mutation_session()); assert_ok!(PeoplePallet::end_people_set_mutation_session()); let mut meter = WeightMeter::with_limit(base_weight); PeoplePallet::on_poll(0, &mut meter); assert_eq!(KeyMigrationQueue::::iter().count(), 5); assert!(RingsState::::get().key_migration()); let mut meter = WeightMeter::with_limit(base_weight + step_migration_weight); PeoplePallet::on_poll(0, &mut meter); assert_eq!(KeyMigrationQueue::::iter().count(), 4); assert!(RingsState::::get().key_migration()); let mut meter = WeightMeter::with_limit(base_weight + step_migration_weight.saturating_mul(4)); PeoplePallet::on_poll(0, &mut meter); assert_eq!(KeyMigrationQueue::::iter().count(), 0); assert!(RingsState::::get().key_migration()); let mut meter = WeightMeter::with_limit(base_weight + step_migration_weight); PeoplePallet::on_poll(0, &mut meter); assert_eq!(KeyMigrationQueue::::iter().count(), 0); assert!(RingsState::::get().append_only()); let db_weight: RuntimeDbWeight = ::DbWeight::get(); // 1R + 1W on `RingsState` for the state transition, 1R for `KeyMigrationQueue`. let db_weight = db_weight.reads_writes(2, 1); assert_eq!(meter.consumed(), base_weight + db_weight); }); } #[test] fn on_poll_removes_stale_keys_once() { new_test_ext().execute_with(|| { OnboardingSize::::put(1); // First ring full, 10 people // Second ring with 8 people generate_people_with_index(0, 17); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 2, 3, 4, 5, 11, 12, 13, 14, 15]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(1).into_inner(), vec![1, 2, 3, 4, 5]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); RingsState::::put(RingMembersState::AppendOnly); let base_weight = <::WeightInfo as crate::WeightInfo>::on_poll_base(); let step_remove_keys_weight = <::WeightInfo as crate::WeightInfo>::remove_suspended_people( ::MaxRingSize::get(), ); let mut meter = WeightMeter::with_limit(base_weight); PeoplePallet::on_poll(0, &mut meter); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(1).into_inner(), vec![1, 2, 3, 4, 5]); let mut meter = WeightMeter::new(); PeoplePallet::on_poll(0, &mut meter); assert_eq!(PendingSuspensions::::iter().count(), 1); let mut actual_consumed = meter.consumed(); let mut meter = WeightMeter::with_limit(base_weight + step_remove_keys_weight); PeoplePallet::on_poll(0, &mut meter); assert_eq!(PendingSuspensions::::iter().count(), 0); actual_consumed += meter.consumed(); assert_eq!( actual_consumed, base_weight .saturating_mul(2) .saturating_add( <::WeightInfo as crate::WeightInfo>::remove_suspended_people( 10, ) ) .saturating_add( <::WeightInfo as crate::WeightInfo>::remove_suspended_people(8) ) ); }); } #[test] fn on_poll_merges_queue_pages() { new_test_ext().execute_with(|| { // Two full pages exist let queue_size: u32 = ::OnboardingQueuePageSize::get(); let mut meter = WeightMeter::new(); generate_people_with_index(1, queue_size as u8 * 2); // So the queue has two full pages in it assert_eq!(QueuePageIndices::::get(), (0, 1)); assert!(OnboardingQueue::::get(0).is_full()); assert!(OnboardingQueue::::get(1).is_full()); // Half of the people become suspended in the first page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let first_page_suspensions: Vec = (0..19).collect(); assert_ok!(PeoplePallet::suspend_personhood(&first_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); // More than half of the people become suspended in the second page assert_ok!(PeoplePallet::start_people_set_mutation_session()); let second_page_suspensions: Vec = (40..61).collect(); assert_ok!(PeoplePallet::suspend_personhood(&second_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); PeoplePallet::migrate_keys(&mut meter); assert!(matches!( PeoplePallet::should_merge_queue_pages(), QueueMergeAction::Merge { .. } )); let base_weight = <::WeightInfo as crate::WeightInfo>::on_poll_base(); let merge_pages_weight = <::WeightInfo as crate::WeightInfo>::merge_queue_pages(); // Not enough weight let mut meter = WeightMeter::with_limit(base_weight); PeoplePallet::on_poll(0, &mut meter); assert_eq!(QueuePageIndices::::get(), (0, 1)); // Give it enough weight let mut meter = WeightMeter::with_limit(base_weight + merge_pages_weight); PeoplePallet::on_poll(0, &mut meter); // The queue pages have changed assert_eq!(QueuePageIndices::::get(), (1, 1)); assert!(OnboardingQueue::::get(0).is_empty()); let page = OnboardingQueue::::get(1); assert!(page.is_full()); }); } } #[test] fn on_poll_works() { new_test_ext().execute_with(|| { let queue_size: u32 = ::OnboardingQueuePageSize::get(); let ring_size: u32 = ::MaxRingSize::get(); OnboardingSize::::put(1); generate_people_with_index(1, 2 * ring_size as u8 + 2 * queue_size as u8); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); // 20 in rings 1 and 2, onboarding pages [20, 40, 20] assert_eq!(QueuePageIndices::::get(), (0, 2)); for i in 0..5 { let new_secret = Simple::new_secret([250 + i; 32]); let new_public = Simple::member_from_secret(&new_secret); let origin = RuntimeOrigin::from(PeopleOrigin::PersonalIdentity(i as PersonalId)); assert_ok!(PeoplePallet::migrate_included_key(origin, new_public)); assert_eq!(KeyMigrationQueue::::get(i as PersonalId).unwrap(), new_public); } assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 2, 3, 4, 5, 11, 12, 13, 14, 15]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(1).into_inner(), vec![1, 2, 3, 4, 5]); let second_page_suspensions: Vec = (41..61).collect(); assert_ok!(PeoplePallet::suspend_personhood(&second_page_suspensions)); assert_ok!(PeoplePallet::end_people_set_mutation_session()); assert_eq!(QueuePageIndices::::get(), (0, 2)); // Give it enough weight let mut meter = WeightMeter::new(); PeoplePallet::on_poll(0, &mut meter); assert_eq!(KeyMigrationQueue::::iter().count(), 0); assert!(RingsState::::get().append_only()); assert_eq!(PendingSuspensions::::iter().count(), 1); assert_eq!(QueuePageIndices::::get(), (1, 2)); assert!(OnboardingQueue::::get(0).is_empty()); let page = OnboardingQueue::::get(1); assert!(page.is_full()); let db_weight: RuntimeDbWeight = ::DbWeight::get(); let migrate_db_weight = db_weight.reads_writes(1, 1); let end_migrate_db_weight = db_weight.reads_writes(2, 1); let base_weight = <::WeightInfo as crate::WeightInfo>::on_poll_base(); let step_migration_weight = <::WeightInfo as crate::WeightInfo>::migrate_keys_single_included_key() .saturating_add(migrate_db_weight); let step_remove_keys_weight = <::WeightInfo as crate::WeightInfo>::remove_suspended_people(ring_size); let merge_pages_weight = <::WeightInfo as crate::WeightInfo>::merge_queue_pages(); let expected_consumed = base_weight + step_migration_weight.saturating_mul(5) + end_migrate_db_weight + step_remove_keys_weight + merge_pages_weight; assert_eq!(meter.consumed(), expected_consumed); }); } #[test] fn on_idle_works() { new_test_ext().execute_with(|| { OnboardingSize::::put(1); // First ring full, 10 people // Second ring full, 10 people // Third ring with 8 people generate_people_with_index(0, 27); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); assert_ok!(build_ring(1, None)); assert_ok!(build_ring(2, None)); assert_ok!(PeoplePallet::start_people_set_mutation_session()); let suspensions: &[PersonalId] = &[1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 21, 22, 23, 24, 25]; assert_ok!(PeoplePallet::suspend_personhood(suspensions)); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(1).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(2).into_inner(), vec![1, 2, 3, 4, 5]); assert_ok!(PeoplePallet::end_people_set_mutation_session()); RingsState::::put(RingMembersState::AppendOnly); let poll_base_weight = <::WeightInfo as crate::WeightInfo>::on_poll_base(); let idle_base_weight = <::WeightInfo as crate::WeightInfo>::on_idle_base(); let step_remove_keys_weight = <::WeightInfo as crate::WeightInfo>::remove_suspended_people( ::MaxRingSize::get(), ); let step_pending_suspensions_weight = <::WeightInfo as crate::WeightInfo>::pending_suspensions_iteration(); let idle_step_weight = step_remove_keys_weight; assert_eq!(PeoplePallet::on_idle(0, Weight::zero()), Weight::zero()); assert_eq!(suspended_indices_list(RI_ZERO).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(1).into_inner(), vec![1, 2, 3, 4, 5]); assert_eq!(suspended_indices_list(2).into_inner(), vec![1, 2, 3, 4, 5]); let mut meter = WeightMeter::with_limit(poll_base_weight + step_remove_keys_weight.saturating_mul(3)); PeoplePallet::on_poll(0, &mut meter); assert_eq!(PendingSuspensions::::iter().count(), 2); assert_eq!(meter.consumed(), poll_base_weight.saturating_add(step_remove_keys_weight)); let expected_idle_weight = idle_base_weight .saturating_add(idle_step_weight.saturating_mul(2)) .saturating_add(step_pending_suspensions_weight.saturating_mul(2)); let actual_idle_weight = PeoplePallet::on_idle(0, expected_idle_weight.saturating_mul(2)); assert_eq!(PendingSuspensions::::iter().count(), 0); assert_eq!( actual_idle_weight, idle_base_weight .saturating_add(step_remove_keys_weight) .saturating_add( <::WeightInfo as crate::WeightInfo>::remove_suspended_people(8) ) .saturating_add( <::WeightInfo as crate::WeightInfo>::onboard_people() ) .saturating_add(step_pending_suspensions_weight.saturating_mul(2)) ); }); } #[test] fn test_under_alias_revision_check() { new_test_ext().execute_with(|| { OnboardingSize::::set(1); // Setup a person and its alias account let (_, pk, sk) = generate_people_with_index(0, 0).pop().unwrap(); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(0, None)); let ring_info = Root::::get(0).expect("Ring must exist after building"); assert_eq!(ring_info.revision, 0); let alias_account: u64 = 42; setup_alias_account(&pk, &sk, MOCK_CONTEXT, alias_account); // The account can now use `under_alias` successfully let dummy_call = Box::new(RuntimeCall::from(pezframe_system::Call::remark { remark: vec![] })); assert_ok!(PeoplePallet::under_alias( RuntimeOrigin::signed(alias_account), dummy_call.clone() )); // Now we change the ring to revision=1, making the stored alias outdated. let mut ring_info = Root::::get(0).unwrap(); ring_info.revision = 1; Root::::insert(0, ring_info); // Attempt `under_alias` again with the *outdated* revision=0 from storage => should fail. assert_noop!( PeoplePallet::under_alias(RuntimeOrigin::signed(alias_account), dummy_call), pezsp_runtime::DispatchError::BadOrigin, ); }); } #[test] fn resetting_alias_account_for_new_revision_is_refunded() { new_test_ext().execute_with(|| { use crate::pezpallet::Origin as PeopleOrigin; OnboardingSize::::set(1); // Create a single person so that `build_ring` can work. let (_, _key, _secret) = generate_people_with_index(0, 0).pop().unwrap(); // Build the ring with the single key we just inserted. This sets the ring revision to 0. assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(0, None)); let ring_info = Root::::get(0).expect("Ring must exist after build_ring"); assert_eq!(ring_info.revision, 0); // Set an alias with revision=0 for an account for the first time. // We expect `Pays::No` because no alias was previously set. let account: u64 = 42; let ca = ContextualAlias { alias: [1u8; 32], context: MOCK_CONTEXT }; let rev_ca = RevisedContextualAlias { revision: 0, ring: 0, ca: ca.clone() }; let origin = RuntimeOrigin::from(PeopleOrigin::PersonalAlias(rev_ca.clone())); let result = PeoplePallet::set_alias_account(origin, account, 0); assert_eq!(result.unwrap(), pezframe_support::pezpallet_prelude::Pays::No.into()); // Fail attempt to set the same alias again with the *same* revision=0 for the same account. let origin = RuntimeOrigin::from(PeopleOrigin::PersonalAlias(rev_ca.clone())); assert_noop!( PeoplePallet::set_alias_account(origin, account, 0), Error::::AliasAccountAlreadySet ); // Attempt to set the same alias again with the *same* revision=0 for a different account. let origin = RuntimeOrigin::from(PeopleOrigin::PersonalAlias(rev_ca.clone())); let account2: u64 = 43; let result = PeoplePallet::set_alias_account(origin, account2, 0); assert_eq!(result.unwrap(), pezframe_support::pezpallet_prelude::Pays::Yes.into()); // Set the ring revision to 1. let ring_info = Root::::get(0).unwrap(); Root::::insert(0, RingRoot { revision: 1, ..ring_info }); // Now set the alias account again, but *with the newer revision=1*. // We expect `Pays::No` because the revision of the alias <-> Account is needed. let rev_ca_new = RevisedContextualAlias { revision: 1, ring: 0, ca: ca.clone() }; let origin = RuntimeOrigin::from(PeopleOrigin::PersonalAlias(rev_ca_new.clone())); let result = PeoplePallet::set_alias_account(origin, account, 0); assert_eq!(result.unwrap(), pezframe_support::pezpallet_prelude::Pays::No.into()); // Move to a different ring. let ring_info = Root::::get(0).unwrap(); Root::::insert(1, ring_info); // Now set the alias account again, but *with the different ring=1*. // We expect `Pays::No` because the revision of the alias <-> Account is needed. let rev_ca_new = RevisedContextualAlias { revision: 1, ring: 1, ca }; let origin = RuntimeOrigin::from(PeopleOrigin::PersonalAlias(rev_ca_new.clone())); let result = PeoplePallet::set_alias_account(origin, account, 0); assert_eq!(result.unwrap(), pezframe_support::pezpallet_prelude::Pays::No.into()); }); } #[test] fn replay_protection_for_identity() { new_test_ext().execute_with(|| { const EXTENSION_VERSION: u8 = 0; // Setup alice as a member. let alice_sec = Simple::new_secret([1u8; 32]); let alice_pub = Simple::member_from_secret(&alice_sec); let alice_index = PeoplePallet::reserve_new_id(); PeoplePallet::recognize_personhood(alice_index, Some(alice_pub)).unwrap(); let generate_setup_account_tx_ext_for_call = |call: RuntimeCall| { let other_tx_ext = (pezframe_system::CheckNonce::::from(0),); // Here we simply ignore implicit as they are null. let msg = (&EXTENSION_VERSION, &call, &other_tx_ext) .using_encoded(pezsp_io::hashing::blake2_256); let signature = Simple::sign(&alice_sec, &msg).unwrap(); ( AsPerson::::new(Some(AsPersonInfo::AsPersonalIdentityWithProof( signature, alice_index, ))), other_tx_ext.0, ) }; // Transaction 1: Alice sets its personal id account to 10. let call = RuntimeCall::PeoplePallet(crate::Call::set_personal_id_account { account: 10, call_valid_at: System::block_number(), }); let tx_ext = generate_setup_account_tx_ext_for_call(call.clone()); assert_ok!(exec_tx(None, tx_ext.clone(), call.clone())); assert_eq!(crate::People::::get(alice_index).unwrap().account, Some(10)); assert_eq!(crate::AccountToPersonalId::::get(10), Some(alice_index)); // Somebody tries to replay the transaction, it must fail, replay is protected. assert_noop!(exec_tx(None, tx_ext.clone(), call.clone()), InvalidTransaction::Stale); // Transaction 2: Alice sets its personal id account to 11, with call valid only in the // future. let call_2 = RuntimeCall::PeoplePallet(crate::Call::set_personal_id_account { account: 11, call_valid_at: System::block_number() + 1, }); let tx_ext_2 = generate_setup_account_tx_ext_for_call(call_2.clone()); // Transaction 2 is not valid yet. assert_noop!(exec_tx(None, tx_ext_2.clone(), call_2.clone()), InvalidTransaction::Future); assert_eq!(crate::People::::get(alice_index).unwrap().account, Some(10)); assert_eq!(crate::AccountToPersonalId::::get(10), Some(alice_index)); assert_eq!(crate::AccountToPersonalId::::get(11), None); // Advance some time. Transaction 1 is still valid, transaction 2 becomes valid. mock::advance_to(System::block_number() + PeoplePallet::account_setup_time_tolerance()); // Transaction 2 is now valid. assert_ok!(exec_tx(None, tx_ext_2.clone(), call_2.clone())); assert_eq!(crate::People::::get(alice_index).unwrap().account, Some(11)); assert_eq!(crate::AccountToPersonalId::::get(11), Some(alice_index)); assert_eq!(crate::AccountToPersonalId::::get(10), None); // Somebody replays the transaction 1, it must succeed. It is within time tolerance. assert_ok!(exec_tx(None, tx_ext.clone(), call.clone())); assert_eq!(crate::People::::get(alice_index).unwrap().account, Some(10)); assert_eq!(crate::AccountToPersonalId::::get(10), Some(alice_index)); assert_eq!(crate::AccountToPersonalId::::get(11), None); // Replay the transaction 2. assert_ok!(exec_tx(None, tx_ext_2.clone(), call_2.clone())); assert_eq!(crate::People::::get(alice_index).unwrap().account, Some(11)); assert_eq!(crate::AccountToPersonalId::::get(11), Some(alice_index)); assert_eq!(crate::AccountToPersonalId::::get(10), None); // Advance some time, Now time tolerance is exceeded for transaction 1. mock::advance_to(System::block_number() + 1); // Somebody replays the first transaction, it is invalid. assert_noop!(exec_tx(None, tx_ext, call), InvalidTransaction::Stale); assert_eq!(crate::People::::get(alice_index).unwrap().account, Some(11)); assert_eq!(crate::AccountToPersonalId::::get(11), Some(alice_index)); assert_eq!(crate::AccountToPersonalId::::get(10), None); }); } #[test] fn replay_protection_for_alias() { new_test_ext().execute_with(|| { const EXTENSION_VERSION: u8 = 0; // Setup Alice as a member. PeoplePallet::set_onboarding_size(RuntimeOrigin::root(), 1).unwrap(); let alice_sec = Simple::new_secret([1u8; 32]); let alice_pub = Simple::member_from_secret(&alice_sec); let alice_index = PeoplePallet::reserve_new_id(); PeoplePallet::recognize_personhood(alice_index, Some(alice_pub)).unwrap(); assert_ok!(PeoplePallet::onboard_people()); assert_ok!(build_ring(RI_ZERO, None)); let generate_alias_tx_ext_for_call = |call: RuntimeCall| { let other_tx_ext = (pezframe_system::CheckNonce::::from(0),); // The message is the hash over the extension version, call, and other extensions. let msg = (&EXTENSION_VERSION, &call, &other_tx_ext) .using_encoded(pezsp_io::hashing::blake2_256); // Open a commitment (using Alice’s public key and public data) let commitment = Simple::open(&alice_pub, Some(alice_pub).into_iter()).unwrap(); // Create a VRF proof and compute the alias output from the call message. let (proof, alias_value) = Simple::create(commitment, &alice_sec, &MOCK_CONTEXT, &msg).unwrap(); let alias = ContextualAlias { context: MOCK_CONTEXT, alias: alias_value }; let tx_ext = ( AsPerson::::new(Some(AsPersonInfo::AsPersonalAliasWithProof( proof, 0, MOCK_CONTEXT, ))), other_tx_ext.0, ); (tx_ext, alias) }; // --- Transaction 1: set alias account to 10 --- // Use the current block number as the valid time. let call1 = RuntimeCall::PeoplePallet(crate::Call::set_alias_account { account: 10, call_valid_at: System::block_number(), }); let (tx_ext1, alias) = generate_alias_tx_ext_for_call(call1.clone()); let rev_alias = RevisedContextualAlias { revision: 0, ring: 0, ca: alias.clone() }; // Execute transaction 1. It should succeed. assert_ok!(exec_tx(None, tx_ext1.clone(), call1.clone())); assert_eq!(crate::AliasToAccount::::get(&alias), Some(10)); assert_eq!(crate::AccountToAlias::::get(10), Some(rev_alias.clone())); // Replay transaction 1 immediately: it must fail (replay protected). assert_noop!(exec_tx(None, tx_ext1.clone(), call1.clone()), InvalidTransaction::Stale); // --- Transaction 2: set alias account to 11 --- // Set its valid time to the future: current block number plus the allowed tolerance + 1. let call2 = RuntimeCall::PeoplePallet(crate::Call::set_alias_account { account: 11, call_valid_at: System::block_number() + 1, }); let (tx_ext2, _) = generate_alias_tx_ext_for_call(call2.clone()); // Transaction 2 is too early: it should be rejected as "Future". assert_noop!(exec_tx(None, tx_ext2.clone(), call2.clone()), InvalidTransaction::Future); // The mapping still reflects transaction 1. assert_eq!(crate::AliasToAccount::::get(&alias), Some(10)); assert_eq!(crate::AccountToAlias::::get(10), Some(rev_alias.clone())); assert_eq!(crate::AccountToAlias::::get(11), None); // Advance time by the allowed tolerance. Now transaction 2 becomes valid. mock::advance_to(System::block_number() + PeoplePallet::account_setup_time_tolerance()); // Execute transaction 2. It now succeeds. assert_ok!(exec_tx(None, tx_ext2.clone(), call2.clone())); assert_eq!(crate::AliasToAccount::::get(&alias), Some(11)); assert_eq!(crate::AccountToAlias::::get(11), Some(rev_alias.clone())); assert_eq!(crate::AccountToAlias::::get(10), None); // --- Replaying old transactions within tolerance --- // Replay transaction 1. Within the tolerance window its replay is allowed. assert_ok!(exec_tx(None, tx_ext1.clone(), call1.clone())); assert_eq!(crate::AliasToAccount::::get(&alias), Some(10)); assert_eq!(crate::AccountToAlias::::get(10), Some(rev_alias.clone())); assert_eq!(crate::AccountToAlias::::get(11), None); // Replay transaction 2 to set it back to 11. assert_ok!(exec_tx(None, tx_ext2.clone(), call2.clone())); assert_eq!(crate::AliasToAccount::::get(&alias), Some(11)); assert_eq!(crate::AccountToAlias::::get(11), Some(rev_alias.clone())); assert_eq!(crate::AccountToAlias::::get(10), None); // --- Advance time beyond tolerance --- // After advancing time a bit more, the time tolerance for transaction 1 is exceeded. mock::advance_to(System::block_number() + 1); // Now replaying transaction 1 must be rejected as stale. assert_noop!(exec_tx(None, tx_ext1, call1), InvalidTransaction::Stale); assert_eq!(crate::AliasToAccount::::get(&alias), Some(11)); assert_eq!(crate::AccountToAlias::::get(11), Some(rev_alias)); }); }