feat: retry ring on slash failure (#479)

## Summary
This PR aligns the `external-validator-slashes` retry flow with the same
general approach used by the rewards retry ring
(https://github.com/datahaven-xyz/datahaven/pull/462).

The main goal is to make slash retries preserve their original era
identity, avoid head-of-line blocking by rotating failed work to the
back of the queue, and add a governance escape hatch for manually
retrying queued slash batches.

## Changes

- Reworked `external-validator-slashes` to use a bounded ring-buffer
queue for unsent slash batches instead of retrying from an unbounded raw
slash queue.
- Stored the original slash era together with each queued batch so
retries keep the same outbound message identity across later eras.
- Added a governance-only retry extrinsic for queued slash eras,
mirroring the rewards retrial ring approach.
- Added new retry-related errors and events for queued slash retries and
queue-full handling.
- Updated retry-path logging so expected send failures are treated as
warnings instead of noisy errors during passing tests.
- Extended pallet tests to cover:
  - preserved original era on retry
  - back-of-queue rotation on failed sends
  - governance retry success and failure cases
  - bounded queue capacity behavior
  - multi-era queue progression
- Updated benchmarking and pallet weights for the new retry path.
- Wired the new governance origin and weight changes into mainnet,
testnet, and stagenet runtime configs.
This commit is contained in:
Gonza Montiel 2026-03-30 11:37:24 +02:00 committed by GitHub
parent a6ce1aef99
commit 9fe972c95d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 593 additions and 105 deletions

View file

@ -35,20 +35,24 @@ const MAX_SLASHES: u32 = 1000;
mod benchmarks {
use super::*;
fn dummy_slash<T: Config>(slash_id: T::SlashId) -> Slash<T::AccountId, T::SlashId> {
let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
Slash {
validator: dummy(),
reporters: vec![],
slash_id,
percentage: Perbill::from_percent(1),
confirmed: false,
offence_kind: OffenceKind::LivenessOffence,
}
}
#[benchmark]
fn cancel_deferred_slash(s: Linear<1, MAX_SLASHES>) -> Result<(), BenchmarkError> {
let mut existing_slashes = Vec::new();
let era = T::EraIndexProvider::active_era().index;
let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
for _ in 0..MAX_SLASHES {
existing_slashes.push(Slash {
validator: dummy(),
reporters: vec![],
slash_id: One::one(),
percentage: Perbill::from_percent(1),
confirmed: false,
offence_kind: OffenceKind::LivenessOffence,
});
existing_slashes.push(dummy_slash::<T>(One::one()));
}
Slashes::<T>::insert(
era.saturating_add(T::SlashDeferDuration::get())
@ -102,35 +106,55 @@ mod benchmarks {
#[benchmark]
fn process_slashes_queue(s: Linear<1, 200>) -> Result<(), BenchmarkError> {
let mut queue = VecDeque::new();
let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
let first_batch = (0..s)
.map(|_| dummy_slash::<T>(One::one()))
.collect::<Vec<_>>();
let second_batch = vec![dummy_slash::<T>(One::one())];
for _ in 0..(s + 1) {
queue.push_back(Slash {
validator: dummy(),
reporters: vec![],
slash_id: One::one(),
percentage: Perbill::from_percent(1),
confirmed: false,
offence_kind: OffenceKind::LivenessOffence,
});
}
UnreportedSlashesQueue::<T>::set(queue);
assert!(ExternalValidatorSlashes::<T>::unsent_queue_push((
1,
first_batch
)));
assert!(ExternalValidatorSlashes::<T>::unsent_queue_push((
2,
second_batch
)));
let processed;
#[block]
{
processed = Pallet::<T>::process_slashes_queue(s).unwrap();
processed = match Pallet::<T>::process_slashes_queue() {
crate::ProcessSlashesQueueOutcome::Sent(count) => count,
crate::ProcessSlashesQueueOutcome::Empty
| crate::ProcessSlashesQueueOutcome::Requeued(_) => {
return Err(BenchmarkError::Stop("unexpected slashes queue outcome"))
}
};
}
assert_eq!(UnreportedSlashesQueue::<T>::get().len(), 1);
assert_eq!(ExternalValidatorSlashes::<T>::unsent_queue_len(), 1);
assert_eq!(processed, s);
Ok(())
}
#[benchmark]
fn retry_unsent_slash_era() -> Result<(), BenchmarkError> {
let batch = vec![dummy_slash::<T>(One::one())];
assert!(ExternalValidatorSlashes::<T>::unsent_queue_push((1, batch)));
let origin =
T::GovernanceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, 1u32);
assert!(ExternalValidatorSlashes::<T>::unsent_queue_is_empty());
Ok(())
}
#[benchmark]
fn set_slashing_mode() -> Result<(), BenchmarkError> {
#[extrinsic_call]

View file

@ -31,7 +31,7 @@ extern crate alloc;
use pallet_external_validators::apply;
use snowbridge_outbound_queue_primitives::SendError;
use {
alloc::{collections::vec_deque::VecDeque, string::String, vec, vec::Vec},
alloc::{string::String, vec, vec::Vec},
frame_support::{pallet_prelude::*, traits::DefensiveSaturating},
frame_system::pallet_prelude::*,
log::log,
@ -132,10 +132,21 @@ pub mod pallet {
},
/// The slashes message was sent correctly.
SlashesMessageSent { message_id: H256 },
/// The slashes message failed to send and the batch was moved to the back
/// of the queue for retry.
SlashesMessageSendFailed { era: EraIndex, count: u32 },
/// A queued slashes batch was retried manually and sent successfully.
SlashesMessageRetried {
message_id: H256,
era: EraIndex,
count: u32,
},
/// We injected a slash
SlashInjected { slash_id: T::SlashId, era: u32 },
/// Number of slashes processed
SlashAddedToQueue { number: u32, era: u32 },
/// The unsent queue is full; this slash era could not be enqueued.
UnsentQueueFull { era: EraIndex },
}
#[pallet::config]
@ -199,6 +210,9 @@ pub mod pallet {
/// The weight information of this pallet.
type WeightInfo: WeightInfo;
/// Origin for governance calls such as retrying an unsent slash batch.
type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
}
#[pallet::error]
@ -226,6 +240,10 @@ pub mod pallet {
/// No PendingOffenceKind found for (session, validator) — offence was not
/// reported through EquivocationReportWrapper, so the offence kind is unknown.
MissingOffenceKind,
/// The specified era is not in the unsent slash queue.
EraNotInUnsentQueue,
/// The message delivery still failed on retry.
MessageSendFailed,
}
#[apply(derive_storage_traits)]
@ -269,12 +287,26 @@ pub mod pallet {
pub type Slashes<T: Config> =
StorageMap<_, Twox64Concat, EraIndex, Vec<Slash<T::AccountId, T::SlashId>>, ValueQuery>;
/// All unreported slashes that will be processed in the future.
/// Maximum number of unsent slash batches in the retry ring buffer.
pub const UNSENT_QUEUE_CAPACITY: u32 = 64;
/// Ring buffer of slash batches whose outbound message still needs to be sent.
/// Each slot stores the original slash era together with a bounded-size batch
/// of slash records. Retries keep the original era so the outbound message id
/// remains stable across later blocks and eras.
#[pallet::storage]
#[pallet::unbounded]
#[pallet::getter(fn unreported_slashes)]
pub type UnreportedSlashesQueue<T: Config> =
StorageValue<_, VecDeque<Slash<T::AccountId, T::SlashId>>, ValueQuery>;
pub type UnsentSlashBatch<T: Config> =
StorageMap<_, Twox64Concat, u32, (EraIndex, Vec<Slash<T::AccountId, T::SlashId>>)>;
/// Ring buffer head: next slot to be processed by `on_initialize`.
#[pallet::storage]
pub type UnsentSlashHead<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Ring buffer tail: next slot to write a new entry into.
/// When head == tail the buffer is empty.
#[pallet::storage]
pub type UnsentSlashTail<T: Config> = StorageValue<_, u32, ValueQuery>;
// Turns slashing on or off
#[pallet::storage]
@ -415,6 +447,44 @@ pub mod pallet {
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::retry_unsent_slash_era())]
pub fn retry_unsent_slash_era(origin: OriginFor<T>, era_index: EraIndex) -> DispatchResult {
T::GovernanceOrigin::ensure_origin(origin)?;
let head = UnsentSlashHead::<T>::get();
let tail = UnsentSlashTail::<T>::get();
let mut found = None;
let mut slot = head;
while slot != tail {
if let Some(entry @ (idx, _)) = UnsentSlashBatch::<T>::get(slot) {
if idx == era_index {
found = Some((slot, entry));
break;
}
}
slot = (slot + 1) % UNSENT_QUEUE_CAPACITY;
}
let (slot, (era, slashes)) = found.ok_or(Error::<T>::EraNotInUnsentQueue)?;
let count = slashes.len() as u32;
let slashes_to_send = slashes
.iter()
.map(Self::slash_to_send_data)
.collect::<Vec<_>>();
let message_id = Self::send_slashes_message(&slashes_to_send, era)
.ok_or(Error::<T>::MessageSendFailed)?;
Self::unsent_queue_remove_slot(slot);
Self::deposit_event(Event::<T>::SlashesMessageRetried {
message_id,
era,
count,
});
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::set_slashing_mode())]
pub fn set_slashing_mode(origin: OriginFor<T>, mode: SlashingModeOption) -> DispatchResult {
@ -429,12 +499,12 @@ pub mod pallet {
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
let processed = Self::process_slashes_queue(T::QueuedSlashesProcessedPerBlock::get());
if let Some(p) = processed {
T::WeightInfo::process_slashes_queue(p)
} else {
T::WeightInfo::process_slashes_queue(0)
match Self::process_slashes_queue() {
ProcessSlashesQueueOutcome::Empty => T::WeightInfo::process_slashes_queue(0),
ProcessSlashesQueueOutcome::Sent(count)
| ProcessSlashesQueueOutcome::Requeued(count) => {
T::WeightInfo::process_slashes_queue(count)
}
}
}
}
@ -655,70 +725,65 @@ where
impl<T: Config> Pallet<T> {
fn add_era_slashes_to_queue(active_era: EraIndex) {
let mut slashes: VecDeque<_> = Slashes::<T>::get(active_era).into();
let slashes = Slashes::<T>::get(active_era);
if slashes.is_empty() {
return;
}
let len = slashes.len();
let batch_size = T::QueuedSlashesProcessedPerBlock::get().max(1) as usize;
let mut enqueued = 0u32;
UnreportedSlashesQueue::<T>::mutate(|queue| queue.append(&mut slashes));
for batch in slashes.chunks(batch_size) {
if Self::unsent_queue_push((active_era, batch.to_vec())) {
enqueued = enqueued.saturating_add(batch.len() as u32);
} else {
log::warn!(
target: "ext_validators_slashes",
"Unsent slash queue full, cannot enqueue era {active_era}",
);
Self::deposit_event(Event::<T>::UnsentQueueFull { era: active_era });
break;
}
}
if len > 0 {
if enqueued > 0 {
Self::deposit_event(Event::<T>::SlashAddedToQueue {
number: len as u32,
number: enqueued,
era: active_era,
});
}
}
/// Returns number of slashes that were sent to ethereum.
fn process_slashes_queue(amount: u32) -> Option<u32> {
let mut slashes_to_send: Vec<SlashData<T::AccountId>> = vec![];
let era_index = T::EraIndexProvider::active_era().index;
fn slash_to_send_data(slash: &Slash<T::AccountId, T::SlashId>) -> SlashData<T::AccountId> {
// Keep the original slash batch intact until delivery succeeds so failed
// batches can be moved to the back of the queue instead of being dropped.
let max_wad = T::MaxSlashWad::get();
let wad_to_slash = (slash.percentage.deconstruct() as u128)
.saturating_mul(max_wad)
.checked_div(1_000_000_000u128)
.unwrap_or(0)
.min(max_wad);
UnreportedSlashesQueue::<T>::mutate(|queue| {
for _ in 0..amount {
let Some(slash) = queue.pop_front() else {
// no more slashes to process in the queue
break;
};
// Convert Perbill to EigenLayer WAD format with linear mapping.
// Perbill(100%) → MaxSlashWad (e.g. 5% WAD = 5e16).
// Formula: perbill_inner * MaxSlashWad / 1e9
// Clamp to MaxSlashWad to guard against overflow if governance
// sets MaxSlashWad high enough for saturating_mul to hit u128::MAX.
let max_wad = T::MaxSlashWad::get();
let wad_to_slash = (slash.percentage.deconstruct() as u128)
.saturating_mul(max_wad)
.checked_div(1_000_000_000u128)
.unwrap_or(0)
.min(max_wad);
slashes_to_send.push(SlashData {
validator: slash.validator,
wad_to_slash,
description: slash.offence_kind.to_description(),
});
}
});
if slashes_to_send.is_empty() {
return None;
SlashData {
validator: slash.validator.clone(),
wad_to_slash,
description: slash.offence_kind.to_description(),
}
}
let slashes_count = slashes_to_send.len() as u32;
fn send_slashes_message(
slashes_to_send: &[SlashData<T::AccountId>],
era_index: EraIndex,
) -> Option<H256> {
let outbound =
T::SendMessage::build(&slashes_to_send.to_vec(), era_index).or_else(|| {
log::warn!(target: "ext_validators_slashes", "Failed to build outbound message");
None
})?;
let outbound = match T::SendMessage::build(&slashes_to_send, era_index) {
Some(send_msg) => send_msg,
None => {
log::error!(target: "ext_validators_slashes", "Failed to build outbound message");
return None;
}
};
// Validate and deliver the message
let ticket = T::SendMessage::validate(outbound)
.map_err(|e| {
log::error!(
log::warn!(
target: "ext_validators_slashes",
"Failed to validate outbound message: {:?}",
e
@ -726,20 +791,126 @@ impl<T: Config> Pallet<T> {
})
.ok()?;
let message_id = T::SendMessage::deliver(ticket)
T::SendMessage::deliver(ticket)
.map_err(|e| {
log::error!(
log::warn!(
target: "ext_validators_slashes",
"Failed to deliver outbound message: {:?}",
e
);
})
.ok()?;
Self::deposit_event(Event::<T>::SlashesMessageSent { message_id });
Some(slashes_count)
.ok()
}
#[allow(dead_code)]
pub(crate) fn unsent_queue_is_empty() -> bool {
UnsentSlashHead::<T>::get() == UnsentSlashTail::<T>::get()
}
#[allow(dead_code)]
pub(crate) fn unsent_queue_len() -> u32 {
let head = UnsentSlashHead::<T>::get();
let tail = UnsentSlashTail::<T>::get();
tail.wrapping_sub(head) % UNSENT_QUEUE_CAPACITY
}
pub(crate) fn unsent_queue_push(
entry: (EraIndex, Vec<Slash<T::AccountId, T::SlashId>>),
) -> bool {
let head = UnsentSlashHead::<T>::get();
let tail = UnsentSlashTail::<T>::get();
let next_tail = (tail + 1) % UNSENT_QUEUE_CAPACITY;
if next_tail == head {
return false;
}
UnsentSlashBatch::<T>::insert(tail, entry);
UnsentSlashTail::<T>::put(next_tail);
true
}
fn unsent_queue_remove_slot(slot: u32) {
let tail = UnsentSlashTail::<T>::get();
let mut cur = slot;
loop {
let next = (cur + 1) % UNSENT_QUEUE_CAPACITY;
if next == tail {
break;
}
if let Some(entry) = UnsentSlashBatch::<T>::get(next) {
UnsentSlashBatch::<T>::insert(cur, entry);
}
cur = next;
}
UnsentSlashBatch::<T>::remove(cur);
let new_tail = if tail == 0 {
UNSENT_QUEUE_CAPACITY - 1
} else {
tail - 1
};
UnsentSlashTail::<T>::put(new_tail);
let head = UnsentSlashHead::<T>::get();
if head == tail {
UnsentSlashHead::<T>::put(new_tail);
}
}
/// Retry contract shared with rewards:
/// - process the current head batch,
/// - if send succeeds, remove it from the queue,
/// - if send fails, move the same batch to the back so later slash batches can progress.
pub(crate) fn process_slashes_queue() -> ProcessSlashesQueueOutcome {
let head = UnsentSlashHead::<T>::get();
let tail = UnsentSlashTail::<T>::get();
if head == tail {
return ProcessSlashesQueueOutcome::Empty;
}
let Some((era_index, slashes)) = UnsentSlashBatch::<T>::get(head) else {
UnsentSlashHead::<T>::put((head + 1) % UNSENT_QUEUE_CAPACITY);
return ProcessSlashesQueueOutcome::Empty;
};
let slashes_count = slashes.len() as u32;
let slashes_to_send = slashes
.iter()
.map(Self::slash_to_send_data)
.collect::<Vec<_>>();
match Self::send_slashes_message(&slashes_to_send, era_index) {
Some(message_id) => {
UnsentSlashBatch::<T>::remove(head);
UnsentSlashHead::<T>::put((head + 1) % UNSENT_QUEUE_CAPACITY);
Self::deposit_event(Event::<T>::SlashesMessageSent { message_id });
ProcessSlashesQueueOutcome::Sent(slashes_count)
}
None => {
UnsentSlashBatch::<T>::remove(head);
UnsentSlashHead::<T>::put((head + 1) % UNSENT_QUEUE_CAPACITY);
UnsentSlashBatch::<T>::insert(tail, (era_index, slashes));
UnsentSlashTail::<T>::put((tail + 1) % UNSENT_QUEUE_CAPACITY);
log::warn!(
target: "ext_validators_slashes",
"Failed to send {slashes_count} slash entries for era {era_index}, moved batch to back of queue",
);
Self::deposit_event(Event::<T>::SlashesMessageSendFailed {
era: era_index,
count: slashes_count,
});
ProcessSlashesQueueOutcome::Requeued(slashes_count)
}
}
}
}
pub(crate) enum ProcessSlashesQueueOutcome {
Empty,
Sent(u32),
Requeued(u32),
}
/// A pending slash record. The value of the slash has been computed but not applied yet,

View file

@ -134,7 +134,9 @@ thread_local! {
pub static SENT_ETHEREUM_MESSAGE_NONCE: RefCell<u64> = const { RefCell::new(0) };
pub static MOCK_REPORT_OFFENCE_SHOULD_FAIL: RefCell<bool> = const { RefCell::new(false) };
pub static MOCK_REPORT_OFFENCE_CALLED: RefCell<bool> = const { RefCell::new(false) };
pub static MOCK_SEND_MESSAGE_SHOULD_FAIL: RefCell<bool> = const { RefCell::new(false) };
pub static LAST_SENT_SLASHES: RefCell<Vec<crate::SlashData<AccountId>>> = RefCell::new(Vec::new());
pub static LAST_BUILT_ERA: RefCell<Option<EraIndex>> = const { RefCell::new(None) };
}
impl MockEraIndexProvider {
@ -222,19 +224,32 @@ impl MockOkOutboundQueue {
pub fn last_sent_slashes() -> Vec<crate::SlashData<AccountId>> {
LAST_SENT_SLASHES.with(|r| r.borrow().clone())
}
pub fn last_built_era() -> Option<EraIndex> {
LAST_BUILT_ERA.with(|r| *r.borrow())
}
pub fn set_should_fail(fail: bool) {
MOCK_SEND_MESSAGE_SHOULD_FAIL.with(|r| *r.borrow_mut() = fail);
}
}
impl crate::SendMessage<AccountId> for MockOkOutboundQueue {
type Ticket = ();
type Message = ();
fn build(slashes: &Vec<crate::SlashData<AccountId>>, _: u32) -> Option<Self::Ticket> {
fn build(slashes: &Vec<crate::SlashData<AccountId>>, era: u32) -> Option<Self::Ticket> {
LAST_SENT_SLASHES.with(|r| *r.borrow_mut() = slashes.clone());
LAST_BUILT_ERA.with(|r| *r.borrow_mut() = Some(era));
Some(())
}
fn validate(_: Self::Ticket) -> Result<Self::Ticket, SendError> {
Ok(())
}
fn deliver(_: Self::Ticket) -> Result<H256, SendError> {
Ok(H256::zero())
if MOCK_SEND_MESSAGE_SHOULD_FAIL.with(|r| *r.borrow()) {
Err(SendError::MessageTooLarge)
} else {
Ok(H256::zero())
}
}
}
@ -271,6 +286,7 @@ impl external_validator_slashes::Config for Test {
type QueuedSlashesProcessedPerBlock = ConstU32<20>;
type WeightInfo = ();
type SendMessage = MockOkOutboundQueue;
type GovernanceOrigin = frame_system::EnsureRoot<u64>;
}
pub struct FullIdentificationOf;
@ -286,6 +302,9 @@ impl pallet_session::historical::Config for Test {
}
// Build genesis storage according to the mock runtime.
pub fn new_test_ext() -> sp_io::TestExternalities {
MOCK_SEND_MESSAGE_SHOULD_FAIL.with(|r| *r.borrow_mut() = false);
LAST_SENT_SLASHES.with(|r| r.borrow_mut().clear());
LAST_BUILT_ERA.with(|r| *r.borrow_mut() = None);
system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap()

View file

@ -28,6 +28,40 @@ use {
sp_staking::offence::ReportOffence,
};
fn queued_slash_ids() -> Vec<u32> {
let mut queued = Vec::new();
let mut slot = UnsentSlashHead::<Test>::get();
let tail = UnsentSlashTail::<Test>::get();
while slot != tail {
if let Some((_, batch)) = UnsentSlashBatch::<Test>::get(slot) {
queued.extend(batch.into_iter().map(|slash| slash.slash_id));
}
slot = (slot + 1) % UNSENT_QUEUE_CAPACITY;
}
queued
}
fn queued_batch_eras() -> Vec<u32> {
let mut queued = Vec::new();
let mut slot = UnsentSlashHead::<Test>::get();
let tail = UnsentSlashTail::<Test>::get();
while slot != tail {
if let Some((era, _)) = UnsentSlashBatch::<Test>::get(slot) {
queued.push(era);
}
slot = (slot + 1) % UNSENT_QUEUE_CAPACITY;
}
queued
}
fn unsent_queue_len() -> u32 {
ExternalValidatorSlashes::unsent_queue_len()
}
#[test]
fn root_can_inject_manual_offence() {
new_test_ext().execute_with(|| {
@ -574,14 +608,228 @@ fn test_on_offence_defer_period_0_messages_get_queued() {
assert_eq!(Slashes::<Test>::get(get_slashing_era(1)).len(), 25);
start_era(2, 2, 2);
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 25);
assert_eq!(unsent_queue_len(), 2);
assert_eq!(queued_batch_eras(), vec![2, 2]);
// this triggers on_initialize
run_block();
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 5);
assert_eq!(unsent_queue_len(), 1);
assert_eq!(queued_slash_ids(), (20..25).collect::<Vec<_>>());
run_block();
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 0);
assert!(ExternalValidatorSlashes::unsent_queue_is_empty());
});
}
#[test]
fn failed_slashes_batch_is_moved_to_back_of_queue() {
new_test_ext().execute_with(|| {
crate::mock::DeferPeriodGetter::with_defer_period(0);
MockOkOutboundQueue::set_should_fail(true);
start_era(0, 0, 0);
start_era(1, 1, 1);
for i in 0..25 {
PendingOffenceKind::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3 + i, ()),
reporters: vec![],
}],
&[Perbill::from_percent(75)],
0,
);
}
start_era(2, 2, 2);
assert_eq!(queued_slash_ids(), (0..25).collect::<Vec<_>>());
assert_eq!(queued_batch_eras(), vec![2, 2]);
run_block();
assert_eq!(unsent_queue_len(), 2);
assert_eq!(
queued_slash_ids(),
(20..25).chain(0..20).collect::<Vec<_>>()
);
System::assert_has_event(RuntimeEvent::ExternalValidatorSlashes(
crate::Event::SlashesMessageSendFailed { era: 2, count: 20 },
));
});
}
#[test]
fn failed_slashes_batch_retries_after_send_is_reenabled() {
new_test_ext().execute_with(|| {
crate::mock::DeferPeriodGetter::with_defer_period(0);
MockOkOutboundQueue::set_should_fail(true);
start_era(0, 0, 0);
start_era(1, 1, 1);
for i in 0..25 {
PendingOffenceKind::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3 + i, ()),
reporters: vec![],
}],
&[Perbill::from_percent(75)],
0,
);
}
start_era(2, 2, 2);
run_block();
assert_eq!(
queued_slash_ids(),
(20..25).chain(0..20).collect::<Vec<_>>()
);
start_era(3, 3, 3);
MockOkOutboundQueue::set_should_fail(false);
run_block();
assert_eq!(unsent_queue_len(), 1);
assert_eq!(queued_slash_ids(), (0..20).collect::<Vec<_>>());
assert_eq!(MockOkOutboundQueue::last_sent_slashes().len(), 5);
assert_eq!(MockOkOutboundQueue::last_built_era(), Some(2));
System::assert_has_event(RuntimeEvent::ExternalValidatorSlashes(
crate::Event::SlashesMessageSent {
message_id: Default::default(),
},
));
run_block();
assert!(ExternalValidatorSlashes::unsent_queue_is_empty());
});
}
#[test]
fn retry_extrinsic_succeeds_for_matching_era() {
new_test_ext().execute_with(|| {
crate::mock::DeferPeriodGetter::with_defer_period(0);
start_era(0, 0, 0);
start_era(1, 1, 1);
for i in 0..25 {
PendingOffenceKind::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3 + i, ()),
reporters: vec![],
}],
&[Perbill::from_percent(75)],
0,
);
}
start_era(2, 2, 2);
start_era(5, 5, 5);
assert_ok!(ExternalValidatorSlashes::retry_unsent_slash_era(
RuntimeOrigin::root(),
2,
));
assert_eq!(unsent_queue_len(), 1);
assert_eq!(queued_slash_ids(), (20..25).collect::<Vec<_>>());
assert_eq!(MockOkOutboundQueue::last_built_era(), Some(2));
});
}
#[test]
fn retry_extrinsic_errors_when_era_not_queued() {
new_test_ext().execute_with(|| {
assert_noop!(
ExternalValidatorSlashes::retry_unsent_slash_era(RuntimeOrigin::root(), 2),
Error::<Test>::EraNotInUnsentQueue
);
});
}
#[test]
fn retry_extrinsic_requires_root() {
new_test_ext().execute_with(|| {
assert_noop!(
ExternalValidatorSlashes::retry_unsent_slash_era(RuntimeOrigin::signed(1), 2),
sp_runtime::DispatchError::BadOrigin
);
});
}
#[test]
fn retry_extrinsic_preserves_failed_batch_when_send_still_fails() {
new_test_ext().execute_with(|| {
crate::mock::DeferPeriodGetter::with_defer_period(0);
MockOkOutboundQueue::set_should_fail(true);
start_era(0, 0, 0);
start_era(1, 1, 1);
for i in 0..25 {
PendingOffenceKind::<Test>::insert(0, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
&[OffenceDetails {
offender: (3 + i, ()),
reporters: vec![],
}],
&[Perbill::from_percent(75)],
0,
);
}
start_era(2, 2, 2);
let before = queued_slash_ids();
assert_noop!(
ExternalValidatorSlashes::retry_unsent_slash_era(RuntimeOrigin::root(), 2),
Error::<Test>::MessageSendFailed
);
assert_eq!(queued_slash_ids(), before);
assert_eq!(unsent_queue_len(), 2);
});
}
#[test]
fn unsent_queue_full_emits_event() {
new_test_ext().execute_with(|| {
crate::mock::DeferPeriodGetter::with_defer_period(0);
for i in 0..63u32 {
let slash = Slash {
validator: 1000 + i as u64,
reporters: vec![],
slash_id: i,
percentage: Perbill::from_percent(1),
confirmed: true,
offence_kind: OffenceKind::LivenessOffence,
};
assert!(ExternalValidatorSlashes::unsent_queue_push((
1,
vec![slash]
)));
}
Slashes::<Test>::insert(
2,
vec![Slash {
validator: 5000u64,
reporters: vec![],
slash_id: 999,
percentage: Perbill::from_percent(10),
confirmed: true,
offence_kind: OffenceKind::LivenessOffence,
}],
);
start_era(2, 2, 2);
assert_eq!(unsent_queue_len(), 63);
assert_eq!(Slashes::<Test>::get(2).len(), 1);
});
}
@ -628,14 +876,13 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() {
}
assert_eq!(Slashes::<Test>::get(get_slashing_era(1)).len(), 25);
start_era(2, 2, 2);
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 25);
assert_eq!(unsent_queue_len(), 2);
// this triggers on_initialize
run_block();
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 5);
assert_eq!(unsent_queue_len(), 1);
assert_eq!(queued_slash_ids(), (20..25).collect::<Vec<_>>());
// We have 5 non-dispatched, which should accumulate
// We shoulld have 30 after we initialie era 3
for i in 0..25 {
PendingOffenceKind::<Test>::insert(2, 3 + i, OffenceKind::LivenessOffence);
Pallet::<Test>::on_offence(
@ -651,15 +898,20 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() {
}
start_era(3, 3, 3);
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 30);
assert_eq!(unsent_queue_len(), 3);
assert_eq!(queued_batch_eras(), vec![2, 3, 3]);
// this triggers on_initialize
run_block();
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 10);
assert_eq!(unsent_queue_len(), 2);
assert_eq!(queued_batch_eras(), vec![3, 3]);
// this triggers on_initialize
run_block();
assert_eq!(UnreportedSlashesQueue::<Test>::get().len(), 0);
assert_eq!(unsent_queue_len(), 1);
run_block();
assert!(ExternalValidatorSlashes::unsent_queue_is_empty());
});
}

