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 dev_accounts: None,
123 },
124 domains: DomainsConfig {
125 genesis_domains: vec![GenesisDomain {
126 runtime_name: "evm".to_owned(),
127 runtime_type: RuntimeType::Evm,
128 runtime_version: Default::default(),
129 raw_genesis_storage,
130 owner_account_id: *accounts.first().unwrap(),
131 domain_name: "evm-domain".to_owned(),
132 bundle_slot_probability: (1, 1),
133 operator_allow_list: OperatorAllowList::Anyone,
134 signing_key: pair.public(),
135 minimum_nominator_stake: MIN_NOMINATOR_STAKE * AI3,
136 nomination_tax: Percent::from_percent(5),
137 initial_balances: vec![],
138 domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(),
139 }],
140 permissioned_action_allowed_by: Some(PermissionedActionAllowedBy::Anyone),
141 },
142 subspace: Default::default(),
143 system: Default::default(),
144 }
145 .build_storage()
146 .unwrap()
147}
148
149pub fn run_staking_fuzz(data: &[u8]) {
150 let accounts: Vec<AccountId> = (0..5).map(|i| (i as u128)).collect();
151 let mint = (u16::MAX as u128) * 2 * AI3;
152 let genesis = create_genesis_storage(&accounts, mint);
153 let Ok(data) = bincode::deserialize(data) else {
154 return;
155 };
156
157 let mut ext = BasicExternalities::new(genesis);
158 ext.execute_with(|| {
159 fuzz(&data, accounts.clone());
160 });
161}
162
163fn fuzz(data: &FuzzData, accounts: Vec<AccountId>) {
164 let mut operators = BTreeMap::new();
165 let mut nominators = BTreeMap::new();
166 let mut invalid_ers = Vec::new();
167
168 let initial_issuance = accounts
170 .iter()
171 .map(<Test as Config>::Currency::free_balance)
172 .sum();
173
174 for (skip, epoch) in &data.epochs {
175 for (user, action) in epoch.actions.iter() {
176 let user = accounts.get(*user as usize % accounts.len()).unwrap();
177 match action {
178 FuzzAction::RegisterOperator { amount, tax } => {
179 let res = register_operator(*user, *amount as u128, *tax);
180 if let Some(operator) = res {
181 operators.insert(user, operator);
182 nominators
183 .entry(*user)
184 .and_modify(|list: &mut Vec<u64>| list.push(operator))
185 .or_insert(vec![operator]);
186
187 println!(
188 "Registering {user:?} as Operator {operator:?} with amount {amount:?}\n-->{res:?}"
189 );
190 } else {
191 println!(
192 "Registering {user:?} as Operator (failed) with amount {amount:?} AI3 \n-->{res:?}"
193 );
194 }
195 }
196 FuzzAction::NominateOperator {
197 operator_id,
198 amount,
199 } => {
200 if operators.is_empty() {
201 println!("skipping NominateOperator");
202 continue;
203 }
204 let amount = (*amount as u128).max(MIN_NOMINATOR_STAKE) * AI3;
205 let operator = operators
206 .iter()
207 .collect::<Vec<_>>()
208 .get(*operator_id as usize % operators.len())
209 .unwrap()
210 .1;
211 let res = do_nominate_operator::<Test>(*operator, *user, amount);
212 if res.is_ok() {
213 nominators
214 .entry(*user)
215 .and_modify(|list: &mut Vec<u64>| list.push(*operator))
216 .or_insert(vec![*operator]);
217 }
218
219 println!(
220 "Nominating as Nominator {user:?} for Operator {operator:?} with amount {amount:?}\n-->{res:?}"
221 );
222 }
223 FuzzAction::DeregisterOperator { operator_id } => {
224 if operators.is_empty() {
225 println!("skipping DeregisterOperator");
226 continue;
227 }
228 let (owner, operator) = *operators
229 .iter()
230 .collect::<Vec<_>>()
231 .get(*operator_id as usize % operators.len())
232 .unwrap();
233 let res = do_deregister_operator::<Test>(**owner, *operator);
234
235 println!("de-registering Operator {operator:?} \n-->{res:?}");
236 }
237 FuzzAction::WithdrawStake {
238 nominator_id,
239 operator_id,
240 shares,
241 } => {
242 if operators.is_empty() {
243 println!("skipping WithdrawStake");
244 continue;
245 }
246 let (nominator, operators) = *nominators
247 .iter()
248 .collect::<Vec<_>>()
249 .get(*nominator_id as usize % nominators.len())
250 .unwrap();
251 let operator = operators
252 .get(*operator_id as usize % operators.len())
253 .unwrap();
254 let res =
255 do_withdraw_stake::<Test>(*operator, *nominator, *shares as u128 * AI3);
256
257 println!(
258 "Withdrawing stake from Operator {operator:?} as Nominator {nominator:?} of shares {shares:?}\n-->{res:?}"
259 );
260 }
261 FuzzAction::UnlockFunds {
262 operator_id,
263 nominator_id,
264 } => {
265 if operators.is_empty() {
266 println!("skipping UnlockFunds");
267 continue;
268 }
269 let (nominator, operators) = *nominators
270 .iter()
271 .collect::<Vec<_>>()
272 .get(*nominator_id as usize % nominators.len())
273 .unwrap();
274 let operator = operators
275 .get(*operator_id as usize % operators.len())
276 .unwrap();
277 let res = do_unlock_funds::<Test>(*operator, *nominator);
278
279 println!(
280 "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}"
281 );
282 }
283 FuzzAction::UnlockNominator {
284 operator_id,
285 nominator_id,
286 } => {
287 if operators.is_empty() {
288 println!("skipping UnlockNominator");
289 continue;
290 }
291 let (nominator, operators) = *nominators
292 .iter()
293 .collect::<Vec<_>>()
294 .get(*nominator_id as usize % nominators.len())
295 .unwrap();
296 let operator = operators
297 .get(*operator_id as usize % operators.len())
298 .unwrap();
299 let res = do_unlock_nominator::<Test>(*operator, *nominator);
300
301 println!(
302 "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}"
303 );
304 }
305 FuzzAction::MarkOperatorsAsSlashed {
306 operator_id,
307 slash_reason,
308 } => {
309 if operators.is_empty() {
310 println!("skipping MarkOperatorsAsSlashed");
311 continue;
312 }
313 let operator = operators
314 .iter()
315 .collect::<Vec<_>>()
316 .get(*operator_id as usize % operators.len())
317 .unwrap()
318 .1;
319 let slash_reason = match slash_reason % 2 {
320 0 => SlashedReason::InvalidBundle(0),
321 _ => SlashedReason::BadExecutionReceipt(H256::from([0u8; 32])),
322 };
323 let res = do_mark_operators_as_slashed::<Test>(vec![*operator], slash_reason);
324
325 println!("Marking {operator:?} as slashed\n-->{res:?}");
326 do_slash_operator::<Test>(DOMAIN_ID, u32::MAX).unwrap();
327 }
328 FuzzAction::SlashOperator => {
329 if operators.is_empty() {
330 println!("skipping SlashOperator");
331 continue;
332 }
333 let res = do_slash_operator::<Test>(DOMAIN_ID, u32::MAX);
334 assert!(res.is_ok());
335
336 {
337 let pending_slashes = get_pending_slashes::<Test>(DOMAIN_ID);
338 println!("Slashing: {pending_slashes:?} -->{res:?}");
339 }
340 }
341 FuzzAction::RewardOperator {
342 operator_id,
343 amount,
344 } => {
345 if operators.is_empty() {
346 println!("skipping RewardOperator");
347 continue;
348 }
349 let operator = operators
350 .iter()
351 .collect::<Vec<_>>()
352 .get(*operator_id as usize % operators.len())
353 .unwrap()
354 .1;
355 let reward_amount = 10u128 * AI3;
356 let res = do_reward_operators::<Test>(
357 DOMAIN_ID,
358 sp_domains::OperatorRewardSource::Dummy,
359 vec![*operator].into_iter(),
360 reward_amount,
361 );
362 assert!(res.is_ok());
363
364 println!("Rewarding operator {operator:?} with {amount:?} AI3 \n-->{res:?}");
365 }
366 FuzzAction::MarkInvalidBundleAuthors { operator_id } => {
367 if operators.is_empty() {
368 println!("skipping MarkInvalidBundleAuthors");
369 continue;
370 }
371 let operator = operators
372 .iter()
373 .collect::<Vec<_>>()
374 .get(*operator_id as usize % operators.len())
375 .unwrap()
376 .1;
377 if let Some(invalid_er) =
378 fuzz_mark_invalid_bundle_authors::<Test>(*operator, DOMAIN_ID)
379 {
380 invalid_ers.push(invalid_er)
381 }
382 }
383 FuzzAction::UnmarkInvalidBundleAuthors { operator_id, er_id } => {
384 if operators.is_empty() {
385 println!("skipping UnmarkInvalidBundleAuthors");
386 continue;
387 }
388 if invalid_ers.is_empty() {
389 println!("skipping UnmarkInvalidBundleAuthors");
390 continue;
391 }
392 let operator = operators
393 .iter()
394 .collect::<Vec<_>>()
395 .get(*operator_id as usize % operators.len())
396 .unwrap()
397 .1;
398 let er = invalid_ers
399 .get(*er_id as usize % invalid_ers.len())
400 .unwrap();
401 fuzz_unmark_invalid_bundle_authors::<Test>(DOMAIN_ID, *operator, *er);
402 }
403 FuzzAction::DeactivateOperator { operator_id } => {
404 if operators.is_empty() {
405 println!("skipping DeactivateOperator");
406 continue;
407 }
408 let operator = operators
409 .iter()
410 .collect::<Vec<_>>()
411 .get(*operator_id as usize % operators.len())
412 .unwrap()
413 .1;
414 let res = do_deactivate_operator::<Test>(*operator);
415
416 println!("Deactivating {operator:?} \n-->{res:?}");
417 }
418 FuzzAction::ReactivateOperator { operator_id } => {
419 if operators.is_empty() {
420 println!("skipping ReactivateOperator");
421 continue;
422 }
423 let operator = operators
424 .iter()
425 .collect::<Vec<_>>()
426 .get(*operator_id as usize % operators.len())
427 .unwrap()
428 .1;
429 let res = do_reactivate_operator::<Test>(*operator);
430
431 println!("Deactivating {operator:?} \n-->{res:?}");
432 }
433 }
434 check_invariants_before_finalization::<Test>(DOMAIN_ID);
435 let prev_validator_states = get_next_operators::<Test>(DOMAIN_ID);
436 conclude_domain_epoch::<Test>(DOMAIN_ID);
437 check_invariants_after_finalization::<Test>(DOMAIN_ID, prev_validator_states);
438 check_general_invariants::<Test>(initial_issuance);
439
440 println!("skipping {skip:?} epochs");
441 for _ in 0..*skip {
442 conclude_domain_epoch::<Test>(DOMAIN_ID);
443 }
444 }
445 }
446}
447
448fn register_operator(operator: AccountId, amount: Balance, tax: u8) -> Option<OperatorId> {
450 let pair = OperatorPair::from_seed(&[operator as u8; 32]);
451 let config = OperatorConfig {
452 signing_key: pair.public(),
453 minimum_nominator_stake: MIN_NOMINATOR_STAKE * AI3,
454 nomination_tax: sp_runtime::Percent::from_percent(tax.min(100)),
455 };
456 let res = do_register_operator::<Test>(operator, DOMAIN_ID, amount * AI3, config);
457 if let Ok((id, _)) = res {
458 Some(id)
459 } else {
460 None
461 }
462}