Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
01d514b
Add migration to clean bloated ismp storages
F3Joule Feb 20, 2026
fad069a
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Feb 20, 2026
fc90045
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Feb 27, 2026
9634c72
Refactor unhashed::kill to clear_prefix
F3Joule Mar 2, 2026
d215349
Fix tests
F3Joule Mar 2, 2026
409f4eb
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Mar 4, 2026
c9fdc1c
Fix clippy error
F3Joule Mar 4, 2026
15ba87d
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Mar 5, 2026
e7abf3f
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Mar 18, 2026
c7d69cf
Refactor from pallet_migrations to on_idle impl
F3Joule Mar 18, 2026
f15eaf7
Add tests
F3Joule Mar 18, 2026
a2d2af9
Fix tests
F3Joule Mar 18, 2026
1bf392b
Enable migration by default
F3Joule Mar 19, 2026
7387399
Add benchmarks
F3Joule Mar 19, 2026
000e3f4
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Mar 23, 2026
cece29f
Fix after merge
F3Joule Mar 24, 2026
a060730
Add more tests
F3Joule Mar 24, 2026
cd318ad
Fix benchmarks
F3Joule Mar 24, 2026
4550778
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Mar 25, 2026
a21e8bd
Fix tests
F3Joule Mar 25, 2026
a157650
Use proper weights for on_idle
F3Joule Mar 25, 2026
1e01d4e
Add new declarations to WeightInfo
F3Joule Mar 25, 2026
7896667
Fix benchmarks
F3Joule Mar 25, 2026
f126f9f
Add migration status events
F3Joule Mar 25, 2026
ae2f69b
Add weights to runtime to fix build
F3Joule Mar 25, 2026
949efbc
Fix weights accounting in on_idle
F3Joule Mar 25, 2026
9e4a6eb
Fix code formatting
F3Joule Mar 25, 2026
cdf1171
Fix clippy error
F3Joule Mar 25, 2026
841a0b5
Update pallets weights [ignore benchmarks]
F3Joule Mar 26, 2026
19f49a5
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Mar 26, 2026
04342b8
Make events names more specific
F3Joule Mar 26, 2026
c04eb66
Bump versions
F3Joule Mar 26, 2026
2b930cf
Update Cargo.lock
F3Joule Mar 27, 2026
e1e1085
Cap migration budget to 70% of either ref_time or proof_size
F3Joule Mar 27, 2026
b5bfa9c
Cap migration step to a max of 2_500 records per block
F3Joule Mar 30, 2026
7a309ac
Update pallets weights [ignore benchmarks]
F3Joule Mar 31, 2026
9edfd90
Merge remote-tracking branch 'origin/master' into fix/hyperbridge-cle…
F3Joule Apr 3, 2026
c38bc24
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Apr 6, 2026
b754440
Merge branch 'master' into fix/hyperbridge-cleanup
F3Joule Apr 7, 2026
File filter

Filter by extension

Filter by extension


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

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

4 changes: 2 additions & 2 deletions pallets/dispatcher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-dispatcher"
version = "1.7.0"
version = "1.7.1"
authors = ['GalacticCouncil']
edition = "2021"
license = "Apache-2.0"
Expand All @@ -19,6 +19,7 @@ hex = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }

# FRAME
frame-support = { workspace = true }
Expand All @@ -35,7 +36,6 @@ pallet-evm = { workspace = true }
frame-benchmarking = { workspace = true, optional = true }

[dev-dependencies]
sp-io = { workspace = true }
orml-tokens = { workspace = true }
orml-traits = { workspace = true }
test-utils = { workspace = true }
Expand Down
33 changes: 33 additions & 0 deletions pallets/dispatcher/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use super::*;

use frame_benchmarking::{account, benchmarks};
use frame_support::traits::OnIdle;
use frame_system::RawOrigin;
use sp_std::boxed::Box;

Expand Down Expand Up @@ -77,5 +78,37 @@ benchmarks! {
let caller: T::AccountId = account("caller", 0, 1);
}: _(RawOrigin::Signed(caller), Box::new(call))

pause_hyperbridge_cleanup {
}: pause_hyperbridge_cleanup(RawOrigin::Root, true)
verify {
assert!(!CleanupEnabled::<T>::get());
}

cleanup_on_idle {
let n in 1..crate::hyperbridge_cleanup::MAX_KEYS_PER_BLOCK;

let prefix = Stage::StateCommitments.storage_prefix();
let tail = 100_000u32;
for i in 0..(n + tail) {
let mut key = prefix.to_vec();
key.extend_from_slice(&i.to_le_bytes());
sp_io::storage::set(&key, &i.to_le_bytes());
}

let per_key = T::DbWeight::get().reads_writes(2, 1);
let remaining = per_key.saturating_mul(n as u64 * 2);
}: {
Pallet::<T>::on_idle(1u32.into(), remaining);
}
// No verify block — unit tests cover correctness.
// In TestExternalities clear_prefix ignores the limit (removes all keys at once),
// in production RocksDB it respects it. A verify that satisfies both is not possible.

