From 829fe1951387ba64f39192543713775431bd551a Mon Sep 17 00:00:00 2001 From: Kailai-Wang <7630809+Kailai-Wang@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:16:29 +0000 Subject: [PATCH 1/4] fix(paseo): use FixedVelocityConsensusHook for 6s block time (spec 9263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Paseo runtime's parachain_system::Config used the legacy `ExpectParentIncluded` consensus hook (kept for standalone launch). That hook requires each block's parent to already be included by the relay chain and has no unincluded segment, so it cannot pipeline blocks for async backing. After the 12s->6s upgrade (spec 9262) Paseo stalled at 12s with hundreds of collator panics (`expected parent to be included`, `Slot must increase`) and reorgs. Switch to the `FixedVelocityConsensusHook` already defined in the file (parameterised by RELAY_CHAIN_SLOT_DURATION_MILLIS / BLOCK_PROCESSING_VELOCITY / UNINCLUDED_SEGMENT_CAPACITY) — the same hook the heima runtime uses. heima mainnet was never affected. Also remove the one-shot block_time_6s migrations: they already ran and completed on Paseo at spec 9262 (verified on-chain: OnePassRescale guard set, staking Round.length 10->20, score-staking interval ->100800, vesting per_block halved, MBM done). The heima runtime keeps its copy for the still- pending mainnet upgrade. spec_version 9262 -> 9263. --- parachain/runtime/paseo/src/lib.rs | 26 +- .../paseo/src/migration/block_time_6s/mod.rs | 33 -- .../src/migration/block_time_6s/onepass.rs | 285 ------------------ .../src/migration/block_time_6s/vesting.rs | 284 ----------------- parachain/runtime/paseo/src/migration/mod.rs | 2 - 5 files changed, 10 insertions(+), 620 deletions(-) delete mode 100644 parachain/runtime/paseo/src/migration/block_time_6s/mod.rs delete mode 100644 parachain/runtime/paseo/src/migration/block_time_6s/onepass.rs delete mode 100644 parachain/runtime/paseo/src/migration/block_time_6s/vesting.rs diff --git a/parachain/runtime/paseo/src/lib.rs b/parachain/runtime/paseo/src/lib.rs index ca643cd321..c51902065c 100644 --- a/parachain/runtime/paseo/src/lib.rs +++ b/parachain/runtime/paseo/src/lib.rs @@ -154,10 +154,6 @@ pub type SignedPayload = generic::SignedPayload; /// Migrations to apply on runtime upgrade. pub type Migrations = ( - // one-shot: rescale bounded block-number/per-block state for the 12s -> 6s block-time change - // (spec_version 9262). Remove in the release after this one. The large `pallet_vesting` map is - // migrated separately as a multi-block migration, see `pallet_migrations::Config::Migrations`. - migration::block_time_6s::OnePassRescale, // permanent pallet_xcm::migration::MigrateToLatestXcmVersion, ); @@ -250,7 +246,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: alloc::borrow::Cow::Borrowed("heima"), authoring_version: 1, // same versioning-mechanism as polkadot: use last digit for minor updates - spec_version: 9262, + spec_version: 9263, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 2, @@ -480,12 +476,7 @@ parameter_types! { impl pallet_migrations::Config for Runtime { type RuntimeEvent = RuntimeEvent; #[cfg(not(feature = "runtime-benchmarks"))] - type Migrations = ( - pallet_identity::migration::v2::LazyMigrationV1ToV2, - // one-shot: rescale vesting schedules for the 12s -> 6s block-time change (spec 9262). - // Remove in the release after this one. - migration::block_time_6s::VestingRescaleMigration, - ); + type Migrations = pallet_identity::migration::v2::LazyMigrationV1ToV2; // Benchmarks need mocked migrations to guarantee that they succeed. #[cfg(feature = "runtime-benchmarks")] type Migrations = pallet_migrations::mock_helpers::MockedMigrations; @@ -898,11 +889,14 @@ impl cumulus_pallet_parachain_system::Config for Runtime { type XcmpMessageHandler = XcmpQueue; type ReservedXcmpWeight = ReservedXcmpWeight; type CheckAssociatedRelayNumber = RelayNumberStrictlyIncreases; - // note here we intentionally use this hook to ignore relay chain proof so that - // it's possible to launch as standalone chain - // - // Litentry parachain has a different(standard) setting - type ConsensusHook = cumulus_pallet_parachain_system::consensus_hook::ExpectParentIncluded; + // Use the fixed-velocity hook (same as the heima runtime). The legacy `ExpectParentIncluded` + // hook requires each block's parent to already be included by the relay chain and does not + // support an unincluded segment, so it cannot pipeline blocks for async backing — at 6s block + // time it stalls the chain (`expected parent to be included` panics / reorgs). The + // `ConsensusHook` defined above is parameterised by RELAY_CHAIN_SLOT_DURATION_MILLIS / + // BLOCK_PROCESSING_VELOCITY / UNINCLUDED_SEGMENT_CAPACITY and is the correct hook for a + // relay-attached parachain. + type ConsensusHook = ConsensusHook; type SelectCore = cumulus_pallet_parachain_system::DefaultCoreSelector; type WeightInfo = (); } diff --git a/parachain/runtime/paseo/src/migration/block_time_6s/mod.rs b/parachain/runtime/paseo/src/migration/block_time_6s/mod.rs deleted file mode 100644 index e4b5f2768c..0000000000 --- a/parachain/runtime/paseo/src/migration/block_time_6s/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2020-2025 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Migrations for the 12s -> 6s block-time change. -//! -//! Two pieces, wired into the runtime in different places: -//! * [`onepass::OnePassRescale`] — a single-pass `OnRuntimeUpgrade` for the bounded state -//! (parachain-staking round, score-staking round config, scheduler agenda). Register it in the -//! executive `Migrations` tuple in `lib.rs`. -//! * [`vesting::VestingRescaleMigration`] — a `SteppedMigration` (multi-block) for the large -//! `pallet_vesting` map. Register it in `pallet_migrations::Config::Migrations` in `lib.rs`. -//! -//! Both are one-shot: remove them in the release *after* the one that ships spec_version 9262, once -//! the upgrade has been enacted and finalized on every network. - -pub mod onepass; -pub mod vesting; - -pub use onepass::OnePassRescale; -pub use vesting::VestingRescaleMigration; diff --git a/parachain/runtime/paseo/src/migration/block_time_6s/onepass.rs b/parachain/runtime/paseo/src/migration/block_time_6s/onepass.rs deleted file mode 100644 index 474c3fa1bf..0000000000 --- a/parachain/runtime/paseo/src/migration/block_time_6s/onepass.rs +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2020-2025 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Single-pass `OnRuntimeUpgrade` for the bounded pieces of on-chain state that encode an absolute -//! block number or a per-block rate, run once when block time is halved from 12s to 6s. -//! -//! All time-denominated *constants* (governance/treasury periods, `DefaultBlocksPerRound`, the -//! score-staking default interval, ...) are derived from `MINUTES/HOURS/DAYS` in `heima_primitives` -//! and therefore auto-double when `MILLISECS_PER_BLOCK` is halved — they need no migration. This -//! pass handles only the stored values that do **not** auto-rescale: -//! -//! * `parachain_staking::Round.length` — a stored block count (set at genesis / via -//! `set_blocks_per_round`); reset it to the new (doubled) `DefaultBlocksPerRound` and rebase -//! `first` to `now` so the current round neither ends early nor late on the transition. -//! * `score_staking::RoundConfig.interval` — a stored block count; reset to the new default -//! (`7 * DAYS`) while preserving the on-chain stake coefficients, and rebase `Round.start_block`. -//! * `pallet_scheduler::Agenda` — absolute future firing blocks. We rebase **only anonymous, -//! non-periodic** tasks (the only kind present on-chain: 1 on mainnet, 0 on Paseo), which lets us -//! avoid touching the pallet-private `Lookup`/`Retries` maps. Named/periodic tasks (none exist) -//! are asserted-against in try-runtime so this can never silently corrupt scheduler state. -//! -//! `vesting` is handled separately as a multi-block migration (see `vesting.rs`) because its map is -//! large. - -extern crate alloc; -use alloc::vec::Vec; -use core::marker::PhantomData; -use frame_support::{ - storage::unhashed, - traits::{Get, OnRuntimeUpgrade}, - weights::Weight, -}; -use frame_system::pallet_prelude::BlockNumberFor; -#[cfg(feature = "try-runtime")] -use parity_scale_codec::Encode; -use sp_runtime::Saturating; - -/// Storage key for the once-only guard. The rebases below are **not** naturally idempotent -/// (re-rebasing a scheduler task would push it out again), so we record completion under this key -/// and short-circuit on any subsequent execution. Derived as -/// `twox_128("BlockTime6s") ++ twox_128("OnePassRescaleDone")` — a regular pallet-style storage -/// prefix that cannot collide with any real pallet (no pallet is named `BlockTime6s`). -fn done_key() -> [u8; 32] { - let mut key = [0u8; 32]; - key[..16].copy_from_slice(&sp_core::hashing::twox_128(b"BlockTime6s")); - key[16..].copy_from_slice(&sp_core::hashing::twox_128(b"OnePassRescaleDone")); - key -} - -/// Rebase an absolute future block `b` so the same number of *remaining* blocks, at 2x speed, take -/// the same wall-clock time: `b' = now + 2 * (b - now)`. Past blocks are left untouched. -fn rebase_future(now: B, b: B) -> B -where - B: Copy + PartialOrd + Saturating + From, -{ - if b > now { - now.saturating_add(b.saturating_sub(now).saturating_mul(2u32.into())) - } else { - b - } -} - -pub struct OnePassRescale(PhantomData); - -impl OnRuntimeUpgrade for OnePassRescale -where - T: frame_system::Config - + pallet_parachain_staking::Config - + pallet_score_staking::Config - + pallet_scheduler::Config, -{ - fn on_runtime_upgrade() -> Weight { - let key = done_key(); - // Idempotency guard: the rebases below are not safe to apply twice, so run at most once. - if unhashed::get_raw(&key).is_some() { - log::info!("OnePassRescale: already applied, skipping"); - return T::DbWeight::get().reads(1); - } - - let now = frame_system::Pallet::::block_number(); - let mut reads: u64 = 1; - let mut writes: u64 = 1; - - // --- parachain-staking Round --- - pallet_parachain_staking::Round::::mutate(|r| { - r.length = ::DefaultBlocksPerRound::get(); - r.first = now; - }); - reads += 1; - writes += 1; - - // --- score-staking RoundConfig.interval + Round.start_block --- - // Take the (now-doubled) default interval but keep the chain's existing stake coefficients - // (mainnet uses m=3, Paseo m=2 — both differ from the default, so only `interval` is reset). - let default_interval = pallet_score_staking::DefaultRoundSetting::::get().interval; - pallet_score_staking::RoundConfig::::mutate(|c| { - c.interval = default_interval; - }); - pallet_score_staking::Round::::mutate(|r| { - r.start_block = now; - }); - reads += 2; - writes += 2; - - // --- scheduler Agenda: rebase anonymous, non-periodic future tasks only --- - let agenda_keys: Vec> = - pallet_scheduler::Agenda::::iter_keys().collect(); - for when in agenda_keys { - reads += 1; - if when <= now { - continue; - } - let new_when = rebase_future(now, when); - if new_when == when { - continue; - } - let agenda = pallet_scheduler::Agenda::::take(when); - writes += 1; - // Merge into the destination block (it is virtually always empty). - pallet_scheduler::Agenda::::mutate(new_when, |dest| { - for slot in agenda.into_iter() { - // Push best-effort; if the destination block is somehow full the task is - // dropped rather than panicking — try-runtime asserts the agenda is tiny. - let _ = dest.try_push(slot); - } - }); - writes += 1; - } - - // Mark complete so a re-execution is a no-op (idempotency). - unhashed::put_raw(&key, &[1u8]); - writes += 1; - - T::DbWeight::get().reads_writes(reads, writes) - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { - // Snapshot the total number of scheduled slots so we can assert the rebase neither drops nor - // duplicates any task. (The `Scheduled` struct's fields are pallet-private, so we cannot - // inspect named/periodic-ness here; the rebase relocates opaque slots wholesale, which is - // correct for anonymous non-periodic tasks — the only kind on these chains. Named/periodic - // tasks would also need their private `Lookup`/period rebased, which this migration does not - // do; that pre-condition is verified out-of-band via RPC before deployment.) - let slot_count: u32 = pallet_scheduler::Agenda::::iter() - .map(|(_, a)| a.into_iter().flatten().count() as u32) - .sum(); - Ok(slot_count.encode()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { - use parity_scale_codec::Decode; - let before = u32::decode(&mut &state[..]).map_err(|_| "bad pre_upgrade state")?; - let after: u32 = pallet_scheduler::Agenda::::iter() - .map(|(_, a)| a.into_iter().flatten().count() as u32) - .sum(); - frame_support::ensure!(before == after, "scheduler slot count changed during rebase"); - - let round = pallet_parachain_staking::Round::::get(); - frame_support::ensure!( - round.length == ::DefaultBlocksPerRound::get(), - "parachain-staking Round.length not reset" - ); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Runtime, RuntimeCall, RuntimeOrigin}; - use frame_support::traits::OnRuntimeUpgrade; - use sp_runtime::BuildStorage; - - fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| frame_system::Pallet::::set_block_number(1)); - ext - } - - // Mirrors the real heima on-chain scheduled burn: an anonymous, non-periodic Root-origin call - // (there it is a `utility.batchAll([utility.dispatchAs(Signed(_), balances.burn{..})])`; the - // rebase treats the call opaquely, so a plain `balances.burn` exercises the same code path). - #[test] - fn scheduled_burn_keeps_wall_clock_expiry_after_block_time_halving() { - new_test_ext().execute_with(|| { - let now: crate::BlockNumber = 1_000; - frame_system::Pallet::::set_block_number(now); - - // Schedule a burn 100_000 blocks out — at the OLD 12s block time that is ~13.9 days. - let blocks_until_fire: crate::BlockNumber = 100_000; - let when = now + blocks_until_fire; - let burn = RuntimeCall::Balances(pallet_balances::Call::burn { - value: 1_000_000_000_000_000_000_000, - keep_alive: false, - }); - pallet_scheduler::Pallet::::schedule( - RuntimeOrigin::root(), - when, - None, // non-periodic - 0, - Box::new(burn), - ) - .expect("schedule should succeed"); - - // Sanity: exactly one slot, sitting at `when`, none at the rebased target yet. - assert_eq!(pallet_scheduler::Agenda::::iter().count(), 1); - let live = |b: crate::BlockNumber| { - pallet_scheduler::Agenda::::get(b) - .iter() - .filter(|s| s.is_some()) - .count() - }; - assert_eq!(live(when), 1); - - let rebased = now + 2 * blocks_until_fire; - assert_eq!(live(rebased), 0); - - // Run the migration (block time is now halved 12s -> 6s). - let _ = OnePassRescale::::on_runtime_upgrade(); - - // The task moved from `when` to `now + 2*(when-now)`: same number of slots, none left - // behind at the old block. - assert_eq!(live(when), 0, "old agenda slot must be cleared"); - assert_eq!(live(rebased), 1, "task must be rebased to now + 2*(when-now)"); - assert_eq!( - pallet_scheduler::Agenda::::iter() - .map(|(_, a)| a.iter().filter(|s| s.is_some()).count()) - .sum::(), - 1, - "no task dropped or duplicated" - ); - - // Wall-clock invariant: old (12s) and new (6s) fire at the same real-world time. - let old_secs = (when - now) as u64 * 12; - let new_secs = (rebased - now) as u64 * 6; - assert_eq!(old_secs, new_secs, "real-world expiry must be unchanged"); - }); - } - - #[test] - fn migration_is_idempotent() { - new_test_ext().execute_with(|| { - let now: crate::BlockNumber = 1_000; - frame_system::Pallet::::set_block_number(now); - let when = now + 50_000; - let burn = RuntimeCall::Balances(pallet_balances::Call::burn { - value: 1_000, - keep_alive: false, - }); - pallet_scheduler::Pallet::::schedule( - RuntimeOrigin::root(), - when, - None, - 0, - Box::new(burn), - ) - .unwrap(); - - OnePassRescale::::on_runtime_upgrade(); - let after_first: Vec<_> = pallet_scheduler::Agenda::::iter_keys().collect(); - - // Second run must be a no-op (guarded), leaving the rebased agenda untouched. - OnePassRescale::::on_runtime_upgrade(); - let after_second: Vec<_> = pallet_scheduler::Agenda::::iter_keys().collect(); - - assert_eq!(after_first, after_second, "second run must not rebase again"); - }); - } -} diff --git a/parachain/runtime/paseo/src/migration/block_time_6s/vesting.rs b/parachain/runtime/paseo/src/migration/block_time_6s/vesting.rs deleted file mode 100644 index 4ad6cbab04..0000000000 --- a/parachain/runtime/paseo/src/migration/block_time_6s/vesting.rs +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2020-2025 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Multi-block migration that rescales every `pallet_vesting` schedule when the chain block time -//! is halved from 12s to 6s. -//! -//! `VestingInfo` stores `{ locked, per_block, starting_block }`. `per_block` is a *per-block* drip -//! rate and `starting_block` is an *absolute* block. Once blocks arrive twice as fast in wall-clock -//! terms, an unmigrated schedule would finish vesting in **half** the intended real time. We rescale -//! so the *remaining* lock drains over the same wall-clock duration as before: -//! -//! Anchored at the upgrade block `now`: -//! * For a schedule that has **already started** (`starting_block <= now`): -//! - `still_locked = locked_at(now)` (funds already vested stay vested), -//! - new schedule `{ locked: still_locked, per_block: old_per_block / 2, starting_block: now }`. -//! At half the drip rate, `still_locked` now drains over ~2x the blocks => same wall-clock. -//! * For a schedule that has **not started yet** (`starting_block > now`): -//! - keep `locked`, halve `per_block`, and push the start out proportionally: -//! `starting_block' = now + 2 * (starting_block - now)`. -//! -//! `per_block` is clamped to `>= 1` so a schedule can never become non-terminating, and any -//! schedule that is already fully vested at `now` is dropped (its tokens are unlocked anyway). -//! -//! This is a `SteppedMigration` (registered via `pallet_migrations::Config::Migrations`) because the -//! `Vesting` map is large on mainnet (~2770 accounts) and a single-block pass could exceed block -//! weight. - -extern crate alloc; -use alloc::vec::Vec; -use core::marker::PhantomData; -use frame_support::{ - migrations::{SteppedMigration, SteppedMigrationError}, - pallet_prelude::*, - weights::WeightMeter, -}; -use frame_system::pallet_prelude::BlockNumberFor; -use pallet_vesting::{Vesting, VestingInfo}; -use sp_runtime::{traits::Zero, Saturating}; - -/// Unique identifier for this migration. Bump the version byte if it ever needs to re-run. -const PALLET_MIGRATION_ID: &[u8; 18] = b"vesting-rescale-6s"; - -type BalanceOf = <::Currency as frame_support::traits::Currency< - ::AccountId, ->>::Balance; - -pub struct VestingRescaleMigration(PhantomData); - -impl VestingRescaleMigration -where - T: pallet_vesting::Config, - BalanceOf: - Saturating + Copy + Zero + PartialOrd + core::ops::Div> + From, -{ - /// Rescale a single account's schedule list. Returns `None` when, after rescaling, the account - /// no longer has any live schedule (everything was fully vested). - fn rescale_account( - now: BlockNumberFor, - schedules: BoundedVec< - VestingInfo, BlockNumberFor>, - pallet_vesting::MaxVestingSchedulesGet, - >, - ) -> Option< - BoundedVec< - VestingInfo, BlockNumberFor>, - pallet_vesting::MaxVestingSchedulesGet, - >, - > { - let mut out: Vec, BlockNumberFor>> = Vec::new(); - - for s in schedules.into_iter() { - // `locked_at` needs the runtime's `BlockNumberToBalance` converter; compute it here and - // hand the result to the pure rescale helper so the arithmetic is independently testable. - let still_locked = - s.locked_at::<::BlockNumberToBalance>(now); - if let Some(info) = rescale_schedule::, BlockNumberFor>( - now, - s.locked(), - s.per_block(), - s.starting_block(), - still_locked, - ) { - out.push(info); - } - } - - if out.is_empty() { - None - } else { - // The list came from a `BoundedVec` of the same bound and we never grow it, so this - // `try_from` cannot fail. - Some(BoundedVec::try_from(out).expect("rescaled list never exceeds the original bound")) - } - } -} - -impl SteppedMigration for VestingRescaleMigration -where - T: pallet_vesting::Config, - BalanceOf: - Saturating + Copy + Zero + PartialOrd + core::ops::Div> + From, -{ - type Cursor = T::AccountId; - type Identifier = MigrationId<18>; - - fn id() -> Self::Identifier { - MigrationId { pallet_id: *PALLET_MIGRATION_ID, version_from: 0, version_to: 1 } - } - - fn step( - mut cursor: Option, - meter: &mut WeightMeter, - ) -> Result, SteppedMigrationError> { - // One read + one write per account; require enough headroom for at least one account so the - // migration can always make progress. - let required = T::DbWeight::get().reads_writes(1, 1); - if meter.remaining().any_lt(required) { - return Err(SteppedMigrationError::InsufficientWeight { required }); - } - - let now = frame_system::Pallet::::block_number(); - - loop { - if meter.try_consume(required).is_err() { - // Out of weight for this block; resume from `cursor` next block. - return Ok(cursor); - } - - // Resume just past the last processed account, else start at the top of the map. - let mut iter = match &cursor { - Some(last) => Vesting::::iter_from(Vesting::::hashed_key_for(last)), - None => Vesting::::iter(), - }; - - match iter.next() { - Some((account, schedules)) => { - match Self::rescale_account(now, schedules) { - Some(new_schedules) => Vesting::::insert(&account, new_schedules), - None => Vesting::::remove(&account), - } - cursor = Some(account); - }, - // Reached the end of the map: migration complete. - None => return Ok(None), - } - } - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { - let count = Vesting::::iter().count() as u32; - Ok(count.encode()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { - // Every remaining schedule must be valid (non-zero locked & per_block). - for (_who, schedules) in Vesting::::iter() { - for s in schedules.into_iter() { - frame_support::ensure!(s.is_valid(), "vesting schedule invalid after rescale"); - } - } - Ok(()) - } -} - -/// A small, self-describing identifier so two block-time migrations can never collide. -#[derive(MaxEncodedLen, Encode, Decode)] -pub struct MigrationId { - pub pallet_id: [u8; N], - pub version_from: u8, - pub version_to: u8, -} - -/// Pure rescale of a single vesting schedule for the 12s -> 6s block-time change. -/// -/// `still_locked` must be the schedule's `locked_at(now)` (computed by the caller using the -/// runtime's `BlockNumberToBalance`). Returns the new schedule, or `None` if it is already fully -/// vested at `now` (in which case the schedule is dropped — its funds are unlocked anyway). -/// -/// * Already started (`starting_block <= now`): keep already-vested funds vested; the remaining -/// `still_locked` drains from `now` at half the old rate, i.e. over ~2x the blocks => same -/// wall-clock. -/// * Not started yet (`starting_block > now`): keep `locked`, halve the rate, and push the start -/// out proportionally so it still begins at the same wall-clock moment. -/// -/// `per_block` is clamped to `>= 1` so the schedule can never become non-terminating. -fn rescale_schedule( - now: BlockNumber, - locked: Balance, - per_block: Balance, - starting_block: BlockNumber, - still_locked: Balance, -) -> Option> -where - Balance: sp_runtime::traits::AtLeast32BitUnsigned + Copy, - BlockNumber: sp_runtime::traits::AtLeast32BitUnsigned + Copy + sp_runtime::traits::Bounded, -{ - let halved_rate = per_block / Balance::from(2u32); - let new_per_block = if halved_rate.is_zero() { Balance::from(1u32) } else { halved_rate }; - - let info = if starting_block > now { - // Not started yet: delay the start proportionally, keep the full locked amount. - let delay = starting_block.saturating_sub(now); - let new_start = now.saturating_add(delay.saturating_mul(2u32.into())); - VestingInfo::new(locked, new_per_block, new_start) - } else { - // Already vesting: rebase the still-locked remainder to start draining from `now`. - if still_locked.is_zero() { - return None; - } - VestingInfo::new(still_locked, new_per_block, now) - }; - - if info.is_valid() { - Some(info) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::rescale_schedule; - - // Balance = u128, BlockNumber = u32 (matches the heima runtime). - type Info = pallet_vesting::VestingInfo; - - fn rescale( - now: u32, - locked: u128, - per_block: u128, - starting_block: u32, - still_locked: u128, - ) -> Option { - rescale_schedule::(now, locked, per_block, starting_block, still_locked) - } - - #[test] - fn already_vesting_halves_rate_and_rebases_to_now() { - // 1000 locked, 10/block from block 0; at now=40, 400 vested, 600 still locked. - let out = rescale(40, 1000, 10, 0, 600).expect("still vesting"); - assert_eq!(out.locked(), 600, "only the still-locked remainder carries over"); - assert_eq!(out.per_block(), 5, "drip rate halved"); - assert_eq!(out.starting_block(), 40, "rebased to the upgrade block"); - // Same wall-clock: old remaining 600/10 = 60 blocks @12s; new 600/5 = 120 blocks @6s. - } - - #[test] - fn not_started_delays_start_proportionally() { - // Starts at block 100, now is 40 => 60 blocks away. After: 40 + 2*60 = 160. - let out = rescale(40, 1000, 10, 100, 1000).expect("not fully vested"); - assert_eq!(out.locked(), 1000, "full amount preserved before start"); - assert_eq!(out.per_block(), 5, "drip rate halved"); - assert_eq!(out.starting_block(), 160, "start pushed out so wall-clock start is unchanged"); - } - - #[test] - fn fully_vested_is_dropped() { - // now past the end; nothing still locked. - assert!(rescale(1000, 1000, 10, 0, 0).is_none()); - } - - #[test] - fn per_block_clamped_to_at_least_one() { - // per_block = 1 halves to 0 -> clamped to 1 so the schedule still terminates. - let out = rescale(0, 100, 1, 0, 100).expect("still vesting"); - assert_eq!(out.per_block(), 1, "never drops below 1"); - assert!(out.is_valid()); - } -} diff --git a/parachain/runtime/paseo/src/migration/mod.rs b/parachain/runtime/paseo/src/migration/mod.rs index 9bfaf1361d..20182fa371 100644 --- a/parachain/runtime/paseo/src/migration/mod.rs +++ b/parachain/runtime/paseo/src/migration/mod.rs @@ -13,5 +13,3 @@ // // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . - -pub mod block_time_6s; From 012c3689b4dc233dc721999d618bca1cc5637892 Mon Sep 17 00:00:00 2001 From: Kailai-Wang <7630809+Kailai-Wang@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:18:13 +0000 Subject: [PATCH 2/4] fix(release-notes): match only the spec_version field, not comments `generate-release-notes.sh` extracted the runtime version with `grep spec_version`, which also matched the migration comment line that mentions "spec_version 9262". That pulled the comment text into the version field, so release notes rendered: version : // (spec_version 9262). Remove in the release after this one. ... 9262 Anchor the grep to the actual `spec_version:` field (leading whitespace + colon) and take the first match, so only the number is emitted. --- parachain/scripts/generate-release-notes.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/parachain/scripts/generate-release-notes.sh b/parachain/scripts/generate-release-notes.sh index 58b4189c27..e74ef47d51 100755 --- a/parachain/scripts/generate-release-notes.sh +++ b/parachain/scripts/generate-release-notes.sh @@ -140,7 +140,9 @@ if is_runtime_release; then echo "## Parachain runtime" >> "$1" for CHAIN in heima paseo; do SRTOOL_DIGEST_FILE=$CHAIN-runtime/$CHAIN-srtool-digest.json - RUNTIME_VERSION=$(grep spec_version parachain/runtime/$CHAIN/src/lib.rs | sed 's/.*version: //;s/,//') + # Match only the actual `spec_version:` field, not comments that mention "spec_version" + # (e.g. migration notes), which would otherwise pull extra lines into the version string. + RUNTIME_VERSION=$(grep -E '^[[:space:]]*spec_version:' parachain/runtime/$CHAIN/src/lib.rs | head -n1 | sed 's/.*spec_version: //;s/,.*//') RUNTIME_COMPRESSED_SIZE=$(cat "$SRTOOL_DIGEST_FILE" | jq .runtimes.compressed.size | sed 's/"//g') RUNTIME_RUSTC_VERSION=$(cat "$SRTOOL_DIGEST_FILE" | jq .rustc | sed 's/"//g') RUNTIME_COMPRESSED_SHA256=$(cat "$SRTOOL_DIGEST_FILE" | jq .runtimes.compressed.sha256 | sed 's/"//g') From d999031f7cc2cbd5c6bd6d1d6620281894adadbc Mon Sep 17 00:00:00 2001 From: Kailai-Wang <7630809+Kailai-Wang@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:01:06 +0000 Subject: [PATCH 3/4] fix(runtime): silence clippy::type_complexity in vesting migration `cargo clippy --workspace -- -D warnings` (the parachain-check CI job) failed on the nested `Option, MaxVestingSchedulesGet>>` return type of `VestingRescaleMigration::rescale_account`. Introduce a `ScheduleList` type alias for the account's schedule list and use it for both the argument and return type. No behaviour change. --- .../src/migration/block_time_6s/vesting.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/parachain/runtime/heima/src/migration/block_time_6s/vesting.rs b/parachain/runtime/heima/src/migration/block_time_6s/vesting.rs index 4ad6cbab04..379ccb3fdb 100644 --- a/parachain/runtime/heima/src/migration/block_time_6s/vesting.rs +++ b/parachain/runtime/heima/src/migration/block_time_6s/vesting.rs @@ -57,6 +57,12 @@ type BalanceOf = <::Currency as frame_support::t ::AccountId, >>::Balance; +/// An account's full vesting-schedule list, as stored by `pallet_vesting`. +type ScheduleList = BoundedVec< + VestingInfo, BlockNumberFor>, + pallet_vesting::MaxVestingSchedulesGet, +>; + pub struct VestingRescaleMigration(PhantomData); impl VestingRescaleMigration @@ -69,16 +75,8 @@ where /// no longer has any live schedule (everything was fully vested). fn rescale_account( now: BlockNumberFor, - schedules: BoundedVec< - VestingInfo, BlockNumberFor>, - pallet_vesting::MaxVestingSchedulesGet, - >, - ) -> Option< - BoundedVec< - VestingInfo, BlockNumberFor>, - pallet_vesting::MaxVestingSchedulesGet, - >, - > { + schedules: ScheduleList, + ) -> Option> { let mut out: Vec, BlockNumberFor>> = Vec::new(); for s in schedules.into_iter() { From b06399c9985162ed971e82061705309052175a5b Mon Sep 17 00:00:00 2001 From: Kailai-Wang <7630809+Kailai-Wang@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:25:45 +0000 Subject: [PATCH 4/4] test(parachain-staking): update per-round inflation for 6s block time The 12s->6s change doubled YEARS (and thus BLOCKS_PER_YEAR = YEARS), so with the mock's fixed DefaultBlocksPerRound the rounds-per-year doubled and the per-round inflation derived from the annual rate halved. Three pallet tests hard-coded the old (12s) per-round values and failed: - set_blocks_per_round_event_emits_correctly: from_parts(926) -> 463 - set_inflation_event_emits_correctly: 57/75/93 -> 29/38/47 - set_inflation_storage_updates_correctly: 57/75/93 -> 29/38/47 Values updated to what the code now computes (annual rates unchanged). This was fallout from the block-time constant change that the earlier PR missed. --- .../pallets/parachain-staking/src/tests.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/parachain/pallets/parachain-staking/src/tests.rs b/parachain/pallets/parachain-staking/src/tests.rs index 149cece88e..4b6c56ff70 100644 --- a/parachain/pallets/parachain-staking/src/tests.rs +++ b/parachain/pallets/parachain-staking/src/tests.rs @@ -228,9 +228,9 @@ fn set_blocks_per_round_event_emits_correctly() { first_block: 0, old: 5, new: 6, - new_per_round_inflation_min: Perbill::from_parts(926), - new_per_round_inflation_ideal: Perbill::from_parts(926), - new_per_round_inflation_max: Perbill::from_parts(926), + new_per_round_inflation_min: Perbill::from_parts(463), + new_per_round_inflation_ideal: Perbill::from_parts(463), + new_per_round_inflation_max: Perbill::from_parts(463), })); }); } @@ -424,9 +424,9 @@ fn set_inflation_event_emits_correctly() { annual_min: min, annual_ideal: ideal, annual_max: max, - round_min: Perbill::from_parts(57), - round_ideal: Perbill::from_parts(75), - round_max: Perbill::from_parts(93), + round_min: Perbill::from_parts(29), + round_ideal: Perbill::from_parts(38), + round_max: Perbill::from_parts(47), })); }); } @@ -460,9 +460,9 @@ fn set_inflation_storage_updates_correctly() { assert_eq!( ParachainStaking::inflation_config().round, Range { - min: Perbill::from_parts(57), - ideal: Perbill::from_parts(75), - max: Perbill::from_parts(93) + min: Perbill::from_parts(29), + ideal: Perbill::from_parts(38), + max: Perbill::from_parts(47) } ); });