From f74d1cbe02e065a8b7235a54df4e345f981c9381 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:51:24 +0100 Subject: [PATCH 01/17] feat: add retry mechanism for failed rewards messages via Snowbridge When `send_rewards_message` fails at era end (bridge paused, queue full, etc.), tokens have already been minted but the message to EigenLayer is lost. This adds automatic retry and a governance escape hatch. Storage: ring buffer (StorageMap + head/tail pointers, capacity 64) tracks failed eras with their timestamp and scaled inflation amount. Retry logic (on_initialize): processes one entry per block. On success the entry is removed. On failure the entry is moved to the back of the queue so a single stuck era does not block retries for subsequent ones. Expired entries (reward points pruned) are discarded automatically. Governance extrinsic: `retry_unsent_reward_era` gated by configurable `GovernanceOrigin` allows manual retry of a specific era. Cleanup: `on_era_start` proactively removes all unsent entries whose reward points have been pruned (idx <= era_index_to_delete). Co-Authored-By: Claude Opus 4.6 --- .../src/benchmarking.rs | 107 +++- .../external-validators-rewards/src/lib.rs | 314 +++++++++++- .../external-validators-rewards/src/mock.rs | 6 + .../external-validators-rewards/src/tests.rs | 471 +++++++++++++++++- .../src/weights.rs | 60 +++ operator/runtime/mainnet/src/configs/mod.rs | 1 + .../pallet_external_validators_rewards.rs | 25 + operator/runtime/stagenet/src/configs/mod.rs | 1 + .../pallet_external_validators_rewards.rs | 25 + operator/runtime/testnet/src/configs/mod.rs | 1 + .../pallet_external_validators_rewards.rs | 25 + 11 files changed, 1010 insertions(+), 26 deletions(-) diff --git a/operator/pallets/external-validators-rewards/src/benchmarking.rs b/operator/pallets/external-validators-rewards/src/benchmarking.rs index 335557f48..d0de5e26d 100644 --- a/operator/pallets/external-validators-rewards/src/benchmarking.rs +++ b/operator/pallets/external-validators-rewards/src/benchmarking.rs @@ -21,7 +21,7 @@ use super::*; #[allow(unused)] use crate::Pallet as ExternalValidatorsRewards; use { - crate::{types::BenchmarkHelper, OnEraEnd}, + crate::types::BenchmarkHelper, frame_benchmarking::{account, v2::*, BenchmarkError}, frame_support::traits::Currency, sp_std::prelude::*, @@ -43,6 +43,11 @@ fn create_funded_user( user } +/// Helper: insert a single entry into the ring buffer at slot 0. +fn push_unsent_entry(era_index: u32, timestamp: u32, inflation: u128) { + ExternalValidatorsRewards::::unsent_queue_push((era_index, timestamp, inflation)); +} + #[allow(clippy::multiple_bound_locations)] #[benchmarks(where T: pallet_balances::Config)] mod benchmarks { @@ -72,6 +77,106 @@ mod benchmarks { Ok(()) } + /// Helper to populate reward points for an era with 1000 validators. + fn setup_era_reward_points(era_index: u32) { + let mut era_reward_points = EraRewardPoints::default(); + era_reward_points.total = 20 * 1000; + + for i in 0..1000 { + let account_id = create_funded_user::("candidate", i, 100); + era_reward_points.individual.insert(account_id, 20); + } + + >::insert(era_index, era_reward_points); + } + + // on_initialize: unsent queue is empty (2 reads for head+tail) + #[benchmark] + fn process_unsent_reward_eras_empty() -> Result<(), BenchmarkError> { + // Ensure queue is empty (default state: head == tail == 0) + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + Ok(()) + } + + // on_initialize: oldest entry has pruned reward points + #[benchmark] + fn process_unsent_reward_eras_expired() -> Result<(), BenchmarkError> { + // Push an entry whose reward points do NOT exist in storage + push_unsent_entry::(999, 0, 42); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + // Entry should have been removed + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + Ok(()) + } + + // on_initialize: oldest entry retried successfully + #[benchmark] + fn process_unsent_reward_eras_success() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + T::BenchmarkHelper::setup(); + setup_era_reward_points::(1); + + push_unsent_entry::(1, 0, 42); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + Ok(()) + } + + // Use success weight as upper bound for the failed path + #[benchmark] + fn process_unsent_reward_eras_failed() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + T::BenchmarkHelper::setup(); + setup_era_reward_points::(1); + + push_unsent_entry::(1, 0, 42); + + #[block] + { + ExternalValidatorsRewards::::process_unsent_reward_eras(); + } + + Ok(()) + } + + // Governance extrinsic: retry a specific unsent era + #[benchmark] + fn retry_unsent_reward_era() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + T::BenchmarkHelper::setup(); + setup_era_reward_points::(1); + + push_unsent_entry::(1, 0, 42); + + let origin = + T::GovernanceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, 1u32); + + assert!(ExternalValidatorsRewards::::unsent_queue_is_empty()); + + Ok(()) + } + impl_benchmark_test_suite!( ExternalValidatorsRewards, crate::mock::new_test_ext(), diff --git a/operator/pallets/external-validators-rewards/src/lib.rs b/operator/pallets/external-validators-rewards/src/lib.rs index d346c67b9..bba0425e8 100644 --- a/operator/pallets/external-validators-rewards/src/lib.rs +++ b/operator/pallets/external-validators-rewards/src/lib.rs @@ -66,13 +66,13 @@ pub mod pallet { pub use crate::weights::WeightInfo; use { - super::*, frame_support::pallet_prelude::*, + super::*, frame_support::pallet_prelude::*, frame_system::pallet_prelude::OriginFor, pallet_external_validators::traits::EraIndexProvider, sp_runtime::Saturating, sp_std::collections::btree_map::BTreeMap, }; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); pub type RewardPoints = u32; pub type EraIndex = u32; @@ -168,6 +168,9 @@ pub mod pallet { /// Hook for minting inflation tokens. type HandleInflation: HandleInflation; + /// Origin for governance calls (e.g., retrying unsent reward messages). + type GovernanceOrigin: EnsureOrigin; + #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: types::BenchmarkHelper; } @@ -175,6 +178,62 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_n: frame_system::pallet_prelude::BlockNumberFor) -> Weight { + Self::process_unsent_reward_eras() + } + } + + #[pallet::call] + impl Pallet { + /// Governance escape hatch: manually retry sending a rewards message for + /// an era that is stuck in the unsent queue. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::retry_unsent_reward_era())] + pub fn retry_unsent_reward_era( + origin: OriginFor, + era_index: EraIndex, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + // Scan the ring buffer for the requested era + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + let mut found = None; + let mut slot = head; + while slot != tail { + if let Some(entry @ (idx, _, _)) = UnsentRewardEra::::get(slot) { + if idx == era_index { + found = Some((slot, entry)); + break; + } + } + slot = (slot + 1) % UNSENT_QUEUE_CAPACITY; + } + let (slot, (_, timestamp, inflation)) = found.ok_or(Error::::EraNotInUnsentQueue)?; + + let reward_points = RewardPointsForEra::::get(era_index); + let info = reward_points + .generate_era_rewards_info(era_index, inflation, timestamp) + .ok_or(Error::::RewardPointsPruned)?; + + let message_id = + Self::send_rewards_message(&info).ok_or(Error::::MessageSendFailed)?; + + Self::unsent_queue_remove_slot(slot); + + Self::deposit_event(Event::RewardsMessageRetried { + message_id, + era_index, + total_points: info.total_points, + inflation_amount: inflation, + }); + + Ok(()) + } + } + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -185,6 +244,29 @@ pub mod pallet { total_points: u128, inflation_amount: u128, }, + /// The rewards message failed to send; era queued for retry. + RewardsMessageSendFailed { era_index: EraIndex }, + /// A previously failed rewards message was retried and sent successfully. + RewardsMessageRetried { + message_id: H256, + era_index: EraIndex, + total_points: u128, + inflation_amount: u128, + }, + /// An unsent era was dropped because its reward points have been pruned. + UnsentEraExpired { era_index: EraIndex }, + /// The unsent queue is full; this era could not be enqueued for retry. + UnsentQueueFull { era_index: EraIndex }, + } + + #[pallet::error] + pub enum Error { + /// The specified era is not in the unsent queue. + EraNotInUnsentQueue, + /// Reward points for the era have been pruned from storage. + RewardPointsPruned, + /// The message delivery still failed on retry. + MessageSendFailed, } /// Keep tracks of distributed points per validator and total. @@ -200,7 +282,7 @@ pub mod pallet { /// - individual_points: (address, points) tuples for each validator. /// - inflation_amount: total inflation tokens to distribute. /// - era_start_timestamp: timestamp when the era started (seconds since Unix epoch). - pub fn generate_era_rewards_utils( + pub fn generate_era_rewards_info( &self, era_index: EraIndex, inflation_amount: u128, @@ -260,6 +342,33 @@ pub mod pallet { pub type BlocksProducedInEra = StorageMap<_, Twox64Concat, EraIndex, u32, ValueQuery>; + /// Maximum number of unsent reward entries in the ring buffer. + pub const UNSENT_QUEUE_CAPACITY: u32 = 64; + + /// Ring buffer of eras whose rewards messages failed to send. + /// Each slot stores (era_index, era_start_timestamp, scaled_inflation). + /// Keyed by slot index [0, UNSENT_QUEUE_CAPACITY). + #[pallet::storage] + pub type UnsentRewardEra = StorageMap< + _, + Twox64Concat, + u32, + ( + EraIndex, + /* era_start_timestamp */ u32, + /* scaled_inflation */ u128, + ), + >; + + /// Ring buffer head: next slot to be processed by `on_initialize`. + #[pallet::storage] + pub type UnsentRewardHead = 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 UnsentRewardTail = StorageValue<_, u32, ValueQuery>; + impl Pallet { /// Reward validators. Does not check if the validators are valid, caller needs to make sure of that. pub fn reward_by_ids(points: impl IntoIterator) { @@ -276,8 +385,8 @@ pub mod pallet { /// Helper to build, validate and deliver an outbound message. /// Logs any error and returns None on failure. - fn send_rewards_message(utils: &EraRewardsUtils) -> Option { - let outbound = T::SendMessage::build(utils).or_else(|| { + fn send_rewards_message(info: &EraRewardsUtils) -> Option { + let outbound = T::SendMessage::build(info).or_else(|| { log::error!(target: "ext_validators_rewards", "Failed to build outbound message"); None })?; @@ -303,6 +412,147 @@ pub mod pallet { .ok() } + // ── Ring-buffer helpers ────────────────────────────────────────── + + /// Returns true when the ring buffer is empty (head == tail). + #[allow(dead_code)] + pub(crate) fn unsent_queue_is_empty() -> bool { + UnsentRewardHead::::get() == UnsentRewardTail::::get() + } + + /// Number of entries currently in the ring buffer. + #[allow(dead_code)] + pub(crate) fn unsent_queue_len() -> u32 { + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + tail.wrapping_sub(head) % UNSENT_QUEUE_CAPACITY + } + + /// Push a new entry into the ring buffer. + /// Returns `true` on success, `false` if the buffer is full. + pub(crate) fn unsent_queue_push(entry: (EraIndex, u32, u128)) -> bool { + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + let next_tail = (tail + 1) % UNSENT_QUEUE_CAPACITY; + if next_tail == head { + // Buffer full + return false; + } + UnsentRewardEra::::insert(tail, entry); + UnsentRewardTail::::put(next_tail); + true + } + + /// Remove the entry at a given slot and compact the buffer by shifting + /// subsequent entries back. Used by the extrinsic and `on_era_start`. + fn unsent_queue_remove_slot(slot: u32) { + let tail = UnsentRewardTail::::get(); + // Shift entries after `slot` backward to fill the gap + let mut cur = slot; + loop { + let next = (cur + 1) % UNSENT_QUEUE_CAPACITY; + if next == tail { + break; + } + // Move next → cur + if let Some(entry) = UnsentRewardEra::::get(next) { + UnsentRewardEra::::insert(cur, entry); + } + cur = next; + } + // Remove the now-duplicate last entry and shrink tail + UnsentRewardEra::::remove(cur); + let new_tail = if tail == 0 { + UNSENT_QUEUE_CAPACITY - 1 + } else { + tail - 1 + }; + UnsentRewardTail::::put(new_tail); + + // If head was after the removed slot, adjust it too + let head = UnsentRewardHead::::get(); + // We also need to handle head potentially pointing past the buffer + // after a removal. Since we shifted everything between slot..tail back, + // the head only needs adjustment if it was == tail (now new_tail) — but + // that means the buffer just became empty, which is fine (head == new_tail). + // However, if head was pointing *at* a slot beyond the removed one, the + // entry it pointed to slid back by one, so head should also slide back. + // In practice, removal only happens when we know the slot, so we can + // simply recalculate emptiness. + if head == tail { + // Was already at tail, buffer must be empty now + UnsentRewardHead::::put(new_tail); + } + } + + // ── Core retry logic ────────────────────────────────────────────── + + /// Process at most one unsent reward era per block. + /// On failure the head pointer advances to the next entry so a single + /// stuck era does not block retries for subsequent eras. + pub(crate) fn process_unsent_reward_eras() -> Weight { + let head = UnsentRewardHead::::get(); + let tail = UnsentRewardTail::::get(); + + if head == tail { + return T::WeightInfo::process_unsent_reward_eras_empty(); + } + + let Some((era_index, timestamp, inflation)) = UnsentRewardEra::::get(head) else { + // Slot unexpectedly empty — advance head past it + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + return T::WeightInfo::process_unsent_reward_eras_empty(); + }; + + // Check if reward points are still available + let reward_points = RewardPointsForEra::::get(era_index); + let info = + match reward_points.generate_era_rewards_info(era_index, inflation, timestamp) { + Some(info) => info, + None => { + // Reward points have been pruned — discard this entry + log::warn!( + target: "ext_validators_rewards", + "Unsent era {era_index} expired: reward points pruned", + ); + UnsentRewardEra::::remove(head); + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + Self::deposit_event(Event::UnsentEraExpired { era_index }); + return T::WeightInfo::process_unsent_reward_eras_expired(); + } + }; + + // Attempt to resend + match Self::send_rewards_message(&info) { + Some(message_id) => { + UnsentRewardEra::::remove(head); + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + Self::deposit_event(Event::RewardsMessageRetried { + message_id, + era_index, + total_points: info.total_points, + inflation_amount: inflation, + }); + T::WeightInfo::process_unsent_reward_eras_success() + } + None => { + // Move the failed entry to the back of the queue so the + // next block tries a different era (avoids head-of-line + // blocking). The entry is not lost — it will be retried + // after all other pending entries. + UnsentRewardEra::::remove(head); + UnsentRewardHead::::put((head + 1) % UNSENT_QUEUE_CAPACITY); + UnsentRewardEra::::insert(tail, (era_index, timestamp, inflation)); + UnsentRewardTail::::put((tail + 1) % UNSENT_QUEUE_CAPACITY); + log::warn!( + target: "ext_validators_rewards", + "Retry for unsent era {era_index} still failing, moved to back of queue", + ); + T::WeightInfo::process_unsent_reward_eras_failed() + } + } + } + /// Track a block authored by a validator pub fn note_block_author(author: T::AccountId) { // Track per-session authorship for performance points @@ -619,6 +869,24 @@ pub mod pallet { RewardPointsForEra::::remove(era_index_to_delete); BlocksProducedInEra::::remove(era_index_to_delete); + + // Proactively clean up any unsent entries whose reward points + // have been pruned (this era and any older ones still lingering). + let head = UnsentRewardHead::::get(); + let mut tail = UnsentRewardTail::::get(); + let mut slot = head; + while slot != tail { + if let Some((idx, _, _)) = UnsentRewardEra::::get(slot) { + if idx <= era_index_to_delete { + Self::unsent_queue_remove_slot(slot); + tail = UnsentRewardTail::::get(); + Self::deposit_event(Event::UnsentEraExpired { era_index: idx }); + // Don't advance slot — next entry slid into this position + continue; + } + } + slot = (slot + 1) % UNSENT_QUEUE_CAPACITY; + } } } @@ -636,19 +904,19 @@ pub mod pallet { .map(|ms| (ms / 1000) as u32) .unwrap_or(0); - // Generate era rewards utils with the scaled inflation amount. + // Generate era rewards info with the scaled inflation amount. // This ensures the message to EigenLayer matches the actual minted amount. - let utils = match RewardPointsForEra::::get(&era_index).generate_era_rewards_utils( + let info = match RewardPointsForEra::::get(&era_index).generate_era_rewards_info( era_index, scaled_inflation, era_start_timestamp, ) { - Some(utils) => utils, + Some(info) => info, None => { // Returns None when total_points is zero or no validators have rewards log::error!( target: "ext_validators_rewards", - "Failed to generate era rewards utils (no rewards to distribute)" + "Failed to generate era rewards info (no rewards to distribute)" ); return; } @@ -670,13 +938,27 @@ pub mod pallet { DispatchClass::Mandatory, ); - if let Some(message_id) = Self::send_rewards_message(&utils) { - Self::deposit_event(Event::RewardsMessageSent { - message_id, - era_index, - total_points: utils.total_points, - inflation_amount: scaled_inflation, - }); + match Self::send_rewards_message(&info) { + Some(message_id) => { + Self::deposit_event(Event::RewardsMessageSent { + message_id, + era_index, + total_points: info.total_points, + inflation_amount: scaled_inflation, + }); + } + None => { + // Message failed — queue for automatic retry via on_initialize + if Self::unsent_queue_push((era_index, era_start_timestamp, scaled_inflation)) { + Self::deposit_event(Event::RewardsMessageSendFailed { era_index }); + } else { + log::error!( + target: "ext_validators_rewards", + "Unsent reward queue full, cannot enqueue era {era_index}", + ); + Self::deposit_event(Event::UnsentQueueFull { era_index }); + } + } } } } diff --git a/operator/pallets/external-validators-rewards/src/mock.rs b/operator/pallets/external-validators-rewards/src/mock.rs index 4d8547519..558398151 100644 --- a/operator/pallets/external-validators-rewards/src/mock.rs +++ b/operator/pallets/external-validators-rewards/src/mock.rs @@ -131,6 +131,9 @@ impl crate::types::SendMessage for MockOkOutboundQueue { } fn validate(ticket: Self::Ticket) -> Result { + if Mock::mock().send_message_fails { + return Err(SendError::MessageTooLarge); + } Ok(ticket) } @@ -223,6 +226,7 @@ impl pallet_external_validators_rewards::Config for Test { type HandleInflation = InflationMinter; type Currency = Balances; type RewardsEthereumSovereignAccount = RewardsEthereumSovereignAccount; + type GovernanceOrigin = frame_system::EnsureRoot; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -286,6 +290,8 @@ pub mod mock_data { pub offline_validators: sp_std::vec::Vec, /// Set of (era_index, validator_id) pairs that are slashed pub slashed_validators: sp_std::vec::Vec<(u32, sp_core::H160)>, + /// When true, MockOkOutboundQueue::validate will return Err(SendError::MessageTooLarge) + pub send_message_fails: bool, } #[pallet::config] diff --git a/operator/pallets/external-validators-rewards/src/tests.rs b/operator/pallets/external-validators-rewards/src/tests.rs index 986c624c2..b8ecabaac 100644 --- a/operator/pallets/external-validators-rewards/src/tests.rs +++ b/operator/pallets/external-validators-rewards/src/tests.rs @@ -16,7 +16,7 @@ use { crate::{self as pallet_external_validators_rewards, mock::*}, - frame_support::traits::fungible::Mutate, + frame_support::{assert_noop, assert_ok, traits::fungible::Mutate}, pallet_external_validators::traits::{ActiveEraInfo, OnEraEnd, OnEraStart}, sp_core::H160, sp_std::collections::btree_map::BTreeMap, @@ -161,8 +161,8 @@ fn test_on_era_end() { let inflation = ::EraInflationProvider::get(); // Use 0 for era_start_timestamp in tests - let rewards_utils = era_rewards.generate_era_rewards_utils(1, inflation, 0); - assert!(rewards_utils.is_some()); + let rewards_info = era_rewards.generate_era_rewards_info(1, inflation, 0); + assert!(rewards_info.is_some()); System::assert_last_event(RuntimeEvent::ExternalValidatorsRewards( crate::Event::RewardsMessageSent { message_id: Default::default(), @@ -203,8 +203,8 @@ fn test_on_era_end_with_zero_inflation() { let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); let inflation = ::EraInflationProvider::get(); - let rewards_utils = era_rewards.generate_era_rewards_utils(1, inflation, 0); - assert!(rewards_utils.is_some()); + let rewards_info = era_rewards.generate_era_rewards_info(1, inflation, 0); + assert!(rewards_info.is_some()); // With zero inflation, no RewardsMessageSent event should be emitted let events = System::events(); assert!( @@ -242,15 +242,15 @@ fn test_on_era_end_with_zero_points() { ExternalValidatorsRewards::reward_by_ids(accounts_points); ExternalValidatorsRewards::on_era_end(1); - // When all validators have zero points, generate_era_rewards_utils should return None + // When all validators have zero points, generate_era_rewards_info should return None // to prevent inflation from being minted with no way to distribute it let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); let inflation = ::EraInflationProvider::get(); - let rewards_utils = era_rewards.generate_era_rewards_utils(1, inflation, 0); + let rewards_info = era_rewards.generate_era_rewards_info(1, inflation, 0); assert!( - rewards_utils.is_none(), - "generate_era_rewards_utils should return None when total_points is zero" + rewards_info.is_none(), + "generate_era_rewards_info should return None when total_points is zero" ); // Verify no RewardsMessageSent event was emitted @@ -3718,3 +3718,456 @@ fn test_era_end_uses_correct_era_blocks_not_session() { ); }) } + +// ═══════════════════════════════════════════════════════════════════════════ +// Retry mechanism tests (ring-buffer storage) +// ═══════════════════════════════════════════════════════════════════════════ + +/// Helper: push an entry into the unsent ring buffer via the pallet API. +fn push_unsent(era_index: u32, timestamp: u32, inflation: u128) { + assert!( + ExternalValidatorsRewards::unsent_queue_push((era_index, timestamp, inflation)), + "unsent_queue_push should succeed" + ); +} + +/// Helper: return the number of entries in the unsent ring buffer. +fn unsent_len() -> u32 { + ExternalValidatorsRewards::unsent_queue_len() +} + +/// Helper: check if unsent queue is empty. +fn unsent_is_empty() -> bool { + ExternalValidatorsRewards::unsent_queue_is_empty() +} + +#[test] +fn send_failure_queues_era() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Give validators some points + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + // Author expected blocks for 100% inflation + for _ in 0..600 { + ExternalValidatorsRewards::note_block_author(H160::from_low_u64_be(1)); + } + + ExternalValidatorsRewards::on_era_end(1); + + // Verify era is queued + assert_eq!(unsent_len(), 1); + + // Verify event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageSendFailed { era_index: 1 }, + )); + }) +} + +#[test] +fn on_initialize_retries_and_succeeds() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + }); + + // Set up reward points for era 1 + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + + // Manually populate the unsent queue + push_unsent(1, 30, 42); + + // Sending should succeed (send_message_fails is false by default) + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Queue should be empty + assert!(unsent_is_empty()); + + // Verify retry event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 100, + inflation_amount: 42, + }, + )); + }) +} + +#[test] +fn on_initialize_moves_failed_entry_to_back() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 2, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Set up reward points for eras 1 and 2 + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + }); + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 200)]); + + // Push two entries: era 1 then era 2 + push_unsent(1, 30, 42); + push_unsent(2, 30, 84); + + // First call: tries era 1, fails, moves era 1 to back of queue + ExternalValidatorsRewards::process_unsent_reward_eras(); + // Queue length stays the same (entry moved, not removed) + assert_eq!(unsent_len(), 2); + + // Second call: tries era 2 (NOT era 1 again), fails, moves era 2 to back + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert_eq!(unsent_len(), 2); + + // Re-enable sending + Mock::mutate(|mock| mock.send_message_fails = false); + + // Third call: era 1 (now at front again), succeeds + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert_eq!(unsent_len(), 1); + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 200, + inflation_amount: 42, + }, + )); + + // Fourth call: era 2, succeeds + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert!(unsent_is_empty()); + }) +} + +#[test] +fn on_initialize_removes_expired_era() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Populate unsent queue with era 999 but do NOT add RewardPointsForEra for it + push_unsent(999, 0, 42); + + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Entry should be removed + assert!(unsent_is_empty()); + + // Verify expired event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::UnsentEraExpired { era_index: 999 }, + )); + }) +} + +#[test] +fn on_initialize_noop_when_queue_empty() { + new_test_ext().execute_with(|| { + run_to_block(1); + System::reset_events(); + + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // No events should be emitted + let events = System::events(); + assert!( + events.is_empty(), + "No events should be emitted when unsent queue is empty" + ); + }) +} + +#[test] +fn on_initialize_processes_only_head() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 3, + start: Some(30_000), + }); + }); + + // Set up reward points for both eras + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 2, + start: Some(30_000), + }); + }); + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(2), 200)]); + + // Push two entries + push_unsent(3, 30, 42); + push_unsent(2, 20, 84); + + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Only the head entry (era 3) should be processed (and removed on success) + assert_eq!(unsent_len(), 1); + }) +} + +#[test] +fn retry_extrinsic_success() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + }); + + // Set up reward points + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + + // Populate unsent queue + push_unsent(1, 30, 42); + + System::reset_events(); + assert_ok!(ExternalValidatorsRewards::retry_unsent_reward_era( + RuntimeOrigin::root(), + 1 + )); + + // Queue should be empty + assert!(unsent_is_empty()); + + // Verify retry event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 100, + inflation_amount: 42, + }, + )); + }) +} + +#[test] +fn retry_extrinsic_era_not_in_queue() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era(RuntimeOrigin::root(), 1), + crate::Error::::EraNotInUnsentQueue + ); + }) +} + +#[test] +fn retry_extrinsic_pruned_data() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Queue an era but don't create reward points for it + push_unsent(999, 0, 42); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era(RuntimeOrigin::root(), 999), + crate::Error::::RewardPointsPruned + ); + }) +} + +#[test] +fn retry_extrinsic_requires_root() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era( + RuntimeOrigin::signed(H160::from_low_u64_be(1)), + 1 + ), + sp_runtime::DispatchError::BadOrigin + ); + }) +} + +#[test] +fn unsent_queue_full() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 65, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Fill the ring buffer to capacity (63 entries, since capacity=64 + // means 63 usable slots in a ring buffer with head==tail==empty). + for i in 0..63u32 { + push_unsent(i, 0, 42); + } + assert_eq!(unsent_len(), 63); + + // Give validators some points so on_era_end doesn't bail early + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + for _ in 0..600 { + ExternalValidatorsRewards::note_block_author(H160::from_low_u64_be(1)); + } + + System::reset_events(); + ExternalValidatorsRewards::on_era_end(65); + + // Verify UnsentQueueFull event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::UnsentQueueFull { era_index: 65 }, + )); + + // Queue should still be at 63 + assert_eq!(unsent_len(), 63); + }) +} + +#[test] +fn on_era_start_prunes_unsent_entry() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Set up: era 1 has an unsent entry + push_unsent(1, 0, 42); + + // HistoryDepth is 10, so era 11 should prune era 1 + System::reset_events(); + ExternalValidatorsRewards::on_era_start(11, 0, 11); + + // Unsent entry should be removed + assert!(unsent_is_empty()); + + // Verify expired event + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::UnsentEraExpired { era_index: 1 }, + )); + }) +} + +#[test] +fn retry_extrinsic_send_still_fails() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: Some(30_000), + }); + mock.send_message_fails = true; + }); + + // Set up reward points + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + + // Populate unsent queue + push_unsent(1, 30, 42); + + assert_noop!( + ExternalValidatorsRewards::retry_unsent_reward_era(RuntimeOrigin::root(), 1), + crate::Error::::MessageSendFailed + ); + + // Queue should still have the entry + assert_eq!(unsent_len(), 1); + }) +} + +#[test] +fn head_of_line_blocking_avoided() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Set up reward points for eras 1, 2, 3 + for era in 1..=3u32 { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: era, + start: Some(30_000), + }); + }); + ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 100)]); + } + + // Push eras 1, 2, 3 into the queue + push_unsent(1, 30, 10); + push_unsent(2, 30, 20); + push_unsent(3, 30, 30); + + // Make sending fail + Mock::mutate(|mock| mock.send_message_fails = true); + + // Block 1: tries era 1, fails, advances head → era 2 + ExternalValidatorsRewards::process_unsent_reward_eras(); + // Block 2: tries era 2, fails, advances head → era 3 + ExternalValidatorsRewards::process_unsent_reward_eras(); + + // Now re-enable sending + Mock::mutate(|mock| mock.send_message_fails = false); + + // Block 3: tries era 3, succeeds + System::reset_events(); + ExternalValidatorsRewards::process_unsent_reward_eras(); + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 3, + total_points: 100, + inflation_amount: 30, + }, + )); + + // Block 4: wraps around to era 1, succeeds + ExternalValidatorsRewards::process_unsent_reward_eras(); + System::assert_has_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageRetried { + message_id: Default::default(), + era_index: 1, + total_points: 100, + inflation_amount: 10, + }, + )); + + // Block 5: era 2, succeeds + ExternalValidatorsRewards::process_unsent_reward_eras(); + assert!(unsent_is_empty()); + }) +} diff --git a/operator/pallets/external-validators-rewards/src/weights.rs b/operator/pallets/external-validators-rewards/src/weights.rs index 766adfcfb..a75857785 100644 --- a/operator/pallets/external-validators-rewards/src/weights.rs +++ b/operator/pallets/external-validators-rewards/src/weights.rs @@ -54,6 +54,11 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_external_validators_rewards. pub trait WeightInfo { fn on_era_end() -> Weight; + fn process_unsent_reward_eras_empty() -> Weight; + fn process_unsent_reward_eras_expired() -> Weight; + fn process_unsent_reward_eras_success() -> Weight; + fn process_unsent_reward_eras_failed() -> Weight; + fn retry_unsent_reward_era() -> Weight; } /// Weights for pallet_external_validators_rewards using the Substrate node and recommended hardware. @@ -84,6 +89,36 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + // 1 read for UnsentRewardEras + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + // 1 read UnsentRewardEras + 1 read RewardPointsForEra + 1 write UnsentRewardEras + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + // Same as on_era_end + queue read/write + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + // Use success weight as upper bound + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + // Same as success path + Self::process_unsent_reward_eras_success() + } } // For backwards compatibility and tests @@ -113,4 +148,29 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index fe1523175..d83e3c0bf 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -1568,6 +1568,7 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; + type GovernanceOrigin = EnsureRoot; type WeightInfo = mainnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); diff --git a/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs b/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs index b8be1393f..10854100d 100644 --- a/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs +++ b/operator/runtime/mainnet/src/weights/pallet_external_validators_rewards.rs @@ -74,4 +74,29 @@ impl pallet_external_validators_rewards::WeightInfo for .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_905_623_000, 29162) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index b07e9a5c2..af9927950 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -1564,6 +1564,7 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; + type GovernanceOrigin = EnsureRoot; type WeightInfo = stagenet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); diff --git a/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs b/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs index 4d223163d..34d31953b 100644 --- a/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs +++ b/operator/runtime/stagenet/src/weights/pallet_external_validators_rewards.rs @@ -74,4 +74,29 @@ impl pallet_external_validators_rewards::WeightInfo for .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_894_953_000, 29162) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 2ea416973..e66a47684 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -1568,6 +1568,7 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; + type GovernanceOrigin = EnsureRoot; type WeightInfo = testnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); diff --git a/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs b/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs index b2403bcf1..9b7e752d6 100644 --- a/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs +++ b/operator/runtime/testnet/src/weights/pallet_external_validators_rewards.rs @@ -74,4 +74,29 @@ impl pallet_external_validators_rewards::WeightInfo for .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } + + fn process_unsent_reward_eras_empty() -> Weight { + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + + fn process_unsent_reward_eras_expired() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + + fn process_unsent_reward_eras_success() -> Weight { + Weight::from_parts(1_893_280_000, 29162) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + + fn process_unsent_reward_eras_failed() -> Weight { + Self::process_unsent_reward_eras_success() + } + + fn retry_unsent_reward_era() -> Weight { + Self::process_unsent_reward_eras_success() + } } From 21d05f6cd2b1f9dbd5a09a97304c037a17a5b09f Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:15:23 +0100 Subject: [PATCH 02/17] fix: use EitherOfDiverse for GovernanceOrigin Match the IdentityForceOrigin pattern so the retry extrinsic can be called by either Root or the GeneralAdmin governance origin. Also add missing EnsureOrigin import in benchmarking.rs required when compiling with runtime-benchmarks feature. Co-Authored-By: Claude Opus 4.6 --- .../pallets/external-validators-rewards/src/benchmarking.rs | 2 +- operator/runtime/mainnet/src/configs/mod.rs | 3 ++- operator/runtime/stagenet/src/configs/mod.rs | 3 ++- operator/runtime/testnet/src/configs/mod.rs | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/operator/pallets/external-validators-rewards/src/benchmarking.rs b/operator/pallets/external-validators-rewards/src/benchmarking.rs index d0de5e26d..4b84bc3f0 100644 --- a/operator/pallets/external-validators-rewards/src/benchmarking.rs +++ b/operator/pallets/external-validators-rewards/src/benchmarking.rs @@ -23,7 +23,7 @@ use crate::Pallet as ExternalValidatorsRewards; use { crate::types::BenchmarkHelper, frame_benchmarking::{account, v2::*, BenchmarkError}, - frame_support::traits::Currency, + frame_support::traits::{Currency, EnsureOrigin}, sp_std::prelude::*, }; diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index d83e3c0bf..49435b7f7 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -1568,7 +1568,8 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; - type GovernanceOrigin = EnsureRoot; + type GovernanceOrigin = + EitherOfDiverse, governance::custom_origins::GeneralAdmin>; type WeightInfo = mainnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index af9927950..f6463ac91 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -1564,7 +1564,8 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; - type GovernanceOrigin = EnsureRoot; + type GovernanceOrigin = + EitherOfDiverse, governance::custom_origins::GeneralAdmin>; type WeightInfo = stagenet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index e66a47684..c447839f8 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -1568,7 +1568,8 @@ impl pallet_external_validators_rewards::Config for Runtime { type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; type HandleInflation = ExternalRewardsInflationHandler; - type GovernanceOrigin = EnsureRoot; + type GovernanceOrigin = + EitherOfDiverse, governance::custom_origins::GeneralAdmin>; type WeightInfo = testnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); From 22f9fcd995429c9d2a3e1035deffc962dbebf771 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:33:14 +0100 Subject: [PATCH 03/17] =?UTF-8?q?chore:=20=E2=99=BB=20=20update=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/.papi/descriptors/package.json | 2 +- test/.papi/metadata/datahaven.scale | Bin 631232 -> 633028 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index d7be6139e..4cc340c05 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.13357056092938763018", + "version": "0.1.0-autogenerated.13997571550449807545", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/.papi/metadata/datahaven.scale b/test/.papi/metadata/datahaven.scale index 5b2fcad3cab1d654fcef964afa8a0cdcfeac95f9..c69fcd3a2cb802a47418e17da5717f261e3e05ee 100644 GIT binary patch delta 14482 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 Date: Fri, 27 Feb 2026 15:44:36 +0100 Subject: [PATCH 04/17] =?UTF-8?q?chore:=20=E2=99=BB=20=20cargo=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operator/pallets/external-validators-rewards/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/operator/pallets/external-validators-rewards/src/lib.rs b/operator/pallets/external-validators-rewards/src/lib.rs index 80d2d38f6..8aeaa1234 100644 --- a/operator/pallets/external-validators-rewards/src/lib.rs +++ b/operator/pallets/external-validators-rewards/src/lib.rs @@ -971,7 +971,11 @@ pub mod pallet { } None => { // Message failed — queue for automatic retry via on_initialize - if Self::unsent_queue_push((era_index, era_start_timestamp, mint_result.rewards_amount)) { + if Self::unsent_queue_push(( + era_index, + era_start_timestamp, + mint_result.rewards_amount, + )) { Self::deposit_event(Event::RewardsMessageSendFailed { era_index }); } else { log::error!( From e680632636ed3889fbbf375308acc63cd0b2d259 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:34:32 +0100 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=E2=9C=A8=20Bump=20client=20versi?= =?UTF-8?q?on=20to=20v0.26.0=20and=20runtime=20to=20RT1400=20(#465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operator/Cargo.lock | 122 +++++++++++++-------------- operator/Cargo.toml | 2 +- operator/runtime/mainnet/src/lib.rs | 2 +- operator/runtime/stagenet/src/lib.rs | 2 +- operator/runtime/testnet/src/lib.rs | 2 +- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 702cc2d45..661208018 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -1521,7 +1521,7 @@ dependencies = [ "pallet-message-queue", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "sp-core", "sp-runtime", "sp-std", @@ -2607,7 +2607,7 @@ dependencies = [ [[package]] name = "datahaven-mainnet-runtime" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bridge-hub-common 0.13.1", @@ -2720,8 +2720,8 @@ dependencies = [ "shp-treasury-funding", "shp-tx-implicits-runtime-api", "smallvec", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -2764,7 +2764,7 @@ dependencies = [ [[package]] name = "datahaven-node" -version = "0.25.0" +version = "0.26.0" dependencies = [ "async-channel 1.9.0", "clap", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "datahaven-runtime-common" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "fp-account", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "datahaven-stagenet-runtime" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bridge-hub-common 0.13.1", @@ -3024,8 +3024,8 @@ dependencies = [ "shp-treasury-funding", "shp-tx-implicits-runtime-api", "smallvec", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -3068,7 +3068,7 @@ dependencies = [ [[package]] name = "datahaven-testnet-runtime" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bridge-hub-common 0.13.1", @@ -3181,8 +3181,8 @@ dependencies = [ "shp-treasury-funding", "shp-tx-implicits-runtime-api", "smallvec", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "dhp-bridge" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "frame-system", @@ -3382,7 +3382,7 @@ dependencies = [ "pallet-datahaven-native-transfer", "pallet-external-validators", "parity-scale-codec", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "sp-core", "sp-std", @@ -8719,7 +8719,7 @@ dependencies = [ [[package]] name = "pallet-datahaven-native-transfer" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -8727,7 +8727,7 @@ dependencies = [ "pallet-balances", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -8831,7 +8831,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-balances-erc20" -version = "0.25.0" +version = "0.26.0" dependencies = [ "fp-evm", "frame-support", @@ -8854,7 +8854,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-batch" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -8893,7 +8893,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-call-permit" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -8959,7 +8959,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-datahaven-native-transfer" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -8973,7 +8973,7 @@ dependencies = [ "parity-scale-codec", "precompile-utils", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -9052,7 +9052,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-proxy" -version = "0.25.0" +version = "0.26.0" dependencies = [ "evm", "fp-evm", @@ -9096,7 +9096,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-registry" -version = "0.25.0" +version = "0.26.0" dependencies = [ "fp-evm", "frame-support", @@ -9147,7 +9147,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -9157,7 +9157,7 @@ dependencies = [ [[package]] name = "pallet-external-validators" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9181,7 +9181,7 @@ dependencies = [ [[package]] name = "pallet-external-validators-rewards" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9194,7 +9194,7 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "sp-core", "sp-io", @@ -9287,7 +9287,7 @@ dependencies = [ [[package]] name = "pallet-grandpa-benchmarking" -version = "0.25.0" +version = "0.26.0" dependencies = [ "finality-grandpa", "frame-benchmarking", @@ -9439,7 +9439,7 @@ dependencies = [ [[package]] name = "pallet-outbound-commitment-store" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "frame-system", @@ -9563,7 +9563,7 @@ dependencies = [ [[package]] name = "pallet-proxy-genesis-companion" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "frame-system", @@ -9674,7 +9674,7 @@ dependencies = [ [[package]] name = "pallet-session-benchmarking" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -14827,7 +14827,7 @@ dependencies = [ [[package]] name = "snowbridge-beacon-primitives" -version = "0.25.0" +version = "0.26.0" dependencies = [ "byte-slice-cast", "frame-support", @@ -14872,7 +14872,7 @@ dependencies = [ [[package]] name = "snowbridge-core" -version = "0.25.0" +version = "0.26.0" dependencies = [ "bp-relayers", "ethabi-decode", @@ -14949,8 +14949,8 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-verification-primitives", "sp-core", "sp-io", @@ -14963,7 +14963,7 @@ dependencies = [ [[package]] name = "snowbridge-merkle-tree" -version = "0.25.0" +version = "0.26.0" dependencies = [ "array-bytes", "hex", @@ -15004,7 +15004,7 @@ dependencies = [ [[package]] name = "snowbridge-outbound-queue-primitives" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "ethabi-decode", @@ -15016,7 +15016,7 @@ dependencies = [ "parity-scale-codec", "polkadot-parachain-primitives", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-verification-primitives", "sp-arithmetic", "sp-core", @@ -15030,12 +15030,12 @@ dependencies = [ [[package]] name = "snowbridge-outbound-queue-v2-runtime-api" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "parity-scale-codec", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", "sp-api", @@ -15045,7 +15045,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-ethereum-client" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15058,8 +15058,8 @@ dependencies = [ "scale-info", "serde", "serde_json", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-ethereum 0.3.0", "snowbridge-inbound-queue-primitives", "snowbridge-pallet-ethereum-client-fixtures", @@ -15075,8 +15075,8 @@ name = "snowbridge-pallet-ethereum-client-fixtures" version = "0.9.0" dependencies = [ "hex-literal 0.3.4", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "sp-core", "sp-std", @@ -15084,7 +15084,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-inbound-queue-v2" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bp-relayers", @@ -15098,8 +15098,8 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-inbound-queue-v2-fixtures", @@ -15120,8 +15120,8 @@ name = "snowbridge-pallet-inbound-queue-v2-fixtures" version = "0.10.0" dependencies = [ "hex-literal 0.3.4", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "sp-core", "sp-std", @@ -15151,7 +15151,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-outbound-queue-v2" -version = "0.25.0" +version = "0.26.0" dependencies = [ "alloy-core", "bp-relayers", @@ -15165,8 +15165,8 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "snowbridge-beacon-primitives 0.25.0", - "snowbridge-core 0.25.0", + "snowbridge-beacon-primitives 0.26.0", + "snowbridge-core 0.26.0", "snowbridge-inbound-queue-primitives", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", @@ -15197,7 +15197,7 @@ dependencies = [ "parity-scale-codec", "polkadot-primitives", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "snowbridge-pallet-outbound-queue", "sp-core", @@ -15210,7 +15210,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-system-v2" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15222,7 +15222,7 @@ dependencies = [ "parity-scale-codec", "polkadot-primitives", "scale-info", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "snowbridge-outbound-queue-primitives", "snowbridge-pallet-outbound-queue-v2", "snowbridge-pallet-system", @@ -15238,10 +15238,10 @@ dependencies = [ [[package]] name = "snowbridge-system-v2-runtime-api" -version = "0.25.0" +version = "0.26.0" dependencies = [ "parity-scale-codec", - "snowbridge-core 0.25.0", + "snowbridge-core 0.26.0", "sp-api", "sp-std", "staging-xcm", @@ -15249,7 +15249,7 @@ dependencies = [ [[package]] name = "snowbridge-test-utils" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15269,12 +15269,12 @@ dependencies = [ [[package]] name = "snowbridge-verification-primitives" -version = "0.25.0" +version = "0.26.0" dependencies = [ "frame-support", "parity-scale-codec", "scale-info", - "snowbridge-beacon-primitives 0.25.0", + "snowbridge-beacon-primitives 0.26.0", "sp-core", "sp-std", ] diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 194e5f4ea..b27e5176e 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" homepage = "https://datahaven.xyz/" license = "GPL-3" repository = "https://github.com/datahavenxyz/datahaven" -version = "0.25.0" +version = "0.26.0" [workspace] members = [ diff --git a/operator/runtime/mainnet/src/lib.rs b/operator/runtime/mainnet/src/lib.rs index 94b4ec5bf..f295318aa 100644 --- a/operator/runtime/mainnet/src/lib.rs +++ b/operator/runtime/mainnet/src/lib.rs @@ -142,7 +142,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 200 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 1300, + spec_version: 1400, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/operator/runtime/stagenet/src/lib.rs b/operator/runtime/stagenet/src/lib.rs index 0655d3be8..df43090fa 100644 --- a/operator/runtime/stagenet/src/lib.rs +++ b/operator/runtime/stagenet/src/lib.rs @@ -145,7 +145,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 200 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 1300, + spec_version: 1400, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/operator/runtime/testnet/src/lib.rs b/operator/runtime/testnet/src/lib.rs index c177d49b9..a8248dbd6 100644 --- a/operator/runtime/testnet/src/lib.rs +++ b/operator/runtime/testnet/src/lib.rs @@ -142,7 +142,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 200 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 1300, + spec_version: 1400, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 0f6b83ea63012372decffef98aef58dffefcb01e Mon Sep 17 00:00:00 2001 From: undercover-cactus Date: Wed, 4 Mar 2026 08:49:14 +0100 Subject: [PATCH 06/17] refactor: remove the unused pallet-staking from dependencies (#466) ## Summary Remove unused dependency `pallet-staking`. ## What changes * Remove `pallet-staking` from the Cargo.toml file * Update Cargo.lock Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> --- operator/Cargo.lock | 1 - operator/Cargo.toml | 1 - operator/pallets/external-validator-slashes/Cargo.toml | 4 ---- 3 files changed, 6 deletions(-) diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 661208018..2843c5785 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -9142,7 +9142,6 @@ dependencies = [ "log", "pallet-external-validators", "pallet-session", - "pallet-staking", "pallet-timestamp", "parity-scale-codec", "scale-info", diff --git a/operator/Cargo.toml b/operator/Cargo.toml index b27e5176e..ab8b61859 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -146,7 +146,6 @@ pallet-referenda = { git = "https://github.com/paritytech/polkadot-sdk", tag = " pallet-safe-mode = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-scheduler = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-session = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } -pallet-staking = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false } diff --git a/operator/pallets/external-validator-slashes/Cargo.toml b/operator/pallets/external-validator-slashes/Cargo.toml index cf9a9804b..7dd77a282 100644 --- a/operator/pallets/external-validator-slashes/Cargo.toml +++ b/operator/pallets/external-validator-slashes/Cargo.toml @@ -18,7 +18,6 @@ frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } pallet-session = { workspace = true } -pallet-staking = { workspace = true } parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] } scale-info = { workspace = true } snowbridge-core = { workspace = true } @@ -42,7 +41,6 @@ std = [ "frame-system/std", "log/std", "pallet-session/std", - "pallet-staking/std", "pallet-timestamp/std", "parity-scale-codec/std", "pallet-external-validators/std", @@ -58,7 +56,6 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-staking/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-external-validators/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", @@ -70,7 +67,6 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "pallet-session/try-runtime", - "pallet-staking/try-runtime", "pallet-timestamp/try-runtime", "sp-runtime/try-runtime", ] From 38e9293732539f321be6d357837ab0f160c3361e Mon Sep 17 00:00:00 2001 From: Tobi Demeco <50408393+TDemeco@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:56:06 -0300 Subject: [PATCH 07/17] =?UTF-8?q?build:=20=E2=AC=86=EF=B8=8F=20upgrade=20t?= =?UTF-8?q?o=20StorageHub=20v0.4.3=20(#468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New update to StorageHub containing only a simple bugfix. No changes required in DataHaven Co-authored-by: undercover-cactus --- operator/Cargo.lock | 148 ++++++++++++++++++++++---------------------- operator/Cargo.toml | 68 ++++++++++---------- 2 files changed, 108 insertions(+), 108 deletions(-) diff --git a/operator/Cargo.lock b/operator/Cargo.lock index 2843c5785..586435c36 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -8642,8 +8642,8 @@ dependencies = [ [[package]] name = "pallet-bucket-nfts" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -8699,8 +8699,8 @@ dependencies = [ [[package]] name = "pallet-cr-randomness" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "frame-system", @@ -8983,8 +8983,8 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-file-system" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "fp-account", "fp-evm", @@ -9222,8 +9222,8 @@ dependencies = [ [[package]] name = "pallet-file-system" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9251,8 +9251,8 @@ dependencies = [ [[package]] name = "pallet-file-system-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9466,8 +9466,8 @@ dependencies = [ [[package]] name = "pallet-payment-streams" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9486,8 +9486,8 @@ dependencies = [ [[package]] name = "pallet-payment-streams-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9514,8 +9514,8 @@ dependencies = [ [[package]] name = "pallet-proofs-dealer" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9540,8 +9540,8 @@ dependencies = [ [[package]] name = "pallet-proofs-dealer-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -9579,8 +9579,8 @@ dependencies = [ [[package]] name = "pallet-randomness" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9717,8 +9717,8 @@ dependencies = [ [[package]] name = "pallet-storage-providers" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-benchmarking", "frame-support", @@ -9739,8 +9739,8 @@ dependencies = [ [[package]] name = "pallet-storage-providers-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -13902,8 +13902,8 @@ dependencies = [ [[package]] name = "shc-actors-derive" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "once_cell", "proc-macro2", @@ -13915,8 +13915,8 @@ dependencies = [ [[package]] name = "shc-actors-framework" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "bincode", @@ -13934,8 +13934,8 @@ dependencies = [ [[package]] name = "shc-blockchain-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -13990,8 +13990,8 @@ dependencies = [ [[package]] name = "shc-blockchain-service-db" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "chrono", "diesel", @@ -14014,8 +14014,8 @@ dependencies = [ [[package]] name = "shc-client" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14089,8 +14089,8 @@ dependencies = [ [[package]] name = "shc-common" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "bigdecimal", @@ -14154,8 +14154,8 @@ dependencies = [ [[package]] name = "shc-file-manager" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "bincode", "hash-db", @@ -14179,8 +14179,8 @@ dependencies = [ [[package]] name = "shc-file-transfer-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14210,8 +14210,8 @@ dependencies = [ [[package]] name = "shc-fisherman-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "async-trait", "diesel", @@ -14241,8 +14241,8 @@ dependencies = [ [[package]] name = "shc-forest-manager" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "async-trait", @@ -14267,8 +14267,8 @@ dependencies = [ [[package]] name = "shc-indexer-db" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "bigdecimal", "chrono", @@ -14295,8 +14295,8 @@ dependencies = [ [[package]] name = "shc-indexer-service" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "anyhow", "array-bytes", @@ -14346,8 +14346,8 @@ dependencies = [ [[package]] name = "shc-rpc" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "array-bytes", "async-trait", @@ -14392,8 +14392,8 @@ dependencies = [ [[package]] name = "shc-telemetry" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "log", "substrate-prometheus-endpoint", @@ -14409,8 +14409,8 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "shp-constants" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-core", "sp-runtime", @@ -14418,8 +14418,8 @@ dependencies = [ [[package]] name = "shp-data-price-updater" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14433,8 +14433,8 @@ dependencies = [ [[package]] name = "shp-file-key-verifier" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14451,8 +14451,8 @@ dependencies = [ [[package]] name = "shp-file-metadata" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "hex", "num-bigint", @@ -14467,8 +14467,8 @@ dependencies = [ [[package]] name = "shp-forest-verifier" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14484,16 +14484,16 @@ dependencies = [ [[package]] name = "shp-opaque" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-runtime", ] [[package]] name = "shp-session-keys" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "async-trait", "parity-scale-codec", @@ -14507,8 +14507,8 @@ dependencies = [ [[package]] name = "shp-traits" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "frame-support", "parity-scale-codec", @@ -14521,8 +14521,8 @@ dependencies = [ [[package]] name = "shp-treasury-funding" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "log", "shp-traits", @@ -14532,8 +14532,8 @@ dependencies = [ [[package]] name = "shp-tx-implicits-runtime-api" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "parity-scale-codec", "scale-info", @@ -14545,8 +14545,8 @@ dependencies = [ [[package]] name = "shp-types" -version = "0.4.2" -source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614" +version = "0.4.3" +source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.3#0176413b97dbbacb9148f1134e2797e174afa713" dependencies = [ "sp-core", "sp-runtime", diff --git a/operator/Cargo.toml b/operator/Cargo.toml index ab8b61859..40a689e9e 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -272,42 +272,42 @@ fc-storage = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl # StorageHub ## Runtime -pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } ## Client -shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } -shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } +shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } ## Precompiles -pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false } +pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.3", default-features = false } # Static linking From 5b2608852024d8b74902fae8ca6be9deabbb52cc Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:25:17 +0100 Subject: [PATCH 08/17] feat(slashes): typed offence kinds, Perbill-to-WAD conversion, historical filtering, and liveness E2E test (#447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces typed offence classification, a linear Perbill-to-WAD conversion for EigenLayer slashing, historical offence filtering, and a new E2E test proving end-to-end liveness detection through `pallet_im_online`. --- New `OffenceKind` enum classifies consensus offences: - `LivenessOffence` — missed heartbeats (ImOnline) - `BabeEquivocation` — double block production - `GrandpaEquivocation` — double finality votes - `BeefyEquivocation` — double BEEFY votes / fork voting / future block voting - `Custom(BoundedVec)` — manual / governance slashes Each variant carries a human-readable description string through the Snowbridge message to EigenLayer's `DatahavenServiceManager.slashValidatorsOperator()`. Generic wrapper around `ReportOffence` wired for BABE, GRANDPA, BEEFY, and ImOnline in all three runtimes: 1. **Filters historical offences** — discards reports whose session predates the bonding period, using `BondedEras` storage (analogous to `FilterHistoricalOffences` in `pallet_staking`, but adapted to this pallet's own era tracking). 2. **Tags offence kind** — stores the `OffenceKind` in `PendingOffenceKind` double-map `(SessionIndex, ValidatorId)` before delegating to `pallet_offences`. The `on_offence` handler reads it via `take()` in the same block. 3. **Cleans up on failure** — removes stale `PendingOffenceKind` entries if the inner reporter returns an error (e.g. duplicate report), preventing them from leaking into unrelated future offences. Each offence type in Substrate defines its own `slash_fraction(offenders_count)` returning a `Perbill`: | Offence | Formula | Typical range | |---|---|---| | **BABE equivocation** | `min((3k/n)^2, 1)` | 1 offender / 100 validators: ~0.09%; 1/2: capped to 100% | | **GRANDPA equivocation** | `min((3k/n)^2, 1)` | Same as BABE | | **BEEFY double-vote** | `min((3k/n)^2, 1)` | Same as BABE/GRANDPA | | **BEEFY fork/future voting** | Fixed `50%` | Always 50% | | **ImOnline liveness** | `min(3*(k - floor(n/10) - 1)/n, 1) * 7%` | 10% or fewer offline: **0%**; ~33% offline: ~5%; ~43% offline: 7% (max) | Where `k` = number of concurrent offenders, `n` = validator set size. **Key behavior for small validator sets (E2E):** With n=2, the ImOnline threshold is `floor(2/10) + 1 = 1`. A single offender (`k=1`) fails `checked_sub(1)` giving `Perbill(0)`. This means no `Slashes` storage entry is created (since `compute_slash` returns `None` when the new fraction doesn't exceed the prior slash), but the `SlashReported` event is still emitted, proving the full detection pipeline works. The Substrate `Perbill` is linearly mapped to a WAD value capped by `MaxSlashWad`: ``` WAD = perbill.deconstruct() * MaxSlashWad / 1_000_000_000 ``` - `MaxSlashWad` default: **5e16** (= 5% in WAD format, where 1e18 = 100%) - Governance-changeable dynamic runtime parameter (codec index 46) - `Perbill(100%)` maps to exactly `MaxSlashWad` (the cap) - `Perbill(0%)` maps to 0 (no slash sent to EigenLayer) | Scenario | Substrate Perbill | WAD sent to EigenLayer | EigenLayer % | |---|---|---|---| | BABE equivocation (1 of 100 validators) | ~0.09% | ~4.5e13 | ~0.0045% | | BABE equivocation (1 of 2 validators) | 100% (capped) | 5e16 | 5% (max) | | BEEFY fork voting | 50% | 2.5e16 | 2.5% | | ImOnline liveness (1 of 2 offline) | 0% | 0 (no slash) | 0% | | ImOnline liveness (~33% of large set offline) | ~5% | ~2.5e15 | ~0.25% | | Manual `force_inject_slash` at 20% | 20% | 1e16 | 1% | | Manual `force_inject_slash` at 100% | 100% | 5e16 | 5% (max) | The same WAD value is applied uniformly to all configured strategies via the `SlashingRequest` struct sent through Snowbridge to `DatahavenServiceManager.slashValidatorsOperator()`. New test scenario (`should detect and slash an unresponsive validator`) validates the full liveness detection pipeline: 1. Pauses bob's Docker container (preserving GRANDPA state via `docker pause`) 2. Waits 200s (>= 2 full sessions) for `pallet_im_online` to detect missed heartbeats 3. Unpauses bob to restore GRANDPA finality (2/2 validators needed) 4. Polls for `SlashReported` event (not `Slashes` storage — see slash fraction note above) 5. Verifies the event confirms the full pipeline: `pallet_im_online -> EquivocationReportWrapper -> pallet_offences -> on_offence` The test uses `try/finally` to always unpause bob, `{ at: "best" }` queries for non-finalized chain state during the pause, and drains prior `SlashReported` events before starting. - **10 new unit tests**: `PendingOffenceKind` double-map semantics, session isolation, wrapper historical filtering, error cleanup, WAD conversion (100%, 50%, 0%), offence kind description propagation - **New mock infrastructure**: `MockInnerReporter`, `MockOffence`, `MockOkOutboundQueue` with slash data capture - **E2E**: Updated `force_inject_slash` test to use `offence_kind` enum, new liveness detection test --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Gonza Montiel Co-authored-by: undercover-cactus --- .../src/benchmarking.rs | 26 +- .../external-validator-slashes/src/lib.rs | 229 +++++++++- .../external-validator-slashes/src/mock.rs | 84 +++- .../external-validator-slashes/src/tests.rs | 402 +++++++++++++++++- .../runtime/common/src/slashes_adapter.rs | 4 +- operator/runtime/mainnet/src/configs/mod.rs | 37 +- .../mainnet/src/configs/runtime_params.rs | 10 + operator/runtime/stagenet/src/configs/mod.rs | 37 +- .../stagenet/src/configs/runtime_params.rs | 10 + operator/runtime/testnet/src/configs/mod.rs | 37 +- .../testnet/src/configs/runtime_params.rs | 10 + test/.papi/descriptors/package.json | 2 +- test/e2e/suites/slash.test.ts | 116 ++++- 13 files changed, 936 insertions(+), 68 deletions(-) diff --git a/operator/pallets/external-validator-slashes/src/benchmarking.rs b/operator/pallets/external-validator-slashes/src/benchmarking.rs index 51693fb73..7e8d7a007 100644 --- a/operator/pallets/external-validator-slashes/src/benchmarking.rs +++ b/operator/pallets/external-validator-slashes/src/benchmarking.rs @@ -41,7 +41,14 @@ mod benchmarks { 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::::default_from(dummy())); + existing_slashes.push(Slash { + validator: dummy(), + reporters: vec![], + slash_id: One::one(), + percentage: Perbill::from_percent(1), + confirmed: false, + offence_kind: OffenceKind::LivenessOffence, + }); } Slashes::::insert( era.saturating_add(T::SlashDeferDuration::get()) @@ -74,7 +81,13 @@ mod benchmarks { let era = T::EraIndexProvider::active_era().index; let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); #[extrinsic_call] - _(RawOrigin::Root, era, dummy(), Perbill::from_percent(50)); + _( + RawOrigin::Root, + era, + dummy(), + Perbill::from_percent(50), + OffenceKind::LivenessOffence, + ); assert_eq!( Slashes::::get( @@ -93,7 +106,14 @@ mod benchmarks { let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); for _ in 0..(s + 1) { - queue.push_back(Slash::::default_from(dummy())); + queue.push_back(Slash { + validator: dummy(), + reporters: vec![], + slash_id: One::one(), + percentage: Perbill::from_percent(1), + confirmed: false, + offence_kind: OffenceKind::LivenessOffence, + }); } UnreportedSlashesQueue::::set(queue); diff --git a/operator/pallets/external-validator-slashes/src/lib.rs b/operator/pallets/external-validator-slashes/src/lib.rs index 8ef718d3d..b24b85dba 100644 --- a/operator/pallets/external-validator-slashes/src/lib.rs +++ b/operator/pallets/external-validator-slashes/src/lib.rs @@ -31,7 +31,7 @@ extern crate alloc; use pallet_external_validators::apply; use snowbridge_outbound_queue_primitives::SendError; use { - alloc::{collections::vec_deque::VecDeque, vec, vec::Vec}, + alloc::{collections::vec_deque::VecDeque, string::String, vec, vec::Vec}, frame_support::{pallet_prelude::*, traits::DefensiveSaturating}, frame_system::pallet_prelude::*, log::log, @@ -46,7 +46,7 @@ use { DispatchResult, Perbill, }, sp_staking::{ - offence::{OffenceDetails, OnOffenceHandler}, + offence::{Offence, OffenceDetails, OffenceError, OnOffenceHandler, ReportOffence}, EraIndex, SessionIndex, }, }; @@ -63,10 +63,45 @@ mod tests; mod benchmarking; pub mod weights; +/// Identifies the type of consensus offence for EigenLayer slash reporting. +#[derive( + Encode, + Decode, + DecodeWithMemTracking, + RuntimeDebug, + TypeInfo, + Clone, + PartialEq, + Eq, + MaxEncodedLen, +)] +pub enum OffenceKind { + /// Liveness offence (i.e. Unresponsiveness) + LivenessOffence, + BabeEquivocation, + GrandpaEquivocation, + BeefyEquivocation, + Custom(BoundedVec>), +} + +impl OffenceKind { + pub fn to_description(&self) -> String { + match self { + Self::LivenessOffence => "Liveness offence".into(), + Self::BabeEquivocation => "BABE equivocation".into(), + Self::GrandpaEquivocation => "GRANDPA equivocation".into(), + Self::BeefyEquivocation => "BEEFY equivocation".into(), + Self::Custom(desc) => String::from_utf8(desc.to_vec()) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()), + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct SlashData { pub validator: AccountId, pub wad_to_slash: u128, + pub description: String, } // FIXME (nice to have): Merge with SendMessage trait from pallet external-validator-reward (similar trait) @@ -153,6 +188,11 @@ pub mod pallet { /// Provider to retrieve the current external index of validators type ExternalIndexProvider: ExternalIndexProvider; + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// Default: 5e16 = 5% in WAD format (1e18 = 100%). + #[pallet::constant] + type MaxSlashWad: Get; + /// How many queued slashes are being processed per block. #[pallet::constant] type QueuedSlashesProcessedPerBlock: Get; @@ -183,6 +223,9 @@ pub mod pallet { EthereumDeliverFail, /// Invalid params for root_test_send_msg_to_eth RootTestInvalidParams, + /// No PendingOffenceKind found for (session, validator) — offence was not + /// reported through EquivocationReportWrapper, so the offence kind is unknown. + MissingOffenceKind, } #[apply(derive_storage_traits)] @@ -237,6 +280,27 @@ pub mod pallet { #[pallet::storage] pub type SlashingMode = StorageValue<_, SlashingModeOption, ValueQuery>; + /// Temporarily stores the offence kind per (session, offender), set by + /// `EquivocationReportWrapper` before `on_offence` is called synchronously within + /// the same block. Keyed by session index and validator ID so that offences from + /// different sessions or for different validators cannot interfere with each other. + /// + /// SAFETY: relies on `pallet_offences::report_offence` calling `on_offence` + /// synchronously in the same block. Entries are cleaned up via `take()` in + /// `on_offence` on success, or explicit `remove()` in the wrapper on error. + /// If the offence pipeline ever becomes asynchronous, this storage should be + /// replaced with an offence-payload-based approach. + #[pallet::storage] + pub type PendingOffenceKind = StorageDoubleMap< + _, + Twox64Concat, + SessionIndex, + Twox64Concat, + T::ValidatorId, + OffenceKind, + OptionQuery, + >; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -305,6 +369,7 @@ pub mod pallet { era: EraIndex, validator: T::AccountId, percentage: Perbill, + offence_kind: OffenceKind, ) -> DispatchResult { ensure_root(origin)?; let active_era = T::EraIndexProvider::active_era().index; @@ -324,6 +389,7 @@ pub mod pallet { era, validator, slash_defer_duration, + offence_kind, ) .ok_or(Error::::ErrorComputingSlash)?; @@ -374,7 +440,8 @@ pub mod pallet { } } -/// This is intended to be used with `FilterHistoricalOffences`. +/// This is intended to be used with `EquivocationReportWrapper`, which filters +/// out historical offences (before the bonding period) and tags the offence kind. impl OnOffenceHandler, Weight> for Pallet @@ -453,6 +520,25 @@ where for (details, slash_fraction) in offenders.iter().zip(slash_fraction) { let (stash, _) = &details.offender; + // Read the per-(session, offender) offence kind set by EquivocationReportWrapper. + // This is set synchronously before on_offence is called, so take() clears it. + // Type safety: `stash` is T::ValidatorId (from IdentificationTuple), matching + // the key used by the wrapper. The trait bounds above enforce ValidatorId == AccountId. + let offence_kind = match pallet::PendingOffenceKind::::take(slash_session, stash) { + Some(kind) => kind, + None => { + log!( + log::Level::Error, + "MissingOffenceKind for session {:?}, validator {:?} — skipping slash", + slash_session, + stash, + ); + add_db_reads_writes(1, 1); + continue; + } + }; + add_db_reads_writes(1, 1); + // Skip if the validator is invulnerable. if invulnerables.contains(stash) { continue; @@ -477,6 +563,7 @@ where slash_era, stash.clone(), slash_defer_duration, + offence_kind.clone(), ); if let Some(mut slash) = slash { @@ -594,9 +681,22 @@ impl Pallet { 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: u128::from_str_radix("10000000000000000", 10).unwrap(), // TODO: need to compute how much we slash (for now it is 1e16) + wad_to_slash, + description: slash.offence_kind.to_description(), }); } }); @@ -655,19 +755,8 @@ pub struct Slash { pub percentage: Perbill, // Whether the slash is confirmed or still needs to go through deferred period pub confirmed: bool, -} - -impl Slash { - /// Initializes the default object using the given `validator`. - pub fn default_from(validator: AccountId) -> Self { - Self { - validator, - reporters: vec![], - slash_id: One::one(), - percentage: Perbill::from_percent(50), - confirmed: false, - } - } + /// The type of consensus offence (relayed to EigenLayer as a description string). + pub offence_kind: OffenceKind, } /// Computes a slash of a validator and nominators. It returns an unapplied @@ -682,6 +771,7 @@ pub(crate) fn compute_slash( slash_era: EraIndex, stash: T::AccountId, slash_defer_duration: EraIndex, + offence_kind: OffenceKind, ) -> Option> { let prior_slash_p = ValidatorSlashInEra::::get(slash_era, &stash).unwrap_or(Zero::zero()); @@ -707,6 +797,7 @@ pub(crate) fn compute_slash( slash_id, reporters: Vec::new(), confirmed, + offence_kind, }) } @@ -714,3 +805,107 @@ pub(crate) fn compute_slash( fn is_sorted_and_unique(list: &[u32]) -> bool { list.windows(2).all(|w| w[0] < w[1]) } + +/// Trait for associating an `OffenceKind` with a reporter type. +pub trait OffenceKindProvider { + fn kind() -> OffenceKind; +} + +/// Extracts the validator (account) ID from an offender identification tuple. +pub trait HasValidatorId { + fn validator_id(&self) -> &ValidatorId; +} + +impl HasValidatorId for (V, F) { + fn validator_id(&self) -> &V { + &self.0 + } +} + +/// Wraps a `ReportOffence` implementation to: +/// 1. **Filter historical offences**: discard reports whose session predates the bonding +/// period (similar to `FilterHistoricalOffences` in `pallet_staking`, but using this +/// pallet's own `BondedEras` storage instead of staking eras). +/// 2. **Tag offence kind**: store the `OffenceKind` per offender in `PendingOffenceKind` +/// before delegating to the inner reporter, so that `on_offence` can read it via +/// `PendingOffenceKind::take()`. +/// +/// If the inner `report_offence` fails (e.g. duplicate report), stale `PendingOffenceKind` +/// entries are cleaned up to prevent leaking into unrelated future offences. +pub struct EquivocationReportWrapper(PhantomData<(T, Inner, Kind)>); + +impl ReportOffence for EquivocationReportWrapper +where + T: Config, + Inner: ReportOffence, + O: Offence, + Kind: OffenceKindProvider, + Id: HasValidatorId, +{ + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { + // Discard offences from before the bonding period. + let offence_session = offence.session_index(); + let bonded_eras = pallet::BondedEras::::get(); + if bonded_eras + .first() + .filter(|(_, start, _)| offence_session >= *start) + .is_none() + { + log!( + log::Level::Debug, + "discarding offence from session {} — predates bonded eras {:?}", + offence_session, + bonded_eras.first(), + ); + return Ok(()); + } + + let offenders = offence.offenders(); + for offender in &offenders { + pallet::PendingOffenceKind::::insert( + offence_session, + offender.validator_id(), + Kind::kind(), + ); + } + let result = Inner::report_offence(reporters, offence); + if result.is_err() { + for offender in &offenders { + pallet::PendingOffenceKind::::remove(offence_session, offender.validator_id()); + } + } + result + } + + fn is_known_offence(offenders: &[Id], time_slot: &O::TimeSlot) -> bool { + Inner::is_known_offence(offenders, time_slot) + } +} + +pub struct BabeEquivocation; +impl OffenceKindProvider for BabeEquivocation { + fn kind() -> OffenceKind { + OffenceKind::BabeEquivocation + } +} + +pub struct GrandpaEquivocation; +impl OffenceKindProvider for GrandpaEquivocation { + fn kind() -> OffenceKind { + OffenceKind::GrandpaEquivocation + } +} + +pub struct BeefyEquivocation; +impl OffenceKindProvider for BeefyEquivocation { + fn kind() -> OffenceKind { + OffenceKind::BeefyEquivocation + } +} + +pub struct ImOnlineUnresponsive; +impl OffenceKindProvider for ImOnlineUnresponsive { + fn kind() -> OffenceKind { + OffenceKind::LivenessOffence + } +} diff --git a/operator/pallets/external-validator-slashes/src/mock.rs b/operator/pallets/external-validator-slashes/src/mock.rs index c21f13c5c..efe2c5097 100644 --- a/operator/pallets/external-validator-slashes/src/mock.rs +++ b/operator/pallets/external-validator-slashes/src/mock.rs @@ -25,7 +25,7 @@ use { core::cell::RefCell, frame_support::{ parameter_types, - traits::{ConstU16, ConstU32, ConstU64, Get}, + traits::{ConstU128, ConstU16, ConstU32, ConstU64, Get}, weights::constants::RocksDbWeight, }, frame_system as system, @@ -132,7 +132,9 @@ thread_local! { pub static ERA_INDEX: RefCell = const { RefCell::new(0) }; pub static DEFER_PERIOD: RefCell = const { RefCell::new(2) }; pub static SENT_ETHEREUM_MESSAGE_NONCE: RefCell = const { RefCell::new(0) }; - + pub static MOCK_REPORT_OFFENCE_SHOULD_FAIL: RefCell = const { RefCell::new(false) }; + pub static MOCK_REPORT_OFFENCE_CALLED: RefCell = const { RefCell::new(false) }; + pub static LAST_SENT_SLASHES: RefCell>> = RefCell::new(Vec::new()); } impl MockEraIndexProvider { @@ -215,10 +217,16 @@ impl DeferPeriodGetter { } pub struct MockOkOutboundQueue; +impl MockOkOutboundQueue { + pub fn last_sent_slashes() -> Vec> { + LAST_SENT_SLASHES.with(|r| r.borrow().clone()) + } +} impl crate::SendMessage for MockOkOutboundQueue { type Ticket = (); type Message = (); - fn build(_: &Vec>, _: u32) -> Option { + fn build(slashes: &Vec>, _: u32) -> Option { + LAST_SENT_SLASHES.with(|r| *r.borrow_mut() = slashes.clone()); Some(()) } fn validate(_: Self::Ticket) -> Result { @@ -258,6 +266,7 @@ impl external_validator_slashes::Config for Test { type EraIndexProvider = MockEraIndexProvider; type InvulnerablesProvider = MockInvulnerableProvider; type ExternalIndexProvider = TimestampProvider; + type MaxSlashWad = ConstU128<50_000_000_000_000_000>; type QueuedSlashesProcessedPerBlock = ConstU32<20>; type WeightInfo = (); type SendMessage = MockOkOutboundQueue; @@ -289,6 +298,75 @@ impl sp_runtime::traits::Convert> for IdentityValidator { } } +// --- Mock infrastructure for testing EquivocationReportWrapper --- + +use sp_staking::offence::{Offence, OffenceError, ReportOffence}; + +/// A mock inner ReportOffence that can be configured to succeed or fail. +pub struct MockInnerReporter; + +impl MockInnerReporter { + pub fn set_should_fail(fail: bool) { + MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow_mut() = fail); + } + pub fn was_called() -> bool { + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow()) + } + pub fn reset() { + MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow_mut() = false); + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow_mut() = false); + } +} + +impl> ReportOffence for MockInnerReporter { + fn report_offence(_reporters: Vec, _offence: O) -> Result<(), OffenceError> { + MOCK_REPORT_OFFENCE_CALLED.with(|r| *r.borrow_mut() = true); + if MOCK_REPORT_OFFENCE_SHOULD_FAIL.with(|r| *r.borrow()) { + Err(OffenceError::DuplicateReport) + } else { + Ok(()) + } + } + fn is_known_offence(_offenders: &[Id], _time_slot: &O::TimeSlot) -> bool { + false + } +} + +/// A minimal mock Offence for testing the wrapper. +pub struct MockOffence { + pub session_index: SessionIndex, + pub offenders: Vec<(u64, ())>, +} + +impl Offence<(u64, ())> for MockOffence { + const ID: sp_staking::offence::Kind = *b"mock:offence0000"; + type TimeSlot = u128; + + fn offenders(&self) -> Vec<(u64, ())> { + self.offenders.clone() + } + fn session_index(&self) -> SessionIndex { + self.session_index + } + fn validator_set_count(&self) -> u32 { + 3 + } + fn time_slot(&self) -> Self::TimeSlot { + self.session_index as u128 + } + fn slash_fraction(&self, _offenders_count: u32) -> sp_runtime::Perbill { + sp_runtime::Perbill::from_percent(50) + } +} + +/// Type alias for the wrapper using the mock reporter with BabeEquivocation kind. +pub type MockBabeWrapper = + crate::EquivocationReportWrapper; + +/// Type alias for the wrapper using the mock reporter with GrandpaEquivocation kind. +pub type MockGrandpaWrapper = + crate::EquivocationReportWrapper; + pub fn run_block() { run_to_block(System::block_number() + 1); } diff --git a/operator/pallets/external-validator-slashes/src/tests.rs b/operator/pallets/external-validator-slashes/src/tests.rs index 0c21466ce..126485b8f 100644 --- a/operator/pallets/external-validator-slashes/src/tests.rs +++ b/operator/pallets/external-validator-slashes/src/tests.rs @@ -18,12 +18,14 @@ use { super::*, crate::{ mock::{ - new_test_ext, run_block, DeferPeriodGetter, ExternalValidatorSlashes, - MockEraIndexProvider, RuntimeEvent, RuntimeOrigin, System, Test, + new_test_ext, run_block, DeferPeriodGetter, ExternalValidatorSlashes, MockBabeWrapper, + MockEraIndexProvider, MockGrandpaWrapper, MockInnerReporter, MockOffence, + MockOkOutboundQueue, RuntimeEvent, RuntimeOrigin, System, Test, }, - Slash, + OffenceKind, Slash, }, - frame_support::{assert_noop, assert_ok}, + frame_support::{assert_noop, assert_ok, BoundedVec}, + sp_staking::offence::ReportOffence, }; #[test] @@ -35,6 +37,7 @@ fn root_can_inject_manual_offence() { 0, 1u64, Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( Slashes::::get(get_slashing_era(0)), @@ -43,7 +46,10 @@ fn root_can_inject_manual_offence() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); assert_eq!(NextSlashId::::get(), 1); @@ -59,7 +65,8 @@ fn cannot_inject_future_era_offence() { RuntimeOrigin::root(), 1, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), ), Error::::ProvidedFutureEra ); @@ -76,7 +83,8 @@ fn cannot_inject_era_offence_too_far_in_the_past() { RuntimeOrigin::root(), 1, 4u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), ), Error::::ProvidedNonSlashableEra ); @@ -91,7 +99,8 @@ fn root_can_cancel_deferred_slash() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_ok!(ExternalValidatorSlashes::cancel_deferred_slash( RuntimeOrigin::root(), @@ -111,7 +120,8 @@ fn root_cannot_cancel_deferred_slash_if_outside_deferring_period() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); start_era(4, 0, 4); @@ -131,7 +141,8 @@ fn root_cannot_cancel_out_of_bounds() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash( @@ -152,7 +163,8 @@ fn root_cannot_cancel_duplicates() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![0, 0]), @@ -169,13 +181,15 @@ fn root_cannot_cancel_if_not_sorted() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_ok!(ExternalValidatorSlashes::force_inject_slash( RuntimeOrigin::root(), 0, 2u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![1, 0]), @@ -196,7 +210,8 @@ fn test_after_bonding_period_we_can_remove_slashes() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( @@ -206,7 +221,10 @@ fn test_after_bonding_period_we_can_remove_slashes() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); @@ -226,6 +244,7 @@ fn test_on_offence_injects_offences() { new_test_ext().execute_with(|| { start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -242,7 +261,8 @@ fn test_on_offence_injects_offences() { percentage: Perbill::from_percent(75), confirmed: false, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); }); @@ -253,7 +273,8 @@ fn test_on_offence_does_not_work_for_invulnerables() { new_test_ext().execute_with(|| { start_era(0, 0, 0); start_era(1, 1, 1); - // account 1 invulnerable + // account 1 invulnerable — populate kind so we test the invulnerable check, not missing kind + PendingOffenceKind::::insert(0, 1u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { offender: (1, ()), @@ -276,6 +297,7 @@ fn test_on_offence_does_not_work_if_slashing_disabled() { RuntimeOrigin::root(), SlashingModeOption::Disabled, )); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); let weight = Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -303,7 +325,8 @@ fn defer_period_of_zero_confirms_immediately_slashes() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_eq!( Slashes::::get(get_slashing_era(0)), @@ -312,7 +335,10 @@ fn defer_period_of_zero_confirms_immediately_slashes() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::Custom(BoundedVec::truncate_from( + b"Test slash".to_vec() + )), }] ); }); @@ -327,7 +353,8 @@ fn we_cannot_cancel_anything_with_defer_period_zero() { RuntimeOrigin::root(), 0, 1u64, - Perbill::from_percent(75) + Perbill::from_percent(75), + OffenceKind::Custom(BoundedVec::truncate_from(b"Test slash".to_vec())), )); assert_noop!( ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 0, vec![0]), @@ -342,6 +369,7 @@ fn test_on_offence_defer_period_0() { crate::mock::DeferPeriodGetter::with_defer_period(0); start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -359,7 +387,8 @@ fn test_on_offence_defer_period_0() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); start_era(2, 2, 2); @@ -373,6 +402,7 @@ fn test_slashes_command_matches_event() { crate::mock::DeferPeriodGetter::with_defer_period(0); start_era(0, 0, 0); start_era(1, 1, 1); + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -391,7 +421,8 @@ fn test_slashes_command_matches_event() { percentage: Perbill::from_percent(75), confirmed: true, reporters: vec![], - slash_id: 0 + slash_id: 0, + offence_kind: OffenceKind::LivenessOffence, }] ); start_era(2, 2, 2); @@ -405,6 +436,122 @@ fn test_slashes_command_matches_event() { }); } +// ── WAD conversion tests ── +// MaxSlashWad in mock = 50_000_000_000_000_000 (5e16 = 5% in WAD format). +// Perbill(100%) = 1_000_000_000 inner. +// Formula: wad = perbill_inner * MaxSlashWad / 1e9 + +#[test] +fn wad_conversion_100_percent_slash_maps_to_max_slash_wad() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(100)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 100% → full MaxSlashWad = 5e16 + assert_eq!(sent[0].wad_to_slash, 50_000_000_000_000_000u128); + assert_eq!(sent[0].validator, 3); + }); +} + +#[test] +fn wad_conversion_50_percent_slash_maps_to_half_max_slash_wad() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 50% → MaxSlashWad / 2 = 2.5e16 + assert_eq!(sent[0].wad_to_slash, 25_000_000_000_000_000u128); + }); +} + +#[test] +fn wad_conversion_zero_percent_slash_maps_to_zero() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + PendingOffenceKind::::insert(0, 3u64, OffenceKind::LivenessOffence); + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + // 0% slash → no slash recorded (compute_slash returns None for 0%) + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 0); + }); +} + +#[test] +fn wad_conversion_carries_offence_kind_description() { + new_test_ext().execute_with(|| { + crate::mock::DeferPeriodGetter::with_defer_period(0); + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Pre-populate a BabeEquivocation kind for session 0, validator 3. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(75)], + 0, + ); + + start_era(2, 2, 2); + run_block(); + + let sent = MockOkOutboundQueue::last_sent_slashes(); + assert_eq!(sent.len(), 1); + // 75% → 75% of MaxSlashWad = 3.75e16 + assert_eq!(sent[0].wad_to_slash, 37_500_000_000_000_000u128); + assert_eq!(sent[0].description, "BABE equivocation"); + }); +} + #[test] fn test_on_offence_defer_period_0_messages_get_queued() { new_test_ext().execute_with(|| { @@ -413,6 +560,7 @@ fn test_on_offence_defer_period_0_messages_get_queued() { start_era(1, 1, 1); // The limit is 20, for i in 0..25 { + PendingOffenceKind::::insert(0, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -450,6 +598,7 @@ fn test_account_id_encoding() { slash_id: 1, percentage: Perbill::default(), confirmed: true, + offence_kind: OffenceKind::LivenessOffence, }; let encoded_account = slash.validator.encode(); @@ -466,6 +615,7 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { start_era(1, 1, 1); // The limit is 20, for i in 0..25 { + PendingOffenceKind::::insert(0, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -487,6 +637,7 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { // We have 5 non-dispatched, which should accumulate // We shoulld have 30 after we initialie era 3 for i in 0..25 { + PendingOffenceKind::::insert(2, 3 + i, OffenceKind::LivenessOffence); Pallet::::on_offence( &[OffenceDetails { // 1 and 2 are invulnerables @@ -512,6 +663,213 @@ fn test_on_offence_defer_period_0_messages_get_queued_across_eras() { }); } +// ── PendingOffenceKind & EquivocationReportWrapper tests ── + +#[test] +fn on_offence_reads_pending_offence_kind_from_double_map() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Pre-populate PendingOffenceKind for validator 3 at session 0. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(75)], + 0, + ); + + assert_eq!( + Slashes::::get(get_slashing_era(0)), + vec![Slash { + validator: 3, + percentage: Perbill::from_percent(75), + confirmed: false, + reporters: vec![], + slash_id: 0, + offence_kind: OffenceKind::BabeEquivocation, + }] + ); + + // Entry should have been consumed. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + }); +} + +#[test] +fn pending_offence_kind_is_session_isolated() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + + // Same validator, different kinds in different sessions. + PendingOffenceKind::::insert(0, 3u64, OffenceKind::BabeEquivocation); + PendingOffenceKind::::insert(1, 3u64, OffenceKind::GrandpaEquivocation); + + // Report at session 0 — should use BabeEquivocation. + Pallet::::on_offence( + &[OffenceDetails { + offender: (3, ()), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + 0, + ); + + // Session 0 consumed, session 1 untouched. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + assert_eq!( + PendingOffenceKind::::get(1, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + }); +} + +#[test] +fn wrapper_filters_historical_offence_before_bonding_period() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + // BondedEras now contains [(0,0,0), (1,1,1)]. + // An offence at session 0 is within the bonding period — should pass. + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_ok()); + assert!(MockInnerReporter::was_called()); + + // The mock reporter doesn't trigger on_offence, so manually consume the entry. + assert_eq!( + PendingOffenceKind::::take(0, 3u64), + Some(OffenceKind::BabeEquivocation), + ); + + // Advance eras until era 0 drops out of BondedEras. + // BondingDuration = 5, so after era 6 starts, era 0 is pruned. + for i in 2..=7 { + start_era(i, i, i as u64); + } + + MockInnerReporter::reset(); + + // Session 0 now predates the bonding period — should be silently discarded. + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_ok()); + assert!(!MockInnerReporter::was_called()); + + // No PendingOffenceKind should have been written. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + }); +} + +#[test] +fn wrapper_sets_pending_offence_kind_per_session_and_offender() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + let _ = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ()), (4, ())], + }, + ); + + // Both offenders should have entries at session 0. + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::BabeEquivocation), + ); + assert_eq!( + PendingOffenceKind::::get(0, 4u64), + Some(OffenceKind::BabeEquivocation), + ); + // No entry at a different session. + assert_eq!(PendingOffenceKind::::get(1, 3u64), None); + }); +} + +#[test] +fn wrapper_cleans_up_pending_offence_kind_on_error() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + MockInnerReporter::set_should_fail(true); + + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ()), (4, ())], + }, + ); + + assert!(result.is_err()); + // Entries should have been cleaned up. + assert_eq!(PendingOffenceKind::::get(0, 3u64), None); + assert_eq!(PendingOffenceKind::::get(0, 4u64), None); + }); +} + +#[test] +fn wrapper_error_cleanup_does_not_affect_other_sessions() { + new_test_ext().execute_with(|| { + start_era(0, 0, 0); + start_era(1, 1, 1); + MockInnerReporter::reset(); + + // Successfully report at session 0. + let _ = MockGrandpaWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 0, + offenders: vec![(3, ())], + }, + ); + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + + // Now fail a report at session 1 for the same validator. + MockInnerReporter::set_should_fail(true); + let result = MockBabeWrapper::report_offence( + Vec::::new(), + MockOffence { + session_index: 1, + offenders: vec![(3, ())], + }, + ); + assert!(result.is_err()); + + // Session 1 cleaned up, session 0 untouched. + assert_eq!(PendingOffenceKind::::get(1, 3u64), None); + assert_eq!( + PendingOffenceKind::::get(0, 3u64), + Some(OffenceKind::GrandpaEquivocation), + ); + }); +} + fn start_era(era_index: EraIndex, session_index: SessionIndex, external_idx: u64) { Pallet::::on_era_start(era_index, session_index, external_idx); crate::mock::MockEraIndexProvider::with_era(era_index); diff --git a/operator/runtime/common/src/slashes_adapter.rs b/operator/runtime/common/src/slashes_adapter.rs index e28aabc46..74fa5c589 100644 --- a/operator/runtime/common/src/slashes_adapter.rs +++ b/operator/runtime/common/src/slashes_adapter.rs @@ -111,8 +111,8 @@ fn encode_slashing_request( let slashing_request = SlashingRequest { operator: Address::from(slash_operator.validator.0), strategies: strategies.clone(), - wadsToSlash: wads_to_slash, // We only have one strategy deployed - description: "Slashing validator".into(), + wadsToSlash: wads_to_slash, + description: slash_operator.description.clone().into(), }; slashings.push(slashing_request); diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index 1f02f3ebc..ea5a49fac 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -401,7 +409,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -424,7 +436,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -501,8 +517,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1703,6 +1727,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = mainnet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/mainnet/src/configs/runtime_params.rs b/operator/runtime/mainnet/src/configs/runtime_params.rs index 605c9f733..aa35f269d 100644 --- a/operator/runtime/mainnet/src/configs/runtime_params.rs +++ b/operator/runtime/mainnet/src/configs/runtime_params.rs @@ -417,6 +417,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index ee1be149b..4b0fc4a7a 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -400,7 +408,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -498,8 +514,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1699,6 +1723,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = stagenet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/stagenet/src/configs/runtime_params.rs b/operator/runtime/stagenet/src/configs/runtime_params.rs index f05caff5e..d7775020d 100644 --- a/operator/runtime/stagenet/src/configs/runtime_params.rs +++ b/operator/runtime/stagenet/src/configs/runtime_params.rs @@ -424,6 +424,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 427c66911..6749b567c 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -321,8 +321,16 @@ impl pallet_babe::Config for Runtime { type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_babe::EquivocationReportSystem; + type EquivocationReportSystem = pallet_babe::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BabeEquivocation, + >, + Historical, + ReportLongevity, + >; } impl pallet_timestamp::Config for Runtime { @@ -400,7 +408,11 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorSet = Historical; type NextSessionRotation = Babe; - type ReportUnresponsiveness = Offences; + type ReportUnresponsiveness = pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::ImOnlineUnresponsive, + >; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = crate::weights::pallet_im_online::WeightInfo; } @@ -423,7 +435,11 @@ impl pallet_grandpa::Config for Runtime { type KeyOwnerProof = >::Proof; type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem< Self, - Offences, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::GrandpaEquivocation, + >, Historical, EquivocationReportPeriodInBlocks, >; @@ -501,8 +517,16 @@ impl pallet_beefy::Config for Runtime { type AncestryHelper = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; - type EquivocationReportSystem = - pallet_beefy::EquivocationReportSystem; + type EquivocationReportSystem = pallet_beefy::EquivocationReportSystem< + Self, + pallet_external_validator_slashes::EquivocationReportWrapper< + Runtime, + Offences, + pallet_external_validator_slashes::BeefyEquivocation, + >, + Historical, + ReportLongevity, + >; } parameter_types! { @@ -1702,6 +1726,7 @@ impl pallet_external_validator_slashes::Config for Runtime { type EraIndexProvider = ExternalValidators; type InvulnerablesProvider = ExternalValidators; type ExternalIndexProvider = ExternalValidators; + type MaxSlashWad = runtime_params::dynamic_params::runtime_config::MaxSlashWad; type QueuedSlashesProcessedPerBlock = ConstU32<10>; type WeightInfo = testnet_weights::pallet_external_validator_slashes::WeightInfo; type SendMessage = SlashesSendAdapter; diff --git a/operator/runtime/testnet/src/configs/runtime_params.rs b/operator/runtime/testnet/src/configs/runtime_params.rs index bc218f6cd..5753d215e 100644 --- a/operator/runtime/testnet/src/configs/runtime_params.rs +++ b/operator/runtime/testnet/src/configs/runtime_params.rs @@ -419,6 +419,16 @@ pub mod dynamic_params { BoundedVec::truncate_from(vec![]); // ╚══════════════════════ EigenLayer Rewards V2 ═══════════════════════╝ + + // ╔══════════════════════ EigenLayer Slashing ═══════════════════════╗ + + #[codec(index = 46)] + #[allow(non_upper_case_globals)] + /// Maximum WAD value for EigenLayer slashing. Maps Perbill(100%) to this value. + /// 5e16 = 5% in WAD format (1e18 = 100%). + pub static MaxSlashWad: u128 = 50_000_000_000_000_000u128; + + // ╚══════════════════════ EigenLayer Slashing ═══════════════════════╝ } } diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index 4cc340c05..d5f24906b 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.13997571550449807545", + "version": "0.1.0-autogenerated.18296316742446681711", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/e2e/suites/slash.test.ts b/test/e2e/suites/slash.test.ts index 87e370f63..c212cd90e 100644 --- a/test/e2e/suites/slash.test.ts +++ b/test/e2e/suites/slash.test.ts @@ -1,9 +1,11 @@ import { beforeAll, describe, expect, it } from "bun:test"; -import { FixedSizeBinary } from "polkadot-api"; +import { $ } from "bun"; +import { Binary, FixedSizeBinary } from "polkadot-api"; import { CROSS_CHAIN_TIMEOUTS, getPapiSigner, logger } from "utils"; import type { Address } from "viem"; import { getContractInstance, parseDeploymentsFile } from "../../utils/contracts"; import { waitForDataHavenEvent } from "../../utils/events"; +import { waitFor } from "../../utils/waits"; import { BaseTestSuite } from "../framework"; class SlashTestSuite extends BaseTestSuite { @@ -15,6 +17,10 @@ class SlashTestSuite extends BaseTestSuite { // Set up hooks in constructor this.setupHooks(); } + + getNetworkId(): string { + return this.networkId; + } } // Create the test suite instance @@ -129,7 +135,11 @@ describe("Should slash an operator", () => { const sudoSlashCall = dhApi.tx.ExternalValidatorsSlashes.force_inject_slash({ validator, era: activeEra?.index || 0, // Will fail if active era is 0. !! Important !! Sometimes for the inject to work (because of some latency) we need to inject in the `activeEra.index + 1` - percentage: 20 + percentage: 20, + offence_kind: { + type: "Custom", + value: Binary.fromText("Manual slash: E2E test") + } }); const sudoTx = dhApi.tx.Sudo.sudo({ call: sudoSlashCall.decodedCall @@ -159,4 +169,106 @@ describe("Should slash an operator", () => { } logger.info("Slashes message sent"); }, 560000); + + it("should detect and slash an unresponsive validator (liveness)", async () => { + const networkId = suite.getNetworkId(); + const bobContainer = `datahaven-bob-${networkId}`; + + // Drain any prior SlashReported events so we only see events from this test. + await dhApi.event.ExternalValidatorsSlashes.SlashReported.pull(); + + const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue({ at: "best" }); + const eraAtStart = activeEra?.index ?? 0; + const sessionAtStart = await dhApi.query.Session.CurrentIndex.getValue({ at: "best" }); + logger.info(`Liveness test start — era: ${eraAtStart}, session: ${sessionAtStart}`); + + // Pause bob to simulate a liveness failure (missed heartbeats). + // Using pause/unpause instead of stop/start preserves bob's process + // state (GRANDPA voter, peer connections, keystore) so finality can + // resume quickly once unpaused. + logger.info(`Pausing bob container: ${bobContainer}`); + await $`docker pause ${bobContainer}`.quiet(); + logger.info("Bob container paused. Waiting for sessions to elapse..."); + + let slashReportedEvent: any; + try { + // Wait for at least TWO full sessions so pallet_im_online detects bob's + // missing heartbeats at a session boundary. + // + // Why two sessions: + // 1. Bob may have already sent his heartbeat for the current session + // before being paused, so the first session boundary won't report him. + // 2. The NEXT full session (where bob is offline from start to finish) + // will trigger the unresponsiveness report at its boundary. + // + // Timing with BABE c=(1,4) and PrimaryAndSecondaryVRFSlots: + // - Alice alone produces ~62.5% of blocks → ~9.6s per block on average. + // - 10 blocks/session → ~96s per session with alice only. + // - 2 sessions = ~192s. We use 200s for margin. + await Bun.sleep(200_000); + + // Unpause bob to restore GRANDPA finality (needs 2/2 validators). + // Bob's process resumes immediately with full state, so he can vote + // on the pending blocks that alice produced while he was paused. + logger.info("Unpausing bob container..."); + await $`docker unpause ${bobContainer}`.quiet(); + logger.info("Bob unpaused. Waiting for finality and SlashReported event..."); + + // Poll for a SlashReported event from the ExternalValidatorsSlashes pallet. + // + // Why SlashReported instead of Slashes storage: + // pallet_im_online's UnresponsivenessOffence::slash_fraction() formula + // gives 0% for small validator sets (1 out of 2 offline is below the + // 10%+1 threshold). With 0% fraction, compute_slash returns None and + // no Slashes entry is created. However, on_offence still emits the + // SlashReported event, which proves the full detection pipeline works: + // pallet_im_online → EquivocationReportWrapper → pallet_offences → on_offence. + // + // After unpausing bob, GRANDPA finality catches up and the events from + // alice's blocks (produced during the pause) become finalized and visible + // to the PAPI event subscription. + let pollCount = 0; + const collectedEvents: any[] = []; + await waitFor({ + lambda: async () => { + pollCount++; + const events = await dhApi.event.ExternalValidatorsSlashes.SlashReported.pull(); + for (const event of events) { + const payload = (event as any)?.payload ?? event; + collectedEvents.push(payload); + logger.info( + `[poll ${pollCount}] SlashReported event: validator=${payload?.validator}, ` + + `fraction=${JSON.stringify(payload?.fraction)}, slash_era=${payload?.slash_era}` + ); + } + if (collectedEvents.length > 0) { + slashReportedEvent = collectedEvents[0]; + return true; + } + if (pollCount % 10 === 0) { + const curEra = await dhApi.query.ExternalValidators.ActiveEra.getValue({ at: "best" }); + const curSession = await dhApi.query.Session.CurrentIndex.getValue({ at: "best" }); + logger.info( + `[poll ${pollCount}] era: ${curEra?.index}, session: ${curSession} (started at era=${eraAtStart}, session=${sessionAtStart})` + ); + } + return false; + }, + iterations: 60, + delay: 5000, + errorMessage: "SlashReported event not found after pausing bob for liveness detection" + }); + } finally { + // Ensure bob is always unpaused so the network stays healthy for teardown + await $`docker unpause ${bobContainer}`.nothrow().quiet(); + } + + expect(slashReportedEvent).toBeDefined(); + logger.info( + "Liveness offence confirmed via SlashReported: " + + `validator=${slashReportedEvent.validator}, ` + + `fraction=${JSON.stringify(slashReportedEvent.fraction)}, ` + + `slash_era=${slashReportedEvent.slash_era}` + ); + }, 600_000); }); From 6a2eb38481f0925e49d19a0eef92f3d8e79510e1 Mon Sep 17 00:00:00 2001 From: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:08:13 +0100 Subject: [PATCH 09/17] fix(e2e): stabilize submitter CI and local relayer startup (#470) ## Summary - fixes the untrusted CI failure in `e2e-tests / E2E Tests with Kurtosis Ethereum Network` - keeps validator-set-submitter startup actionable by avoiding test-only contract config imports during container startup - improves submitter readiness diagnostics by capturing both stdout and stderr from container logs and making streamed log matching robust to chunked UTF-8 output - reduces validator-set-submitter Docker build time in CI by building from `test/` and adding a tight `test/.dockerignore` - makes local arm64 E2E runs use a native local Snowbridge relayer image instead of forcing `linux/amd64` emulation - auto-builds the local relayer image when needed for `:local` tags ## Why The original failing untrusted test started as a submitter container startup problem, but the branch now also addresses a second timeout path that showed up while debugging: - the submitter image was being built from the repository root with a large Docker context, which made the `validator-set-update` suite spend most of its hook timeout budget inside `docker build` - on Apple Silicon, forcing `datahavenxyz/snowbridge-relay:latest` through `linux/amd64` caused `generate-beacon-checkpoint` to segfault during local runs These changes make the submitter failure actionable, cut the CI Docker build context down substantially, and keep local E2E runs reliable on arm64. ## Validation - `cd test && bun fmt` - `cd test && bun x tsc --noEmit` - `bun test e2e/suites/validator-set-update.test.ts --timeout 900000` - `cd test && docker build -f tools/validator-set-submitter/Dockerfile -t datahavenxyz/validator-set-submitter:local .` --- test/.dockerignore | 15 ++++++ test/e2e/framework/submitter.ts | 14 ++--- test/e2e/framework/suite.ts | 3 +- test/launcher/network/index.ts | 18 ++++--- test/launcher/relayers.ts | 53 ++++++++++++++++--- test/tools/validator-set-submitter/Dockerfile | 17 +++--- test/tools/validator-set-submitter/config.ts | 2 +- test/utils/docker.ts | 21 +++++--- 8 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 test/.dockerignore diff --git a/test/.dockerignore b/test/.dockerignore new file mode 100644 index 000000000..4731dc5aa --- /dev/null +++ b/test/.dockerignore @@ -0,0 +1,15 @@ +# Keep submitter image build context minimal. +* + +!package.json +!bun.lock +!tsconfig.json +!bunfig.toml +!.papi/ +!.papi/** +!tools/validator-set-submitter/ +!tools/validator-set-submitter/** +!contract-bindings/ +!contract-bindings/** +!utils/ +!utils/** diff --git a/test/e2e/framework/submitter.ts b/test/e2e/framework/submitter.ts index cad168ebb..66ce466fb 100644 --- a/test/e2e/framework/submitter.ts +++ b/test/e2e/framework/submitter.ts @@ -17,13 +17,13 @@ const SUBMITTER_READY_TIMEOUT_SECONDS = 30; const SUBMITTER_LOG_TAIL_LINES = 200; /** - * Builds the validator-set-submitter Docker image from the repo root. + * Builds the validator-set-submitter Docker image from the test directory. */ export async function buildSubmitterImage(): Promise { logger.debug("Building validator-set-submitter Docker image..."); - const repoRoot = path.resolve(import.meta.dir, "../../.."); - await $`docker build -f test/tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .` - .cwd(repoRoot) + const testRoot = path.resolve(import.meta.dir, "../.."); + await $`docker build -f tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .` + .cwd(testRoot) .quiet(); logger.debug("Validator-set-submitter image built successfully"); } @@ -106,9 +106,11 @@ export async function launchSubmitter(options: LaunchSubmitterOptions): Promise< timeoutSeconds: SUBMITTER_READY_TIMEOUT_SECONDS }); } catch (error) { + const logResult = await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}` + .nothrow() + .quiet(); const logs = - (await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`.nothrow().text()) || - ""; + `${logResult.stdout.toString()}${logResult.stderr.toString()}`.trim() || ""; await stopSubmitter(containerName); throw new Error( `Submitter did not become ready. Expected log "${SUBMITTER_READY_LOG}". Last ${SUBMITTER_LOG_TAIL_LINES} log lines:\n${logs}`, diff --git a/test/e2e/framework/suite.ts b/test/e2e/framework/suite.ts index 5afb7144b..4a74252aa 100644 --- a/test/e2e/framework/suite.ts +++ b/test/e2e/framework/suite.ts @@ -3,6 +3,7 @@ import readline from "node:readline"; import { isCI } from "launcher/network"; import { logger } from "utils"; import { launchNetwork } from "../../launcher"; +import { getDefaultRelayerImageTag } from "../../launcher/network"; import type { LaunchNetworkResult } from "../../launcher/types"; import { ConnectorFactory, type TestConnectors } from "./connectors"; import { TestSuiteManager } from "./manager"; @@ -57,7 +58,7 @@ export abstract class BaseTestSuite { datahavenImageTag: this.options.networkOptions?.datahavenImageTag || "datahavenxyz/datahaven:local", relayerImageTag: - this.options.networkOptions?.relayerImageTag || "datahavenxyz/snowbridge-relay:latest", + this.options.networkOptions?.relayerImageTag || getDefaultRelayerImageTag(), buildDatahaven: false, // default to false in the test suite so we can speed up the CI ...this.options.networkOptions }); diff --git a/test/launcher/network/index.ts b/test/launcher/network/index.ts index 55c1139f4..783eb162e 100644 --- a/test/launcher/network/index.ts +++ b/test/launcher/network/index.ts @@ -144,6 +144,7 @@ export const launchNetwork = async ( options: NetworkLaunchOptions ): Promise => { const networkId = options.networkId; + const relayerImageTag = options.relayerImageTag || getDefaultRelayerImageTag(); const launchedNetwork = new LaunchedNetwork(); launchedNetwork.networkName = networkId; let injectContracts = false; @@ -177,7 +178,7 @@ export const launchNetwork = async ( { networkId, datahavenImageTag: options.datahavenImageTag || "datahavenxyz/datahaven:local", - relayerImageTag: options.relayerImageTag || "datahavenxyz/snowbridge-relay:latest", + relayerImageTag, authorityIds: TEST_AUTHORITY_IDS, buildDatahaven: options.buildDatahaven ?? !isCI, // if not specified, default to false for CI, true for local testing datahavenBuildExtraArgs: options.datahavenBuildExtraArgs || "--features=fast-runtime" @@ -248,14 +249,10 @@ export const launchNetwork = async ( // 7. Launch relayers logger.info("❄️ Launching Snowbridge relayers..."); - if (!options.relayerImageTag) { - throw new Error("Relayer image tag not specified"); - } - await launchRelayers( { networkId, - relayerImageTag: options.relayerImageTag, + relayerImageTag, kurtosisEnclaveName }, launchedNetwork @@ -297,4 +294,13 @@ export const launchNetwork = async ( } }; +export const getDefaultRelayerImageTag = (): string => { + if (process.env.RELAYER_IMAGE_TAG) { + return process.env.RELAYER_IMAGE_TAG; + } + return process.arch === "arm64" + ? "datahavenxyz/snowbridge-relay:local" + : "datahavenxyz/snowbridge-relay:latest"; +}; + export const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; diff --git a/test/launcher/relayers.ts b/test/launcher/relayers.ts index a64cbad47..d1f335693 100644 --- a/test/launcher/relayers.ts +++ b/test/launcher/relayers.ts @@ -93,6 +93,47 @@ export const RELAYER_CONFIG_PATHS = { SOLOCHAIN: path.join(RELAYER_CONFIG_DIR, "solochain-relay.json") }; +const LOCAL_RELAYER_SOURCE_DIR = path.resolve( + import.meta.dir, + "..", + "..", + "contracts", + "lib", + "snowbridge", + "relayer" +); + +const isLocalRelayerImage = (relayerImageTag: string): boolean => + relayerImageTag.endsWith(":local"); + +const ensureLocalRelayerImage = async (relayerImageTag: string): Promise => { + if (!isLocalRelayerImage(relayerImageTag)) { + return; + } + + const localImageExists = await $`docker image inspect ${relayerImageTag}`.nothrow().quiet(); + if (localImageExists.exitCode === 0) { + logger.debug(`Local relayer image already available: ${relayerImageTag}`); + return; + } + + const dockerfilePath = path.join(LOCAL_RELAYER_SOURCE_DIR, "Dockerfile"); + const dockerfileExists = await Bun.file(dockerfilePath).exists(); + invariant( + dockerfileExists, + `❌ Local relayer Dockerfile not found at ${dockerfilePath}. Cannot build ${relayerImageTag}` + ); + + logger.info( + `🐳 Local relayer image ${relayerImageTag} not found. Building from ${LOCAL_RELAYER_SOURCE_DIR} for ${process.arch}...` + ); + await runShellCommandWithLogger(`docker build -f Dockerfile -t ${relayerImageTag} .`, { + cwd: LOCAL_RELAYER_SOURCE_DIR, + logLevel: "debug" + }); + logger.success(`✅ Built local relayer image: ${relayerImageTag}`); +}; + /** * Generates configuration files for relayers. * @@ -278,16 +319,16 @@ export const initEthClientPallet = async ( process.platform === "linux" ? "--add-host host.docker.internal:host-gateway" : ""; // Opportunistic pull - pull the image from Docker Hub only if it's not a local image - const isLocal = relayerImageTag.endsWith(":local"); + const isLocal = isLocalRelayerImage(relayerImageTag); + const platformParam = isLocal ? "" : "--platform linux/amd64"; logger.debug("Generating beacon checkpoint"); const datastoreHostPath = path.resolve(datastorePath); - const command = `docker run \ + const command = `docker run ${platformParam} \ -v ${beaconConfigHostPath}:${beaconConfigContainerPath}:ro \ -v ${checkpointHostPath}:${checkpointContainerPath} \ -v ${datastoreHostPath}:/data \ --name generate-beacon-checkpoint-${networkId} \ - --platform linux/amd64 \ --workdir /app \ ${addHostParam} \ ${launchedNetwork.networkName ? `--network ${launchedNetwork.networkName}` : ""} \ @@ -400,6 +441,7 @@ export const launchRelayers = async ( const { relayerImageTag, kurtosisEnclaveName } = options; invariant(relayerImageTag, "❌ relayerImageTag is required"); + await ensureLocalRelayerImage(relayerImageTag); await killExistingContainers("snowbridge-"); @@ -623,7 +665,7 @@ const launchRelayerContainers = async ( launchedNetwork: LaunchedNetwork, networkId: string ): Promise => { - const isLocal = relayerImageTag.endsWith(":local"); + const isLocal = isLocalRelayerImage(relayerImageTag); const networkName = launchedNetwork.networkName; invariant(networkName, "❌ Docker network name not found in LaunchedNetwork instance"); const restartArgs = ["--restart", "on-failure:5"]; @@ -641,8 +683,7 @@ const launchRelayerContainers = async ( "docker", "run", "-d", - "--platform", - "linux/amd64", + ...(isLocal ? [] : ["--platform", "linux/amd64"]), "--add-host", "host.docker.internal:host-gateway", "--name", diff --git a/test/tools/validator-set-submitter/Dockerfile b/test/tools/validator-set-submitter/Dockerfile index 84edba6d1..584a0f203 100644 --- a/test/tools/validator-set-submitter/Dockerfile +++ b/test/tools/validator-set-submitter/Dockerfile @@ -1,7 +1,8 @@ # Validator Set Submitter image # -# Build from the repository root: -# docker build -f test/tools/validator-set-submitter/Dockerfile \ +# Build from the test directory: +# cd test +# docker build -f tools/validator-set-submitter/Dockerfile \ # -t datahavenxyz/validator-set-submitter:local . # # Runtime expectations: @@ -13,8 +14,8 @@ FROM oven/bun:1.3.3-slim AS deps WORKDIR /app -COPY test/package.json test/bun.lock test/tsconfig.json ./ -COPY test/.papi ./.papi +COPY package.json bun.lock tsconfig.json ./ +COPY .papi ./.papi RUN bun install --frozen-lockfile --production FROM oven/bun:1.3.3-slim @@ -24,10 +25,10 @@ WORKDIR /app RUN useradd -m -u 1001 -U -s /bin/sh -d /submitter submitter COPY --from=deps /app/node_modules ./node_modules -COPY test/tsconfig.json test/bunfig.toml ./ -COPY test/tools/validator-set-submitter/ ./tools/validator-set-submitter/ -COPY test/contract-bindings/ ./contract-bindings/ -COPY test/utils/ ./utils/ +COPY tsconfig.json bunfig.toml ./ +COPY tools/validator-set-submitter/ ./tools/validator-set-submitter/ +COPY contract-bindings/ ./contract-bindings/ +COPY utils/ ./utils/ ENV NODE_ENV=production diff --git a/test/tools/validator-set-submitter/config.ts b/test/tools/validator-set-submitter/config.ts index dc9a73d84..4c6107c94 100644 --- a/test/tools/validator-set-submitter/config.ts +++ b/test/tools/validator-set-submitter/config.ts @@ -1,4 +1,3 @@ -import { parseDeploymentsFile } from "utils"; import { parseEther } from "viem"; import { parse as parseYaml } from "yaml"; @@ -37,6 +36,7 @@ export async function loadConfig( let serviceManagerAddress = optionalHexString(raw, "service_manager_address"); if (!serviceManagerAddress) { + const { parseDeploymentsFile } = await import("../../utils/contracts.ts"); const deployments = await parseDeploymentsFile(networkId); serviceManagerAddress = deployments.ServiceManager; } diff --git a/test/utils/docker.ts b/test/utils/docker.ts index 7029e7650..b1c00b62d 100644 --- a/test/utils/docker.ts +++ b/test/utils/docker.ts @@ -178,6 +178,13 @@ export async function waitForLog(opts: { const { readable } = Transform.toWeb(pass); const decoder = new TextDecoder(); + let bufferedLogs = ""; + const hasHit = (text: string): boolean => { + if (typeof opts.search === "string") return text.includes(opts.search); + // Avoid stateful regex surprises with /g or /y across multiple checks. + opts.search.lastIndex = 0; + return opts.search.test(text); + }; const timer = setTimeout( () => pass.destroy( @@ -190,14 +197,16 @@ export async function waitForLog(opts: { try { for await (const chunk of readable) { - const text = decoder.decode(chunk as Uint8Array, { stream: false }); - - const hit = - typeof opts.search === "string" ? text.includes(opts.search) : opts.search.test(text); - - if (hit) return text.trim(); + bufferedLogs += decoder.decode(chunk as Uint8Array, { stream: true }); + if (hasHit(bufferedLogs)) return bufferedLogs.trim(); + if (bufferedLogs.length > 64_000) { + bufferedLogs = bufferedLogs.slice(-64_000); + } } + bufferedLogs += decoder.decode(); + if (hasHit(bufferedLogs)) return bufferedLogs.trim(); + throw new Error( `Log stream ended before "${opts.search}" appeared for container ${opts.containerName}` ); From fe8d65aa7e40ccac86b682394d374da4e9b82091 Mon Sep 17 00:00:00 2001 From: undercover-cactus Date: Mon, 9 Mar 2026 14:33:43 +0100 Subject: [PATCH 10/17] fix: Register the snowbridge agent in the Dathaven Service instead of the operator node (#428) This PR rename the `rewardsAgentOrigin`, `rewardsMessageOrigin`, etc... into a less specific less now that the Snowbrige Agent is also being used to relay slashing messages. This PR also have a fix to register the Agent address instead of the operator node address to check the sender of the message. Without this fix we could never relay rewards or execute slashing because we would get an error regarding the message. * Removing the prefix `rewards` everytime we were refering the snowbridge agent (to clarify that the agent is not only being used by the reward feature) * Fix the deployment script to register the `agentAddress` as the required sender for relaying substrate message [ ] ~~Rename `onlyRewardsInitiator` and `rewardsInitiator` in the `DatahavenServiceManager.sol ` for something that would include slashing~~ This should be done in another PR. [x] Check the Testnet Deploy script to make sure it is using the agent address --------- Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> --- contracts/config/anvil.json | 2 +- contracts/config/example.jsonc | 6 +- contracts/config/mainnet-ethereum.json | 2 +- contracts/config/stagenet-hoodi.json | 2 +- contracts/config/testnet-hoodi.json | 2 +- contracts/deployments/anvil-agent-info.json | 1 + contracts/deployments/anvil-rewards-info.json | 2 +- .../stagenet-hoodi-rewards-info.json | 2 +- contracts/script/deploy/Config.sol | 2 +- contracts/script/deploy/DeployBase.s.sol | 51 ++++++++-------- contracts/script/deploy/DeployParams.s.sol | 3 +- operator/runtime/mainnet/src/configs/mod.rs | 6 +- .../mainnet/src/configs/runtime_params.rs | 4 +- operator/runtime/stagenet/src/configs/mod.rs | 16 ++--- .../stagenet/src/configs/runtime_params.rs | 4 +- operator/runtime/testnet/src/configs/mod.rs | 17 +++--- .../testnet/src/configs/runtime_params.rs | 4 +- test/.papi/descriptors/package.json | 2 +- test/cli/handlers/contracts/rewards-origin.ts | 58 +++++++++---------- test/cli/index.ts | 9 +-- .../parameters/datahaven-parameters.json | 2 +- test/e2e/suites/slash.test.ts | 18 +++++- test/utils/types.ts | 2 +- 23 files changed, 115 insertions(+), 102 deletions(-) create mode 100644 contracts/deployments/anvil-agent-info.json diff --git a/contracts/config/anvil.json b/contracts/config/anvil.json index 8e3796c37..4a134ced9 100644 --- a/contracts/config/anvil.json +++ b/contracts/config/anvil.json @@ -35,7 +35,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 2, "startBlock": 1, - "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + "messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", "initialValidatorSetId": 0, "initialValidatorHashes": [ "0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7", diff --git a/contracts/config/example.jsonc b/contracts/config/example.jsonc index e57c1ec72..aeef7cc45 100644 --- a/contracts/config/example.jsonc +++ b/contracts/config/example.jsonc @@ -99,9 +99,9 @@ /// Initial BEEFY block number. Set to latest finalized block by update-beefy-checkpoint. /// The BeefyClient will only accept BEEFY commitments with block numbers > startBlock. "startBlock": 1, - /// The origin linked to the Rewards Agent, the Agent contract who's allowed to submit - /// new reward merkle roots to the RewardsRegistry contract. - "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + /// The origin linked to the Agent, the Agent contract who's allowed to submit + /// new reward merkle roots or slashes. + "messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", /// The BEEFY validator set ID for the initial validators. /// This is fetched from Beefy.ValidatorSetId on the DataHaven chain. "initialValidatorSetId": 0, diff --git a/contracts/config/mainnet-ethereum.json b/contracts/config/mainnet-ethereum.json index 5a0e13fb0..28775dad8 100644 --- a/contracts/config/mainnet-ethereum.json +++ b/contracts/config/mainnet-ethereum.json @@ -43,7 +43,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 16, "startBlock": 1, - "rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", + "messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000", "initialValidatorSetId": 0, "initialValidatorHashes": [], "nextValidatorSetId": 1, diff --git a/contracts/config/stagenet-hoodi.json b/contracts/config/stagenet-hoodi.json index a79ac73ab..40d80dbc7 100644 --- a/contracts/config/stagenet-hoodi.json +++ b/contracts/config/stagenet-hoodi.json @@ -44,7 +44,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 3, "startBlock": 1303065, - "rewardsMessageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd", + "messageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd", "initialValidatorSetId": 2186, "initialValidatorHashes": [ "0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10", diff --git a/contracts/config/testnet-hoodi.json b/contracts/config/testnet-hoodi.json index 1aa56fee0..3396b7b41 100644 --- a/contracts/config/testnet-hoodi.json +++ b/contracts/config/testnet-hoodi.json @@ -44,7 +44,7 @@ "randaoCommitExpiration": 24, "minNumRequiredSignatures": 5, "startBlock": 1381173, - "rewardsMessageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215", + "messageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215", "initialValidatorSetId": 2303, "initialValidatorHashes": [ "0x0ec3102f334aba804c18b843e45ec874005587122a1b49273b1b04d6fd98b1a2", diff --git a/contracts/deployments/anvil-agent-info.json b/contracts/deployments/anvil-agent-info.json new file mode 100644 index 000000000..b0bc68827 --- /dev/null +++ b/contracts/deployments/anvil-agent-info.json @@ -0,0 +1 @@ +{"Agent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file diff --git a/contracts/deployments/anvil-rewards-info.json b/contracts/deployments/anvil-rewards-info.json index ea1ee44d4..c91dba57e 100644 --- a/contracts/deployments/anvil-rewards-info.json +++ b/contracts/deployments/anvil-rewards-info.json @@ -1 +1 @@ -{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","RewardsAgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file +{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file diff --git a/contracts/deployments/stagenet-hoodi-rewards-info.json b/contracts/deployments/stagenet-hoodi-rewards-info.json index 32bba16a1..fc588b28d 100644 --- a/contracts/deployments/stagenet-hoodi-rewards-info.json +++ b/contracts/deployments/stagenet-hoodi-rewards-info.json @@ -1 +1 @@ -{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","RewardsAgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"} \ No newline at end of file +{"RewardsAgent": "0x2E039a88838241d1Ac738cf2e3C5763ba12571e7","AgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"} \ No newline at end of file diff --git a/contracts/script/deploy/Config.sol b/contracts/script/deploy/Config.sol index 635589886..4d076407d 100644 --- a/contracts/script/deploy/Config.sol +++ b/contracts/script/deploy/Config.sol @@ -12,7 +12,7 @@ contract Config { bytes32[] initialValidatorHashes; uint128 nextValidatorSetId; bytes32[] nextValidatorHashes; - bytes32 rewardsMessageOrigin; + bytes32 messageOrigin; } // AVS parameters diff --git a/contracts/script/deploy/DeployBase.s.sol b/contracts/script/deploy/DeployBase.s.sol index 64de00e45..145c54aa0 100644 --- a/contracts/script/deploy/DeployBase.s.sol +++ b/contracts/script/deploy/DeployBase.s.sol @@ -123,7 +123,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { BeefyClient beefyClient, AgentExecutor agentExecutor, IGatewayV2 gateway, - address payable rewardsAgentAddress + address payable agentAddress ) = _deploySnowbridge(snowbridgeConfig); Logging.logFooter(); _logProgress(); @@ -132,14 +132,14 @@ abstract contract DeployBase is Script, DeployParams, Accounts { ( DataHavenServiceManager serviceManager, DataHavenServiceManager serviceManagerImplementation - ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway); + ) = _deployDataHavenContracts(avsConfig, proxyAdmin, gateway, agentAddress); Logging.logFooter(); _logProgress(); // Final configuration (same for both modes) Logging.logHeader("FINAL CONFIGURATION"); - Logging.logContractDeployed("Rewards Agent Address", rewardsAgentAddress); + Logging.logContractDeployed("Agent Address", agentAddress); Logging.logFooter(); _logProgress(); @@ -150,11 +150,11 @@ abstract contract DeployBase is Script, DeployParams, Accounts { gateway, serviceManager, serviceManagerImplementation, - rewardsAgentAddress, + agentAddress, proxyAdmin ); - _outputRewardsAgentInfo(rewardsAgentAddress, snowbridgeConfig.rewardsMessageOrigin); + _outputAgentInfo(agentAddress, snowbridgeConfig.messageOrigin); } /** @@ -201,11 +201,11 @@ abstract contract DeployBase is Script, DeployParams, Accounts { // Create Agent Logging.logSection("Creating Snowbridge Agent"); vm.broadcast(_deployerPrivateKey); - gateway.v2_createAgent(config.rewardsMessageOrigin); - address payable rewardsAgentAddress = payable(gateway.agentOf(config.rewardsMessageOrigin)); - Logging.logContractDeployed("Rewards Agent", rewardsAgentAddress); + gateway.v2_createAgent(config.messageOrigin); + address payable agentAddress = payable(gateway.agentOf(config.messageOrigin)); + Logging.logContractDeployed("Rewards Agent", agentAddress); - return (beefyClient, agentExecutor, gateway, rewardsAgentAddress); + return (beefyClient, agentExecutor, gateway, agentAddress); } /** @@ -240,7 +240,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts { function _deployDataHavenContracts( AVSConfig memory avsConfig, ProxyAdmin proxyAdmin, - IGatewayV2 gateway + IGatewayV2 gateway, + address agentAddress ) internal returns (DataHavenServiceManager, DataHavenServiceManager) { Logging.logHeader("DATAHAVEN CUSTOM CONTRACTS DEPLOYMENT"); @@ -269,7 +270,7 @@ abstract contract DeployBase is Script, DeployParams, Accounts { // Create service manager initialisation parameters struct ServiceManagerInitParams memory initParams = ServiceManagerInitParams({ avsOwner: avsConfig.avsOwner, - rewardsInitiator: avsConfig.rewardsInitiator, + rewardsInitiator: agentAddress, validatorsStrategiesAndMultipliers: strategiesAndMultipliers, gateway: address(gateway), validatorSetSubmitter: avsConfig.validatorSetSubmitter, @@ -313,38 +314,38 @@ abstract contract DeployBase is Script, DeployParams, Accounts { IGatewayV2 gateway, DataHavenServiceManager serviceManager, DataHavenServiceManager serviceManagerImplementation, - address rewardsAgent, + address agent, ProxyAdmin proxyAdmin ) internal virtual; /** - * @notice Output rewards agent info (shared across all deployment types) + * @notice Output agent info (shared across all deployment types) */ - function _outputRewardsAgentInfo( - address rewardsAgent, - bytes32 rewardsAgentOrigin + function _outputAgentInfo( + address agent, + bytes32 agentOrigin ) internal { - Logging.logHeader("REWARDS AGENT INFO"); - Logging.logContractDeployed("RewardsAgent", rewardsAgent); - Logging.logAgentOrigin("RewardsAgentOrigin", vm.toString(rewardsAgentOrigin)); + Logging.logHeader("AGENT INFO"); + Logging.logContractDeployed("Agent", agent); + Logging.logAgentOrigin("AgentOrigin", vm.toString(agentOrigin)); Logging.logFooter(); // Write to deployment file for future reference string memory network = _getNetworkName(); - string memory rewardsInfoPath = - string.concat(vm.projectRoot(), "/deployments/", network, "-rewards-info.json"); + string memory agentInfoPath = + string.concat(vm.projectRoot(), "/deployments/", network, "-agent-info.json"); // Create directory if it doesn't exist vm.createDir(string.concat(vm.projectRoot(), "/deployments"), true); // Create JSON with rewards info string memory json = "{"; - json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",'); - json = string.concat(json, '"RewardsAgentOrigin": "', vm.toString(rewardsAgentOrigin), '"'); + json = string.concat(json, '"Agent": "', vm.toString(agent), '",'); + json = string.concat(json, '"AgentOrigin": "', vm.toString(agentOrigin), '"'); json = string.concat(json, "}"); // Write to file - vm.writeFile(rewardsInfoPath, json); - Logging.logInfo(string.concat("Rewards info saved to: ", rewardsInfoPath)); + vm.writeFile(agentInfoPath, json); + Logging.logInfo(string.concat("Agent info saved to: ", agentInfoPath)); } } diff --git a/contracts/script/deploy/DeployParams.s.sol b/contracts/script/deploy/DeployParams.s.sol index eda4630fb..c77f99b9e 100644 --- a/contracts/script/deploy/DeployParams.s.sol +++ b/contracts/script/deploy/DeployParams.s.sol @@ -24,8 +24,7 @@ contract DeployParams is Script, Config { config.minNumRequiredSignatures = vm.parseJsonUint(configJson, ".snowbridge.minNumRequiredSignatures"); config.startBlock = vm.parseJsonUint(configJson, ".snowbridge.startBlock").toUint64(); - config.rewardsMessageOrigin = - vm.parseJsonBytes32(configJson, ".snowbridge.rewardsMessageOrigin"); + config.messageOrigin = vm.parseJsonBytes32(configJson, ".snowbridge.messageOrigin"); // Load validators from file or generate placeholder ones in dev mode bool isDevMode = keccak256(abi.encodePacked(vm.envOr("DEV_MODE", string("false")))) diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index ea5a49fac..48f1aad9b 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -1518,7 +1518,7 @@ impl datahaven_runtime_common::rewards_adapter::RewardsSubmissionConfig for Main } fn rewards_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies_and_multipliers() -> Vec<(H160, u128)> { @@ -1694,9 +1694,9 @@ impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for Main runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get() } + // TODO: remove `slashes_` prefix and just call it `agent_origin` fn slashes_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() - // TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ? + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies() -> Vec
{ diff --git a/operator/runtime/mainnet/src/configs/runtime_params.rs b/operator/runtime/mainnet/src/configs/runtime_params.rs index aa35f269d..9d3cdeb3c 100644 --- a/operator/runtime/mainnet/src/configs/runtime_params.rs +++ b/operator/runtime/mainnet/src/configs/runtime_params.rs @@ -48,9 +48,9 @@ pub mod dynamic_params { #[codec(index = 3)] #[allow(non_upper_case_globals)] - /// The RewardsAgentOrigin is the hash of the string "external_validators_rewards" + /// The AgentOrigin is the hash of the string "external_validators_rewards" /// TODO: Decide which agent origin we want to use. Currently for testing it's the zero hash - pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!( + pub static AgentOrigin: H256 = H256::from_slice(&hex!( "c505dfb2df107d106d08bd0f1a0acd10052ca9aa078629a4ccfd0c90c6e69b65" )); diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index 4b0fc4a7a..12b4a960d 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -1514,7 +1514,7 @@ impl datahaven_runtime_common::rewards_adapter::RewardsSubmissionConfig for Stag } fn rewards_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies_and_multipliers() -> Vec<(H160, u128)> { @@ -1691,7 +1691,7 @@ impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for Stag } fn slashes_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() // TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ? } @@ -1953,8 +1953,8 @@ mod tests { /// Test that the Rewards Agent ID (used for Snowbridge outbound messages from the rewards pallet) /// is correctly computed from the chain's genesis hash and the ExternalValidatorRewardsAccount. /// - /// This test verifies the value that should be set as `RewardsAgentOrigin` in runtime parameters - /// and as `rewardsMessageOrigin` in the AVS contract configuration. + /// This test verifies the value that should be set as `AgentOrigin` in runtime parameters + /// and as `messageOrigin` in the AVS contract configuration. /// /// The Agent ID is computed following Snowbridge's pattern for GlobalConsensus locations: /// blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), compact_len, "AccountKey20", account_key)) @@ -1994,8 +1994,8 @@ mod tests { // Hash with blake2_256 let computed_agent_id = H256(blake2_256(&encoded)); - // Expected Agent ID - this value must match RewardsAgentOrigin in runtime_params.rs - // If this test fails, update RewardsAgentOrigin to match the computed value. + // Expected Agent ID - this value must match AgentOrigin in runtime_params.rs + // If this test fails, update AgentOrigin to match the computed value. let expected_agent_id = H256(hex_literal::hex!( "56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd" )); @@ -2005,8 +2005,8 @@ mod tests { expected_agent_id, "Computed Rewards Agent ID must match expected value.\n\ This value should be set as:\n\ - - RewardsAgentOrigin in runtime_params.rs\n\ - - rewardsMessageOrigin in AVS contract config\n\ + - AgentOrigin in runtime_params.rs\n\ + - messageOrigin in AVS contract config\n\ \n\ Rewards account: 0x{}\n\ Genesis hash: 0x{}\n\ diff --git a/operator/runtime/stagenet/src/configs/runtime_params.rs b/operator/runtime/stagenet/src/configs/runtime_params.rs index d7775020d..dcf136c92 100644 --- a/operator/runtime/stagenet/src/configs/runtime_params.rs +++ b/operator/runtime/stagenet/src/configs/runtime_params.rs @@ -51,13 +51,13 @@ pub mod dynamic_params { #[codec(index = 3)] #[allow(non_upper_case_globals)] - /// The RewardsAgentOrigin is the Agent ID for the rewards pallet's outbound Snowbridge messages. + /// The AgentOrigin is the Agent ID for the rewards/slashes pallet's outbound Snowbridge messages. /// Computed as: blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), interior)) /// where interior = SCALE_ENCODE("AccountKey20", ExternalValidatorRewardsAccount) /// /// For stagenet with genesis hash 0x72d0856fd339e09cb21df7bac8ac3120bd871e327ec0e1658395df68acef9bee /// and rewards account 0x6d6f646c64682f65767265770000000000000000 (from PalletId "dh/evrew"): - pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!( + pub static AgentOrigin: H256 = H256::from_slice(&hex!( "56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd" )); diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 6749b567c..27dbc5384 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -1518,7 +1518,7 @@ impl datahaven_runtime_common::rewards_adapter::RewardsSubmissionConfig for Test } fn rewards_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies_and_multipliers() -> Vec<(H160, u128)> { @@ -1695,8 +1695,7 @@ impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for Test } fn slashes_agent_origin() -> H256 { - runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get() - // TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ? + runtime_params::dynamic_params::runtime_config::AgentOrigin::get() } fn strategies() -> Vec
{ @@ -1975,8 +1974,8 @@ mod tests { /// Test that the Rewards Agent ID (used for Snowbridge outbound messages from the rewards pallet) /// is correctly computed from the chain's genesis hash and the ExternalValidatorRewardsAccount. /// - /// This test verifies the value that should be set as `RewardsAgentOrigin` in runtime parameters - /// and as `rewardsMessageOrigin` in the AVS contract configuration. + /// This test verifies the value that should be set as `AgentOrigin` in runtime parameters + /// and as `messageOrigin` in the AVS contract configuration. /// /// The Agent ID is computed following Snowbridge's pattern for GlobalConsensus locations: /// blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), compact_len, "AccountKey20", account_key)) @@ -2016,8 +2015,8 @@ mod tests { // Hash with blake2_256 let computed_agent_id = H256(blake2_256(&encoded)); - // Expected Agent ID - this value must match RewardsAgentOrigin in runtime_params.rs - // If this test fails, update RewardsAgentOrigin to match the computed value. + // Expected Agent ID - this value must match AgentOrigin in runtime_params.rs + // If this test fails, update AgentOrigin to match the computed value. let expected_agent_id = H256(hex_literal::hex!( "d0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215" )); @@ -2027,8 +2026,8 @@ mod tests { expected_agent_id, "Computed Rewards Agent ID must match expected value.\n\ This value should be set as:\n\ - - RewardsAgentOrigin in runtime_params.rs\n\ - - rewardsMessageOrigin in AVS contract config\n\ + - AgentOrigin in runtime_params.rs\n\ + - messageOrigin in AVS contract config\n\ \n\ Rewards account: 0x{}\n\ Genesis hash: 0x{}\n\ diff --git a/operator/runtime/testnet/src/configs/runtime_params.rs b/operator/runtime/testnet/src/configs/runtime_params.rs index 5753d215e..40c891085 100644 --- a/operator/runtime/testnet/src/configs/runtime_params.rs +++ b/operator/runtime/testnet/src/configs/runtime_params.rs @@ -49,13 +49,13 @@ pub mod dynamic_params { #[codec(index = 3)] #[allow(non_upper_case_globals)] - /// The RewardsAgentOrigin is the Agent ID for the rewards pallet's outbound Snowbridge messages. + /// The AgentOrigin is the Agent ID for the rewards/slashes pallet's outbound Snowbridge messages. /// Computed as: blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), interior)) /// where interior = SCALE_ENCODE("AccountKey20", ExternalValidatorRewardsAccount) /// /// For testnet with genesis hash 0xdbf403d348916fb0694485bc7f9c0d8c53fdf86664ebac019af209c090c3df99 /// and rewards account 0x6d6f646c64682f65767265770000000000000000 (from PalletId "dh/evrew"): - pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!( + pub static AgentOrigin: H256 = H256::from_slice(&hex!( "d0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215" )); diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index d5f24906b..5ad366a9f 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.18296316742446681711", + "version": "0.1.0-autogenerated.15484599658830368838", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/cli/handlers/contracts/rewards-origin.ts b/test/cli/handlers/contracts/rewards-origin.ts index d89c31bd2..4da1b1169 100644 --- a/test/cli/handlers/contracts/rewards-origin.ts +++ b/test/cli/handlers/contracts/rewards-origin.ts @@ -48,7 +48,7 @@ const palletIdToAccountId20 = (palletId: string): Hex => { * blake2_256(SCALE_ENCODE(("GlobalConsensus", ByGenesis(genesis_hash), ("AccountKey20", account_key)))) * * NOTE: This computation follows Snowbridge's pattern but may need verification against - * the actual on-chain Agent ID. The preferred approach is to set RewardsAgentOrigin on + * the actual on-chain Agent ID. The preferred approach is to set AgentOrigin on * the chain and fetch it via this command. * * @param genesisHash - The chain's genesis hash (32 bytes, hex string with 0x prefix) @@ -116,36 +116,36 @@ const computeAgentId = async (genesisHash: Hex, accountKey20: Hex): Promise }; /** - * Fetches the RewardsAgentOrigin from the runtime parameters. + * Fetches the AgentOrigin from the runtime parameters. * * @param rpcUrl - WebSocket RPC endpoint of the DataHaven chain - * @returns The RewardsAgentOrigin as a hex string, or null if not set or zero + * @returns The AgentOrigin as a hex string, or null if not set or zero */ -const fetchRewardsAgentOrigin = async (rpcUrl: string): Promise => { +const fetchAgentOrigin = async (rpcUrl: string): Promise => { logger.info(`📡 Connecting to DataHaven chain at ${rpcUrl}...`); const { client: papiClient, typedApi: dhApi } = createPapiConnectors(rpcUrl); try { - logger.info("🔍 Fetching RewardsAgentOrigin from runtime parameters..."); + logger.info("🔍 Fetching AgentOrigin from runtime parameters..."); - // Query the Parameters pallet for RewardsAgentOrigin + // Query the Parameters pallet for AgentOrigin const parameter = await dhApi.query.Parameters.Parameters.getValue( { type: "RuntimeConfig", - value: { type: "RewardsAgentOrigin", value: undefined } + value: { type: "AgentOrigin", value: undefined } }, { at: "best" } ); if (!parameter) { - logger.info("ℹ️ RewardsAgentOrigin parameter not found (using default)"); + logger.info("ℹ️ AgentOrigin parameter not found (using default)"); return null; } // Extract the value from the parameter result // The parameter is wrapped in the RuntimeConfig enum variant - if (parameter.type === "RuntimeConfig" && parameter.value.type === "RewardsAgentOrigin") { + if (parameter.type === "RuntimeConfig" && parameter.value.type === "AgentOrigin") { const origin = parameter.value.value; if (origin) { const originHex = origin.asHex(); @@ -153,15 +153,15 @@ const fetchRewardsAgentOrigin = async (rpcUrl: string): Promise => { const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex; if (originHex === zeroHash) { - logger.info("ℹ️ RewardsAgentOrigin is set to zero (placeholder)"); + logger.info("ℹ️ AgentOrigin is set to zero (placeholder)"); return null; } - logger.success(`Found RewardsAgentOrigin: ${originHex}`); + logger.success(`Found AgentOrigin: ${originHex}`); return originHex as Hex; } } - logger.info("ℹ️ RewardsAgentOrigin value not available"); + logger.info("ℹ️ AgentOrigin value not available"); return null; } finally { papiClient.destroy(); @@ -193,9 +193,9 @@ const fetchGenesisHash = async (rpcUrl: string): Promise => { * Updates the config file with the rewards message origin. * * @param networkId - The network identifier (e.g., "hoodi", "stagenet-hoodi") - * @param rewardsMessageOrigin - The rewards message origin (Agent ID) + * @param messageOrigin - The rewards message origin (Agent ID) */ -const updateConfigFile = async (networkId: string, rewardsMessageOrigin: Hex): Promise => { +const updateConfigFile = async (networkId: string, messageOrigin: Hex): Promise => { const configFilePath = `../contracts/config/${networkId}.json`; const configFile = Bun.file(configFilePath); @@ -211,21 +211,21 @@ const updateConfigFile = async (networkId: string, rewardsMessageOrigin: Hex): P configJson.snowbridge = {}; } - const oldOrigin = configJson.snowbridge.rewardsMessageOrigin; - configJson.snowbridge.rewardsMessageOrigin = rewardsMessageOrigin; + const oldOrigin = configJson.snowbridge.messageOrigin; + configJson.snowbridge.messageOrigin = messageOrigin; await Bun.write(configFilePath, `${JSON.stringify(configJson, null, 2)}\n`); logger.success(`Config file updated: ${configFilePath}`); - if (oldOrigin !== rewardsMessageOrigin) { - logger.info(` rewardsMessageOrigin: ${oldOrigin ?? "unset"} -> ${rewardsMessageOrigin}`); + if (oldOrigin !== messageOrigin) { + logger.info(` messageOrigin: ${oldOrigin ?? "unset"} -> ${messageOrigin}`); } }; /** * Main handler for the update-rewards-origin command. - * Fetches or computes the RewardsAgentOrigin and updates the config file. + * Fetches or computes the AgentOrigin and updates the config file. */ export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): Promise => { const networkId = buildNetworkId(options.chain, options.environment); @@ -246,17 +246,17 @@ export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): printDivider(); try { - // Step 1: Try to fetch RewardsAgentOrigin from the chain - let rewardsMessageOrigin = await fetchRewardsAgentOrigin(options.rpcUrl); + // Step 1: Try to fetch AgentOrigin from the chain + let messageOrigin = await fetchAgentOrigin(options.rpcUrl); printDivider(); - if (rewardsMessageOrigin) { + if (messageOrigin) { // Use the value from the chain - logger.info("✅ Using RewardsAgentOrigin from chain runtime parameters"); + logger.info("✅ Using AgentOrigin from chain runtime parameters"); } else { // Compute the Agent ID from genesis hash and pallet account - logger.info("🔧 Computing RewardsAgentOrigin from genesis hash and pallet account..."); + logger.info("🔧 Computing AgentOrigin from genesis hash and pallet account..."); // Get genesis hash (from option or fetch from chain) const genesisHash = options.genesisHash @@ -272,22 +272,22 @@ export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): // Compute the Agent ID logger.info("🔐 Computing Agent ID..."); logger.warn( - "⚠️ Note: Computed Agent ID may need verification. Prefer setting RewardsAgentOrigin on-chain." + "⚠️ Note: Computed Agent ID may need verification. Prefer setting AgentOrigin on-chain." ); - rewardsMessageOrigin = await computeAgentId(genesisHash, rewardsAccount); - logger.info(` Agent ID: ${rewardsMessageOrigin}`); + messageOrigin = await computeAgentId(genesisHash, rewardsAccount); + logger.info(` Agent ID: ${messageOrigin}`); } printDivider(); // Display the final value logger.info("📝 Rewards Message Origin:"); - logger.info(` ${rewardsMessageOrigin}`); + logger.info(` ${messageOrigin}`); printDivider(); // Update the config file - await updateConfigFile(networkId, rewardsMessageOrigin); + await updateConfigFile(networkId, messageOrigin); printDivider(); logger.success(`Rewards message origin updated successfully for ${networkId}`); diff --git a/test/cli/index.ts b/test/cli/index.ts index 1ed926efb..27763287b 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -212,7 +212,7 @@ const contractsCommand = program - upgrade: Upgrade contracts by deploying new implementations - verify: Verify deployed contracts on block explorer - update-beefy-checkpoint: Fetch BEEFY authorities from a live chain and update config - - update-rewards-origin: Fetch or compute the RewardsAgentOrigin and update config + - update-rewards-origin: Fetch or compute the AgentOrigin and update config - update-metadata: Update the metadata URI of an existing AVS contract Common options: @@ -385,17 +385,14 @@ contractsCommand contractsCommand .command("update-rewards-origin") .description( - "Fetch or compute the RewardsAgentOrigin and update the config file with the rewards message origin" + "Fetch or compute the AgentOrigin and update the config file with the rewards message origin" ) .option("--chain ", "Target chain (hoodi, ethereum, anvil)") .option( "--environment ", "Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value." ) - .option( - "--rpc-url ", - "WebSocket RPC URL of the DataHaven chain to fetch RewardsAgentOrigin from" - ) + .option("--rpc-url ", "WebSocket RPC URL of the DataHaven chain to fetch AgentOrigin from") .option( "--genesis-hash ", "Chain genesis hash (32 bytes hex). If not provided, will be fetched from the chain." diff --git a/test/configs/parameters/datahaven-parameters.json b/test/configs/parameters/datahaven-parameters.json index ce875f03c..0072a4995 100644 --- a/test/configs/parameters/datahaven-parameters.json +++ b/test/configs/parameters/datahaven-parameters.json @@ -8,7 +8,7 @@ "value": null }, { - "name": "RewardsAgentOrigin", + "name": "AgentOrigin", "value": null }, { diff --git a/test/e2e/suites/slash.test.ts b/test/e2e/suites/slash.test.ts index c212cd90e..7547a84cd 100644 --- a/test/e2e/suites/slash.test.ts +++ b/test/e2e/suites/slash.test.ts @@ -3,8 +3,9 @@ import { $ } from "bun"; import { Binary, FixedSizeBinary } from "polkadot-api"; import { CROSS_CHAIN_TIMEOUTS, getPapiSigner, logger } from "utils"; import type { Address } from "viem"; +import { gatewayAbi } from "../../contract-bindings"; import { getContractInstance, parseDeploymentsFile } from "../../utils/contracts"; -import { waitForDataHavenEvent } from "../../utils/events"; +import { waitForDataHavenEvent, waitForEthereumEvent } from "../../utils/events"; import { waitFor } from "../../utils/waits"; import { BaseTestSuite } from "../framework"; @@ -122,6 +123,8 @@ describe("Should slash an operator", () => { }, 40000); it("use sudo to slash operator", async () => { + const { publicClient } = suite.getTestConnectors(); + // get era number const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue(); @@ -168,6 +171,19 @@ describe("Should slash an operator", () => { throw new Error("SlashesMessageSent event not found"); } logger.info("Slashes message sent"); + + const fromBlock = await publicClient.getBlockNumber(); + const deployments = await parseDeploymentsFile(); + const _ethEvent = await waitForEthereumEvent({ + client: publicClient, + address: deployments.Gateway, + abi: gatewayAbi, + eventName: "SlashingComplete", + fromBlock: fromBlock > 0n ? fromBlock - 1n : fromBlock, + timeout: CROSS_CHAIN_TIMEOUTS.DH_TO_ETH_MS + }); + + logger.info("Got Ethereum event!"); }, 560000); it("should detect and slash an unresponsive validator (liveness)", async () => { diff --git a/test/utils/types.ts b/test/utils/types.ts index 695788fff..df4f81434 100644 --- a/test/utils/types.ts +++ b/test/utils/types.ts @@ -184,7 +184,7 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint => const DATAHAVEN_PARAM_NAMES = [ "EthereumGatewayAddress", "RewardsUpdateSelector", - "RewardsAgentOrigin", + "AgentOrigin", "DatahavenServiceManagerAddress" ] as const; From a484f93e4d3b3856272b001334fa3917a61a1781 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:56:29 +0100 Subject: [PATCH 11/17] =?UTF-8?q?chore:=20=E2=99=BB=20=20update=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/.papi/descriptors/package.json | 2 +- test/.papi/metadata/datahaven.scale | Bin 633028 -> 634460 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index 5ad366a9f..185bc6783 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.15484599658830368838", + "version": "0.1.0-autogenerated.18139584469151706411", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/.papi/metadata/datahaven.scale b/test/.papi/metadata/datahaven.scale index c69fcd3a2cb802a47418e17da5717f261e3e05ee..3f4882b211f71d8afcb306069413c129280f4b53 100644 GIT binary patch delta 29455 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 delta 27983 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 B*@plC From 450c1727cee60911e018c87a5ab299f2ffa67472 Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:09:32 +0100 Subject: [PATCH 12/17] =?UTF-8?q?chore:=20=E2=99=BB=20=20update=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/.papi/descriptors/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index 5ad366a9f..185bc6783 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.15484599658830368838", + "version": "0.1.0-autogenerated.18139584469151706411", "name": "@polkadot-api/descriptors", "files": [ "dist" From 8c443bedbd51f3f48781ade75c6d980cad2b435a Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Wed, 11 Mar 2026 15:02:22 +0100 Subject: [PATCH 13/17] fix: include PoC base cost estimation in create() --- operator/runtime/mainnet/src/lib.rs | 27 +++++++++++++++++++++++---- operator/runtime/stagenet/src/lib.rs | 27 +++++++++++++++++++++++---- operator/runtime/testnet/src/lib.rs | 27 +++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/operator/runtime/mainnet/src/lib.rs b/operator/runtime/mainnet/src/lib.rs index f295318aa..45ad24926 100644 --- a/operator/runtime/mainnet/src/lib.rs +++ b/operator/runtime/mainnet/src/lib.rs @@ -1121,6 +1121,8 @@ impl_runtime_apis! { estimate: bool, access_list: Option)>>, ) -> Result { + use pallet_evm::GasWeightMapping as _; + let config = if estimate { let mut config = ::config().clone(); config.estimate = true; @@ -1131,6 +1133,16 @@ impl_runtime_apis! { let is_transactional = false; let validate = true; + // Estimated encoded transaction size for create (EIP1559-style) for PoV validation. + // Base: from 20 + value 32 + gas_limit 32 + nonce 32 + action variant 1 + chain_id 8 + signature 65 = 190. + let mut estimated_transaction_len = data.len() + + 190 + + if max_fee_per_gas.is_some() { 32 } else { 0 } + + if max_priority_fee_per_gas.is_some() { 32 } else { 0 }; + if let Some(ref list) = access_list { + estimated_transaction_len += list.encode().len(); + } + let gas_limit = if gas_limit > U256::from(u64::MAX) { u64::MAX } else { @@ -1140,8 +1152,14 @@ impl_runtime_apis! { let without_base_extrinsic_weight = true; let weight_limit = ::GasWeightMapping::gas_to_weight( gas_limit, - without_base_extrinsic_weight + without_base_extrinsic_weight, ); + let (weight_limit, proof_size_base_cost) = + if weight_limit.proof_size() > 0 { + (Some(weight_limit), Some(estimated_transaction_len as u64)) + } else { + (None, None) + }; #[allow(clippy::or_fun_call)] ::Runner::create( @@ -1155,10 +1173,11 @@ impl_runtime_apis! { access_list.unwrap_or_default(), is_transactional, validate, - Some(weight_limit), - None, + weight_limit, + proof_size_base_cost, config.as_ref().unwrap_or(::config()), - ).map_err(|err| err.error.into()) + ) + .map_err(|err| err.error.into()) } fn current_transaction_statuses() -> Option> { diff --git a/operator/runtime/stagenet/src/lib.rs b/operator/runtime/stagenet/src/lib.rs index df43090fa..0a3b98870 100644 --- a/operator/runtime/stagenet/src/lib.rs +++ b/operator/runtime/stagenet/src/lib.rs @@ -1123,6 +1123,8 @@ impl_runtime_apis! { estimate: bool, access_list: Option)>>, ) -> Result { + use pallet_evm::GasWeightMapping as _; + let config = if estimate { let mut config = ::config().clone(); config.estimate = true; @@ -1133,6 +1135,16 @@ impl_runtime_apis! { let is_transactional = false; let validate = true; + // Estimated encoded transaction size for create (EIP1559-style) for PoV validation. + // Base: from 20 + value 32 + gas_limit 32 + nonce 32 + action variant 1 + chain_id 8 + signature 65 = 190. + let mut estimated_transaction_len = data.len() + + 190 + + if max_fee_per_gas.is_some() { 32 } else { 0 } + + if max_priority_fee_per_gas.is_some() { 32 } else { 0 }; + if let Some(ref list) = access_list { + estimated_transaction_len += list.encode().len(); + } + let gas_limit = if gas_limit > U256::from(u64::MAX) { u64::MAX } else { @@ -1142,8 +1154,14 @@ impl_runtime_apis! { let without_base_extrinsic_weight = true; let weight_limit = ::GasWeightMapping::gas_to_weight( gas_limit, - without_base_extrinsic_weight + without_base_extrinsic_weight, ); + let (weight_limit, proof_size_base_cost) = + if weight_limit.proof_size() > 0 { + (Some(weight_limit), Some(estimated_transaction_len as u64)) + } else { + (None, None) + }; #[allow(clippy::or_fun_call)] ::Runner::create( @@ -1157,10 +1175,11 @@ impl_runtime_apis! { access_list.unwrap_or_default(), is_transactional, validate, - Some(weight_limit), - None, + weight_limit, + proof_size_base_cost, config.as_ref().unwrap_or(::config()), - ).map_err(|err| err.error.into()) + ) + .map_err(|err| err.error.into()) } fn current_transaction_statuses() -> Option> { diff --git a/operator/runtime/testnet/src/lib.rs b/operator/runtime/testnet/src/lib.rs index a8248dbd6..79748303d 100644 --- a/operator/runtime/testnet/src/lib.rs +++ b/operator/runtime/testnet/src/lib.rs @@ -1121,6 +1121,8 @@ impl_runtime_apis! { estimate: bool, access_list: Option)>>, ) -> Result { + use pallet_evm::GasWeightMapping as _; + let config = if estimate { let mut config = ::config().clone(); config.estimate = true; @@ -1131,6 +1133,16 @@ impl_runtime_apis! { let is_transactional = false; let validate = true; + // Estimated encoded transaction size for create (EIP1559-style) for PoV validation. + // Base: from 20 + value 32 + gas_limit 32 + nonce 32 + action variant 1 + chain_id 8 + signature 65 = 190. + let mut estimated_transaction_len = data.len() + + 190 + + if max_fee_per_gas.is_some() { 32 } else { 0 } + + if max_priority_fee_per_gas.is_some() { 32 } else { 0 }; + if let Some(ref list) = access_list { + estimated_transaction_len += list.encode().len(); + } + let gas_limit = if gas_limit > U256::from(u64::MAX) { u64::MAX } else { @@ -1140,8 +1152,14 @@ impl_runtime_apis! { let without_base_extrinsic_weight = true; let weight_limit = ::GasWeightMapping::gas_to_weight( gas_limit, - without_base_extrinsic_weight + without_base_extrinsic_weight, ); + let (weight_limit, proof_size_base_cost) = + if weight_limit.proof_size() > 0 { + (Some(weight_limit), Some(estimated_transaction_len as u64)) + } else { + (None, None) + }; #[allow(clippy::or_fun_call)] ::Runner::create( @@ -1155,10 +1173,11 @@ impl_runtime_apis! { access_list.unwrap_or_default(), is_transactional, validate, - Some(weight_limit), - None, + weight_limit, + proof_size_base_cost, config.as_ref().unwrap_or(::config()), - ).map_err(|err| err.error.into()) + ) + .map_err(|err| err.error.into()) } fn current_transaction_statuses() -> Option> { From cfda13d5237072f3737db9732325865478c00576 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Wed, 11 Mar 2026 15:02:22 +0100 Subject: [PATCH 14/17] fix: include PoC base cost estimation in create() --- operator/runtime/stagenet/tests/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/operator/runtime/stagenet/tests/lib.rs b/operator/runtime/stagenet/tests/lib.rs index 4277d4634..f022fdaeb 100644 --- a/operator/runtime/stagenet/tests/lib.rs +++ b/operator/runtime/stagenet/tests/lib.rs @@ -76,3 +76,13 @@ fn validate_transaction_fails_on_filtered_call() { ); }); } + +/// Sanity-check for the create transaction size estimation used in EthereumRuntimeRPCApi::create +/// for proof_size_base_cost (see Frontier template). Ensures the base constant stays correct. +#[test] +fn ethereum_create_proof_size_base_cost_estimation_constants() { + // Base size for create tx (EIP1559-style): from 20 + value 32 + gas_limit 32 + nonce 32 + // + action variant 1 + chain_id 8 + signature 65 = 190. Optional: +32 per max_fee/max_priority_fee. + const BASE: usize = 20 + 32 + 32 + 32 + 1 + 8 + 65; + assert_eq!(BASE, 190, "create proof_size_base_cost base must match Frontier template"); +} From b3c95f988e89d6df7545bce952d0bb1e2112e5a9 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Thu, 12 Mar 2026 12:10:02 +0100 Subject: [PATCH 15/17] style: format --- operator/runtime/stagenet/tests/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/operator/runtime/stagenet/tests/lib.rs b/operator/runtime/stagenet/tests/lib.rs index f022fdaeb..bb1a79fa0 100644 --- a/operator/runtime/stagenet/tests/lib.rs +++ b/operator/runtime/stagenet/tests/lib.rs @@ -84,5 +84,8 @@ fn ethereum_create_proof_size_base_cost_estimation_constants() { // Base size for create tx (EIP1559-style): from 20 + value 32 + gas_limit 32 + nonce 32 // + action variant 1 + chain_id 8 + signature 65 = 190. Optional: +32 per max_fee/max_priority_fee. const BASE: usize = 20 + 32 + 32 + 32 + 1 + 8 + 65; - assert_eq!(BASE, 190, "create proof_size_base_cost base must match Frontier template"); + assert_eq!( + BASE, 190, + "create proof_size_base_cost base must match Frontier template" + ); } From cdf438d6dbeef00fb6e35235acef711acd8a624a Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Mon, 16 Mar 2026 12:17:53 +0100 Subject: [PATCH 16/17] test: fix gas expectation --- .../suites/dev/stagenet/gas/test-gas-contract-creation.ts | 2 +- .../suites/dev/stagenet/gas/test-gas-estimation-contracts.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/moonwall/suites/dev/stagenet/gas/test-gas-contract-creation.ts b/test/moonwall/suites/dev/stagenet/gas/test-gas-contract-creation.ts index 46dfcdd56..cfab7e4ac 100644 --- a/test/moonwall/suites/dev/stagenet/gas/test-gas-contract-creation.ts +++ b/test/moonwall/suites/dev/stagenet/gas/test-gas-contract-creation.ts @@ -16,7 +16,7 @@ describeSuite({ account: ALITH_ADDRESS, data: bytecode }) - ).to.equal(210541n); + ).to.equal(156082n); } }); } diff --git a/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts b/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts index 3ebfcec94..2fb5f28ec 100644 --- a/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts +++ b/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts @@ -129,7 +129,7 @@ describeSuite({ account: PRECOMPILE_BATCH_ADDRESS, data: bytecode }) - ).toBe(210541n); + ).toBe(156082n); } }); From 201ebbe34aa5566dc10537bb29a6082264794c54 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Mon, 16 Mar 2026 13:39:46 +0100 Subject: [PATCH 17/17] fix(tests): update gas estimation expectations after PoV base cost eth_estimateGas now applies proof_size_base_cost and a proof-size cap, so the runner reserves PoV for the extrinsic and limits execution to the remaining budget. Estimates can be lower than before because they are now bounded by this PoV limit instead of unbounded; update moonwall expectations (contract creation and Incrementor create) to the new values. --- .../suites/dev/stagenet/gas/test-gas-estimation-contracts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts b/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts index 2fb5f28ec..fd613ebcd 100644 --- a/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts +++ b/test/moonwall/suites/dev/stagenet/gas/test-gas-estimation-contracts.ts @@ -40,13 +40,13 @@ describeSuite({ data: bytecode, gasPrice: 0n }); - expect(result).to.equal(255341n); + expect(result).to.equal(172902n); const result2 = await context.viem().estimateGas({ account: ALITH_ADDRESS, data: bytecode }); - expect(result2).to.equal(255341n); + expect(result2).to.equal(172902n); } });