1use crate::fuzz::fuzz_utils::{
14 check_general_invariants, check_invariants_after_finalization,
15 check_invariants_before_finalization, conclude_domain_epoch, fuzz_mark_invalid_bundle_authors,
16 fuzz_unmark_invalid_bundle_authors, get_next_operators, get_pending_slashes,
17};
18use crate::mock::{
19 AccountId, Balance, BalancesConfig, DOMAIN_ID, DomainsConfig, RuntimeGenesisConfig, Test,
20};
21use crate::staking::{
22 do_deactivate_operator, do_deregister_operator, do_mark_operators_as_slashed,
23 do_nominate_operator, do_reactivate_operator, do_register_operator, do_reward_operators,
24 do_unlock_funds, do_unlock_nominator, do_withdraw_stake,
25};
26use crate::staking_epoch::do_slash_operator;
27use crate::{Config, OperatorConfig, SlashedReason};
28use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID;
29use parity_scale_codec::Encode;
30use sp_core::storage::Storage;
31use sp_core::{H256, Pair};
32use sp_domains::storage::RawGenesis;
33use sp_domains::{
34 GenesisDomain, OperatorAllowList, OperatorId, OperatorPair, PermissionedActionAllowedBy,
35 RuntimeType,
36};
37use sp_runtime::{BuildStorage, Percent};
38use sp_state_machine::BasicExternalities;
39use std::collections::BTreeMap;
40use subspace_runtime_primitives::AI3;
41
42const ACTIONS_PER_EPOCH: usize = 5;
44const NUM_EPOCHS: usize = 5;
46const MIN_NOMINATOR_STAKE: Balance = 20;
48
49#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
50struct FuzzData {
51 pub epochs: [(u8, Epoch); NUM_EPOCHS],
53}
54
55#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
56struct Epoch {
57 actions: [(u8, FuzzAction); ACTIONS_PER_EPOCH],
59}
60
61#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65enum FuzzAction {
66 RegisterOperator {
67 amount: u16,
68 tax: u8,
69 },
70 NominateOperator {
71 operator_id: u8,
72 amount: u16,
73 },
74 DeregisterOperator {
75 operator_id: u64,
76 },
77 WithdrawStake {
78 nominator_id: u8,
79 operator_id: u8,
80 shares: u16,
81 },
82 UnlockFunds {
83 operator_id: u8,
84 nominator_id: u8,
85 },
86 UnlockNominator {
87 operator_id: u8,
88 nominator_id: u8,
89 },
90 MarkOperatorsAsSlashed {
91 operator_id: u8,
92 slash_reason: u8, },
94 MarkInvalidBundleAuthors {
95 operator_id: u8,
96 },
97 UnmarkInvalidBundleAuthors {
98 operator_id: u8,
99 er_id: u8,
100 },
101 RewardOperator {
102 operator_id: u8,
103 amount: u16,
104 },
105 DeactivateOperator {
106 operator_id: u8,
107 },
108 ReactivateOperator {
109 operator_id: u8,
110 },
111 SlashOperator,
112}
113
114fn create_genesis_storage(accounts: &[AccountId], mint: u128) -> Storage {
117 let raw_genesis_storage = RawGenesis::dummy(vec![1, 2, 3, 4]).encode();
118 let pair = OperatorPair::from_seed(&[*accounts.first().unwrap() as u8; 32]);
119 RuntimeGenesisConfig {
120 balances: BalancesConfig {
121 balances: accounts.iter().cloned().map(|k| (k, mint)).collect(),
122 },
123 domains: DomainsConfig {
124 genesis_domains: vec![GenesisDomain {
125 runtime_name: "evm".to_owned(),
126 runtime_type: RuntimeType::Evm,
127 runtime_version: Default::default(),
128 raw_genesis_storage,
129 owner_account_id: *accounts.first().unwrap(),
130 domain_name: "evm-domain".to_owned(),
131 bundle_slot_probability: (1, 1),
132 operator_allow_list: OperatorAllowList::Anyone,
133 signing_key: pair.public(),
134 minimum_nominator_stake: MIN_NOMINATOR_STAKE * AI3,
135 nomination_tax: Percent::from_percent(5),
136 initial_balances: vec![],
137 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
138 }],
139 permissioned_action_allowed_by: Some(PermissionedActionAllowedBy::Anyone),
140 },
141 subspace: Default::default(),
142 system: Default::default(),
143 }
144 .build_storage()
145 .unwrap()
146}
147
148pub fn run_staking_fuzz(data: &[u8]) {
149 let accounts: Vec<AccountId> = (0..5).map(|i| (i as u128)).collect();
150 let mint = (u16::MAX as u128) * 2 * AI3;
151 let genesis = create_genesis_storage(&accounts, mint);
152 let Ok(data) = bincode::deserialize(data) else {
153 return;
154 };
155
156 let mut ext = BasicExternalities::new(genesis);
157 ext.execute_with(|| {
158 fuzz(&data, accounts.clone());
159 });
160}
161
162fn fuzz(data: &FuzzData, accounts: Vec<AccountId>) {
163 let mut operators = BTreeMap::new();
164 let mut nominators = BTreeMap::new();
165 let mut invalid_ers = Vec::new();
166
167 let initial_issuance = accounts
169 .iter()
170 .map(<Test as Config>::Currency::free_balance)
171 .sum();
172
173 for (skip, epoch) in &data.epochs {
174 for (user, action) in epoch.actions.iter() {
175 let user = accounts.get(*user as usize % accounts.len()).unwrap();
176 match action {
177 FuzzAction::RegisterOperator { amount, tax } => {
178 let res = register_operator(*user, *amount as u128, *tax);
179 if let Some(operator) = res {
180 operators.insert(user, operator);
181 nominators
182 .entry(*user)
183 .and_modify(|list: &mut Vec<u64>| list.push(operator))
184 .or_insert(vec![operator]);
185
186 println!(
187 "Registering {user:?} as Operator {operator:?} with amount {amount:?}\n-->{res:?}"
188 );
189 } else {
190 println!(
191 "Registering {user:?} as Operator (failed) with amount {amount:?} AI3 \n-->{res:?}"
192 );
193 }
194 }
195 FuzzAction::NominateOperator {
196 operator_id,
197 amount,
198 } => {
199 if operators.is_empty() {
200 println!("skipping NominateOperator");
201 continue;
202 }
203 let amount = (*amount as u128).max(MIN_NOMINATOR_STAKE) * AI3;
204 let operator = operators
205 .iter()
206 .collect::<Vec<_>>()
207 .get(*operator_id as usize % operators.len())
208 .unwrap()
209 .1;
210 let res = do_nominate_operator::<Test>(*operator, *user, amount);
211 if res.is_ok() {
212 nominators
213 .entry(*user)
214 .and_modify(|list: &mut Vec<u64>| list.push(*operator))
215 .or_insert(vec![*operator]);
216 }
217
218 println!(
219 "Nominating as Nominator {user:?} for Operator {operator:?} with amount {amount:?}\n-->{res:?}"
220 );
221 }
222 FuzzAction::DeregisterOperator { operator_id } => {
223 if operators.is_empty() {
224 println!("skipping DeregisterOperator");
225 continue;
226 }
227 let (owner, operator) = *operators
228 .iter()
229 .collect::<Vec<_>>()
230 .get(*operator_id as usize % operators.len())
231 .unwrap();
232 let res = do_deregister_operator::<Test>(**owner, *operator);
233
234 println!("de-registering Operator {operator:?} \n-->{res:?}");
235 }
236 FuzzAction::WithdrawStake {
237 nominator_id,
238 operator_id,
239 shares,
240 } => {
241 if operators.is_empty() {
242 println!("skipping WithdrawStake");
243 continue;
244 }
245 let (nominator, operators) = *nominators
246 .iter()
247 .collect::<Vec<_>>()
248 .get(*nominator_id as usize % nominators.len())
249 .unwrap();
250 let operator = operators
251 .get(*operator_id as usize % operators.len())
252 .unwrap();
253 let res =
254 do_withdraw_stake::<Test>(*operator, *nominator, *shares as u128 * AI3);
255
256 println!(
257 "Withdrawing stake from Operator {operator:?} as Nominator {nominator:?} of shares {shares:?}\n-->{res:?}"
258 );
259 }
260 FuzzAction::UnlockFunds {
261 operator_id,
262 nominator_id,
263 } => {
264 if operators.is_empty() {
265 println!("skipping UnlockFunds");
266 continue;
267 }
268 let (nominator, operators) = *nominators
269 .iter()
270 .collect::<Vec<_>>()
271 .get(*nominator_id as usize % nominators.len())
272 .unwrap();
273 let operator = operators
274 .get(*operator_id as usize % operators.len())
275 .unwrap();
276 let res = do_unlock_funds::<Test>(*operator, *nominator);
277
278 println!(
279 "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}"
280 );
281 }
282 FuzzAction::UnlockNominator {
283 operator_id,
284 nominator_id,
285 } => {
286 if operators.is_empty() {
287 println!("skipping UnlockNominator");
288 continue;
289 }
290 let (nominator, operators) = *nominators
291 .iter()
292 .collect::<Vec<_>>()
293 .get(*nominator_id as usize % nominators.len())
294 .unwrap();
295 let operator = operators
296 .get(*operator_id as usize % operators.len())
297 .unwrap();
298 let res = do_unlock_nominator::<Test>(*operator, *nominator);
299
300 println!(
301 "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}"
302 );
303 }
304 FuzzAction::MarkOperatorsAsSlashed {
305 operator_id,
306 slash_reason,
307 } => {
308 if operators.is_empty() {
309 println!("skipping MarkOperatorsAsSlashed");
310 continue;
311 }
312 let operator = operators
313 .iter()
314 .collect::<Vec<_>>()
315 .get(*operator_id as usize % operators.len())
316 .unwrap()
317 .1;
318 let slash_reason = match slash_reason % 2 {
319 0 => SlashedReason::InvalidBundle(0),
320 _ => SlashedReason::BadExecutionReceipt(H256::from([0u8; 32])),
321 };
322 let res = do_mark_operators_as_slashed::<Test>(vec![*operator], slash_reason);
323
324 println!("Marking {operator:?} as slashed\n-->{res:?}");
325 do_slash_operator::<Test>(DOMAIN_ID, u32::MAX).unwrap();
326 }
327 FuzzAction::SlashOperator => {
328 if operators.is_empty() {
329 println!("skipping SlashOperator");
330 continue;
331 }
332 let res = do_slash_operator::<Test>(DOMAIN_ID, u32::MAX);
333 assert!(res.is_ok());
334
335 {
336 let pending_slashes = get_pending_slashes::<Test>(DOMAIN_ID);
337 println!("Slashing: {pending_slashes:?} -->{res:?}");
338 }
339 }
340 FuzzAction::RewardOperator {
341 operator_id,
342 amount,
343 } => {
344 if operators.is_empty() {
345 println!("skipping RewardOperator");
346 continue;
347 }
348 let operator = operators
349 .iter()
350 .collect::<Vec<_>>()
351 .get(*operator_id as usize % operators.len())
352 .unwrap()
353 .1;
354 let reward_amount = 10u128 * AI3;
355 let res = do_reward_operators::<Test>(
356 DOMAIN_ID,
357 sp_domains::OperatorRewardSource::Dummy,
358 vec![*operator].into_iter(),
359 reward_amount,
360 );
361 assert!(res.is_ok());
362
363 println!("Rewarding operator {operator:?} with {amount:?} AI3 \n-->{res:?}");
364 }
365 FuzzAction::MarkInvalidBundleAuthors { operator_id } => {
366 if operators.is_empty() {
367 println!("skipping MarkInvalidBundleAuthors");
368 continue;
369 }
370 let operator = operators
371 .iter()
372 .collect::<Vec<_>>()
373 .get(*operator_id as usize % operators.len())
374 .unwrap()
375 .1;
376 if let Some(invalid_er) =
377 fuzz_mark_invalid_bundle_authors::<Test>(*operator, DOMAIN_ID)
378 {
379 invalid_ers.push(invalid_er)
380 }
381 }
382 FuzzAction::UnmarkInvalidBundleAuthors { operator_id, er_id } => {
383 if operators.is_empty() {
384 println!("skipping UnmarkInvalidBundleAuthors");
385 continue;
386 }
387 if invalid_ers.is_empty() {
388 println!("skipping UnmarkInvalidBundleAuthors");
389 continue;
390 }
391 let operator = operators
392 .iter()
393 .collect::<Vec<_>>()
394 .get(*operator_id as usize % operators.len())
395 .unwrap()
396 .1;
397 let er = invalid_ers
398 .get(*er_id as usize % invalid_ers.len())
399 .unwrap();
400 fuzz_unmark_invalid_bundle_authors::<Test>(DOMAIN_ID, *operator, *er);
401 }
402 FuzzAction::DeactivateOperator { operator_id } => {
403 if operators.is_empty() {
404 println!("skipping DeactivateOperator");
405 continue;
406 }
407 let operator = operators
408 .iter()
409 .collect::<Vec<_>>()
410 .get(*operator_id as usize % operators.len())
411 .unwrap()
412 .1;
413 let res = do_deactivate_operator::<Test>(*operator);
414
415 println!("Deactivating {operator:?} \n-->{res:?}");
416 }
417 FuzzAction::ReactivateOperator { operator_id } => {
418 if operators.is_empty() {
419 println!("skipping ReactivateOperator");
420 continue;
421 }
422 let operator = operators
423 .iter()
424 .collect::<Vec<_>>()
425 .get(*operator_id as usize % operators.len())
426 .unwrap()
427 .1;
428 let res = do_reactivate_operator::<Test>(*operator);
429
430 println!("Deactivating {operator:?} \n-->{res:?}");
431 }
432 }
433 check_invariants_before_finalization::<Test>(DOMAIN_ID);
434 let prev_validator_states = get_next_operators::<Test>(DOMAIN_ID);
435 conclude_domain_epoch::<Test>(DOMAIN_ID);
436 check_invariants_after_finalization::<Test>(DOMAIN_ID, prev_validator_states);
437 check_general_invariants::<Test>(initial_issuance);
438
439 println!("skipping {skip:?} epochs");
440 for _ in 0..*skip {
441 conclude_domain_epoch::<Test>(DOMAIN_ID);
442 }
443 }
444 }
445}
446
447fn register_operator(operator: AccountId, amount: Balance, tax: u8) -> Option<OperatorId> {
449 let pair = OperatorPair::from_seed(&[operator as u8; 32]);
450 let config = OperatorConfig {
451 signing_key: pair.public(),
452 minimum_nominator_stake: MIN_NOMINATOR_STAKE * AI3,
453 nomination_tax: sp_runtime::Percent::from_percent(tax.min(100)),
454 };
455 let res = do_register_operator::<Test>(operator, DOMAIN_ID, amount * AI3, config);
456 if let Ok((id, _)) = res {
457 Some(id)
458 } else {
459 None
460 }
461}