View file

@ -57,6 +57,7 @@ pub trait WeightInfo {
fn force_inject_slash() -> Weight;
fn root_test_send_msg_to_eth() -> Weight;
fn process_slashes_queue(s: u32, ) -> Weight;
fn retry_unsent_slash_era() -> Weight;
fn set_slashing_mode() -> Weight;
}
@ -136,6 +137,11 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
.saturating_add(Weight::from_parts(0, 42).saturating_mul(s.into()))
}
fn retry_unsent_slash_era() -> Weight {
// Same as the success path for one queued batch.
Self::process_slashes_queue(10)
}
fn set_slashing_mode() -> Weight {
Weight::from_parts(7_402_000, 3601)
.saturating_add(T::DbWeight::get().reads(1_u64))
@ -221,6 +227,10 @@ impl WeightInfo for () {
.saturating_add(Weight::from_parts(0, 42).saturating_mul(s.into()))
}
fn retry_unsent_slash_era() -> Weight {
Self::process_slashes_queue(10)
}
fn set_slashing_mode() -> Weight {
Weight::from_parts(7_402_000, 3601)
.saturating_add(RocksDbWeight::get().reads(1_u64))

View file

@ -1735,6 +1735,7 @@ impl pallet_external_validator_slashes::Config for Runtime {
type QueuedSlashesProcessedPerBlock = ConstU32<10>;
type WeightInfo = mainnet_weights::pallet_external_validator_slashes::WeightInfo<Runtime>;
type SendMessage = SlashesSendAdapter;
type GovernanceOrigin = EnsureRootWithSuccess<AccountId, RootLocation>;
}
parameter_types! {

View file

@ -113,6 +113,9 @@ impl<T: frame_system::Config> pallet_external_validator_slashes::WeightInfo for
.saturating_add(T::DbWeight::get().writes(4_u64))
.saturating_add(Weight::from_parts(0, 38).saturating_mul(s.into()))
}
fn retry_unsent_slash_era() -> Weight {
Self::process_slashes_queue(10)
}
/// Storage: `ExternalValidatorsSlashes::SlashingMode` (r:0 w:1)
/// Proof: `ExternalValidatorsSlashes::SlashingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
fn set_slashing_mode() -> Weight {

View file

@ -1731,6 +1731,7 @@ impl pallet_external_validator_slashes::Config for Runtime {
type QueuedSlashesProcessedPerBlock = ConstU32<10>;
type WeightInfo = stagenet_weights::pallet_external_validator_slashes::WeightInfo<Runtime>;
type SendMessage = SlashesSendAdapter;
type GovernanceOrigin = EnsureRootWithSuccess<AccountId, RootLocation>;
}
parameter_types! {

View file

@ -113,6 +113,9 @@ impl<T: frame_system::Config> pallet_external_validator_slashes::WeightInfo for
.saturating_add(T::DbWeight::get().writes(4_u64))
.saturating_add(Weight::from_parts(0, 38).saturating_mul(s.into()))
}
fn retry_unsent_slash_era() -> Weight {
Self::process_slashes_queue(10)
}
/// Storage: `ExternalValidatorsSlashes::SlashingMode` (r:0 w:1)
/// Proof: `ExternalValidatorsSlashes::SlashingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
fn set_slashing_mode() -> Weight {

View file

@ -1733,6 +1733,7 @@ impl pallet_external_validator_slashes::Config for Runtime {
type QueuedSlashesProcessedPerBlock = ConstU32<10>;
type WeightInfo = testnet_weights::pallet_external_validator_slashes::WeightInfo<Runtime>;
type SendMessage = SlashesSendAdapter;
type GovernanceOrigin = EnsureRootWithSuccess<AccountId, RootLocation>;
}
parameter_types! {

View file

@ -113,6 +113,9 @@ impl<T: frame_system::Config> pallet_external_validator_slashes::WeightInfo for
.saturating_add(T::DbWeight::get().writes(4_u64))
.saturating_add(Weight::from_parts(0, 38).saturating_mul(s.into()))
}
fn retry_unsent_slash_era() -> Weight {
Self::process_slashes_queue(10)
}
/// Storage: `ExternalValidatorsSlashes::SlashingMode` (r:0 w:1)
/// Proof: `ExternalValidatorsSlashes::SlashingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
fn set_slashing_mode() -> Weight {

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.17592112782245438099",
"version": "0.1.0-autogenerated.14952921519994301429",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.