Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
73be128
test to demonstrate the issue
kianenigma Aug 21, 2025
9bffa3e
unfinished approach with a complicated queueing, but not tweaking tim…
kianenigma Aug 25, 2025
9705fe3
e2e test works well
kianenigma Aug 26, 2025
339f754
Merge branch 'master' of github.com:paritytech/polkadot-sdk into kiz-…
kianenigma Aug 26, 2025
32dac67
cleanup and lite self review
kianenigma Aug 26, 2025
6e7ddc6
self review + vmp spamming infra
kianenigma Aug 28, 2025
7840146
more work on vmp spamming demonstration
kianenigma Aug 28, 2025
0864c63
SendQueue -> OffenceSendQueue
kianenigma Aug 29, 2025
3f2ba86
validator set buffering
kianenigma Sep 1, 2025
38ca009
fmtc
kianenigma Sep 1, 2025
7d6659e
self review, westend fixes
kianenigma Sep 2, 2025
33b5298
updates
kianenigma Sep 4, 2025
4ce26f6
Update substrate/frame/staking-async/ah-client/src/lib.rs
kianenigma Sep 4, 2025
6289237
Update substrate/frame/staking-async/ah-client/src/lib.rs
kianenigma Sep 4, 2025
efb4cb1
reuse the same offense sending pipeline for migration buffered offences
kianenigma Sep 8, 2025
72b4184
session reprot retry
kianenigma Sep 8, 2025
bfb93b0
Merge branch 'kiz-offence-dropping' of github.com:paritytech/polkadot…
kianenigma Sep 8, 2025
16b57f5
Master.into()
kianenigma Sep 8, 2025
8a6f7c8
fmt
kianenigma Sep 8, 2025
8310c67
fix
kianenigma Sep 8, 2025
a2ace20
Update from github-actions[bot] running command 'prdoc --bump patch'
github-actions[bot] Sep 8, 2025
0af8f0d
fix prdoc
kianenigma Sep 8, 2025
16694a6
taplo
kianenigma Sep 8, 2025
3b360c6
fully remove old API, fix runtimes
kianenigma Sep 8, 2025
a47b525
call indices
kianenigma Sep 8, 2025
3feb2c6
nits
kianenigma Sep 8, 2025
0e55f64
Merge branch 'master' of github.com:paritytech/polkadot-sdk into kiz-…
kianenigma Sep 8, 2025
aa61622
fix
kianenigma Sep 8, 2025
c04bc62
fix rustdoc
kianenigma Sep 8, 2025
8a5d5b7
remove bench
kianenigma Sep 8, 2025
ad185f4
fix prdoc
kianenigma Sep 8, 2025
8c178fd
Apply suggestion from @kianenigma
kianenigma Sep 9, 2025
1b70662
deeper self review
kianenigma Sep 9, 2025
684b63c
review notes
kianenigma Sep 9, 2025
eba827e
fix
kianenigma Sep 10, 2025
1a2deab
Merge branch 'master' into kiz-offence-dropping
kianenigma Sep 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ hex-literal = { workspace = true, default-features = true }
scale-info = { features = ["derive"], workspace = true }
serde_json = { features = ["alloc"], workspace = true }
tracing = { workspace = true }
log = { workspace = true }

# Substrate
frame-benchmarking = { optional = true, workspace = true }
Expand Down Expand Up @@ -133,6 +134,7 @@ snowbridge-pallet-system-frontend = { workspace = true }
snowbridge-runtime-common = { workspace = true }

