From 96d7831883b6f65ffef52e5bb708f87a1d91f6d0 Mon Sep 17 00:00:00 2001 From: aarnav Date: Mon, 1 Sep 2025 13:49:23 +0200 Subject: [PATCH 01/10] SRLabs: introduce staking harness --- .github/workflows/rust.yml | 16 + .gitignore | 2 + Cargo.lock | 26 ++ Cargo.toml | 1 + crates/pallet-domains/Cargo.toml | 15 + crates/pallet-domains/src/fuzz_utils.rs | 212 ++++++++++ crates/pallet-domains/src/lib.rs | 18 +- crates/pallet-domains/src/staking.rs | 30 +- crates/pallet-domains/src/staking_epoch.rs | 6 +- crates/pallet-domains/src/tests.rs | 133 +++++-- fuzz/staking/Cargo.toml | 32 ++ fuzz/staking/README.md | 20 + fuzz/staking/src/main.rs | 429 +++++++++++++++++++++ scripts/build-fuzzer.sh | 3 + scripts/find-unused-deps.sh | 2 +- 15 files changed, 882 insertions(+), 63 deletions(-) create mode 100644 crates/pallet-domains/src/fuzz_utils.rs create mode 100644 fuzz/staking/Cargo.toml create mode 100644 fuzz/staking/README.md create mode 100644 fuzz/staking/src/main.rs create mode 100755 scripts/build-fuzzer.sh diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bd252f3f752..558fa5e2c4a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -372,6 +372,20 @@ jobs: run: | scripts/runtime-benchmark.sh check + staking-fuzzer-build: + name: staking-fuzzer-build (Linux x86-64) + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: install ziggy + run: cargo install --force ziggy cargo-afl honggfuzz grcov + + - name: build fuzzer + run: scripts/build-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. @@ -499,6 +513,7 @@ jobs: - check-runtime-benchmarks - cargo-check-individually - cargo-unused-deps + - staking-fuzzer-build steps: - name: Check job statuses # Another hack is to actually check the status of the dependencies or else it'll fall through @@ -511,3 +526,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-build.result }}" == "success" ]] || exit 1 diff --git a/.gitignore b/.gitignore index f419dc7baab..b813f4fa530 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.idea /target +./fuzz/staking/target +./fuzz/staking/output diff --git a/Cargo.lock b/Cargo.lock index 092e55b628d..f769233b51b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4628,6 +4628,26 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzz-staking" +version = "0.1.0" +dependencies = [ + "bincode", + "domain-runtime-primitives", + "frame-support", + "pallet-balances", + "pallet-domains", + "parity-scale-codec", + "serde", + "sp-core", + "sp-domains", + "sp-io", + "sp-runtime", + "sp-state-machine", + "subspace-runtime-primitives", + "ziggy", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -16530,6 +16550,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "ziggy" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ae470de366d6fd62f31423eb880c06c73b04bceaeedf87864891e9d32d51d9" + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index a15ae858835..56065bbb70b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "domains/test/service", "domains/test/utils", "shared/*", + "fuzz/staking", "test/subspace-test-client", "test/subspace-test-runtime", "test/subspace-test-service", diff --git a/crates/pallet-domains/Cargo.toml b/crates/pallet-domains/Cargo.toml index c850bef6c68..467f6a6e51e 100644 --- a/crates/pallet-domains/Cargo.toml +++ b/crates/pallet-domains/Cargo.toml @@ -36,6 +36,13 @@ sp-version = { workspace = true, features = ["serde"] } subspace-core-primitives.workspace = true subspace-runtime-primitives.workspace = true +# fuzz feature optional dependencies +domain-pallet-executive = {workspace = true, optional = true} +pallet-timestamp = {workspace = true, optional = true} +pallet-block-fees = {workspace = true, optional = true} +sp-externalities = {workspace = true, optional = true} +sp-keystore = {workspace = true, optional = true} + [dev-dependencies] domain-pallet-executive.workspace = true hex-literal.workspace = true @@ -85,3 +92,11 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "sp-subspace-mmr/runtime-benchmarks", ] + +fuzz = [ + "dep:domain-pallet-executive", + "dep:pallet-timestamp", + "dep:pallet-block-fees", + "dep:sp-externalities", + "dep:sp-keystore", +] diff --git a/crates/pallet-domains/src/fuzz_utils.rs b/crates/pallet-domains/src/fuzz_utils.rs new file mode 100644 index 00000000000..b38e8962d1e --- /dev/null +++ b/crates/pallet-domains/src/fuzz_utils.rs @@ -0,0 +1,212 @@ +// 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, Deposits, DomainBlockNumberFor, DomainStakingSummary, HeadDomainNumber, + InvalidBundleAuthors, Operators, PendingSlashes, ReceiptHashFor, +}; + +/// Fetch the next epoch's operators from the DomainStakingSummary +#[allow(clippy::type_complexity)] +pub fn get_next_operators( + domain_id: DomainId, +) -> Vec, T::Share, DomainBlockNumberFor, ReceiptHashFor>> { + 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 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 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 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 fn get_pending_slashes(domain_id: DomainId) -> BTreeSet { + PendingSlashes::::get(domain_id).unwrap_or_default() +} + +/// Check staking invariants before epoch finalization +pub 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"); + } + } +} + +/// Check staking invariants after epoch finalization +#[allow(clippy::type_complexity)] +pub fn check_invariants_after_finalization>( + domain_id: DomainId, + prev_ops: Vec, T::Share, DomainBlockNumberFor, ReceiptHashFor>>, +) { + 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: 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!(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 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/lib.rs b/crates/pallet-domains/src/lib.rs index 6a6a60c5386..38e942f1633 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -9,14 +9,22 @@ mod benchmarking; #[cfg(test)] mod tests; +#[cfg(all(not(test), feature = "std", feature = "fuzz"))] +pub mod tests; + pub mod block_tree; pub mod bundle_storage_fund; pub mod domain_registry; pub mod extensions; +#[cfg(feature = "fuzz")] +pub mod fuzz_utils; pub mod migrations; mod nominator_position; pub mod runtime_registry; pub mod staking; +#[cfg(feature = "fuzz")] +pub mod staking_epoch; +#[cfg(not(feature = "fuzz"))] mod staking_epoch; pub mod weights; @@ -508,7 +516,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. @@ -540,7 +548,7 @@ mod pallet { /// List of all deposits for given Operator. #[pallet::storage] - pub(super) type Deposits = StorageDoubleMap< + pub(crate) type Deposits = StorageDoubleMap< _, Identity, OperatorId, @@ -552,7 +560,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, @@ -571,7 +579,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 @@ -668,7 +676,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/staking.rs b/crates/pallet-domains/src/staking.rs index 288af63ad0d..71652afd14a 100644 --- a/crates/pallet-domains/src/staking.rs +++ b/crates/pallet-domains/src/staking.rs @@ -564,7 +564,7 @@ pub(crate) fn do_convert_previous_epoch_withdrawal( Ok(()) } -pub(crate) fn do_nominate_operator( +pub fn do_nominate_operator( operator_id: OperatorId, nominator_id: T::AccountId, amount: BalanceOf, @@ -652,7 +652,7 @@ pub(crate) fn hold_deposit( Ok(()) } -pub(crate) fn do_deregister_operator( +pub fn do_deregister_operator( operator_owner: T::AccountId, operator_id: OperatorId, ) -> Result<(), Error> { @@ -732,7 +732,7 @@ pub(crate) fn current_share_price( /// Absolute stake amount and percentage withdrawals can be handled in the frontend. /// Full stake withdrawals are handled by withdrawing everything, if the remaining number of shares /// is less than the minimum nominator stake, and the nominator is not the operator. -pub(crate) fn do_withdraw_stake( +pub fn do_withdraw_stake( operator_id: OperatorId, nominator_id: NominatorId, to_withdraw: T::Share, @@ -930,7 +930,7 @@ pub(crate) fn do_withdraw_stake( /// Unlocks any withdraws that are ready to be unlocked. /// /// Return the number of withdrawals being unlocked -pub(crate) fn do_unlock_funds( +pub fn do_unlock_funds( operator_id: OperatorId, nominator_id: NominatorId, ) -> Result { @@ -1087,9 +1087,8 @@ pub(crate) fn do_unlock_funds( Ok(withdrawal_count) }) } - /// Unlocks an already de-registered operator's nominator given unlock wait period is complete. -pub(crate) fn do_unlock_nominator( +pub fn do_unlock_nominator( operator_id: OperatorId, nominator_id: NominatorId, ) -> Result<(), Error> { @@ -1296,7 +1295,7 @@ pub(crate) fn do_cleanup_operator( } /// Distribute the reward to the operators equally and drop any dust to treasury. -pub(crate) fn do_reward_operators( +pub fn do_reward_operators( domain_id: DomainId, source: OperatorRewardSource>, operators: IntoIter, @@ -1354,10 +1353,9 @@ pub(crate) fn do_reward_operators( ) }) } - /// Freezes the slashed operators and moves the operator to be removed once the domain they are /// operating finishes the epoch. -pub(crate) fn do_mark_operators_as_slashed( +pub fn do_mark_operators_as_slashed( operator_ids: impl AsRef<[OperatorId]>, slash_reason: SlashedReason, ReceiptHashFor>, ) -> Result<(), Error> { @@ -1414,9 +1412,9 @@ pub(crate) fn do_mark_operators_as_slashed( Ok(()) } - /// Mark all the invalid bundle authors from this ER and remove them from operator set. -pub(crate) fn do_mark_invalid_bundle_authors( +/// NOTE: any changes to this must be reflected in the fuzz_utils' equivalent +pub fn do_mark_invalid_bundle_authors( domain_id: DomainId, er: &ExecutionReceiptOf, ) -> Result<(), Error> { @@ -1444,8 +1442,7 @@ pub(crate) fn do_mark_invalid_bundle_authors( InvalidBundleAuthors::::insert(domain_id, invalid_bundle_authors_in_epoch); Ok(()) } - -pub(crate) fn mark_invalid_bundle_author( +pub fn mark_invalid_bundle_author( operator_id: OperatorId, er_hash: ReceiptHashFor, stake_summary: &mut StakingSummary>, @@ -1483,11 +1480,11 @@ pub(crate) fn mark_invalid_bundle_author( Ok(()) }) } - /// 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. -pub(crate) fn do_unmark_invalid_bundle_authors( +/// NOTE: any changes to this must be reflected in the fuzz_utils' equivalent +pub fn do_unmark_invalid_bundle_authors( domain_id: DomainId, er: &ExecutionReceiptOf, ) -> Result<(), Error> { @@ -1517,8 +1514,7 @@ pub(crate) fn do_unmark_invalid_bundle_authors( InvalidBundleAuthors::::insert(domain_id, invalid_bundle_authors_in_epoch); Ok(()) } - -fn unmark_invalid_bundle_author( +pub fn unmark_invalid_bundle_author( operator_id: OperatorId, er_hash: ReceiptHashFor, stake_summary: &mut StakingSummary>, diff --git a/crates/pallet-domains/src/staking_epoch.rs b/crates/pallet-domains/src/staking_epoch.rs index b005b5eac79..6719e127ac6 100644 --- a/crates/pallet-domains/src/staking_epoch.rs +++ b/crates/pallet-domains/src/staking_epoch.rs @@ -33,7 +33,7 @@ pub enum Error { OperatorRewardStaking(TransitionError), } -pub(crate) struct EpochTransitionResult { +pub struct EpochTransitionResult { pub rewarded_operator_count: u32, pub finalized_operator_count: u32, pub completed_epoch_index: EpochIndex, @@ -41,7 +41,7 @@ pub(crate) struct EpochTransitionResult { /// Finalizes the domain's current epoch and begins the next epoch. /// Returns true of the epoch indeed was finished and the number of operator processed. -pub(crate) fn do_finalize_domain_current_epoch( +pub fn do_finalize_domain_current_epoch( domain_id: DomainId, ) -> Result { // Reset pending staking operation count to 0 @@ -427,7 +427,7 @@ pub(crate) fn mint_into_treasury(amount: BalanceOf) -> Result<(), /// Slashes any pending slashed operators. /// At max slashes the `max_nominator_count` under given operator -pub(crate) fn do_slash_operator( +pub fn do_slash_operator( domain_id: DomainId, max_nominator_count: u32, ) -> Result { diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index e427c5e7a9c..b7301b68d69 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -1,74 +1,120 @@ -use crate::block_tree::{BlockTreeNode, verify_execution_receipt}; -use crate::domain_registry::{DomainConfig, DomainConfigParams, DomainObject}; -use crate::runtime_registry::ScheduledRuntimeUpgrade; -use crate::staking_epoch::do_finalize_domain_current_epoch; +#[cfg(test)] +use crate::block_tree::verify_execution_receipt; +#[cfg(test)] +use crate::domain_registry::{DomainConfig, DomainObject}; + +#[cfg(test)] +use crate::Config; +#[cfg(test)] use crate::tests::pallet_mock_version_store::MockPreviousBundleAndExecutionReceiptVersions; +use crate::{self as pallet_domains, BlockSlot, FungibleHoldId}; +#[cfg(test)] use crate::{ - self as pallet_domains, BalanceOf, BlockSlot, BlockTree, BlockTreeNodes, BundleError, Config, - ConsensusBlockHash, DomainBlockNumberFor, DomainHashingFor, DomainRegistry, - DomainRuntimeUpgradeRecords, DomainRuntimeUpgrades, ExecutionInbox, ExecutionReceiptOf, - FraudProofError, FungibleHoldId, HeadDomainNumber, HeadReceiptNumber, NextDomainId, - OperatorConfig, RawOrigin as DomainOrigin, RuntimeRegistry, ScheduledRuntimeUpgrades, + BalanceOf, BlockTree, BlockTreeNodes, BundleError, ConsensusBlockHash, DomainBlockNumberFor, + DomainHashingFor, DomainRegistry, DomainRuntimeUpgradeRecords, DomainRuntimeUpgrades, + ExecutionInbox, ExecutionReceiptOf, FraudProofError, HeadDomainNumber, HeadReceiptNumber, + NextDomainId, OperatorConfig, OperatorId, ProofOfElection, RawOrigin as DomainOrigin, + RuntimeId, RuntimeRegistry, ScheduledRuntimeUpgrades, block_tree::BlockTreeNode, + domain_registry::DomainConfigParams, runtime_registry::ScheduledRuntimeUpgrade, + staking_epoch::do_finalize_domain_current_epoch, }; use core::mem; +use domain_runtime_primitives::BlockNumber as DomainBlockNumber; +#[cfg(test)] +use domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID; 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}; +#[cfg(test)] +use frame_support::assert_err; +use frame_support::dispatch::DispatchInfo; +#[cfg(test)] +use frame_support::dispatch::RawOrigin; +use frame_support::traits::{ConstU64, 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::{PalletId, derive_impl, parameter_types}; +#[cfg(test)] +use frame_support::{ + assert_ok, + traits::{Currency, Hooks}, +}; 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 scale_info::TypeInfo; +#[cfg(test)] use sp_consensus_slots::Slot; +#[cfg(test)] use sp_core::crypto::Pair; use sp_core::{Get, H256}; +use sp_domains::bundle::BundleVersion; +#[cfg(test)] use sp_domains::bundle::bundle_v0::{BundleHeaderV0, BundleV0, SealedBundleHeaderV0}; -use sp_domains::bundle::{BundleVersion, InboxedBundle, OpaqueBundle}; -use sp_domains::bundle_producer_election::make_transcript; +use sp_domains::execution_receipt::ExecutionReceiptVersion; +#[cfg(test)] use sp_domains::execution_receipt::execution_receipt_v0::ExecutionReceiptV0; -use sp_domains::execution_receipt::{ExecutionReceipt, ExecutionReceiptVersion, SingletonReceipt}; -use sp_domains::merkle_tree::MerkleTree; -use sp_domains::storage::RawGenesis; -use sp_domains::{ - BundleAndExecutionReceiptVersion, ChainId, DomainId, EMPTY_EXTRINSIC_ROOT, OperatorAllowList, - OperatorId, OperatorPair, OperatorSignature, ProofOfElection, RuntimeId, RuntimeType, -}; +use sp_domains::{BundleAndExecutionReceiptVersion, ChainId, DomainId}; +#[cfg(test)] +use sp_domains::{EMPTY_EXTRINSIC_ROOT, OperatorSignature}; +#[cfg(test)] +use sp_domains::{OperatorAllowList, bundle::OpaqueBundle}; +#[cfg(test)] +use sp_domains::{OperatorPair, bundle::InboxedBundle, merkle_tree::MerkleTree}; +#[cfg(test)] +use sp_domains::{RuntimeType, execution_receipt::ExecutionReceipt, storage::RawGenesis}; +#[cfg(test)] +use sp_domains::{bundle_producer_election::make_transcript, execution_receipt::SingletonReceipt}; +#[cfg(test)] use sp_domains_fraud_proof::fraud_proof::FraudProof; +#[cfg(test)] use sp_keystore::Keystore; +#[cfg(test)] use sp_keystore::testing::MemoryKeystore; +use sp_runtime::BuildStorage; +#[cfg(test)] +use sp_runtime::OpaqueExtrinsic; +#[cfg(test)] 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, -}; +#[cfg(test)] +use sp_runtime::generic::EXTRINSIC_FORMAT_VERSION; +#[cfg(test)] +use sp_runtime::generic::Preamble; +#[cfg(test)] +use sp_runtime::traits::BlakeTwo256; +#[cfg(test)] +use sp_runtime::traits::Zero; +use sp_runtime::traits::{AccountIdConversion, ConstU16, IdentityLookup}; +#[cfg(test)] +use sp_runtime::traits::{BlockNumberProvider, Bounded, Hash as HashT, One}; use sp_runtime::transaction_validity::TransactionValidityError; +#[cfg(test)] 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::SlotNumber; +#[cfg(test)] +use subspace_core_primitives::U256 as P256; use subspace_core_primitives::pieces::Piece; +#[cfg(test)] 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, + AI3, ConsensusEventSegmentSize, HoldIdentifier, Moment, StorageFee, }; +#[cfg(test)] +use subspace_runtime_primitives::{BlockHashFor, Nonce}; +#[cfg(test)] type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlockU32; -type Balance = u128; +pub type Balance = u128; // TODO: Remove when DomainRegistry is usable. -const DOMAIN_ID: DomainId = DomainId::new(0); +pub const DOMAIN_ID: DomainId = DomainId::new(0); // Operator id used for testing +#[cfg(test)] const OPERATOR_ID: OperatorId = 0u64; // Core Api version ID and default APIs @@ -96,7 +142,7 @@ frame_support::construct_runtime!( type BlockNumber = u32; type Hash = H256; -pub(crate) type AccountId = u128; +pub type AccountId = u128; #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { @@ -183,6 +229,7 @@ parameter_types! { bundle_version: BundleVersion::V0, execution_receipt_version: ExecutionReceiptVersion::V0, }; + pub const OperatorActivationDelayInEpochs: sp_domains::EpochIndex = 5; } pub struct MockRandomness; @@ -439,7 +486,7 @@ pub(crate) mod pallet_mock_version_store { impl pallet_mock_version_store::Config for Test {} -pub(crate) fn new_test_ext() -> sp_io::TestExternalities { +pub fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::::default() .build_storage() .unwrap(); @@ -447,7 +494,7 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { t.into() } -pub(crate) fn new_test_ext_with_extensions() -> sp_io::TestExternalities { +pub fn new_test_ext_with_extensions() -> sp_io::TestExternalities { let version = RuntimeVersion { spec_name: "test".into(), impl_name: Default::default(), @@ -466,6 +513,7 @@ pub(crate) fn new_test_ext_with_extensions() -> sp_io::TestExternalities { ext } +#[cfg(test)] pub(crate) fn create_dummy_receipt( block_number: BlockNumber, consensus_block_hash: Hash, @@ -506,6 +554,7 @@ pub(crate) fn create_dummy_receipt( }) } +#[cfg(test)] fn create_dummy_bundle( domain_id: DomainId, block_number: BlockNumber, @@ -525,6 +574,7 @@ fn create_dummy_bundle( ) } +#[cfg(test)] pub(crate) fn create_dummy_bundle_with_receipts( domain_id: DomainId, operator_id: OperatorId, @@ -560,6 +610,7 @@ impl sp_core::traits::ReadRuntimeVersion for ReadRuntimeVersion { } } +#[cfg(test)] pub(crate) fn run_to_block(block_number: BlockNumberFor, parent_hash: T::Hash) { // Finalize the previous block // on_finalize() does not run on the genesis block @@ -577,6 +628,7 @@ pub(crate) fn run_to_block(block_number: BlockNumberFor, parent_ha } } +#[cfg(test)] pub(crate) fn register_genesis_domain(creator: u128, operator_number: usize) -> DomainId { let raw_genesis_storage = RawGenesis::dummy(vec![1, 2, 3, 4]).encode(); assert_ok!(crate::Pallet::::set_permissioned_action_allowed_by( @@ -631,6 +683,7 @@ pub(crate) fn register_genesis_domain(creator: u128, operator_number: usize) -> } // Submit new head receipt to extend the block tree from the genesis block +#[cfg(test)] pub(crate) fn extend_block_tree_from_zero( domain_id: DomainId, operator_id: u64, @@ -643,6 +696,7 @@ pub(crate) fn extend_block_tree_from_zero( } // Submit new head receipt to extend the block tree +#[cfg(test)] pub(crate) fn extend_block_tree( domain_id: DomainId, operator_id: u64, @@ -690,6 +744,7 @@ pub(crate) fn extend_block_tree( } #[allow(clippy::type_complexity)] +#[cfg(test)] pub(crate) fn get_block_tree_node_at( domain_id: DomainId, block_number: DomainBlockNumberFor, @@ -1006,6 +1061,7 @@ fn test_basic_fraud_proof_processing() { } } +#[cfg(test)] fn schedule_domain_runtime_upgrade( runtime_id: RuntimeId, scheduled_at: BlockNumberFor, @@ -1204,6 +1260,7 @@ fn test_type_with_default_nonce_encode() { /// Code is upgraded from block_number + 1 and any new version from new runtime is considered /// from that point which is block_number + 1 /// until block_number, previous runtime's version is valid. +#[cfg(test)] fn get_mock_upgrades() -> Vec<(u32, MockBundleAndExecutionReceiptVersion, bool)> { vec![ // version from 0..100 @@ -1320,6 +1377,7 @@ fn get_mock_upgrades() -> Vec<(u32, MockBundleAndExecutionReceiptVersion, bool)> /// (block_number, current_version) /// block_number: Consensus block at which ER is derived /// current_version: Version defined at the consensus block number. +#[cfg(test)] fn get_mock_version_queries() -> Vec<(u32, MockBundleAndExecutionReceiptVersion)> { vec![ // version from 0..100 @@ -1521,7 +1579,8 @@ fn generate_fixtures_for_benchmarking() { }; let mock_genesis_er_hash = H256::from_slice( - hex!("5207cc85cfd1f53e11f4b9e85bf2d0a4f33e24d0f0f18b818b935a6aa47d3930").as_slice(), + hex_literal::hex!("5207cc85cfd1f53e11f4b9e85bf2d0a4f33e24d0f0f18b818b935a6aa47d3930") + .as_slice(), ); let trace: Vec<::DomainHash> = vec![ diff --git a/fuzz/staking/Cargo.toml b/fuzz/staking/Cargo.toml new file mode 100644 index 00000000000..f531c50dfd6 --- /dev/null +++ b/fuzz/staking/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "fuzz-staking" +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's staking pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0", features = ["derive"]} +bincode = {version = "1.0" } +pallet-domains = {path = "../../crates/pallet-domains", features = ["std", "fuzz"]} +ziggy = { version = "1.3.2", default-features = false } +sp-domains.workspace = true +parity-scale-codec = { workspace = true, features = ["derive"] } +subspace-runtime-primitives = {workspace = true } +domain-runtime-primitives.workspace = true +frame-support.workspace = true +sp-runtime.workspace = true +sp-core.workspace = true +pallet-balances.workspace = true +sp-state-machine.workspace = true +sp-io.workspace = true + +[features] +fuzzing = [] + diff --git a/fuzz/staking/README.md b/fuzz/staking/README.md new file mode 100644 index 00000000000..0c8e7d082ba --- /dev/null +++ b/fuzz/staking/README.md @@ -0,0 +1,20 @@ +## Fuzzing Harness for pallet-domains + +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 +cargo ziggy fuzz -j$(nproc) -t1 +``` + diff --git a/fuzz/staking/src/main.rs b/fuzz/staking/src/main.rs new file mode 100644 index 00000000000..7522f60d70b --- /dev/null +++ b/fuzz/staking/src/main.rs @@ -0,0 +1,429 @@ +// 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 domain_runtime_primitives::DEFAULT_EVM_CHAIN_ID; +use pallet_domains::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 pallet_domains::staking::{ + do_deregister_operator, do_mark_operators_as_slashed, do_nominate_operator, + do_register_operator, do_reward_operators, do_unlock_funds, do_unlock_nominator, + do_withdraw_stake, +}; +use pallet_domains::staking_epoch::do_slash_operator; +use pallet_domains::tests::{AccountId, Balance, BalancesConfig, DOMAIN_ID, Test}; +use pallet_domains::{Config, OperatorConfig, SlashedReason}; +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; + +const ACTIONS_PER_EPOCH: usize = 5; +const NUM_EPOCHS: usize = 5; +const MIN_NOMINATOR_STAKE: Balance = 20; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FuzzData { + /// NUM_EPOCHS epochs with N epochs skipped + pub epochs: [(u8, Epoch); NUM_EPOCHS], +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Epoch { + /// ACTIONS_PER_EPOCH actions split between N users + actions: [(u8, FuzzAction); ACTIONS_PER_EPOCH], +} + +#[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, + }, + SlashOperator, +} + +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]); + pallet_domains::tests::RuntimeGenesisConfig { + balances: BalancesConfig { + balances: accounts.iter().cloned().map(|k| (k, mint)).collect(), + }, + domains: pallet_domains::tests::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() +} + +fn main() { + 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); + ziggy::fuzz!(|data: &[u8]| { + let Ok(data) = bincode::deserialize::(data) else { + return; + }; + // Clone the genesis storage for this fuzz iteration + let mut ext = BasicExternalities::new(genesis.clone()); + 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]); + #[cfg(not(feature = "fuzzing"))] + println!( + "Registering {user:?} as Operator {operator:?} with amount {amount:?}\n-->{res:?}" + ); + } else { + #[cfg(not(feature = "fuzzing"))] + println!( + "Registering {user:?} as Operator (failed) with amount {amount:?} AI3 \n-->{res:?}" + ); + } + } + FuzzAction::NominateOperator { + operator_id, + amount, + } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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]); + } + #[cfg(not(feature = "fuzzing"))] + println!( + "Nominating as Nominator {user:?} for Operator {operator:?} with amount {amount:?}\n-->{res:?}" + ); + } + FuzzAction::DeregisterOperator { operator_id } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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); + #[cfg(not(feature = "fuzzing"))] + println!("de-registering Operator {operator:?} \n-->{res:?}"); + } + FuzzAction::WithdrawStake { + nominator_id, + operator_id, + shares, + } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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); + #[cfg(not(feature = "fuzzing"))] + println!( + "Withdrawing stake from Operator {operator:?} as Nominator {nominator:?} of shares {shares:?}\n-->{res:?}" + ); + } + FuzzAction::UnlockFunds { + operator_id, + nominator_id, + } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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); + #[cfg(not(feature = "fuzzing"))] + println!( + "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}" + ); + } + FuzzAction::UnlockNominator { + operator_id, + nominator_id, + } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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); + #[cfg(not(feature = "fuzzing"))] + println!( + "Unlocking funds as Nominator {nominator:?} from Operator {operator:?} \n-->{res:?}" + ); + } + FuzzAction::MarkOperatorsAsSlashed { + operator_id, + slash_reason, + } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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); + #[cfg(not(feature = "fuzzing"))] + println!("Marking {operator:?} as slashed\n-->{res:?}"); + do_slash_operator::(DOMAIN_ID, u32::MAX).unwrap(); + } + FuzzAction::SlashOperator => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + println!("skipping SlashOperator"); + continue; + } + let res = do_slash_operator::(DOMAIN_ID, u32::MAX); + assert!(res.is_ok()); + #[cfg(not(feature = "fuzzing"))] + { + let pending_slashes = get_pending_slashes::(DOMAIN_ID); + println!("Slashing: {pending_slashes:?} -->{res:?}"); + } + } + FuzzAction::RewardOperator { + operator_id, + amount, + } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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()); + #[cfg(not(feature = "fuzzing"))] + println!("Rewarding operator {operator:?} with {amount:?} AI3 \n-->{res:?}"); + } + FuzzAction::MarkInvalidBundleAuthors { operator_id } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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() { + #[cfg(not(feature = "fuzzing"))] + println!("skipping UnmarkInvalidBundleAuthors"); + continue; + } + if invalid_ers.is_empty() { + #[cfg(not(feature = "fuzzing"))] + 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); + } + } + 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); + #[cfg(not(feature = "fuzzing"))] + println!("skipping {skip:?} epochs"); + for _ in 0..*skip { + conclude_domain_epoch::(DOMAIN_ID); + } + } + } +} + +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, + 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/scripts/build-fuzzer.sh b/scripts/build-fuzzer.sh new file mode 100755 index 00000000000..9ab923323c6 --- /dev/null +++ b/scripts/build-fuzzer.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +sudo apt install -y protobuf-compiler binutils-dev +cd ./fuzz/staking && cargo ziggy build --no-honggfuzz diff --git a/scripts/find-unused-deps.sh b/scripts/find-unused-deps.sh index 7caddc26446..d43af6d13ab 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,fuzzing" if [[ "$(uname)" == "Darwin" ]]; then echo "Skipping GPU features because we're on macOS" EXTRA_FEATURES=("") From 361620f6220468c6bf528cb6f7cfbaf8ed4ee016 Mon Sep 17 00:00:00 2001 From: aarnav Date: Fri, 10 Oct 2025 10:22:49 +0200 Subject: [PATCH 02/10] Add deactivate_operator and reactivate_operator extrinsics to the staking fuzzing harness --- crates/pallet-domains/src/fuzz_utils.rs | 18 ++++++++- crates/pallet-domains/src/staking.rs | 4 +- fuzz/staking/src/main.rs | 49 ++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/crates/pallet-domains/src/fuzz_utils.rs b/crates/pallet-domains/src/fuzz_utils.rs index b38e8962d1e..3097fd5135f 100644 --- a/crates/pallet-domains/src/fuzz_utils.rs +++ b/crates/pallet-domains/src/fuzz_utils.rs @@ -22,8 +22,9 @@ use crate::staking::{ }; use crate::staking_epoch::do_finalize_domain_current_epoch; use crate::{ - BalanceOf, Config, Deposits, DomainBlockNumberFor, DomainStakingSummary, HeadDomainNumber, - InvalidBundleAuthors, Operators, PendingSlashes, ReceiptHashFor, + BalanceOf, Config, DeactivatedOperators, Deposits, DeregisteredOperators, DomainBlockNumberFor, + DomainStakingSummary, HeadDomainNumber, InvalidBundleAuthors, Operators, PendingSlashes, + ReceiptHashFor, }; /// Fetch the next epoch's operators from the DomainStakingSummary @@ -121,6 +122,12 @@ pub fn check_invariants_before_finalization(domain_id: DomainId) { 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) == false); + } } /// Check staking invariants after epoch finalization @@ -137,6 +144,13 @@ pub fn check_invariants_after_finalization::get(domain_id); + assert!(deactivated_operators.len() == 0); + // INVARIANT: DeregisteredOperators is empty + let deregistered_operators = DeregisteredOperators::::get(domain_id); + assert!(deregistered_operators.len() == 0); + // INVARIANT: Total domain stake == accumulated operators' curent_stake. let aggregated_stake: BalanceOf = domain_summary .current_operators diff --git a/crates/pallet-domains/src/staking.rs b/crates/pallet-domains/src/staking.rs index b8f41796f66..21993b3795b 100644 --- a/crates/pallet-domains/src/staking.rs +++ b/crates/pallet-domains/src/staking.rs @@ -739,7 +739,7 @@ pub fn do_deregister_operator( /// Operator status is marked as Deactivated with epoch_index after which they can reactivate back /// into operator set. Their stake is removed from the total domain stake since they will not be /// producing bundles anymore until re-registration. -pub(crate) fn do_deactivate_operator(operator_id: OperatorId) -> Result<(), Error> { +pub fn do_deactivate_operator(operator_id: OperatorId) -> Result<(), Error> { Operators::::try_mutate(operator_id, |maybe_operator| { let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?; @@ -794,7 +794,7 @@ pub(crate) fn do_deactivate_operator(operator_id: OperatorId) -> Resu /// Reactivate a given deactivated operator if the activation delay in epochs has passed. /// The operator is added to next operator set and will be able to produce bundles from next epoch. -pub(crate) fn do_reactivate_operator(operator_id: OperatorId) -> Result<(), Error> { +pub fn do_reactivate_operator(operator_id: OperatorId) -> Result<(), Error> { Operators::::try_mutate(operator_id, |maybe_operator| { let operator = maybe_operator.as_mut().ok_or(Error::UnknownOperator)?; let operator_status = operator.status::(operator_id); diff --git a/fuzz/staking/src/main.rs b/fuzz/staking/src/main.rs index 7522f60d70b..bc6c6b392e7 100644 --- a/fuzz/staking/src/main.rs +++ b/fuzz/staking/src/main.rs @@ -17,9 +17,9 @@ use pallet_domains::fuzz_utils::{ fuzz_unmark_invalid_bundle_authors, get_next_operators, get_pending_slashes, }; use pallet_domains::staking::{ - do_deregister_operator, do_mark_operators_as_slashed, do_nominate_operator, - do_register_operator, do_reward_operators, do_unlock_funds, do_unlock_nominator, - do_withdraw_stake, + 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 pallet_domains::staking_epoch::do_slash_operator; use pallet_domains::tests::{AccountId, Balance, BalancesConfig, DOMAIN_ID, Test}; @@ -94,6 +94,12 @@ enum FuzzAction { operator_id: u8, amount: u16, }, + DeactivateOperator { + operator_id: u8, + }, + ReactivateOperator { + operator_id: u8, + }, SlashOperator, } @@ -134,7 +140,7 @@ fn main() { let mint = (u16::MAX as u128) * 2 * AI3; let genesis = create_genesis_storage(&accounts, mint); ziggy::fuzz!(|data: &[u8]| { - let Ok(data) = bincode::deserialize::(data) else { + let Ok(data) = bincode::deserialize(&data) else { return; }; // Clone the genesis storage for this fuzz iteration @@ -159,7 +165,6 @@ fn fuzz(data: &FuzzData, accounts: Vec) { 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); @@ -398,6 +403,38 @@ fn fuzz(data: &FuzzData, accounts: Vec) { .unwrap(); fuzz_unmark_invalid_bundle_authors::(DOMAIN_ID, *operator, *er); } + FuzzAction::DeactivateOperator { operator_id } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + println!("skipping DeactivateOperator"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let res = do_deactivate_operator::(*operator); + #[cfg(not(feature = "fuzzing"))] + println!("Deactivating {operator:?} \n-->{res:?}"); + } + FuzzAction::ReactivateOperator { operator_id } => { + if operators.is_empty() { + #[cfg(not(feature = "fuzzing"))] + println!("skipping ReactivateOperator"); + continue; + } + let operator = operators + .iter() + .collect::>() + .get(*operator_id as usize % operators.len()) + .unwrap() + .1; + let res = do_reactivate_operator::(*operator); + #[cfg(not(feature = "fuzzing"))] + println!("Deactivating {operator:?} \n-->{res:?}"); + } } check_invariants_before_finalization::(DOMAIN_ID); let prev_validator_states = get_next_operators::(DOMAIN_ID); @@ -417,7 +454,7 @@ fn register_operator(operator: AccountId, amount: Balance, tax: u8) -> Option(operator, DOMAIN_ID, amount * AI3, config); From 8000bd6863a20fb51ea2e5a56a58b94f712d78ca Mon Sep 17 00:00:00 2001 From: aarnav Date: Fri, 10 Oct 2025 10:26:49 +0200 Subject: [PATCH 03/10] add documentation for the staking fuzzing harness --- fuzz/staking/src/main.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fuzz/staking/src/main.rs b/fuzz/staking/src/main.rs index bc6c6b392e7..c24eb04bff8 100644 --- a/fuzz/staking/src/main.rs +++ b/fuzz/staking/src/main.rs @@ -37,8 +37,11 @@ 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)] @@ -53,6 +56,9 @@ pub struct Epoch { 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 { @@ -103,6 +109,8 @@ enum FuzzAction { 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]); @@ -450,6 +458,7 @@ fn fuzz(data: &FuzzData, accounts: Vec) { } } +/// 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 { From 4d5d7f5b4240d8bc6df695759ea3d2c3e6ba478d Mon Sep 17 00:00:00 2001 From: aarnav Date: Fri, 10 Oct 2025 10:34:13 +0200 Subject: [PATCH 04/10] run the fuzzer in the CI to make sure it actually work --- .github/workflows/rust.yml | 2 +- scripts/{build-fuzzer.sh => run-fuzzer-ci.sh} | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename scripts/{build-fuzzer.sh => run-fuzzer-ci.sh} (81%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 558fa5e2c4a..f122abc82ac 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -384,7 +384,7 @@ jobs: run: cargo install --force ziggy cargo-afl honggfuzz grcov - name: build fuzzer - run: scripts/build-fuzzer.sh + run: scripts/run-fuzzer-ci.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 diff --git a/scripts/build-fuzzer.sh b/scripts/run-fuzzer-ci.sh similarity index 81% rename from scripts/build-fuzzer.sh rename to scripts/run-fuzzer-ci.sh index 9ab923323c6..a82c139ccc4 100755 --- a/scripts/build-fuzzer.sh +++ b/scripts/run-fuzzer-ci.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash sudo apt install -y protobuf-compiler binutils-dev cd ./fuzz/staking && cargo ziggy build --no-honggfuzz +timeout 1m cargo ziggy fuzz From 1b0a25cb5be08dadaf184135ff050753f87a1147 Mon Sep 17 00:00:00 2001 From: aarnav Date: Fri, 10 Oct 2025 10:51:34 +0200 Subject: [PATCH 05/10] fix clippy issues in fuzz_utils --- .github/workflows/rust.yml | 8 ++++---- crates/pallet-domains/src/fuzz_utils.rs | 6 +++--- fuzz/staking/src/main.rs | 2 +- scripts/run-fuzzer-ci.sh | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f122abc82ac..f38d6c53b21 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -372,8 +372,8 @@ jobs: run: | scripts/runtime-benchmark.sh check - staking-fuzzer-build: - name: staking-fuzzer-build (Linux x86-64) + staking-fuzzer-test: + name: staking-fuzzer-test (Linux x86-64) runs-on: ubuntu-22.04 steps: @@ -383,7 +383,7 @@ jobs: - name: install ziggy run: cargo install --force ziggy cargo-afl honggfuzz grcov - - name: build fuzzer + - name: test fuzzer run: scripts/run-fuzzer-ci.sh # This job checks all crates individually, including no_std and other featureless builds. @@ -513,7 +513,7 @@ jobs: - check-runtime-benchmarks - cargo-check-individually - cargo-unused-deps - - staking-fuzzer-build + - 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 diff --git a/crates/pallet-domains/src/fuzz_utils.rs b/crates/pallet-domains/src/fuzz_utils.rs index 3097fd5135f..a17bc26276c 100644 --- a/crates/pallet-domains/src/fuzz_utils.rs +++ b/crates/pallet-domains/src/fuzz_utils.rs @@ -126,7 +126,7 @@ pub fn check_invariants_before_finalization(domain_id: DomainId) { 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) == false); + assert!(!deactivated_operators.contains(operator_id)); } } @@ -146,10 +146,10 @@ pub fn check_invariants_after_finalization::get(domain_id); - assert!(deactivated_operators.len() == 0); + assert!(deactivated_operators.is_empty()); // INVARIANT: DeregisteredOperators is empty let deregistered_operators = DeregisteredOperators::::get(domain_id); - assert!(deregistered_operators.len() == 0); + assert!(deregistered_operators.is_empty()); // INVARIANT: Total domain stake == accumulated operators' curent_stake. let aggregated_stake: BalanceOf = domain_summary diff --git a/fuzz/staking/src/main.rs b/fuzz/staking/src/main.rs index c24eb04bff8..a0900fcbc6c 100644 --- a/fuzz/staking/src/main.rs +++ b/fuzz/staking/src/main.rs @@ -148,7 +148,7 @@ fn main() { let mint = (u16::MAX as u128) * 2 * AI3; let genesis = create_genesis_storage(&accounts, mint); ziggy::fuzz!(|data: &[u8]| { - let Ok(data) = bincode::deserialize(&data) else { + let Ok(data) = bincode::deserialize(data) else { return; }; // Clone the genesis storage for this fuzz iteration diff --git a/scripts/run-fuzzer-ci.sh b/scripts/run-fuzzer-ci.sh index a82c139ccc4..de741ce7f9c 100755 --- a/scripts/run-fuzzer-ci.sh +++ b/scripts/run-fuzzer-ci.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash sudo apt install -y protobuf-compiler binutils-dev cd ./fuzz/staking && cargo ziggy build --no-honggfuzz -timeout 1m cargo ziggy fuzz +AFL_SKIP_CPUFREQ=1 timeout 1m cargo ziggy fuzz From 8e62bf9fbbcc654b64728e52bd076dd54eaa9753 Mon Sep 17 00:00:00 2001 From: aarnav Date: Fri, 10 Oct 2025 12:57:15 +0200 Subject: [PATCH 06/10] fix fuzzer CI script to use AFL_SKIP_CPUFREQ and set pipefail --- .github/workflows/rust.yml | 3 ++- scripts/run-fuzzer-ci.sh | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f38d6c53b21..9b88e7cd0f7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -375,7 +375,8 @@ jobs: staking-fuzzer-test: name: staking-fuzzer-test (Linux x86-64) runs-on: ubuntu-22.04 - + env: + AFL_SKIP_CPUFREQ: 1 steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/scripts/run-fuzzer-ci.sh b/scripts/run-fuzzer-ci.sh index de741ce7f9c..e49d50e7597 100755 --- a/scripts/run-fuzzer-ci.sh +++ b/scripts/run-fuzzer-ci.sh @@ -1,4 +1,9 @@ #!/usr/bin/env bash + +# http://redsymbol.net/articles/unofficial-bash-strict-mode/ +set -euo pipefail + sudo apt install -y protobuf-compiler binutils-dev + cd ./fuzz/staking && cargo ziggy build --no-honggfuzz -AFL_SKIP_CPUFREQ=1 timeout 1m cargo ziggy fuzz +timeout 5m cargo ziggy fuzz From f494fad0e71a92f3974058dd49aa0680559d0ebd Mon Sep 17 00:00:00 2001 From: teor Date: Fri, 10 Oct 2025 13:57:19 +0200 Subject: [PATCH 07/10] Update fuzz settings so it actually runs in CI --- .github/workflows/rust.yml | 5 ++++- scripts/run-fuzzer-ci.sh | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9b88e7cd0f7..b7b7a153079 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -374,9 +374,12 @@ jobs: staking-fuzzer-test: name: staking-fuzzer-test (Linux x86-64) - runs-on: ubuntu-22.04 + # Fuzzing is most efficient on Linux, it doesn't matter if it fails on other OSes. + runs-on: ${{ fromJson(github.repository_owner == 'autonomys' && + '"runs-on=${{ github.run_id }}-${{ github.run_attempt }}/runner=self-hosted-ubuntu-22.04-x86-64/extras=s3-cache${{ env.SPOT }}"' || '"ubuntu-22.04"') }} env: AFL_SKIP_CPUFREQ: 1 + AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: 1 steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/scripts/run-fuzzer-ci.sh b/scripts/run-fuzzer-ci.sh index e49d50e7597..018b867203c 100755 --- a/scripts/run-fuzzer-ci.sh +++ b/scripts/run-fuzzer-ci.sh @@ -6,4 +6,4 @@ set -euo pipefail sudo apt install -y protobuf-compiler binutils-dev cd ./fuzz/staking && cargo ziggy build --no-honggfuzz -timeout 5m cargo ziggy fuzz +cargo ziggy fuzz --timeout 600 --release From a890b1059eca44be7d04b38d5c204897f0659baf Mon Sep 17 00:00:00 2001 From: teor Date: Fri, 10 Oct 2025 14:08:30 +0200 Subject: [PATCH 08/10] Revert runs-on server fuzzer change due to missing packages --- .github/workflows/rust.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b7b7a153079..5591a4658ad 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -375,8 +375,10 @@ jobs: 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. - runs-on: ${{ fromJson(github.repository_owner == 'autonomys' && - '"runs-on=${{ github.run_id }}-${{ github.run_attempt }}/runner=self-hosted-ubuntu-22.04-x86-64/extras=s3-cache${{ env.SPOT }}"' || '"ubuntu-22.04"') }} + # 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 From dd50feb857a1fe0f2369e3ebf969a5741464a031 Mon Sep 17 00:00:00 2001 From: teor Date: Fri, 10 Oct 2025 14:38:21 +0200 Subject: [PATCH 09/10] Apply suggestions --- .github/workflows/rust.yml | 2 +- scripts/run-fuzzer-ci.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5591a4658ad..2190cbcc33d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -532,4 +532,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-build.result }}" == "success" ]] || exit 1 + [[ "${{ needs.staking-fuzzer-test.result }}" == "success" ]] || exit 1 diff --git a/scripts/run-fuzzer-ci.sh b/scripts/run-fuzzer-ci.sh index 018b867203c..a073591c9a2 100755 --- a/scripts/run-fuzzer-ci.sh +++ b/scripts/run-fuzzer-ci.sh @@ -6,4 +6,5 @@ set -euo pipefail sudo apt install -y protobuf-compiler binutils-dev cd ./fuzz/staking && cargo ziggy build --no-honggfuzz -cargo ziggy fuzz --timeout 600 --release +# cargo ziggy fuzz doesn't allow us to set a number of runs or a run time limit +timeout 5m cargo ziggy fuzz --release From d0a0dc3981b4ad3d5bcee56323dde99d0614d827 Mon Sep 17 00:00:00 2001 From: teor Date: Tue, 14 Oct 2025 12:08:39 +0200 Subject: [PATCH 10/10] Preserve fuzzer exit status in scripts/run-fuzzer-ci.sh --- scripts/run-fuzzer-ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run-fuzzer-ci.sh b/scripts/run-fuzzer-ci.sh index a073591c9a2..aa39fa203d2 100755 --- a/scripts/run-fuzzer-ci.sh +++ b/scripts/run-fuzzer-ci.sh @@ -7,4 +7,4 @@ sudo apt install -y protobuf-compiler binutils-dev cd ./fuzz/staking && cargo ziggy build --no-honggfuzz # cargo ziggy fuzz doesn't allow us to set a number of runs or a run time limit -timeout 5m cargo ziggy fuzz --release +timeout --preserve-status 5m cargo ziggy fuzz --release