cleanup_on_idle_limit_zero {
}: {
Pallet::<T>::on_idle(1u32.into(), Weight::zero());
}
// No verify — same TestExternalities limitation as cleanup_on_idle.

impl_benchmark_test_suite!(Pallet, crate::mock::ExtBuilder::default().build(), crate::mock::Test);
}
63 changes: 63 additions & 0 deletions pallets/dispatcher/src/hyperbridge_cleanup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Background cleanup logic for stale ISMP storage entries.
//!
//! Cleans the following storage prefixes across multiple blocks via `on_idle`:
//! - `pallet_ismp::StateCommitments`
//! - `pallet_ismp::StateMachineUpdateTime`
//! - `ismp_parachain::RelayChainStateCommitments`

use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use sp_core::hashing::twox_128;

pub(crate) const MAX_KEYS_PER_BLOCK: u32 = 2_500;

/// Stages of the ISMP storage cleanup.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, MaxEncodedLen, DecodeWithMemTracking, scale_info::TypeInfo,
)]
pub enum Stage {
/// Cleaning pallet_ismp::StateCommitments
StateCommitments,
/// Cleaning pallet_ismp::StateMachineUpdateTime
StateMachineUpdateTime,
/// Cleaning ismp_parachain::RelayChainStateCommitments
RelayChainStateCommitments,
}

impl Stage {
/// Get the storage prefix for the current stage.
pub fn storage_prefix(&self) -> [u8; 32] {
match self {
Stage::StateCommitments => make_prefix(b"Ismp", b"StateCommitments"),
Stage::StateMachineUpdateTime => make_prefix(b"Ismp", b"StateMachineUpdateTime"),
Stage::RelayChainStateCommitments => make_prefix(b"IsmpParachain", b"RelayChainStateCommitments"),
}
}

/// Get the next stage, or `None` if this is the last stage.
pub fn next(&self) -> Option<Self> {
match self {
Stage::StateCommitments => Some(Stage::StateMachineUpdateTime),
Stage::StateMachineUpdateTime => Some(Stage::RelayChainStateCommitments),
Stage::RelayChainStateCommitments => None,
}
}
}

fn make_prefix(pallet: &[u8], storage: &[u8]) -> [u8; 32] {
let mut prefix = [0u8; 32];
prefix[0..16].copy_from_slice(&twox_128(pallet));
prefix[16..32].copy_from_slice(&twox_128(storage));
prefix
}

/// Execute one cleanup step for `stage`, deleting at most `limit` keys.
///
/// Returns `(stage_done, keys_deleted)` where `stage_done` is `true` when no
/// keys remain under the stage's prefix.
pub fn do_cleanup_step(stage: Stage, limit: u32) -> (bool, u32) {
let prefix = stage.storage_prefix();
match sp_io::storage::clear_prefix(&prefix, Some(limit)) {
sp_io::KillStorageResult::AllRemoved(n) => (true, n),
sp_io::KillStorageResult::SomeRemaining(n) => (false, n),
}
}
98 changes: 98 additions & 0 deletions pallets/dispatcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ use frame_support::pallet_prelude::Weight;
use frame_support::traits::Get;
pub use pallet::*;

pub mod hyperbridge_cleanup;
pub use hyperbridge_cleanup::{do_cleanup_step, Stage};