[dev-dependencies]
sp-tracing ={ workspace = true, default-features = true }
alloy-core = { workspace = true, features = ["sol-types"] }
asset-test-utils = { workspace = true, default-features = true }
pallet-revive-fixtures = { workspace = true, default-features = true }
Expand Down Expand Up @@ -301,6 +303,7 @@ std = [
"frame-system-rpc-runtime-api/std",
"frame-system/std",
"frame-try-runtime?/std",
"log/std",
"pallet-ah-ops/std",
"pallet-asset-conversion-ops/std",
"pallet-asset-conversion-tx-payment/std",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ impl pallet_staking_async_rc_client::Config for Runtime {
type RelayChainOrigin = EnsureRoot<AccountId>;
type AHStakingInterface = Staking;
type SendToRelayChain = StakingXcmToRelayChain;
type MaxValidatorSetRetries = ConstU32<64>;
}

#[derive(Encode, Decode)]
Expand Down Expand Up @@ -348,13 +349,13 @@ pub struct StakingXcmToRelayChain;

impl rc_client::SendToRelayChain for StakingXcmToRelayChain {
type AccountId = AccountId;
fn validator_set(report: rc_client::ValidatorSetReport<Self::AccountId>) {
fn validator_set(report: rc_client::ValidatorSetReport<Self::AccountId>) -> Result<(), ()> {
rc_client::XCMSender::<
xcm_config::XcmRouter,
RelayLocation,
rc_client::ValidatorSetReport<Self::AccountId>,
ValidatorSetToXcm,
>::split_then_send(report, Some(8));
>::send(report)
}
}

Expand Down Expand Up @@ -498,3 +499,42 @@ where
UncheckedExtrinsic::new_bare(call)
}
}

#[cfg(test)]
pub mod tests {
use super::*;
use frame_support::weights::constants::{WEIGHT_PROOF_SIZE_PER_KB, WEIGHT_REF_TIME_PER_MILLIS};
use pallet_staking_async::WeightInfo;

fn weight_diff(block: Weight, op: Weight) {
log::info!(
target: "runtime",
"ref_time: {:?}ms {:.4} of total",
op.ref_time() / WEIGHT_REF_TIME_PER_MILLIS,
op.ref_time() as f64 / block.ref_time() as f64
);
log::info!(
target: "runtime",
"proof_size: {:?}kb {:.4} of total",
op.proof_size() / WEIGHT_PROOF_SIZE_PER_KB,
op.proof_size() as f64 / block.proof_size() as f64
);
}

#[test]
fn westend_prune_era() {
sp_tracing::init_for_tests();
let prune_era = <Runtime as pallet_staking_async::Config>::WeightInfo::prune_era(16);
let block_weight = <Runtime as frame_system::Config>::BlockWeights::get().max_block;
weight_diff(block_weight, prune_era);
}

#[test]
fn polkadot_prune_era() {
// Polkadot and westend have very similar configs.
sp_tracing::init_for_tests();
let prune_era = <Runtime as pallet_staking_async::Config>::WeightInfo::prune_era(1000);
let block_weight = <Runtime as frame_system::Config>::BlockWeights::get().max_block;
weight_diff(block_weight, prune_era);
}
}
56 changes: 48 additions & 8 deletions polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,8 @@ enum RcClientCalls<AccountId> {
RelaySessionReport(rc_client::SessionReport<AccountId>),
#[codec(index = 1)]
RelayNewOffence(SessionIndex, Vec<rc_client::Offence<AccountId>>),
#[codec(index = 2)]
RelayNewOffenceQueued(Vec<(SessionIndex, rc_client::Offence<AccountId>)>),
}

pub struct AssetHubLocation;
Expand Down Expand Up @@ -844,23 +846,60 @@ impl sp_runtime::traits::Convert<rc_client::SessionReport<AccountId>, Xcm<()>>
}
}

pub struct QueuedOffenceToXcm;
impl sp_runtime::traits::Convert<Vec<ah_client::QueuedOffenceOf<Runtime>>, Xcm<()>>
for QueuedOffenceToXcm
{
fn convert(offences: Vec<ah_client::QueuedOffenceOf<Runtime>>) -> Xcm<()> {
Xcm(vec![
Instruction::UnpaidExecution {
weight_limit: WeightLimit::Unlimited,
check_origin: None,
},
Instruction::Transact {
origin_kind: OriginKind::Superuser,
fallback_max_weight: None,
call: AssetHubRuntimePallets::RcClient(RcClientCalls::RelayNewOffenceQueued(
offences,
))
.encode()
.into(),
},
])
}
}

