diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 09ea79e42c..4dec9b0e37 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -376,6 +376,32 @@ jobs: run: | scripts/runtime-benchmark.sh + staking-fuzzer-test: + name: staking-fuzzer-test (Linux x86-64) + # Fuzzing is most efficient on Linux, it doesn't matter if it fails on other OSes. + # Our runs-on instances don't have the required packages + runs-on: ubuntu-22.04 + # Don't use the full 6 hours if fuzzing hangs + timeout-minutes: 120 + env: + AFL_SKIP_CPUFREQ: 1 + AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: 1 + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Apt update + run: sudo apt-get update + + - name: install dependencies + run: sudo apt install -y protobuf-compiler binutils-dev coreutils + + - name: install ziggy + run: cargo install --force ziggy cargo-afl honggfuzz grcov + + - name: test fuzzer + run: scripts/run-fuzzer.sh + # This job checks all crates individually, including no_std and other featureless builds. # We need to check crates individually for missing features, because cargo does feature # unification, which hides missing features when crates are built together. @@ -503,6 +529,7 @@ jobs: - check-runtime-benchmarks - cargo-check-individually - cargo-unused-deps + - staking-fuzzer-test steps: - name: Check job statuses # Another hack is to actually check the status of the dependencies or else it'll fall through @@ -515,3 +542,4 @@ jobs: [[ "${{ needs.check-runtime-benchmarks.result }}" == "success" ]] || exit 1 [[ "${{ needs.cargo-check-individually.result }}" == "success" ]] || exit 1 [[ "${{ needs.cargo-unused-deps.result }}" == "success" ]] || exit 1 + [[ "${{ needs.staking-fuzzer-test.result }}" == "success" ]] || exit 1 diff --git a/.gitignore b/.gitignore index f419dc7baa..d4184c40ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.idea /target +/test/subspace-test-fuzzer/target +/test/subspace-test-fuzzer/output diff --git a/Cargo.lock b/Cargo.lock index 2d9b26d364..e9aaa1c899 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7987,6 +7987,7 @@ dependencies = [ name = "pallet-domains" version = "0.1.0" dependencies = [ + "bincode", "domain-pallet-executive", "domain-runtime-primitives", "frame-benchmarking", @@ -8002,6 +8003,7 @@ dependencies = [ "prop-test", "scale-info", "schnorrkel", + "serde", "sp-consensus-slots", "sp-consensus-subspace", "sp-core", @@ -8011,6 +8013,7 @@ dependencies = [ "sp-io", "sp-keystore", "sp-runtime", + "sp-state-machine", "sp-std", "sp-subspace-mmr", "sp-version", @@ -14141,6 +14144,14 @@ dependencies = [ "zeroize", ] +[[package]] +name = "subspace-test-fuzzer" +version = "0.1.0" +dependencies = [ + "pallet-domains", + "ziggy", +] + [[package]] name = "subspace-test-primitives" version = "0.1.0" @@ -16620,6 +16631,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "ziggy" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec0e3b65fea1a3b33ea6be638e233e3f2143ea30d78b7997f25bb44eb372494" + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index 4826a1ddfb..6cdac28a02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "domains/test/utils", "shared/*", "test/subspace-test-client", + "test/subspace-test-fuzzer", "test/subspace-test-runtime", "test/subspace-test-service", ] @@ -36,6 +37,7 @@ auto-id-domain-runtime = { version = "0.1.0", path = "domains/runtime/auto-id" } auto-id-domain-test-runtime = { version = "0.1.0", path = "domains/test/runtime/auto-id" } backoff = "0.4.0" base58 = "0.2.0" +bincode = { version = "1.3.3" } bip39 = "2.0.0" bitvec = "1.0.1" blake2 = { version = "0.10.6", default-features = false } @@ -300,6 +302,7 @@ unsigned-varint = "0.8.0" void = "1.0.2" x509-parser = "0.16.0" zeroize = "1.8.1" +ziggy = { version = "1.3.4", default-features = false } # The list of dependencies below (which can be both direct and indirect dependencies) are crates # that are suspected to be CPU-intensive, and that are unlikely to require debugging (as some of diff --git a/crates/pallet-domains/Cargo.toml b/crates/pallet-domains/Cargo.toml index c850bef6c6..ad35041026 100644 --- a/crates/pallet-domains/Cargo.toml +++ b/crates/pallet-domains/Cargo.toml @@ -36,6 +36,16 @@ sp-version = { workspace = true, features = ["serde"] } subspace-core-primitives.workspace = true subspace-runtime-primitives.workspace = true +# fuzz feature optional dependencies +bincode = { workspace = true, optional = true } +domain-pallet-executive = { workspace = true, optional = true, default-features = true } +pallet-timestamp = { workspace = true, optional = true, default-features = true } +pallet-block-fees = { workspace = true, optional = true, default-features = true } +serde = { workspace = true, optional = true, features = ["derive"] } +sp-externalities = { workspace = true, optional = true, default-features = true } +sp-keystore = { workspace = true, optional = true, default-features = true } +sp-state-machine = { workspace = true, optional = true, default-features = true } + [dev-dependencies] domain-pallet-executive.workspace = true hex-literal.workspace = true @@ -85,3 +95,16 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "sp-subspace-mmr/runtime-benchmarks", ] + +fuzz = [ + "bincode", + "domain-pallet-executive", + "hex-literal", + "pallet-timestamp", + "pallet-block-fees", + "serde", + "sp-externalities", + "sp-keystore", + "sp-state-machine", + "std", +] diff --git a/crates/pallet-domains/src/benchmarking.rs b/crates/pallet-domains/src/benchmarking.rs index 73c20e549c..2599306b8d 100644 --- a/crates/pallet-domains/src/benchmarking.rs +++ b/crates/pallet-domains/src/benchmarking.rs @@ -1487,6 +1487,6 @@ mod benchmarks { impl_benchmark_test_suite!( Domains, crate::tests::new_test_ext_with_extensions(), - crate::tests::Test + crate::mock::Test ); } diff --git a/crates/pallet-domains/src/block_tree.rs b/crates/pallet-domains/src/block_tree.rs index 665cd54e55..9fe2a735a0 100644 --- a/crates/pallet-domains/src/block_tree.rs +++ b/crates/pallet-domains/src/block_tree.rs @@ -639,11 +639,11 @@ pub(crate) fn invalid_bundle_authors_for_receipt( #[cfg(test)] mod tests { use super::*; + use crate::mock::{BlockTreePruningDepth, Domains, Test}; use crate::tests::{ - BlockTreePruningDepth, Domains, Test, create_dummy_bundle_with_receipts, - create_dummy_receipt, extend_block_tree, extend_block_tree_from_zero, - get_block_tree_node_at, new_test_ext_with_extensions, register_genesis_domain, - run_to_block, + create_dummy_bundle_with_receipts, create_dummy_receipt, extend_block_tree, + extend_block_tree_from_zero, get_block_tree_node_at, new_test_ext_with_extensions, + register_genesis_domain, run_to_block, }; use crate::{FrozenDomains, RawOrigin as DomainOrigin}; use frame_support::dispatch::RawOrigin; diff --git a/crates/pallet-domains/src/domain_registry.rs b/crates/pallet-domains/src/domain_registry.rs index 0a8e1ee2c0..e4bb3dfe2c 100644 --- a/crates/pallet-domains/src/domain_registry.rs +++ b/crates/pallet-domains/src/domain_registry.rs @@ -398,7 +398,8 @@ pub(crate) fn do_update_domain_allow_list( #[cfg(test)] mod tests { use super::*; - use crate::tests::{TEST_RUNTIME_APIS, Test, new_test_ext}; + use crate::mock::Test; + use crate::tests::{TEST_RUNTIME_APIS, new_test_ext}; use domain_runtime_primitives::{AccountId20, AccountId20Converter, DEFAULT_EVM_CHAIN_ID}; use frame_support::traits::Currency; use frame_support::{assert_err, assert_ok}; diff --git a/crates/pallet-domains/src/fuzz.rs b/crates/pallet-domains/src/fuzz.rs new file mode 100644 index 0000000000..d6702e0b04 --- /dev/null +++ b/crates/pallet-domains/src/fuzz.rs @@ -0,0 +1,4 @@ +mod fuzz_utils; +mod staking; + +pub use staking::run_staking_fuzz; diff --git a/crates/pallet-domains/src/fuzz/fuzz_utils.rs b/crates/pallet-domains/src/fuzz/fuzz_utils.rs new file mode 100644 index 0000000000..58c61c0ef0 --- /dev/null +++ b/crates/pallet-domains/src/fuzz/fuzz_utils.rs @@ -0,0 +1,225 @@ +// Copyright 2025 Security Research Labs GmbH +// Permission to use, copy, modify, and/or distribute this software for +// any purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +// FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +// DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use alloc::collections::BTreeSet; +use frame_system::Account; +use pallet_balances::{Holds, TotalIssuance}; +use sp_core::H256; +use sp_domains::{DomainId, OperatorId}; +use sp_runtime::traits::One; + +use crate::staking::{ + Operator, OperatorStatus, SharePrice, mark_invalid_bundle_author, unmark_invalid_bundle_author, +}; +use crate::staking_epoch::do_finalize_domain_current_epoch; +use crate::{ + BalanceOf, Config, DeactivatedOperators, Deposits, DeregisteredOperators, DomainBlockNumberFor, + DomainStakingSummary, HeadDomainNumber, InvalidBundleAuthors, Operators, PendingSlashes, + ReceiptHashFor, +}; + +type OperatorOf = + Operator, ::Share, DomainBlockNumberFor, ReceiptHashFor>; + +/// Fetch the next epoch's operators from the DomainStakingSummary +pub(crate) fn get_next_operators(domain_id: DomainId) -> Vec> { + let domain_summary = DomainStakingSummary::::get(domain_id) + .expect("invariant violated: We must have DomainStakingSummary"); + let mut prev_ops = vec![]; + for operator_id in &domain_summary.next_operators { + let operator = Operators::::get(*operator_id).expect( + "invariant violated: Operator in next_operator set is not present in Operators", + ); + prev_ops.push(operator) + } + prev_ops +} + +/// Finalize the epoch and transition to the next one +pub(crate) fn conclude_domain_epoch(domain_id: DomainId) { + let head_domain_number = HeadDomainNumber::::get(domain_id); + HeadDomainNumber::::set(domain_id, head_domain_number + One::one()); + do_finalize_domain_current_epoch::(domain_id) + .expect("invariant violated: we must be able to finalize domain epoch"); +} + +/// Mark an operator as having produced an invalid bundle +pub(crate) fn fuzz_mark_invalid_bundle_authors>( + operator: OperatorId, + domain_id: DomainId, +) -> Option { + let pending_slashes = PendingSlashes::::get(domain_id).unwrap_or_default(); + let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::::get(domain_id); + let mut stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); + if pending_slashes.contains(&operator) { + return None; + } + let er = H256::random(); + mark_invalid_bundle_author::( + operator, + er, + &mut stake_summary, + &mut invalid_bundle_authors_in_epoch, + ) + .expect("invariant violated: could not mark operator as invalid bundle author"); + DomainStakingSummary::::insert(domain_id, stake_summary); + InvalidBundleAuthors::::insert(domain_id, invalid_bundle_authors_in_epoch); + Some(er) +} + +/// Unmark an operator as having produced an invalid bundle +pub(crate) fn fuzz_unmark_invalid_bundle_authors>( + domain_id: DomainId, + operator: OperatorId, + er: H256, +) { + let pending_slashes = PendingSlashes::::get(domain_id).unwrap_or_default(); + let mut invalid_bundle_authors_in_epoch = InvalidBundleAuthors::::get(domain_id); + let mut stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); + + if pending_slashes.contains(&operator) + || crate::Pallet::::is_operator_pending_to_slash(domain_id, operator) + { + return; + } + + unmark_invalid_bundle_author::( + operator, + er, + &mut stake_summary, + &mut invalid_bundle_authors_in_epoch, + ) + .expect("invariant violated: could not unmark operator as invalid bundle author"); + + DomainStakingSummary::::insert(domain_id, stake_summary); + InvalidBundleAuthors::::insert(domain_id, invalid_bundle_authors_in_epoch); +} + +/// Fetch operators who are pending slashing +pub(crate) fn get_pending_slashes(domain_id: DomainId) -> BTreeSet { + PendingSlashes::::get(domain_id).unwrap_or_default() +} + +/// Check staking invariants before epoch finalization +pub(crate) fn check_invariants_before_finalization(domain_id: DomainId) { + let domain_summary = DomainStakingSummary::::get(domain_id).unwrap(); + // INVARIANT: all current_operators are registered and not slashed nor have invalid bundles + for operator_id in &domain_summary.next_operators { + let operator = Operators::::get(*operator_id).unwrap(); + if !matches!( + operator.status::(*operator_id), + OperatorStatus::Registered + ) { + panic!("operator set violated"); + } + } + // INVARIANT: No operator is common between DeactivatedOperator and DeregisteredOperator + let deactivated_operators = DeactivatedOperators::::get(domain_id); + let deregistered_operators = DeregisteredOperators::::get(domain_id); + for operator_id in &deregistered_operators { + assert!(!deactivated_operators.contains(operator_id)); + } +} + +/// Check staking invariants after epoch finalization +pub(crate) fn check_invariants_after_finalization>( + domain_id: DomainId, + prev_ops: Vec>, +) { + let domain_summary = DomainStakingSummary::::get(domain_id).unwrap(); + for operator_id in domain_summary.current_operators.keys() { + let operator = Operators::::get(operator_id).unwrap(); + // INVARIANT: 0 < SharePrice < 1 + SharePrice::new::(operator.current_total_shares, operator.current_total_stake) + .expect("SharePrice to be present"); + } + + // INVARIANT: DeactivatedOperators is empty + let deactivated_operators = DeactivatedOperators::::get(domain_id); + assert!(deactivated_operators.is_empty()); + // INVARIANT: DeregisteredOperators is empty + let deregistered_operators = DeregisteredOperators::::get(domain_id); + assert!(deregistered_operators.is_empty()); + + // INVARIANT: Total domain stake == accumulated operators' curent_stake. + let aggregated_stake: BalanceOf = domain_summary + .current_operators + .values() + .fold(0, |acc, stake| acc.saturating_add(*stake)); + + assert_eq!(aggregated_stake, domain_summary.current_total_stake); + // INVARIANT: all current_operators are registered and not slashed nor have invalid bundles + for operator_id in domain_summary.current_operators.keys() { + let operator = Operators::::get(operator_id).unwrap(); + if !matches!( + operator.status::(*operator_id), + OperatorStatus::Registered + ) { + panic!("operator set violated"); + } + // INVARIANT: Shares add up + let mut shares: T::Share = 0; + for (operator, _nominator, deposit) in Deposits::::iter() { + if *operator_id == operator { + shares += deposit.known.shares; + } + } + assert!(shares <= operator.current_total_shares); + } + + // INVARIANT: all operators which were part of the next operator set before finalization are present now + assert_eq!(prev_ops.len(), domain_summary.current_operators.len()); +} + +/// Check general Substrate invariants that must always hold +pub(crate) fn check_general_invariants< + T: Config + + pallet_balances::Config + + frame_system::Config>, +>( + initial_total_issuance: BalanceOf, +) { + // After execution of all blocks, we run invariants + let mut counted_free: ::Balance = 0; + let mut counted_reserved: ::Balance = 0; + for (account, info) in Account::::iter() { + let consumers = info.consumers; + let providers = info.providers; + assert!( + !(consumers > 0 && providers == 0), + "Invalid account consumers or providers state" + ); + counted_free += info.data.free; + counted_reserved += info.data.reserved; + let max_lock: ::Balance = + pallet_balances::Locks::::get(&account) + .iter() + .map(|l| l.amount) + .max() + .unwrap_or_default(); + assert_eq!( + max_lock, info.data.frozen, + "Max lock should be equal to frozen balance" + ); + let sum_holds: ::Balance = + Holds::::get(&account).iter().map(|l| l.amount).sum(); + assert!( + sum_holds <= info.data.reserved, + "Sum of all holds ({sum_holds}) should be less than or equal to reserved balance {}", + info.data.reserved + ); + } + let total_issuance = TotalIssuance::::get(); + let counted_issuance = counted_free + counted_reserved; + assert_eq!(total_issuance, counted_issuance); + assert!(total_issuance >= initial_total_issuance); +} diff --git a/crates/pallet-domains/src/fuzz/staking.rs b/crates/pallet-domains/src/fuzz/staking.rs new file mode 100644 index 0000000000..4c1e19bdab --- /dev/null +++ b/crates/pallet-domains/src/fuzz/staking.rs @@ -0,0 +1,461 @@ +// Copyright 2025 Security Research Labs GmbH +// Permission to use, copy, modify, and/or distribute this software for +// any purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +// FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +// DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::fuzz::fuzz_utils::{ + check_general_invariants, check_invariants_after_finalization, + check_invariants_before_finalization, conclude_domain_epoch, fuzz_mark_invalid_bundle_authors, + fuzz_unmark_invalid_bundle_authors, get_next_operators, get_pending_slashes, +}; +use crate::mock::{ + AccountId, Balance, BalancesConfig, DOMAIN_ID, DomainsConfig, RuntimeGenesisConfig, Test, +}; +use crate::staking::{ + do_deactivate_operator, do_deregister_operator, do_mark_operators_as_slashed, + do_nominate_operator, do_reactivate_operator, do_register_operator, do_reward_operators, + do_unlock_funds, do_unlock_nominator, do_withdraw_stake, +}; +use crate::staking_epoch::do_slash_operator; +use crate::{Config, OperatorConfig, SlashedReason}; +use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID; +use parity_scale_codec::Encode; +use sp_core::storage::Storage; +use sp_core::{H256, Pair}; +use sp_domains::storage::RawGenesis; +use sp_domains::{ + GenesisDomain, OperatorAllowList, OperatorId, OperatorPair, PermissionedActionAllowedBy, + RuntimeType, +}; +use sp_runtime::{BuildStorage, Percent}; +use sp_state_machine::BasicExternalities; +use std::collections::BTreeMap; +use subspace_runtime_primitives::AI3; + +/// The amount of actions per domain epoch +const ACTIONS_PER_EPOCH: usize = 5; +/// The amount of epochs per fuzz-run +const NUM_EPOCHS: usize = 5; +/// Minimum amount a nominator must stake +const MIN_NOMINATOR_STAKE: Balance = 20; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct FuzzData { + /// NUM_EPOCHS epochs with N epochs skipped + pub epochs: [(u8, Epoch); NUM_EPOCHS], +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct Epoch { + /// ACTIONS_PER_EPOCH actions split between N users + actions: [(u8, FuzzAction); ACTIONS_PER_EPOCH], +} + +/// The actions the harness performs +/// Each action roughly maps to each extrinsic in pallet-domains. +/// Note that all amounts MUST be multiplied by AI3 to be sensible +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +enum FuzzAction { + RegisterOperator { + amount: u16, + tax: u8, + }, + NominateOperator { + operator_id: u8, + amount: u16, + }, + DeregisterOperator { + operator_id: u64, + }, + WithdrawStake { + nominator_id: u8, + operator_id: u8, + shares: u16, + }, + UnlockFunds { + operator_id: u8, + nominator_id: u8, + }, + UnlockNominator { + operator_id: u8, + nominator_id: u8, + }, + MarkOperatorsAsSlashed { + operator_id: u8, + slash_reason: u8, // 0 for InvalidBundle, 1 for BadExecutionReceipt + }, + MarkInvalidBundleAuthors { + operator_id: u8, + }, + UnmarkInvalidBundleAuthors { + operator_id: u8, + er_id: u8, + }, + RewardOperator { + operator_id: u8, + amount: u16, + }, + DeactivateOperator { + operator_id: u8, + }, + ReactivateOperator { + operator_id: u8, + }, + SlashOperator, +} + +/// Creates the genesis for the consensus chain; pre-configuring one EVM domain +/// and minting funds to all test accounts. +fn create_genesis_storage(accounts: &[AccountId], mint: u128) -> Storage { + let raw_genesis_storage = RawGenesis::dummy(vec![1, 2, 3, 4]).encode(); + let pair = OperatorPair::from_seed(&[*accounts.first().unwrap() as u8; 32]); + RuntimeGenesisConfig { + balances: BalancesConfig { + balances: accounts.iter().cloned().map(|k| (k, mint)).collect(), + }, + domains: DomainsConfig { + genesis_domains: vec![GenesisDomain { + runtime_name: "evm".to_owned(), + runtime_type: RuntimeType::Evm, + runtime_version: Default::default(), + raw_genesis_storage, + owner_account_id: *accounts.first().unwrap(), + domain_name: "evm-domain".to_owned(), + bundle_slot_probability: (1, 1), + operator_allow_list: OperatorAllowList::Anyone, + signing_key: pair.public(), + minimum_nominator_stake: MIN_NOMINATOR_STAKE * AI3, + nomination_tax: Percent::from_percent(5), + initial_balances: vec![], + domain_runtime_info: (DEFAULT_EVM_CHAIN_ID, Default::default()).into(), + }], + permissioned_action_allowed_by: Some(PermissionedActionAllowedBy::Anyone), + }, + subspace: Default::default(), + system: Default::default(), + } + .build_storage() + .unwrap() +} + +pub fn run_staking_fuzz(data: &[u8]) { + let accounts: Vec = (0..5).map(|i| (i as u128)).collect(); + let mint = (u16::MAX as u128) * 2 * AI3; + let genesis = create_genesis_storage(&accounts, mint); + let Ok(data) = bincode::deserialize(data) else { + return; + }; + + let mut ext = BasicExternalities::new(genesis); + ext.execute_with(|| { + fuzz(&data, accounts.clone()); + }); +} + +fn fuzz(data: &FuzzData, accounts: Vec) { + let mut operators = BTreeMap::new(); + let mut nominators = BTreeMap::new(); + let mut invalid_ers = Vec::new(); + + // Get initial issuance from the pre-setup state + let initial_issuance = accounts + .iter() + .map(::Currency::free_balance) + .sum(); + + for (skip, epoch) in &data.epochs { + for (user, action) in epoch.actions.iter() { + let user = accounts.get(*user as usize % accounts.len()).unwrap(); + match action { + FuzzAction::RegisterOperator { amount, tax } => { + let res = register_operator(*user, *amount as u128, *tax); + if let Some(operator) = res { + operators.insert(user, operator); + nominators + .entry(*user) + .and_modify(|list: &mut Vec| list.push(operator)) + .or_insert(vec![operator]); + + println!( + "Registering {user:?} as Operator {operator:?} with amount {amount:?}\n-->{res:?}" + ); + } else { + println!( + "Registering {user:?} as Operator (failed) with amount {amount:?} AI3 \n-->{res:?}" + ); + } + } + FuzzAction::NominateOperator { + operator_id, + amount, + } => { + if operators.is_empty() { + println!("skipping NominateOperator"); + continue; + } + let amount = (*amount as u128).max(MIN_NOMINATOR_STAKE) * AI3; + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let res = do_nominate_operator::(*operator, *user, amount); + if res.is_ok() { + nominators + .entry(*user) + .and_modify(|list: &mut Vec| list.push(*operator)) + .or_insert(vec![*operator]); + } + + println!( + "Nominating as Nominator {user:?} for Operator {operator:?} with amount {amount:?}\n-->{res:?}" + ); + } + FuzzAction::DeregisterOperator { operator_id } => { + if operators.is_empty() { + println!("skipping DeregisterOperator"); + continue; + } + let (owner, operator) = *operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap(); + let res = do_deregister_operator::(**owner, *operator); + + println!("de-registering Operator {operator:?} \n-->{res:?}"); + } + FuzzAction::WithdrawStake { + nominator_id, + operator_id, + shares, + } => { + if operators.is_empty() { + println!("skipping WithdrawStake"); + continue; + } + let (nominator, operators) = *nominators + .iter() + .collect::>() + .get(*nominator_id as usize % nominators.len()) + .unwrap(); + let operator = operators + .get(*operator_id as usize % operators.len()) + .unwrap(); + let res = + do_withdraw_stake::(*operator, *nominator, *shares as u128 * AI3); + + println!( + "Withdrawing stake from Operator {operator:?} as Nominator {nominator:?} of shares {shares:?}\n-->{res:?}" + ); + } + FuzzAction::UnlockFunds { + operator_id, + nominator_id, + } => { + if operators.is_empty() { + println!("skipping UnlockFunds"); + continue; + } + let (nominator, operators) = *nominators + .iter() + .collect::>() + .get(*nominator_id as usize % nominators.len()) + .unwrap(); + let operator = operators + .get(*operator_id as usize % operators.len()) + .unwrap(); + let res = do_unlock_funds::(*operator, *nominator); + + println!( + "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}" + ); + } + FuzzAction::UnlockNominator { + operator_id, + nominator_id, + } => { + if operators.is_empty() { + println!("skipping UnlockNominator"); + continue; + } + let (nominator, operators) = *nominators + .iter() + .collect::>() + .get(*nominator_id as usize % nominators.len()) + .unwrap(); + let operator = operators + .get(*operator_id as usize % operators.len()) + .unwrap(); + let res = do_unlock_nominator::(*operator, *nominator); + + println!( + "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}" + ); + } + FuzzAction::MarkOperatorsAsSlashed { + operator_id, + slash_reason, + } => { + if operators.is_empty() { + println!("skipping MarkOperatorsAsSlashed"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let slash_reason = match slash_reason % 2 { + 0 => SlashedReason::InvalidBundle(0), + _ => SlashedReason::BadExecutionReceipt(H256::from([0u8; 32])), + }; + let res = do_mark_operators_as_slashed::(vec![*operator], slash_reason); + + println!("Marking {operator:?} as slashed\n-->{res:?}"); + do_slash_operator::(DOMAIN_ID, u32::MAX).unwrap(); + } + FuzzAction::SlashOperator => { + if operators.is_empty() { + println!("skipping SlashOperator"); + continue; + } + let res = do_slash_operator::(DOMAIN_ID, u32::MAX); + assert!(res.is_ok()); + + { + let pending_slashes = get_pending_slashes::(DOMAIN_ID); + println!("Slashing: {pending_slashes:?} -->{res:?}"); + } + } + FuzzAction::RewardOperator { + operator_id, + amount, + } => { + if operators.is_empty() { + println!("skipping RewardOperator"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let reward_amount = 10u128 * AI3; + let res = do_reward_operators::( + DOMAIN_ID, + sp_domains::OperatorRewardSource::Dummy, + vec![*operator].into_iter(), + reward_amount, + ); + assert!(res.is_ok()); + + println!("Rewarding operator {operator:?} with {amount:?} AI3 \n-->{res:?}"); + } + FuzzAction::MarkInvalidBundleAuthors { operator_id } => { + if operators.is_empty() { + println!("skipping MarkInvalidBundleAuthors"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + if let Some(invalid_er) = + fuzz_mark_invalid_bundle_authors::(*operator, DOMAIN_ID) + { + invalid_ers.push(invalid_er) + } + } + FuzzAction::UnmarkInvalidBundleAuthors { operator_id, er_id } => { + if operators.is_empty() { + println!("skipping UnmarkInvalidBundleAuthors"); + continue; + } + if invalid_ers.is_empty() { + println!("skipping UnmarkInvalidBundleAuthors"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let er = invalid_ers + .get(*er_id as usize % invalid_ers.len()) + .unwrap(); + fuzz_unmark_invalid_bundle_authors::(DOMAIN_ID, *operator, *er); + } + FuzzAction::DeactivateOperator { operator_id } => { + if operators.is_empty() { + println!("skipping DeactivateOperator"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let res = do_deactivate_operator::(*operator); + + println!("Deactivating {operator:?} \n-->{res:?}"); + } + FuzzAction::ReactivateOperator { operator_id } => { + if operators.is_empty() { + println!("skipping ReactivateOperator"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let res = do_reactivate_operator::(*operator); + + println!("Deactivating {operator:?} \n-->{res:?}"); + } + } + check_invariants_before_finalization::(DOMAIN_ID); + let prev_validator_states = get_next_operators::(DOMAIN_ID); + conclude_domain_epoch::(DOMAIN_ID); + check_invariants_after_finalization::(DOMAIN_ID, prev_validator_states); + check_general_invariants::(initial_issuance); + + println!("skipping {skip:?} epochs"); + for _ in 0..*skip { + conclude_domain_epoch::(DOMAIN_ID); + } + } + } +} + +/// Registers an operator for staking with fuzzer provided tax and amount +fn register_operator(operator: AccountId, amount: Balance, tax: u8) -> Option { + let pair = OperatorPair::from_seed(&[operator as u8; 32]); + let config = OperatorConfig { + signing_key: pair.public(), + minimum_nominator_stake: MIN_NOMINATOR_STAKE * AI3, + nomination_tax: sp_runtime::Percent::from_percent(tax.min(100)), + }; + let res = do_register_operator::(operator, DOMAIN_ID, amount * AI3, config); + if let Ok((id, _)) = res { + Some(id) + } else { + None + } +} diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 0687852dc1..2d3ea743f4 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -6,18 +6,21 @@ #[cfg(feature = "runtime-benchmarks")] mod benchmarking; -#[cfg(test)] -mod tests; - pub mod block_tree; pub mod bundle_storage_fund; pub mod domain_registry; pub mod extensions; +#[cfg(feature = "fuzz")] +pub mod fuzz; pub mod migrations; +#[cfg(any(feature = "fuzz", test,))] +pub(crate) mod mock; mod nominator_position; pub mod runtime_registry; pub mod staking; mod staking_epoch; +#[cfg(test)] +mod tests; pub mod weights; extern crate alloc; @@ -545,7 +548,7 @@ mod pallet { #[pallet::storage] #[pallet::getter(fn domain_staking_summary)] - pub(super) type DomainStakingSummary = + pub(crate) type DomainStakingSummary = StorageMap<_, Identity, DomainId, StakingSummary>, OptionQuery>; /// List of all registered operators and their configuration. @@ -577,7 +580,7 @@ mod pallet { /// List of all deposits for given Operator. #[pallet::storage] - pub(super) type Deposits = StorageDoubleMap< + pub(crate) type Deposits = StorageDoubleMap< _, Identity, OperatorId, @@ -589,7 +592,7 @@ mod pallet { /// List of all withdrawals for a given operator. #[pallet::storage] - pub(super) type Withdrawals = StorageDoubleMap< + pub(crate) type Withdrawals = StorageDoubleMap< _, Identity, OperatorId, @@ -608,7 +611,7 @@ mod pallet { /// When the epoch for a given domain is complete, operator total stake is moved to treasury and /// then deleted. #[pallet::storage] - pub(super) type PendingSlashes = + pub(crate) type PendingSlashes = StorageMap<_, Identity, DomainId, BTreeSet, OptionQuery>; /// The pending staking operation count of the current epoch, it should not larger than @@ -705,7 +708,7 @@ mod pallet { // the runtime upgrade tx from the consensus chain and no any user submitted tx from the bundle), use // `domain_best_number` for the actual best domain block #[pallet::storage] - pub(super) type HeadDomainNumber = + pub(crate) type HeadDomainNumber = StorageMap<_, Identity, DomainId, DomainBlockNumberFor, ValueQuery>; /// A temporary storage to hold any previous epoch details for a given domain diff --git a/crates/pallet-domains/src/migrations/v5_to_v6.rs b/crates/pallet-domains/src/migrations/v5_to_v6.rs index e7706742dc..4f294ba015 100644 --- a/crates/pallet-domains/src/migrations/v5_to_v6.rs +++ b/crates/pallet-domains/src/migrations/v5_to_v6.rs @@ -61,8 +61,9 @@ mod tests { use crate::migrations::v5_to_v6::migrate_evm_chain_id::{ NextEVMChainId, migrate_evm_chain_ids, }; + use crate::mock::{AccountId, Test}; use crate::pallet::DomainRegistry; - use crate::tests::{AccountId, Test, new_test_ext}; + use crate::tests::new_test_ext; use domain_runtime_primitives::{Balance, DEFAULT_EVM_CHAIN_ID}; use frame_support::weights::RuntimeDbWeight; use sp_domains::{DomainId, EvmDomainRuntimeConfig, OperatorAllowList}; diff --git a/crates/pallet-domains/src/mock.rs b/crates/pallet-domains/src/mock.rs new file mode 100644 index 0000000000..3caffcba28 --- /dev/null +++ b/crates/pallet-domains/src/mock.rs @@ -0,0 +1,398 @@ +use crate::{self as pallet_domains, BlockSlot, FungibleHoldId}; +use core::mem; +use domain_runtime_primitives::BlockNumber as DomainBlockNumber; +use domain_runtime_primitives::opaque::Header as DomainHeader; +use frame_support::dispatch::DispatchInfo; +use frame_support::pallet_prelude::{Decode, Encode, MaxEncodedLen, TypeInfo}; +use frame_support::traits::{ConstU16, ConstU64, VariantCount}; +use frame_support::weights::constants::ParityDbWeight; +use frame_support::weights::{IdentityFee, Weight}; +use frame_support::{PalletId, derive_impl, parameter_types}; +use frame_system::mocking::MockUncheckedExtrinsic; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_subspace::NormalEraChange; +use sp_consensus_slots::Slot; +use sp_core::H256; +use sp_domains::bundle::BundleVersion; +use sp_domains::execution_receipt::ExecutionReceiptVersion; +use sp_domains::{BundleAndExecutionReceiptVersion, ChainId, DomainId, EpochIndex}; +use sp_runtime::traits::{AccountIdConversion, Get, IdentityLookup}; +use sp_runtime::transaction_validity::TransactionValidityError; +use std::num::NonZeroU64; +use subspace_core_primitives::SlotNumber; +use subspace_core_primitives::pieces::Piece; +use subspace_core_primitives::segments::HistorySize; +use subspace_core_primitives::solutions::SolutionRange; +use subspace_runtime_primitives::{ + AI3, ConsensusEventSegmentSize, HoldIdentifier, Moment, StorageFee, +}; + +pub(crate) type Block = frame_system::mocking::MockBlockU32; +pub(crate) type Balance = u128; + +// TODO: Remove when DomainRegistry is usable. +pub(crate) const DOMAIN_ID: DomainId = DomainId::new(0); + +frame_support::construct_runtime!( + pub struct Test { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + Subspace: pallet_subspace, + Domains: pallet_domains, + DomainExecutive: domain_pallet_executive, + BlockFees: pallet_block_fees, + MockVersionStore: pallet_mock_version_store, + } +); + +pub(crate) type BlockNumber = u32; +pub(crate) type Hash = H256; +pub(crate) type AccountId = u128; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type Hash = Hash; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type AccountData = pallet_balances::AccountData; + type DbWeight = ParityDbWeight; + type EventSegmentSize = ConsensusEventSegmentSize; +} + +parameter_types! { + pub const MaximumReceiptDrift: BlockNumber = 128; + pub const InitialDomainTxRange: u64 = 3; + pub const DomainTxRangeAdjustmentInterval: u64 = 100; + pub const MaxDomainBlockSize: u32 = 1024 * 1024; + pub const MaxDomainBlockWeight: Weight = Weight::from_parts(1024 * 1024, 0); + pub const DomainInstantiationDeposit: Balance = 100; + pub const MaxDomainNameLength: u32 = 16; + pub const BlockTreePruningDepth: u32 = 16; + pub const SlotProbability: (u64, u64) = (1, 6); +} + +pub struct ConfirmationDepthK; + +impl Get for ConfirmationDepthK { + fn get() -> BlockNumber { + 10 + } +} + +#[derive( + PartialEq, Eq, Clone, Encode, Decode, TypeInfo, MaxEncodedLen, Ord, PartialOrd, Copy, Debug, +)] +pub struct HoldIdentifierWrapper(HoldIdentifier); + +impl crate::HoldIdentifier for HoldIdentifierWrapper { + fn staking_staked() -> FungibleHoldId { + Self(HoldIdentifier::DomainStaking) + } + + fn domain_instantiation_id() -> FungibleHoldId { + Self(HoldIdentifier::DomainInstantiation) + } + + fn storage_fund_withdrawal() -> Self { + Self(HoldIdentifier::DomainStorageFund) + } +} + +impl VariantCount for HoldIdentifierWrapper { + const VARIANT_COUNT: u32 = mem::variant_count::() as u32; +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 1; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] +impl pallet_balances::Config for Test { + type Balance = Balance; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type RuntimeHoldReason = HoldIdentifierWrapper; + type DustRemoval = (); +} + +parameter_types! { + pub const MinOperatorStake: Balance = 100 * AI3; + pub const MinNominatorStake: Balance = AI3; + pub const StakeWithdrawalLockingPeriod: DomainBlockNumber = 5; + pub const StakeEpochDuration: DomainBlockNumber = 5; + pub TreasuryAccount: u128 = PalletId(*b"treasury").into_account_truncating(); + pub const BlockReward: Balance = 10 * AI3; + pub const MaxPendingStakingOperation: u32 = 512; + pub const DomainsPalletId: PalletId = PalletId(*b"domains_"); + pub const DomainChainByteFee: Balance = 1; + pub const MaxInitialDomainAccounts: u32 = 5; + pub const MinInitialDomainAccountBalance: Balance = AI3; + pub const BundleLongevity: u32 = 5; + pub const WithdrawalLimit: u32 = 10; + pub const CurrentBundleAndExecutionReceiptVersion: BundleAndExecutionReceiptVersion = BundleAndExecutionReceiptVersion { + bundle_version: BundleVersion::V0, + execution_receipt_version: ExecutionReceiptVersion::V0, + }; + pub const OperatorActivationDelayInEpochs: EpochIndex = 5; +} + +pub struct MockRandomness; + +impl frame_support::traits::Randomness for MockRandomness { + fn random(_: &[u8]) -> (Hash, BlockNumber) { + (Default::default(), Default::default()) + } +} + +const SLOT_DURATION: u64 = 1000; + +impl pallet_timestamp::Config for Test { + /// A timestamp: milliseconds since the unix epoch. + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; + type WeightInfo = (); +} + +pub struct DummyStorageFee; + +impl StorageFee for DummyStorageFee { + fn transaction_byte_fee() -> Balance { + AI3 + } + fn note_storage_fees(_fee: Balance) {} +} + +pub struct DummyBlockSlot; + +impl BlockSlot for DummyBlockSlot { + fn future_slot(_block_number: BlockNumberFor) -> Option { + None + } + + fn slot_produced_after(_slot: sp_consensus_slots::Slot) -> Option> { + Some(0u32) + } + + fn current_slot() -> Slot { + Slot::from(0) + } +} + +pub struct MockDomainsTransfersTracker; + +impl sp_domains::DomainsTransfersTracker for MockDomainsTransfersTracker { + type Error = (); + + fn initialize_domain_balance( + _domain_id: DomainId, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn note_transfer( + _from_chain_id: ChainId, + _to_chain_id: ChainId, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn confirm_transfer( + _from_chain_id: ChainId, + _to_chain_id: ChainId, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn claim_rejected_transfer( + _from_chain_id: ChainId, + _to_chain_id: ChainId, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn reject_transfer( + _from_chain_id: ChainId, + _to_chain_id: ChainId, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn reduce_domain_balance(_domain_id: DomainId, _amount: Balance) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type DomainHash = sp_core::H256; + type Balance = Balance; + type DomainHeader = DomainHeader; + type ConfirmationDepthK = ConfirmationDepthK; + type Currency = Balances; + type Share = Balance; + type HoldIdentifier = HoldIdentifierWrapper; + type BlockTreePruningDepth = BlockTreePruningDepth; + type ConsensusSlotProbability = SlotProbability; + type MaxDomainBlockSize = MaxDomainBlockSize; + type MaxDomainBlockWeight = MaxDomainBlockWeight; + type MaxDomainNameLength = MaxDomainNameLength; + type DomainInstantiationDeposit = DomainInstantiationDeposit; + type WeightInfo = crate::weights::SubstrateWeight; + type InitialDomainTxRange = InitialDomainTxRange; + type DomainTxRangeAdjustmentInterval = DomainTxRangeAdjustmentInterval; + type MinOperatorStake = MinOperatorStake; + type MinNominatorStake = MinNominatorStake; + type StakeWithdrawalLockingPeriod = StakeWithdrawalLockingPeriod; + type StakeEpochDuration = StakeEpochDuration; + type TreasuryAccount = TreasuryAccount; + type MaxPendingStakingOperation = MaxPendingStakingOperation; + type Randomness = MockRandomness; + type PalletId = DomainsPalletId; + type StorageFee = DummyStorageFee; + type BlockTimestamp = pallet_timestamp::Pallet; + type BlockSlot = DummyBlockSlot; + type DomainsTransfersTracker = MockDomainsTransfersTracker; + type MaxInitialDomainAccounts = MaxInitialDomainAccounts; + type MinInitialDomainAccountBalance = MinInitialDomainAccountBalance; + type BundleLongevity = BundleLongevity; + type DomainBundleSubmitted = (); + type OnDomainInstantiated = (); + type MmrHash = H256; + type MmrProofVerifier = (); + type FraudProofStorageKeyProvider = (); + type OnChainRewards = (); + type WithdrawalLimit = WithdrawalLimit; + type DomainOrigin = crate::EnsureDomainOrigin; + type CurrentBundleAndExecutionReceiptVersion = CurrentBundleAndExecutionReceiptVersion; + type OperatorActivationDelayInEpochs = OperatorActivationDelayInEpochs; +} + +pub struct ExtrinsicStorageFees; + +impl domain_pallet_executive::ExtrinsicStorageFees for ExtrinsicStorageFees { + fn extract_signer(_xt: MockUncheckedExtrinsic) -> (Option, DispatchInfo) { + (None, DispatchInfo::default()) + } + + fn on_storage_fees_charged( + _charged_fees: Balance, + _tx_size: u32, + ) -> Result<(), TransactionValidityError> { + Ok(()) + } +} + +impl domain_pallet_executive::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type LengthToFee = IdentityFee; + type ExtrinsicStorageFees = ExtrinsicStorageFees; +} + +impl pallet_block_fees::Config for Test { + type Balance = Balance; + type DomainChainByteFee = DomainChainByteFee; +} + +pub const INITIAL_SOLUTION_RANGE: SolutionRange = + u64::MAX / (1024 * 1024 * 1024 / Piece::SIZE as u64) * 3 / 10; + +parameter_types! { + pub const BlockAuthoringDelay: SlotNumber = 2; + pub const PotEntropyInjectionInterval: BlockNumber = 5; + pub const PotEntropyInjectionLookbackDepth: u8 = 2; + pub const PotEntropyInjectionDelay: SlotNumber = 4; + pub const EraDuration: u32 = 4; + // 1GB + pub const InitialSolutionRange: SolutionRange = INITIAL_SOLUTION_RANGE; + pub const RecentSegments: HistorySize = HistorySize::new(NonZeroU64::new(5).unwrap()); + pub const RecentHistoryFraction: (HistorySize, HistorySize) = ( + HistorySize::new(NonZeroU64::new(1).unwrap()), + HistorySize::new(NonZeroU64::new(10).unwrap()), + ); + pub const MinSectorLifetime: HistorySize = HistorySize::new(NonZeroU64::new(4).unwrap()); + pub const RecordSize: u32 = 3840; + pub const ExpectedVotesPerBlock: u32 = 9; + pub const ReplicationFactor: u16 = 1; + pub const ReportLongevity: u64 = 34; + pub const ShouldAdjustSolutionRange: bool = false; + pub const BlockSlotCount: u32 = 6; +} + +impl pallet_subspace::Config for Test { + type RuntimeEvent = RuntimeEvent; + type SubspaceOrigin = pallet_subspace::EnsureSubspaceOrigin; + type BlockAuthoringDelay = BlockAuthoringDelay; + type PotEntropyInjectionInterval = PotEntropyInjectionInterval; + type PotEntropyInjectionLookbackDepth = PotEntropyInjectionLookbackDepth; + type PotEntropyInjectionDelay = PotEntropyInjectionDelay; + type EraDuration = EraDuration; + type InitialSolutionRange = InitialSolutionRange; + type SlotProbability = SlotProbability; + type ConfirmationDepthK = ConfirmationDepthK; + type RecentSegments = RecentSegments; + type RecentHistoryFraction = RecentHistoryFraction; + type MinSectorLifetime = MinSectorLifetime; + type ExpectedVotesPerBlock = ExpectedVotesPerBlock; + type MaxPiecesInSector = ConstU16<1>; + type ShouldAdjustSolutionRange = ShouldAdjustSolutionRange; + type EraChangeTrigger = NormalEraChange; + type WeightInfo = (); + type BlockSlotCount = BlockSlotCount; + type ExtensionWeightInfo = pallet_subspace::extensions::weights::SubstrateWeight; +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone, Copy)] +pub enum MockBundleVersion { + V0, + V1, + V2, + V3, +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone, Copy)] +pub enum MockExecutionReceiptVersion { + V0, + V1, + V2, + V3, +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone, Copy)] +pub struct MockBundleAndExecutionReceiptVersion { + pub bundle_version: MockBundleVersion, + pub execution_receipt_version: MockExecutionReceiptVersion, +} + +#[frame_support::pallet] +pub(crate) mod pallet_mock_version_store { + use super::{BlockNumberFor, MockBundleAndExecutionReceiptVersion}; + use frame_support::pallet_prelude::*; + use std::collections::BTreeMap; + + #[pallet::config] + pub trait Config: frame_system::Config {} + + /// Pallet domain-id to store self domain id. + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::storage] + pub type MockPreviousBundleAndExecutionReceiptVersions = StorageValue< + _, + BTreeMap, MockBundleAndExecutionReceiptVersion>, + ValueQuery, + >; +} + +impl pallet_mock_version_store::Config for Test {} diff --git a/crates/pallet-domains/src/nominator_position.rs b/crates/pallet-domains/src/nominator_position.rs index 8862987b83..3b41c632e5 100644 --- a/crates/pallet-domains/src/nominator_position.rs +++ b/crates/pallet-domains/src/nominator_position.rs @@ -233,6 +233,7 @@ pub fn nominator_position( #[cfg(test)] mod tests { use super::*; + use crate::mock::{Balances, Test}; use crate::staking::tests::{ PROP_FREE_BALANCE_RANGE, PROP_NOMINATOR_STAKE_RANGE, PROP_OPERATOR_STAKE_RANGE, prop_assert_approx, diff --git a/crates/pallet-domains/src/runtime_registry.rs b/crates/pallet-domains/src/runtime_registry.rs index 83bbd2825c..1507ad1b32 100644 --- a/crates/pallet-domains/src/runtime_registry.rs +++ b/crates/pallet-domains/src/runtime_registry.rs @@ -326,11 +326,10 @@ pub(crate) fn do_upgrade_runtimes(at: BlockNumberFor) { #[cfg(test)] mod tests { use crate::Error; + use crate::mock::{Domains, System, Test}; use crate::pallet::{NextRuntimeId, RuntimeRegistry, ScheduledRuntimeUpgrades}; use crate::runtime_registry::Error as RuntimeRegistryError; - use crate::tests::{ - Domains, ReadRuntimeVersion, System, TEST_RUNTIME_APIS, Test, new_test_ext, - }; + use crate::tests::{ReadRuntimeVersion, TEST_RUNTIME_APIS, new_test_ext}; use domain_runtime_primitives::Hash; use frame_support::dispatch::RawOrigin; use frame_support::traits::OnInitialize; diff --git a/crates/pallet-domains/src/staking.rs b/crates/pallet-domains/src/staking.rs index b64116d719..a4e3ce3fd9 100644 --- a/crates/pallet-domains/src/staking.rs +++ b/crates/pallet-domains/src/staking.rs @@ -1581,6 +1581,7 @@ pub(crate) fn do_mark_operators_as_slashed( } /// Mark all the invalid bundle authors from this ER and remove them from operator set. +/// NOTE: any changes to this must be reflected in the fuzz_utils' equivalent pub(crate) fn do_mark_invalid_bundle_authors( domain_id: DomainId, er: &ExecutionReceiptOf, @@ -1652,6 +1653,7 @@ pub(crate) fn mark_invalid_bundle_author( /// Unmark all the invalid bundle authors from this ER that were marked invalid. /// Assumed the ER is invalid and add the marked operators as registered and add them /// back to next operator set. +/// NOTE: any changes to this must be reflected in the fuzz_utils' equivalent pub(crate) fn do_unmark_invalid_bundle_authors( domain_id: DomainId, er: &ExecutionReceiptOf, @@ -1683,7 +1685,7 @@ pub(crate) fn do_unmark_invalid_bundle_authors( Ok(()) } -fn unmark_invalid_bundle_author( +pub(crate) fn unmark_invalid_bundle_author( operator_id: OperatorId, er_hash: ReceiptHashFor, stake_summary: &mut StakingSummary>, @@ -1714,6 +1716,9 @@ fn unmark_invalid_bundle_author( #[cfg(test)] pub(crate) mod tests { use crate::domain_registry::{DomainConfig, DomainObject}; + use crate::mock::{ + AccountId, ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, TreasuryAccount, + }; use crate::pallet::{ Config, DepositOnHold, Deposits, DomainRegistry, DomainStakingSummary, HeadDomainNumber, NextOperatorId, OperatorIdOwner, Operators, PendingSlashes, Withdrawals, @@ -1725,7 +1730,7 @@ pub(crate) mod tests { do_withdraw_stake, }; use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator}; - use crate::tests::{ExistentialDeposit, MinOperatorStake, RuntimeOrigin, Test, new_test_ext}; + use crate::tests::new_test_ext; use crate::{ BalanceOf, DeactivatedOperators, DeregisteredOperators, Error, MAX_NOMINATORS_TO_SLASH, NominatorId, OperatorEpochSharePrice, SlashedReason, bundle_storage_fund, @@ -2734,8 +2739,8 @@ pub(crate) mod tests { assert_ok!(do_unlock_funds::(operator_id, nominator_id)); let expected_balance = if include_ed { - total_balance += crate::tests::ExistentialDeposit::get(); - previous_usable_balance + withdraw + crate::tests::ExistentialDeposit::get() + total_balance += ExistentialDeposit::get(); + previous_usable_balance + withdraw + ExistentialDeposit::get() } else { previous_usable_balance + withdraw }; @@ -4026,10 +4031,7 @@ pub(crate) mod tests { let pending_slashes = PendingSlashes::::get(domain_id).unwrap(); assert!(pending_slashes.contains(&operator_id)); - assert_eq!( - Balances::total_balance(&crate::tests::TreasuryAccount::get()), - 0 - ); + assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 0); do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap(); assert_eq!(PendingSlashes::::get(domain_id), None); @@ -4045,7 +4047,7 @@ pub(crate) mod tests { nominator_free_balance - nominator_stake ); - assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 320 * AI3); + assert!(Balances::total_balance(&TreasuryAccount::get()) >= 320 * AI3); assert_eq!(bundle_storage_fund::total_balance::(operator_id), 0); }); } @@ -4060,7 +4062,7 @@ pub(crate) mod tests { let operator_extra_withdraw = 5 * AI3; let pair = OperatorPair::from_seed(&[0; 32]); - let nominator_accounts: Vec = (2..22).collect(); + let nominator_accounts: Vec = (2..22).collect(); let nominator_free_balance = 150 * AI3; let nominator_stake = 100 * AI3; let nominator_extra_deposit = 40 * AI3; @@ -4206,10 +4208,7 @@ pub(crate) mod tests { let pending_slashes = PendingSlashes::::get(domain_id).unwrap(); assert!(pending_slashes.contains(&operator_id)); - assert_eq!( - Balances::total_balance(&crate::tests::TreasuryAccount::get()), - 0 - ); + assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 0); // since we only slash 10 nominators a time but we have a total of 21 nominators, // do 3 iterations @@ -4232,10 +4231,7 @@ pub(crate) mod tests { ); } - assert_eq!( - Balances::total_balance(&crate::tests::TreasuryAccount::get()), - 2220 * AI3 - ); + assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 2220 * AI3); assert_eq!(bundle_storage_fund::total_balance::(operator_id), 0); }); } @@ -4360,10 +4356,7 @@ pub(crate) mod tests { OperatorStatus::Slashed ); - assert_eq!( - Balances::total_balance(&crate::tests::TreasuryAccount::get()), - 0 - ); + assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 0); let slashed_operators = PendingSlashes::::get(domain_id).unwrap(); slashed_operators.into_iter().for_each(|_| { @@ -4378,10 +4371,7 @@ pub(crate) mod tests { assert_eq!(Operators::::get(operator_id_3), None); assert_eq!(OperatorIdOwner::::get(operator_id_3), None); - assert_eq!( - Balances::total_balance(&crate::tests::TreasuryAccount::get()), - 600 * AI3 - ); + assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 600 * AI3); for operator_id in [operator_id_1, operator_id_2, operator_id_3] { assert_eq!(bundle_storage_fund::total_balance::(operator_id), 0); } @@ -4460,10 +4450,7 @@ pub(crate) mod tests { assert_eq!(bundle_storage_fund::total_balance::(operator_id), AI3); // The operator `operator_id + 1` not exist thus the refund storage fee added to treasury - assert_eq!( - Balances::total_balance(&crate::tests::TreasuryAccount::get()), - 9 * AI3 - ); + assert_eq!(Balances::total_balance(&TreasuryAccount::get()), 9 * AI3); bundle_storage_fund::charge_bundle_storage_fee::(operator_id, 1).unwrap(); assert_eq!(bundle_storage_fund::total_balance::(operator_id), 0); diff --git a/crates/pallet-domains/src/staking_epoch.rs b/crates/pallet-domains/src/staking_epoch.rs index f936acd64a..0bb71134f5 100644 --- a/crates/pallet-domains/src/staking_epoch.rs +++ b/crates/pallet-domains/src/staking_epoch.rs @@ -672,6 +672,7 @@ pub(crate) fn do_slash_operator( #[cfg(test)] mod tests { use crate::bundle_storage_fund::STORAGE_FEE_RESERVE; + use crate::mock::{HoldIdentifierWrapper, RuntimeOrigin, Test}; use crate::pallet::{ DepositOnHold, Deposits, DomainStakingSummary, HeadDomainNumber, LastEpochStakingDistribution, OperatorIdOwner, Operators, Withdrawals, @@ -685,7 +686,7 @@ mod tests { use crate::staking_epoch::{ do_finalize_domain_current_epoch, do_slash_operator, operator_take_reward_tax_and_stake, }; - use crate::tests::{RuntimeOrigin, Test, new_test_ext}; + use crate::tests::new_test_ext; use crate::{ BalanceOf, Config, HoldIdentifier, InvalidBundleAuthors, MAX_NOMINATORS_TO_SLASH, NominatorId, OperatorEpochSharePrice, SlashedReason, @@ -797,7 +798,7 @@ mod tests { assert_ok!(do_unlock_nominator::(operator_id, operator_account)); - let hold_id = crate::tests::HoldIdentifierWrapper::staking_staked(); + let hold_id = HoldIdentifierWrapper::staking_staked(); for (nominator_id, mut expected_usable_balance) in expected_usable_balances { expected_usable_balance += minimum_free_balance; assert_eq!(Deposits::::get(operator_id, nominator_id), None); diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index 11cf0be8a1..c79263ee2b 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -1,73 +1,59 @@ use crate::block_tree::{BlockTreeNode, verify_execution_receipt}; use crate::domain_registry::{DomainConfig, DomainConfigParams, DomainObject}; +use crate::mock::pallet_mock_version_store::MockPreviousBundleAndExecutionReceiptVersions; +use crate::mock::{ + Balance, Block, BlockNumber, BlockTreePruningDepth, DOMAIN_ID, Domains, Hash, + MockBundleAndExecutionReceiptVersion, MockBundleVersion, MockExecutionReceiptVersion, + RuntimeCall, System, Test, +}; use crate::runtime_registry::ScheduledRuntimeUpgrade; use crate::staking_epoch::do_finalize_domain_current_epoch; -use crate::tests::pallet_mock_version_store::MockPreviousBundleAndExecutionReceiptVersions; use crate::{ - self as pallet_domains, BalanceOf, BlockSlot, BlockTree, BlockTreeNodes, BundleError, Config, + self as pallet_domains, BalanceOf, BlockTree, BlockTreeNodes, BundleError, Config, ConsensusBlockHash, DomainBlockNumberFor, DomainHashingFor, DomainRegistry, DomainRuntimeUpgradeRecords, DomainRuntimeUpgrades, ExecutionInbox, ExecutionReceiptOf, - FraudProofError, FungibleHoldId, HeadDomainNumber, HeadReceiptNumber, NextDomainId, - OperatorConfig, RawOrigin as DomainOrigin, RuntimeRegistry, ScheduledRuntimeUpgrades, + FraudProofError, HeadDomainNumber, HeadReceiptNumber, NextDomainId, OperatorConfig, + RawOrigin as DomainOrigin, RuntimeRegistry, ScheduledRuntimeUpgrades, }; -use core::mem; use domain_runtime_primitives::opaque::Header as DomainHeader; use domain_runtime_primitives::{BlockNumber as DomainBlockNumber, DEFAULT_EVM_CHAIN_ID}; -use frame_support::dispatch::{DispatchInfo, RawOrigin}; -use frame_support::traits::{ConstU64, Currency, Hooks, VariantCount}; -use frame_support::weights::constants::ParityDbWeight; -use frame_support::weights::{IdentityFee, Weight}; -use frame_support::{PalletId, assert_err, assert_ok, derive_impl, parameter_types}; +use frame_support::dispatch::RawOrigin; +use frame_support::traits::{Currency, Hooks}; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_ok}; use frame_system::mocking::MockUncheckedExtrinsic; use frame_system::pallet_prelude::*; use hex_literal::hex; -use pallet_subspace::NormalEraChange; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_consensus_slots::Slot; use sp_core::crypto::Pair; use sp_core::{Get, H256}; use sp_domains::bundle::bundle_v0::{BundleHeaderV0, BundleV0, SealedBundleHeaderV0}; -use sp_domains::bundle::{BundleVersion, InboxedBundle, OpaqueBundle}; +use sp_domains::bundle::{InboxedBundle, OpaqueBundle}; use sp_domains::bundle_producer_election::make_transcript; use sp_domains::execution_receipt::execution_receipt_v0::ExecutionReceiptV0; -use sp_domains::execution_receipt::{ExecutionReceipt, ExecutionReceiptVersion, SingletonReceipt}; +use sp_domains::execution_receipt::{ExecutionReceipt, SingletonReceipt}; use sp_domains::merkle_tree::MerkleTree; use sp_domains::storage::RawGenesis; use sp_domains::{ - BundleAndExecutionReceiptVersion, ChainId, DomainId, EMPTY_EXTRINSIC_ROOT, EpochIndex, - OperatorAllowList, OperatorId, OperatorPair, OperatorSignature, ProofOfElection, RuntimeId, - RuntimeType, + DomainId, EMPTY_EXTRINSIC_ROOT, OperatorAllowList, OperatorId, OperatorPair, OperatorSignature, + ProofOfElection, RuntimeId, RuntimeType, }; use sp_domains_fraud_proof::fraud_proof::FraudProof; use sp_keystore::Keystore; use sp_keystore::testing::MemoryKeystore; use sp_runtime::app_crypto::AppCrypto; use sp_runtime::generic::{EXTRINSIC_FORMAT_VERSION, Preamble}; -use sp_runtime::traits::{ - AccountIdConversion, BlakeTwo256, BlockNumberProvider, Bounded, ConstU16, Hash as HashT, - IdentityLookup, One, Zero, -}; -use sp_runtime::transaction_validity::TransactionValidityError; +use sp_runtime::traits::{BlakeTwo256, BlockNumberProvider, Bounded, Hash as HashT, One, Zero}; use sp_runtime::type_with_default::TypeWithDefault; use sp_runtime::{BuildStorage, OpaqueExtrinsic}; use sp_version::{ApiId, RuntimeVersion, create_apis_vec}; -use std::num::NonZeroU64; -use subspace_core_primitives::pieces::Piece; +use subspace_core_primitives::U256 as P256; use subspace_core_primitives::pot::PotOutput; -use subspace_core_primitives::segments::HistorySize; -use subspace_core_primitives::solutions::SolutionRange; -use subspace_core_primitives::{SlotNumber, U256 as P256}; -use subspace_runtime_primitives::{ - AI3, BlockHashFor, ConsensusEventSegmentSize, HoldIdentifier, Moment, Nonce, StorageFee, -}; +use subspace_runtime_primitives::{AI3, BlockHashFor, Nonce}; -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlockU32; -type Balance = u128; - -// TODO: Remove when DomainRegistry is usable. -const DOMAIN_ID: DomainId = DomainId::new(0); +type UncheckedExtrinsic = MockUncheckedExtrinsic; // Operator id used for testing const OPERATOR_ID: OperatorId = 0u64; @@ -79,373 +65,9 @@ const OPERATOR_ID: OperatorId = 0u64; // to derive the correct TransactionVersion and SystemVersion. // So we should always add the TEST_RUNTIME_APIS to the RuntimeVersion to ensure it is decoded correctly. // More here - https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/version/src/lib.rs#L637 -pub(crate) const CORE_API_ID: [u8; 8] = [223, 106, 203, 104, 153, 7, 96, 155]; +const CORE_API_ID: [u8; 8] = [223, 106, 203, 104, 153, 7, 96, 155]; pub(crate) const TEST_RUNTIME_APIS: [(ApiId, u32); 1] = [(CORE_API_ID, 5)]; -frame_support::construct_runtime!( - pub struct Test { - System: frame_system, - Timestamp: pallet_timestamp, - Balances: pallet_balances, - Subspace: pallet_subspace, - Domains: pallet_domains, - DomainExecutive: domain_pallet_executive, - BlockFees: pallet_block_fees, - MockVersionStore: pallet_mock_version_store, - } -); - -type BlockNumber = u32; -type Hash = H256; -pub(crate) type AccountId = u128; - -#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test { - type Block = Block; - type Hash = Hash; - type AccountId = AccountId; - type Lookup = IdentityLookup; - type AccountData = pallet_balances::AccountData; - type DbWeight = ParityDbWeight; - type EventSegmentSize = ConsensusEventSegmentSize; -} - -parameter_types! { - pub const MaximumReceiptDrift: BlockNumber = 128; - pub const InitialDomainTxRange: u64 = 3; - pub const DomainTxRangeAdjustmentInterval: u64 = 100; - pub const MaxDomainBlockSize: u32 = 1024 * 1024; - pub const MaxDomainBlockWeight: Weight = Weight::from_parts(1024 * 1024, 0); - pub const DomainInstantiationDeposit: Balance = 100; - pub const MaxDomainNameLength: u32 = 16; - pub const BlockTreePruningDepth: u32 = 16; - pub const SlotProbability: (u64, u64) = (1, 6); -} - -pub struct ConfirmationDepthK; - -impl Get for ConfirmationDepthK { - fn get() -> BlockNumber { - 10 - } -} - -#[derive( - PartialEq, Eq, Clone, Encode, Decode, TypeInfo, MaxEncodedLen, Ord, PartialOrd, Copy, Debug, -)] -pub struct HoldIdentifierWrapper(HoldIdentifier); - -impl pallet_domains::HoldIdentifier for HoldIdentifierWrapper { - fn staking_staked() -> FungibleHoldId { - Self(HoldIdentifier::DomainStaking) - } - - fn domain_instantiation_id() -> FungibleHoldId { - Self(HoldIdentifier::DomainInstantiation) - } - - fn storage_fund_withdrawal() -> Self { - Self(HoldIdentifier::DomainStorageFund) - } -} - -impl VariantCount for HoldIdentifierWrapper { - const VARIANT_COUNT: u32 = mem::variant_count::() as u32; -} - -parameter_types! { - pub const ExistentialDeposit: Balance = 1; -} - -#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] -impl pallet_balances::Config for Test { - type Balance = Balance; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type RuntimeHoldReason = HoldIdentifierWrapper; - type DustRemoval = (); -} - -parameter_types! { - pub const MinOperatorStake: Balance = 100 * AI3; - pub const MinNominatorStake: Balance = AI3; - pub const StakeWithdrawalLockingPeriod: DomainBlockNumber = 5; - pub const StakeEpochDuration: DomainBlockNumber = 5; - pub TreasuryAccount: u128 = PalletId(*b"treasury").into_account_truncating(); - pub const BlockReward: Balance = 10 * AI3; - pub const MaxPendingStakingOperation: u32 = 512; - pub const DomainsPalletId: PalletId = PalletId(*b"domains_"); - pub const DomainChainByteFee: Balance = 1; - pub const MaxInitialDomainAccounts: u32 = 5; - pub const MinInitialDomainAccountBalance: Balance = AI3; - pub const BundleLongevity: u32 = 5; - pub const WithdrawalLimit: u32 = 10; - pub const CurrentBundleAndExecutionReceiptVersion: BundleAndExecutionReceiptVersion = BundleAndExecutionReceiptVersion { - bundle_version: BundleVersion::V0, - execution_receipt_version: ExecutionReceiptVersion::V0, - }; - pub const OperatorActivationDelayInEpochs: EpochIndex = 5; -} - -pub struct MockRandomness; - -impl frame_support::traits::Randomness for MockRandomness { - fn random(_: &[u8]) -> (Hash, BlockNumber) { - (Default::default(), Default::default()) - } -} - -const SLOT_DURATION: u64 = 1000; - -impl pallet_timestamp::Config for Test { - /// A timestamp: milliseconds since the unix epoch. - type Moment = Moment; - type OnTimestampSet = (); - type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; - type WeightInfo = (); -} - -pub struct DummyStorageFee; - -impl StorageFee for DummyStorageFee { - fn transaction_byte_fee() -> Balance { - AI3 - } - fn note_storage_fees(_fee: Balance) {} -} - -pub struct DummyBlockSlot; - -impl BlockSlot for DummyBlockSlot { - fn future_slot(_block_number: BlockNumberFor) -> Option { - None - } - - fn slot_produced_after(_slot: sp_consensus_slots::Slot) -> Option> { - Some(0u32) - } - - fn current_slot() -> Slot { - Slot::from(0) - } -} - -pub struct MockDomainsTransfersTracker; - -impl sp_domains::DomainsTransfersTracker for MockDomainsTransfersTracker { - type Error = (); - - fn initialize_domain_balance( - _domain_id: DomainId, - _amount: Balance, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn note_transfer( - _from_chain_id: ChainId, - _to_chain_id: ChainId, - _amount: Balance, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn confirm_transfer( - _from_chain_id: ChainId, - _to_chain_id: ChainId, - _amount: Balance, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn claim_rejected_transfer( - _from_chain_id: ChainId, - _to_chain_id: ChainId, - _amount: Balance, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn reject_transfer( - _from_chain_id: ChainId, - _to_chain_id: ChainId, - _amount: Balance, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn reduce_domain_balance(_domain_id: DomainId, _amount: Balance) -> Result<(), Self::Error> { - Ok(()) - } -} - -impl pallet_domains::Config for Test { - type RuntimeEvent = RuntimeEvent; - type DomainHash = sp_core::H256; - type Balance = Balance; - type DomainHeader = DomainHeader; - type ConfirmationDepthK = ConfirmationDepthK; - type Currency = Balances; - type Share = Balance; - type HoldIdentifier = HoldIdentifierWrapper; - type BlockTreePruningDepth = BlockTreePruningDepth; - type ConsensusSlotProbability = SlotProbability; - type MaxDomainBlockSize = MaxDomainBlockSize; - type MaxDomainBlockWeight = MaxDomainBlockWeight; - type MaxDomainNameLength = MaxDomainNameLength; - type DomainInstantiationDeposit = DomainInstantiationDeposit; - type WeightInfo = pallet_domains::weights::SubstrateWeight; - type InitialDomainTxRange = InitialDomainTxRange; - type DomainTxRangeAdjustmentInterval = DomainTxRangeAdjustmentInterval; - type MinOperatorStake = MinOperatorStake; - type MinNominatorStake = MinNominatorStake; - type StakeWithdrawalLockingPeriod = StakeWithdrawalLockingPeriod; - type StakeEpochDuration = StakeEpochDuration; - type TreasuryAccount = TreasuryAccount; - type MaxPendingStakingOperation = MaxPendingStakingOperation; - type Randomness = MockRandomness; - type PalletId = DomainsPalletId; - type StorageFee = DummyStorageFee; - type BlockTimestamp = pallet_timestamp::Pallet; - type BlockSlot = DummyBlockSlot; - type DomainsTransfersTracker = MockDomainsTransfersTracker; - type MaxInitialDomainAccounts = MaxInitialDomainAccounts; - type MinInitialDomainAccountBalance = MinInitialDomainAccountBalance; - type BundleLongevity = BundleLongevity; - type DomainBundleSubmitted = (); - type OnDomainInstantiated = (); - type MmrHash = H256; - type MmrProofVerifier = (); - type FraudProofStorageKeyProvider = (); - type OnChainRewards = (); - type WithdrawalLimit = WithdrawalLimit; - type DomainOrigin = crate::EnsureDomainOrigin; - type CurrentBundleAndExecutionReceiptVersion = CurrentBundleAndExecutionReceiptVersion; - type OperatorActivationDelayInEpochs = OperatorActivationDelayInEpochs; -} - -pub struct ExtrinsicStorageFees; - -impl domain_pallet_executive::ExtrinsicStorageFees for ExtrinsicStorageFees { - fn extract_signer(_xt: MockUncheckedExtrinsic) -> (Option, DispatchInfo) { - (None, DispatchInfo::default()) - } - - fn on_storage_fees_charged( - _charged_fees: Balance, - _tx_size: u32, - ) -> Result<(), TransactionValidityError> { - Ok(()) - } -} - -impl domain_pallet_executive::Config for Test { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); - type Currency = Balances; - type LengthToFee = IdentityFee; - type ExtrinsicStorageFees = ExtrinsicStorageFees; -} - -impl pallet_block_fees::Config for Test { - type Balance = Balance; - type DomainChainByteFee = DomainChainByteFee; -} - -pub const INITIAL_SOLUTION_RANGE: SolutionRange = - u64::MAX / (1024 * 1024 * 1024 / Piece::SIZE as u64) * 3 / 10; - -parameter_types! { - pub const BlockAuthoringDelay: SlotNumber = 2; - pub const PotEntropyInjectionInterval: BlockNumber = 5; - pub const PotEntropyInjectionLookbackDepth: u8 = 2; - pub const PotEntropyInjectionDelay: SlotNumber = 4; - pub const EraDuration: u32 = 4; - // 1GB - pub const InitialSolutionRange: SolutionRange = INITIAL_SOLUTION_RANGE; - pub const RecentSegments: HistorySize = HistorySize::new(NonZeroU64::new(5).unwrap()); - pub const RecentHistoryFraction: (HistorySize, HistorySize) = ( - HistorySize::new(NonZeroU64::new(1).unwrap()), - HistorySize::new(NonZeroU64::new(10).unwrap()), - ); - pub const MinSectorLifetime: HistorySize = HistorySize::new(NonZeroU64::new(4).unwrap()); - pub const RecordSize: u32 = 3840; - pub const ExpectedVotesPerBlock: u32 = 9; - pub const ReplicationFactor: u16 = 1; - pub const ReportLongevity: u64 = 34; - pub const ShouldAdjustSolutionRange: bool = false; - pub const BlockSlotCount: u32 = 6; -} - -impl pallet_subspace::Config for Test { - type RuntimeEvent = RuntimeEvent; - type SubspaceOrigin = pallet_subspace::EnsureSubspaceOrigin; - type BlockAuthoringDelay = BlockAuthoringDelay; - type PotEntropyInjectionInterval = PotEntropyInjectionInterval; - type PotEntropyInjectionLookbackDepth = PotEntropyInjectionLookbackDepth; - type PotEntropyInjectionDelay = PotEntropyInjectionDelay; - type EraDuration = EraDuration; - type InitialSolutionRange = InitialSolutionRange; - type SlotProbability = SlotProbability; - type ConfirmationDepthK = ConfirmationDepthK; - type RecentSegments = RecentSegments; - type RecentHistoryFraction = RecentHistoryFraction; - type MinSectorLifetime = MinSectorLifetime; - type ExpectedVotesPerBlock = ExpectedVotesPerBlock; - type MaxPiecesInSector = ConstU16<1>; - type ShouldAdjustSolutionRange = ShouldAdjustSolutionRange; - type EraChangeTrigger = NormalEraChange; - type WeightInfo = (); - type BlockSlotCount = BlockSlotCount; - type ExtensionWeightInfo = pallet_subspace::extensions::weights::SubstrateWeight; -} - -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone, Copy)] -pub enum MockBundleVersion { - V0, - V1, - V2, - V3, -} - -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone, Copy)] -pub enum MockExecutionReceiptVersion { - V0, - V1, - V2, - V3, -} - -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone, Copy)] -pub struct MockBundleAndExecutionReceiptVersion { - pub bundle_version: MockBundleVersion, - pub execution_receipt_version: MockExecutionReceiptVersion, -} - -#[frame_support::pallet] -pub(crate) mod pallet_mock_version_store { - use super::{BlockNumberFor, MockBundleAndExecutionReceiptVersion}; - use frame_support::pallet_prelude::*; - use std::collections::BTreeMap; - - #[pallet::config] - pub trait Config: frame_system::Config {} - - /// Pallet domain-id to store self domain id. - #[pallet::pallet] - #[pallet::without_storage_info] - pub struct Pallet(_); - - #[pallet::storage] - pub type MockPreviousBundleAndExecutionReceiptVersions = StorageValue< - _, - BTreeMap, MockBundleAndExecutionReceiptVersion>, - ValueQuery, - >; -} - -impl pallet_mock_version_store::Config for Test {} - pub(crate) fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::::default() .build_storage() diff --git a/scripts/find-unused-deps.sh b/scripts/find-unused-deps.sh index 7caddc2644..f1f7a53c17 100755 --- a/scripts/find-unused-deps.sh +++ b/scripts/find-unused-deps.sh @@ -20,7 +20,7 @@ fi # `--all-features --exclude-feature rocm` # # -BASE_FEATURES="async-trait,binary,cluster,default-library,domain-block-builder,domain-block-preprocessor,frame-benchmarking-cli,frame-system-benchmarking,hex-literal,kzg,numa,pallet-subspace,pallet-timestamp,pallet-utility,parallel,parking_lot,rand,runtime-benchmarks,sc-client-api,sc-executor,schnorrkel,serde,sp-blockchain,sp-core,sp-io,sp-state-machine,sp-std,sp-storage,static_assertions,std,subspace-proof-of-space-gpu,substrate-wasm-builder,testing,wasm-builder,with-tracing,x509-parser" +BASE_FEATURES="async-trait,binary,cluster,default-library,domain-block-builder,domain-block-preprocessor,frame-benchmarking-cli,frame-system-benchmarking,hex-literal,kzg,numa,pallet-subspace,pallet-timestamp,pallet-utility,parallel,parking_lot,rand,runtime-benchmarks,sc-client-api,sc-executor,schnorrkel,serde,sp-blockchain,sp-core,sp-io,sp-state-machine,sp-std,sp-storage,static_assertions,std,subspace-proof-of-space-gpu,substrate-wasm-builder,testing,wasm-builder,with-tracing,x509-parser,fuzz" if [[ "$(uname)" == "Darwin" ]]; then echo "Skipping GPU features because we're on macOS" EXTRA_FEATURES=("") diff --git a/scripts/run-fuzzer.sh b/scripts/run-fuzzer.sh new file mode 100755 index 0000000000..eaefe171af --- /dev/null +++ b/scripts/run-fuzzer.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Cross‑platform fuzzer runner with dependency checks +# Compatible with macOS and Linux + +# Environment variables: +# - FUZZ_TIME: (optional) fuzzer execution time + +# http://redsymbol.net/articles/unofficial-bash-strict-mode/ +set -euo pipefail + +#--------------------------------------- +# Detect platform +#--------------------------------------- +OS="$(uname -s)" +IS_DARWIN=false +IS_LINUX=false + +if [[ "$OS" == "Darwin" ]]; then + IS_DARWIN=true +elif [[ "$OS" == "Linux" ]]; then + IS_LINUX=true +else + echo "Unsupported OS: $OS" + exit 1 +fi + +#--------------------------------------- +# Helper: check for a command +#--------------------------------------- +check_command() { + local cmd="$1" + local install_hint="$2" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "❌ Missing required command: $cmd" + echo "➡️ Install using:" + echo " $install_hint" + exit 1 + fi +} + +#--------------------------------------- +# Verify required tools +#--------------------------------------- + +# Check if "cargo ziggy" subcommand exists +if ! cargo ziggy --help >/dev/null 2>&1; then + echo "❌ Missing required cargo subcommand: ziggy" + echo "➡️ Install using:" + echo " cargo install ziggy cargo-afl honggfuzz grcov" + exit 1 +fi + +if $IS_LINUX; then + check_command timeout "sudo apt install coreutils" +elif $IS_DARWIN; then + check_command gtimeout "brew install coreutils" +fi + +FUZZ_TIME="${FUZZ_TIME:-"10m"}" + +#--------------------------------------- +# Run fuzz test +#--------------------------------------- +cd ./test/subspace-test-fuzzer + +# remove existing afl output so that previous run is not continued +rm -rf output + +# build binary +cargo ziggy build --release --no-honggfuzz + +BINARY="./target/afl/release/subspace-test-fuzzer" + +echo "🚀 Running Ziggy fuzzing for $FUZZ_TIME..." + +# run behind a timeout since ziggy by itself one +# TODO: https://github.com/srlabs/ziggy/issues/115 +if $IS_DARWIN; then + set +e + gtimeout --preserve-status "$FUZZ_TIME" cargo ziggy fuzz -b ${BINARY} + FUZZ_EXIT_CODE=$? + set -e +else + set +e + timeout --preserve-status "$FUZZ_TIME" cargo ziggy fuzz -b ${BINARY} + FUZZ_EXIT_CODE=$? + set -e +fi + +echo "✅ Fuzzing completed with exit code: ${FUZZ_EXIT_CODE}." + +#--------------------------------------- +# Check for crashes +# We need to do this since ziggy does not exit if it encounters any crash +# Hopefully this PR should fix it: https://github.com/srlabs/ziggy/pull/113 +#--------------------------------------- +CRASH_DIR="./output/target/afl/release/subspace-test-fuzzer/crashes" + +if [[ -d "$CRASH_DIR" ]]; then + CRASH_COUNT=$(find "$CRASH_DIR" -type f | wc -l | tr -d ' ') + if [[ "$CRASH_COUNT" -gt 0 ]]; then + echo "⚠️ Found $CRASH_COUNT crashes from this fuzzing run." + echo "🧩 Replaying crashes to print stack traces..." + find "$CRASH_DIR" -type f -exec "$BINARY" {} \; + exit 1 + else + echo "✅ No crashes detected." + exit 0 + fi +else + echo "❌ Crash directory not found: $CRASH_DIR" + echo " (Fuzzer output structure may have changed or fuzzing failed)" + exit 2 +fi diff --git a/test/subspace-test-fuzzer/Cargo.toml b/test/subspace-test-fuzzer/Cargo.toml new file mode 100644 index 0000000000..1d839fc80c --- /dev/null +++ b/test/subspace-test-fuzzer/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "subspace-test-fuzzer" +version = "0.1.0" +edition.workspace = true +authors = ["Aarnav Bos "] +license = "0BSD" +homepage = "https://subspace.network" +repository = "https://github.com/autonomys/subspace" +description = "Fuzzing harness for Subspace" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +pallet-domains = { workspace = true, default-features = false, features = ["fuzz"] } +ziggy.workspace = true diff --git a/test/subspace-test-fuzzer/README.md b/test/subspace-test-fuzzer/README.md new file mode 100644 index 0000000000..db6b678078 --- /dev/null +++ b/test/subspace-test-fuzzer/README.md @@ -0,0 +1,35 @@ +## Fuzzing Harness for Subspace + +### Fuzzing pallet-domains staking + +This harness aims to encompass and encode actions performed by operators in pallet-domains to thoroughly test the +staking implementation in Autonomys. + +## Orchestrating the campaign + +For optimal results, use a grammar fuzzer such as [autarkie](https://github.com/R9295/autarkie) to consistently generate +valid inputs. + +If you cannot use Autarkie, then it is recommended to use [ziggy](https://github.com/srlabs/ziggy/). Ziggy +uses [AFL++](https://github.com/AFLplusplus/AFLplusplus/) and [honggfuzz](https://github.com/google/honggfuzz) under the +hood. +Please refer to its documentation for details. + +Command to install ziggy: + +``` +cargo install --force ziggy cargo-afl honggfuzz grcov +``` + +Quickstart command to fuzz: + +``` bash + ./scripts/run-fuzzer.sh +``` + +## MacOS specifics + +If the fuzzer exits without any executions, most likely an issue with system settings. To fix these, take a look at +`test/subspace-test-fuzzer/output/subspace-test-fuzzer/logs/afl.log` + +The Log file should have clear instructions on what to do to get the fuzzer running. diff --git a/test/subspace-test-fuzzer/src/main.rs b/test/subspace-test-fuzzer/src/main.rs new file mode 100644 index 0000000000..e1fb47d58c --- /dev/null +++ b/test/subspace-test-fuzzer/src/main.rs @@ -0,0 +1,7 @@ +use pallet_domains::fuzz::run_staking_fuzz; + +fn main() { + ziggy::fuzz!(|data: &[u8]| { + run_staking_fuzz(data); + }); +}