referrerpolicy=no-referrer-when-downgrade

pallet_recovery/
benchmarking.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18#![cfg(feature = "runtime-benchmarks")]
19
20use super::*;
21
22use crate::Pallet;
23use alloc::{boxed::Box, vec, vec::Vec};
24use frame::benchmarking::prelude::*;
25
26const SEED: u32 = 0;
27const DEFAULT_DELAY: u32 = 0;
28
29fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
30	frame_system::Pallet::<T>::assert_last_event(generic_event.into());
31}
32
33fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
34	frame_system::Pallet::<T>::assert_has_event(generic_event.into());
35}
36
37fn get_total_deposit<T: Config>(
38	bounded_friends: &FriendsOf<T>,
39) -> Option<<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance>
40{
41	let friend_deposit = T::FriendDepositFactor::get()
42		.checked_mul(&bounded_friends.len().saturated_into())
43		.unwrap();
44
45	T::ConfigDepositBase::get().checked_add(&friend_deposit)
46}
47
48fn generate_friends<T: Config>(num: u32) -> Vec<<T as frame_system::Config>::AccountId> {
49	// Create friends
50	let mut friends = (0..num).map(|x| account("friend", x, SEED)).collect::<Vec<_>>();
51	// Sort
52	friends.sort();
53
54	for friend in 0..friends.len() {
55		// Top up accounts of friends
56		T::Currency::make_free_balance_be(
57			&friends.get(friend).unwrap(),
58			BalanceOf::<T>::max_value(),
59		);
60	}
61
62	friends
63}
64
65fn add_caller_and_generate_friends<T: Config>(
66	caller: T::AccountId,
67	num: u32,
68) -> Vec<<T as frame_system::Config>::AccountId> {
69	// Create friends
70	let mut friends = generate_friends::<T>(num - 1);
71
72	T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
73
74	friends.push(caller);
75
76	// Sort
77	friends.sort();
78
79	friends
80}
81
82fn insert_recovery_config_with_max_friends<T: Config>(account: &T::AccountId) {
83	T::Currency::make_free_balance_be(&account, BalanceOf::<T>::max_value());
84
85	let n = T::MaxFriends::get();
86
87	let friends = generate_friends::<T>(n);
88
89	let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
90
91	// Get deposit for recovery
92	let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
93
94	let recovery_config = RecoveryConfig {
95		delay_period: DEFAULT_DELAY.into(),
96		deposit: total_deposit,
97		friends: bounded_friends,
98		threshold: n as u16,
99	};
100
101	// Reserve deposit for recovery
102	T::Currency::reserve(&account, total_deposit).unwrap();
103
104	<Recoverable<T>>::insert(&account, recovery_config);
105}
106
107fn setup_active_recovery_with_max_friends<T: Config>(
108	caller: &T::AccountId,
109	lost_account: &T::AccountId,
110) {
111	insert_recovery_config_with_max_friends::<T>(&lost_account);
112	let n = T::MaxFriends::get();
113	let friends = generate_friends::<T>(n);
114	let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
115
116	let initial_recovery_deposit = T::RecoveryDeposit::get();
117	T::Currency::reserve(caller, initial_recovery_deposit).unwrap();
118
119	let active_recovery = ActiveRecovery {
120		created: DEFAULT_DELAY.into(),
121		deposit: initial_recovery_deposit,
122		friends: bounded_friends,
123	};
124	<ActiveRecoveries<T>>::insert(lost_account, caller, active_recovery);
125}
126
127#[benchmarks]
128mod benchmarks {
129	use super::*;
130
131	#[benchmark]
132	fn as_recovered() {
133		let caller: T::AccountId = whitelisted_caller();
134		let recovered_account: T::AccountId = account("recovered_account", 0, SEED);
135		let recovered_account_lookup = T::Lookup::unlookup(recovered_account.clone());
136		let call: <T as Config>::RuntimeCall =
137			frame_system::Call::<T>::remark { remark: vec![] }.into();
138
139		Proxy::<T>::insert(&caller, &recovered_account);
140
141		#[extrinsic_call]
142		_(RawOrigin::Signed(caller), recovered_account_lookup, Box::new(call))
143	}
144
145	#[benchmark]
146	fn set_recovered() {
147		let lost: T::AccountId = whitelisted_caller();
148		let lost_lookup = T::Lookup::unlookup(lost.clone());
149		let rescuer: T::AccountId = whitelisted_caller();
150		let rescuer_lookup = T::Lookup::unlookup(rescuer.clone());
151
152		#[extrinsic_call]
153		_(RawOrigin::Root, lost_lookup, rescuer_lookup);
154
155		assert_last_event::<T>(
156			Event::AccountRecovered { lost_account: lost, rescuer_account: rescuer }.into(),
157		);
158	}
159
160	#[benchmark]
161	fn create_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
162		let caller: T::AccountId = whitelisted_caller();
163		T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
164
165		// Create friends
166		let friends = generate_friends::<T>(n);
167
168		#[extrinsic_call]
169		_(RawOrigin::Signed(caller.clone()), friends, n as u16, DEFAULT_DELAY.into());
170
171		assert_last_event::<T>(Event::RecoveryCreated { account: caller }.into());
172	}
173
174	#[benchmark]
175	fn initiate_recovery() {
176		let caller: T::AccountId = whitelisted_caller();
177		T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
178
179		let lost_account: T::AccountId = account("lost_account", 0, SEED);
180		let lost_account_lookup = T::Lookup::unlookup(lost_account.clone());
181
182		insert_recovery_config_with_max_friends::<T>(&lost_account);
183
184		#[extrinsic_call]
185		_(RawOrigin::Signed(caller.clone()), lost_account_lookup);
186
187		assert_last_event::<T>(
188			Event::RecoveryInitiated { lost_account, rescuer_account: caller }.into(),
189		);
190	}
191
192	#[benchmark]
193	fn vouch_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
194		let caller: T::AccountId = whitelisted_caller();
195		let lost_account: T::AccountId = account("lost_account", 0, SEED);
196		let lost_account_lookup = T::Lookup::unlookup(lost_account.clone());
197		let rescuer_account: T::AccountId = account("rescuer_account", 0, SEED);
198		let rescuer_account_lookup = T::Lookup::unlookup(rescuer_account.clone());
199
200		// Create friends
201		let friends = add_caller_and_generate_friends::<T>(caller.clone(), n);
202		let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
203
204		// Get deposit for recovery
205		let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
206
207		let recovery_config = RecoveryConfig {
208			delay_period: DEFAULT_DELAY.into(),
209			deposit: total_deposit,
210			friends: bounded_friends.clone(),
211			threshold: n as u16,
212		};
213
214		// Create the recovery config storage item
215		<Recoverable<T>>::insert(&lost_account, recovery_config.clone());
216
217		// Reserve deposit for recovery
218		T::Currency::reserve(&caller, total_deposit).unwrap();
219
220		// Create an active recovery status
221		let recovery_status = ActiveRecovery {
222			created: DEFAULT_DELAY.into(),
223			deposit: total_deposit,
224			friends: generate_friends::<T>(n - 1).try_into().unwrap(),
225		};
226
227		// Create the active recovery storage item
228		<ActiveRecoveries<T>>::insert(&lost_account, &rescuer_account, recovery_status);
229
230		#[extrinsic_call]
231		_(RawOrigin::Signed(caller.clone()), lost_account_lookup, rescuer_account_lookup);
232		assert_last_event::<T>(
233			Event::RecoveryVouched { lost_account, rescuer_account, sender: caller }.into(),
234		);
235	}
236
237	#[benchmark]
238	fn claim_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
239		let caller: T::AccountId = whitelisted_caller();
240		let lost_account: T::AccountId = account("lost_account", 0, SEED);
241		let lost_account_lookup = T::Lookup::unlookup(lost_account.clone());
242
243		T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
244
245		// Create friends
246		let friends = generate_friends::<T>(n);
247		let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
248
249		// Get deposit for recovery
250		let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
251
252		let recovery_config = RecoveryConfig {
253			delay_period: 0u32.into(),
254			deposit: total_deposit,
255			friends: bounded_friends.clone(),
256			threshold: n as u16,
257		};
258
259		// Create the recovery config storage item
260		<Recoverable<T>>::insert(&lost_account, recovery_config.clone());
261
262		// Reserve deposit for recovery
263		T::Currency::reserve(&caller, total_deposit).unwrap();
264
265		// Create an active recovery status
266		let recovery_status = ActiveRecovery {
267			created: 0u32.into(),
268			deposit: total_deposit,
269			friends: bounded_friends.clone(),
270		};
271
272		// Create the active recovery storage item
273		<ActiveRecoveries<T>>::insert(&lost_account, &caller, recovery_status);
274
275		#[extrinsic_call]
276		_(RawOrigin::Signed(caller.clone()), lost_account_lookup);
277		assert_last_event::<T>(
278			Event::AccountRecovered { lost_account, rescuer_account: caller }.into(),
279		);
280	}
281
282	#[benchmark]
283	fn close_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
284		let caller: T::AccountId = whitelisted_caller();
285		let rescuer_account: T::AccountId = account("rescuer_account", 0, SEED);
286		let rescuer_account_lookup = T::Lookup::unlookup(rescuer_account.clone());
287
288		T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
289		T::Currency::make_free_balance_be(&rescuer_account, BalanceOf::<T>::max_value());
290
291		// Create friends
292		let friends = generate_friends::<T>(n);
293		let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
294
295		// Get deposit for recovery
296		let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
297
298		let recovery_config = RecoveryConfig {
299			delay_period: DEFAULT_DELAY.into(),
300			deposit: total_deposit,
301			friends: bounded_friends.clone(),
302			threshold: n as u16,
303		};
304
305		// Create the recovery config storage item
306		<Recoverable<T>>::insert(&caller, recovery_config.clone());
307
308		// Reserve deposit for recovery
309		T::Currency::reserve(&caller, total_deposit).unwrap();
310
311		// Create an active recovery status
312		let recovery_status = ActiveRecovery {
313			created: DEFAULT_DELAY.into(),
314			deposit: total_deposit,
315			friends: bounded_friends.clone(),
316		};
317
318		// Create the active recovery storage item
319		<ActiveRecoveries<T>>::insert(&caller, &rescuer_account, recovery_status);
320
321		#[extrinsic_call]
322		_(RawOrigin::Signed(caller.clone()), rescuer_account_lookup);
323		assert_last_event::<T>(
324			Event::RecoveryClosed { lost_account: caller, rescuer_account }.into(),
325		);
326	}
327
328	#[benchmark]
329	fn remove_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
330		let caller: T::AccountId = whitelisted_caller();
331
332		T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
333
334		// Create friends
335		let friends = generate_friends::<T>(n);
336		let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
337
338		// Get deposit for recovery
339		let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
340
341		let recovery_config = RecoveryConfig {
342			delay_period: DEFAULT_DELAY.into(),
343			deposit: total_deposit,
344			friends: bounded_friends.clone(),
345			threshold: n as u16,
346		};
347
348		// Create the recovery config storage item
349		<Recoverable<T>>::insert(&caller, recovery_config);
350
351		// Reserve deposit for recovery
352		T::Currency::reserve(&caller, total_deposit).unwrap();
353
354		#[extrinsic_call]
355		_(RawOrigin::Signed(caller.clone()));
356		assert_last_event::<T>(Event::RecoveryRemoved { lost_account: caller }.into());
357	}
358
359	#[benchmark]
360	fn cancel_recovered() -> Result<(), BenchmarkError> {
361		let caller: T::AccountId = whitelisted_caller();
362		let account: T::AccountId = account("account", 0, SEED);
363		let account_lookup = T::Lookup::unlookup(account.clone());
364
365		frame_system::Pallet::<T>::inc_providers(&caller);
366
367		frame_system::Pallet::<T>::inc_consumers(&caller)?;
368
369		Proxy::<T>::insert(&caller, &account);
370
371		#[extrinsic_call]
372		_(RawOrigin::Signed(caller), account_lookup);
373
374		Ok(())
375	}
376
377	#[benchmark]
378	fn poke_deposit(n: Linear<1, { T::MaxFriends::get() }>) -> Result<(), BenchmarkError> {
379		let caller: T::AccountId = whitelisted_caller();
380		let lost_account: T::AccountId = account("lost_account", 0, SEED);
381
382		// Fund caller account
383		T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
384
385		// 1. Setup recovery config for caller
386		insert_recovery_config_with_max_friends::<T>(&caller);
387
388		// 2. Setup active recovery for lost account
389		setup_active_recovery_with_max_friends::<T>(&caller, &lost_account);
390
391		// 3. Get initial deposits
392		let initial_config = <Recoverable<T>>::get(&caller).unwrap();
393		let initial_config_deposit = initial_config.deposit;
394		let initial_recovery_deposit = T::RecoveryDeposit::get();
395		assert_eq!(
396			T::Currency::reserved_balance(&caller),
397			initial_config_deposit.saturating_add(initial_recovery_deposit)
398		);
399
400		// 4. Artificially increase deposits
401		let increased_config_deposit = initial_config_deposit.saturating_add(2u32.into());
402		let increased_recovery_deposit = initial_recovery_deposit.saturating_add(2u32.into());
403
404		<Recoverable<T>>::try_mutate(&caller, |maybe_config| -> Result<(), BenchmarkError> {
405			let config = maybe_config.as_mut().unwrap();
406			T::Currency::reserve(
407				&caller,
408				increased_config_deposit.saturating_sub(initial_config_deposit),
409			)?;
410			config.deposit = increased_config_deposit;
411			Ok(())
412		})
413		.map_err(|_| BenchmarkError::Stop("Failed to mutate storage"))?;
414
415		<ActiveRecoveries<T>>::try_mutate(
416			&lost_account,
417			&caller,
418			|maybe_recovery| -> Result<(), BenchmarkError> {
419				let recovery = maybe_recovery.as_mut().unwrap();
420				T::Currency::reserve(
421					&caller,
422					increased_recovery_deposit.saturating_sub(initial_recovery_deposit),
423				)?;
424				recovery.deposit = increased_recovery_deposit;
425				Ok(())
426			},
427		)
428		.map_err(|_| BenchmarkError::Stop("Failed to mutate storage"))?;
429
430		// 5. Verify increased deposits
431		assert_eq!(
432			T::Currency::reserved_balance(&caller),
433			increased_config_deposit.saturating_add(increased_recovery_deposit)
434		);
435
436		#[extrinsic_call]
437		_(RawOrigin::Signed(caller.clone()), Some(T::Lookup::unlookup(lost_account.clone())));
438
439		// 6. Assert final state
440		assert_eq!(
441			T::Currency::reserved_balance(&caller),
442			initial_config_deposit.saturating_add(initial_recovery_deposit)
443		);
444
445		// 7. Check events were emitted
446		assert_has_event::<T>(
447			Event::DepositPoked {
448				who: caller.clone(),
449				kind: DepositKind::RecoveryConfig,
450				old_deposit: increased_config_deposit,
451				new_deposit: initial_config_deposit,
452			}
453			.into(),
454		);
455		assert_has_event::<T>(
456			Event::DepositPoked {
457				who: caller,
458				kind: DepositKind::ActiveRecoveryFor(lost_account),
459				old_deposit: increased_recovery_deposit,
460				new_deposit: initial_recovery_deposit,
461			}
462			.into(),
463		);
464
465		Ok(())
466	}
467
468	impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
469}