pub struct StakingXcmToAssetHub;
impl ah_client::SendToAssetHub for StakingXcmToAssetHub {
type AccountId = AccountId;

fn relay_session_report(session_report: rc_client::SessionReport<Self::AccountId>) {
fn relay_session_report(
session_report: rc_client::SessionReport<Self::AccountId>,
) -> Result<(), ()> {
rc_client::XCMSender::<
xcm_config::XcmRouter,
AssetHubLocation,
rc_client::SessionReport<AccountId>,
SessionReportToXcm,
>::split_then_send(session_report, Some(8));
>::send(session_report)
}

fn relay_new_offence_paged(
offences: Vec<ah_client::QueuedOffenceOf<Runtime>>,
) -> Result<(), ()> {
rc_client::XCMSender::<
xcm_config::XcmRouter,
AssetHubLocation,
Vec<ah_client::QueuedOffenceOf<Runtime>>,
QueuedOffenceToXcm,
>::send(offences)
}

#[allow(deprecated)]
fn relay_new_offence(
session_index: SessionIndex,
offences: Vec<rc_client::Offence<Self::AccountId>>,
) {
) -> Result<(), ()> {
let message = Xcm(vec![
Instruction::UnpaidExecution {
weight_limit: WeightLimit::Unlimited,
Expand All @@ -877,9 +916,9 @@ impl ah_client::SendToAssetHub for StakingXcmToAssetHub {
.into(),
},
]);
if let Err(err) = send_xcm::<xcm_config::XcmRouter>(AssetHubLocation::get(), message) {
log::error!(target: "runtime::ah-client", "Failed to send relay offence message: {:?}", err);
}
send_xcm::<xcm_config::XcmRouter>(AssetHubLocation::get(), message)
.map_err(|_| ())
.map(|_| ())
}
}

Expand All @@ -895,6 +934,8 @@ impl ah_client::Config for Runtime {
type PointsPerBlock = ConstU32<20>;
type MaxOffenceBatchSize = ConstU32<50>;
type Fallback = Staking;
// Maximum validator set size * 4
type MaximumValidatorsWithPoints = ConstU32<{ MaxActiveValidators::get() * 4 }>;
type WeightInfo = ah_client::weights::SubstrateWeight<Runtime>;
}

Expand Down Expand Up @@ -1308,8 +1349,7 @@ impl InstanceFilter<RuntimeCall> for ProxyType {
matches!(
c,
RuntimeCall::Staking(..) |
RuntimeCall::Session(..) |
RuntimeCall::Utility(..) |
RuntimeCall::Session(..) | RuntimeCall::Utility(..) |
RuntimeCall::FastUnstake(..) |
RuntimeCall::VoterList(..) |
RuntimeCall::NominationPools(..)
Expand Down
90 changes: 86 additions & 4 deletions substrate/frame/root-offences/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,58 @@ mod mock;
mod tests;

extern crate alloc;

use alloc::vec::Vec;
use alloc::{vec, vec::Vec};
pub use pallet::*;
use pallet_session::historical::IdentificationTuple;
use sp_runtime::{traits::Convert, Perbill};
use sp_staking::offence::OnOffenceHandler;
use sp_staking::offence::{Kind, Offence, OnOffenceHandler};

#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use sp_staking::SessionIndex;
use sp_staking::{offence::ReportOffence, SessionIndex};

/// Custom offence type for testing spam scenarios.
///
/// This allows creating offences with arbitrary kinds and time slots.
#[derive(Clone, Debug, Encode, Decode, TypeInfo)]
pub struct TestSpamOffence<Offender> {
/// The validator being slashed
pub offender: Offender,
/// The session in which the offence occurred
pub session_index: SessionIndex,
/// Custom time slot (allows unique offences within same session)
pub time_slot: u128,
/// Slash fraction to apply
pub slash_fraction: Perbill,
}

impl<Offender: Clone> Offence<Offender> for TestSpamOffence<Offender> {
const ID: Kind = *b"spamspamspamspam";
type TimeSlot = u128;

fn offenders(&self) -> Vec<Offender> {
vec![self.offender.clone()]
}

fn session_index(&self) -> SessionIndex {
self.session_index
}

fn time_slot(&self) -> Self::TimeSlot {
self.time_slot
}

fn slash_fraction(&self, _offenders_count: u32) -> Perbill {
self.slash_fraction
}

fn validator_set_count(&self) -> u32 {
unreachable!()
}
}

#[pallet::config]
pub trait Config:
Expand All @@ -51,8 +90,20 @@ pub mod pallet {
{
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

/// The offence handler provided by the runtime.
///
/// This is a way to give the offence directly to the handling system (staking, ah-client).
type OffenceHandler: OnOffenceHandler<Self::AccountId, IdentificationTuple<Self>, Weight>;

/// The offence report system provided by the runtime.
///
/// This is a way to give the offence to the `pallet-offences` next.
type ReportOffence: ReportOffence<
Self::AccountId,
IdentificationTuple<Self>,
TestSpamOffence<IdentificationTuple<Self>>,
>;
}

#[pallet::pallet]
Expand Down Expand Up @@ -116,6 +167,37 @@ pub mod pallet {
Self::deposit_event(Event::OffenceCreated { offenders });
Ok(())
}

/// Same as [`Pallet::create_offence`], but it reports the offence directly to a
/// [`Config::ReportOffence`], aka pallet-offences first.
///
/// This is useful for more accurate testing of the e2e offence processing pipeline, as it
/// won't skip the `pallet-offences` step.
///
/// It generates an offence of type [`TestSpamOffence`], with cas a fixed `ID`, but can have
/// any `time_slot`, `session_index``, and `slash_fraction`. These values are the inputs of
/// transaction, int the same order, with an `IdentiticationTuple` coming first.
#[pallet::call_index(1)]
#[pallet::weight(T::DbWeight::get().reads(2))]
pub fn report_offence(
origin: OriginFor<T>,
offences: Vec<(IdentificationTuple<T>, SessionIndex, u128, u32)>,
) -> DispatchResult {
ensure_root(origin)?;

for (offender, session_index, time_slot, slash_ppm) in offences {
let slash_fraction = Perbill::from_parts(slash_ppm);
Self::deposit_event(Event::OffenceCreated {
offenders: vec![(offender.0.clone(), slash_fraction)],
});
let offence =
TestSpamOffence { offender, session_index, time_slot, slash_fraction };

T::ReportOffence::report_offence(Default::default(), offence).unwrap();
}

Ok(())
}
}

impl<T: Config> Pallet<T> {
Expand Down
1 change: 1 addition & 0 deletions substrate/frame/root-offences/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ impl pallet_timestamp::Config for Test {
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type OffenceHandler = Staking;
type ReportOffence = ();
}

pub struct ExtBuilder {
Expand Down
14 changes: 8 additions & 6 deletions substrate/frame/staking-async/ah-client/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn setup_buffered_offences<T: Config>(n: u32) -> SessionIndex {
let offences_map = create_buffered_offences::<T>(session, &offenders);

// Store the buffered offences
BufferedOffences::<T>::mutate(|buffered| {
MigrationBufferedOffences::<T>::mutate(|buffered| {
buffered.insert(session, offences_map);
});

Expand All @@ -69,25 +69,27 @@ mod benchmarks {
use super::*;

#[benchmark]
fn process_buffered_offences(n: Linear<1, { T::MaxOffenceBatchSize::get() }>) {
fn process_migration_buffered_offences(n: Linear<1, { T::MaxOffenceBatchSize::get() }>) {
// Setup: Create buffered offences and put pallet in Active mode
let session = setup_buffered_offences::<T>(n);

// Transition to Active mode to trigger processing
Mode::<T>::put(OperatingMode::Active);

// Verify offences exist before processing
assert!(BufferedOffences::<T>::get().contains_key(&session));
assert!(MigrationBufferedOffences::<T>::get().contains_key(&session));

#[block]
{
Pallet::<T>::process_buffered_offences();
Pallet::<T>::process_migration_buffered_offences();
}

// Verify some offences were processed
// In a real scenario, either the session is gone or has fewer offences
let remaining_offences =
BufferedOffences::<T>::get().get(&session).map(|m| m.len()).unwrap_or(0);
let remaining_offences = MigrationBufferedOffences::<T>::get()
.get(&session)
.map(|m| m.len())
.unwrap_or(0);
let expected_remaining = if n > T::MaxOffenceBatchSize::get() {
(n - T::MaxOffenceBatchSize::get()) as usize
} else {
Expand Down
Loading
Loading