#[frame_support::pallet]
pub mod pallet {
use super::*;
Expand Down Expand Up @@ -89,6 +92,9 @@ pub mod pallet {
type DefaultAaveManagerAccount: Get<Self::AccountId>;
type EmergencyAdminAccount: Get<Self::AccountId>;

/// The origin to manage hyperbridge migration ongoing status.
type MigrationOperatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;

/// Gas to Weight conversion.
type GasWeightMapping: GasWeightMapping;

Expand All @@ -108,6 +114,19 @@ pub mod pallet {
#[pallet::getter(fn extra_gas)]
pub type ExtraGas<T: Config> = StorageValue<_, u64, ValueQuery>;

/// Whether the background ISMP storage cleanup is active.
#[pallet::storage]
pub type CleanupEnabled<T: Config> = StorageValue<_, bool, ValueQuery, DefaultCleanupState>;

#[pallet::type_value]
pub fn DefaultCleanupState() -> bool {
true
}

/// Current stage of the background ISMP storage cleanup.
#[pallet::storage]
pub type CleanupStage<T: Config> = StorageValue<_, Stage, OptionQuery>;

#[pallet::storage]
#[pallet::whitelist_storage]
#[pallet::unbounded]
Expand Down Expand Up @@ -154,6 +173,14 @@ pub mod pallet {
call_hash: T::Hash,
result: DispatchResultWithPostInfo,
},
/// Emitted each block when cleanup deletes a batch of keys.
HyperbridgeCleanupProgress { stage: Stage, keys_deleted: u32 },
/// Emitted when all keys in a stage are removed and cleanup advances.
HyperbridgeCleanupStageCompleted { stage: Stage },
/// Emitted when all three stages are done and cleanup disables itself.
HyperbridgeCleanupCompleted,
/// Emitted when cleanup is paused or resumed via extrinsic.
HyperbridgeCleanupStatusChanged { paused: bool },
}

#[pallet::hooks]
Expand All @@ -162,6 +189,65 @@ pub mod pallet {
fn on_finalize(_n: BlockNumberFor<T>) {
LastEvmCallExitReason::<T>::kill();
}

/// Run a bounded chunk of ISMP storage cleanup during idle time.
fn on_idle(_n: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
if !CleanupEnabled::<T>::get() {
return T::DbWeight::get().reads(1);
}

// Use the remaining weight capped to at most 70% of max_block.
let max_block = T::BlockWeights::get().max_block;
let cap_perbill = sp_runtime::Perbill::from_percent(70);
let cap = Weight::from_parts(cap_perbill * max_block.ref_time(), cap_perbill * max_block.proof_size());

let budget = remaining_weight.min(cap);
let per_key_weight = T::DbWeight::get().reads_writes(2, 1);

let k_ref = budget.ref_time().checked_div(per_key_weight.ref_time()).unwrap_or(0);
// k_proof is a best-effort guard: benchmark proof_size per key was measured on a small
// trie and underestimates real production depth. MAX_KEYS_PER_BLOCK is the true
// proof_size safeguard; k_proof serves as an additional sanity check.
let k_proof = budget
.proof_size()
.checked_div(per_key_weight.proof_size())
.unwrap_or(k_ref);

// Safe as far as it caps to MAX_KEYS_PER_BLOCK
let limit = k_ref.min(k_proof).min(hyperbridge_cleanup::MAX_KEYS_PER_BLOCK as u64) as u32;
if limit == 0 {
return T::WeightInfo::cleanup_on_idle_limit_zero();
}

let stage = CleanupStage::<T>::get().unwrap_or(Stage::StateCommitments);
let (done, keys_deleted) = do_cleanup_step(stage, limit);

if keys_deleted > 0 {
Self::deposit_event(Event::HyperbridgeCleanupProgress { stage, keys_deleted });
}

let base_cleanup_weight = T::WeightInfo::cleanup_on_idle(keys_deleted);
if done {
Self::deposit_event(Event::HyperbridgeCleanupStageCompleted { stage });

return match stage.next() {
Some(next) => {
CleanupStage::<T>::put(next);
base_cleanup_weight.saturating_add(T::DbWeight::get().writes(1))
}
None => {
// All stages complete.
CleanupEnabled::<T>::put(false);
CleanupStage::<T>::kill();
Self::deposit_event(Event::HyperbridgeCleanupCompleted);

base_cleanup_weight.saturating_add(T::DbWeight::get().writes(2))
}
};
}

base_cleanup_weight
}
}

#[pallet::call]
Expand Down Expand Up @@ -376,6 +462,18 @@ pub mod pallet {

Ok(actual_weight.into())
}

/// Enable/pause the background ISMP storage cleanup. If enabled for the first time,
/// starting from the first stage.
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::pause_hyperbridge_cleanup())]
pub fn pause_hyperbridge_cleanup(origin: OriginFor<T>, do_pause: bool) -> DispatchResult {
T::MigrationOperatorOrigin::ensure_origin(origin)?;
CleanupEnabled::<T>::put(!do_pause);

Self::deposit_event(Event::HyperbridgeCleanupStatusChanged { paused: do_pause });
Ok(())
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion pallets/dispatcher/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,18 @@ impl dispatcher::Config for Test {
type EmergencyAdminAccount = EmergencyAdminAccount;
type WeightInfo = ();
type EvmCallIdentifier = EvmCallIdentifier;
type MigrationOperatorOrigin = EnsureRoot<AccountId>;
type GasWeightMapping = MockGasWeightMapping;
}

parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const SS58Prefix: u8 = 63;
pub const MaxReserves: u32 = 50;
pub const MockDbWeight: frame_support::weights::RuntimeDbWeight = frame_support::weights::RuntimeDbWeight {
read: 25_000_000,
write: 100_000_000,
};
}

impl system::Config for Test {
Expand All @@ -150,7 +155,7 @@ impl system::Config for Test {
type Lookup = IdentityLookup<Self::AccountId>;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type DbWeight = ();
type DbWeight = MockDbWeight;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = AccountData<u128>;
Expand Down
Loading
Loading