From 9144de8e906af7d3340183c62411f3fadd75d848 Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Wed, 1 Apr 2026 11:17:52 +0100 Subject: [PATCH 01/18] add general logic and try-state check --- substrate/frame/treasury/src/lib.rs | 220 ++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 91c58e14e52e..761e2acb027a 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -286,6 +286,14 @@ pub mod pallet { #[pallet::constant] type PayoutPeriod: Get>; + /// Maximum number of spends in the payout queue. + #[pallet::constant] + type MaxQueuedSpends: Get; + + /// Period after which a spend's order expires and can be moved to the end of the queue. + #[pallet::constant] + type OrderExpirationPeriod: Get>; + /// Helper type for benchmarks. #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: ArgumentsFactory; @@ -360,6 +368,16 @@ pub mod pallet { #[pallet::storage] pub type LastSpendPeriod = StorageValue<_, BlockNumberFor, OptionQuery>; + /// The next spend to be paid out, with its order expiration block. + #[pallet::storage] + pub type NextPayout, I: 'static = ()> = + StorageValue<_, (SpendIndex, BlockNumberFor), OptionQuery>; + + /// The queue of mature spends in payout order. + #[pallet::storage] + pub type PayoutQueue, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig, I: 'static = ()> { @@ -418,6 +436,10 @@ pub mod pallet { /// A spend was processed and removed from the storage. It might have been successfully /// paid or it may have expired. SpendProcessed { index: SpendIndex }, + /// A mature spend has been added to the payout queue. + SpendEnqueued { index: SpendIndex }, + /// The payout queue has been rotated due to order expiration. + PayoutQueueRotated { index: SpendIndex }, } /// Error for the treasury pallet. @@ -446,6 +468,12 @@ pub mod pallet { NotAttempted, /// The payment has neither failed nor succeeded yet. Inconclusive, + /// Spend is not the next in the payout queue. + NotNextPayout, + /// Spend is already in the payout queue. + AlreadyQueued, + /// Payout queue is full. + QueueFull, } #[pallet::hooks] @@ -725,6 +753,9 @@ pub mod pallet { /// In case of a payout failure, the spend status must be updated with the `check_status` /// dispatchable before retrying with the current function. /// + /// Only the spend designated as `NextPayout` can be claimed. This ensures FIFO ordering + /// of mature spends. + /// /// ### Parameters /// - `index`: The spend index. /// @@ -735,6 +766,11 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::payout())] pub fn payout(origin: OriginFor, index: SpendIndex) -> DispatchResult { ensure_signed(origin)?; + + // Ensure this is the next payout in the queue + let next_payout = NextPayout::::get().ok_or(Error::::NotNextPayout)?; + ensure!(next_payout.0 == index, Error::::NotNextPayout); + let mut spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; let now = T::BlockNumberProvider::current_block_number(); ensure!(now >= spend.valid_from, Error::::EarlyPayout); @@ -768,6 +804,9 @@ pub mod pallet { /// If a spend has either succeeded or expired, it is removed from the storage by this /// function. In such instances, transaction fees are refunded. /// + /// This function also updates the payout queue, removing completed/expired spends + /// and setting the next payout if available. + /// /// ### Parameters /// - `index`: The spend index. /// @@ -787,6 +826,7 @@ pub mod pallet { if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) { // spend has expired and no further status update is expected. + Self::remove_from_queue(index); Spends::::remove(index); Self::deposit_event(Event::::SpendProcessed { index }); return Ok(Pays::No.into()); @@ -804,6 +844,7 @@ pub mod pallet { Self::deposit_event(Event::::PaymentFailed { index, payment_id }); }, Status::Success | Status::Unknown => { + Self::remove_from_queue(index); Spends::::remove(index); Self::deposit_event(Event::::SpendProcessed { index }); return Ok(Pays::No.into()); @@ -839,10 +880,128 @@ pub mod pallet { Error::::AlreadyAttempted ); + Self::remove_from_queue(index); Spends::::remove(index); Self::deposit_event(Event::::AssetSpendVoided { index }); Ok(()) } + + /// Add a matured spend to the payout queue. + /// + /// ## Dispatch Origin + /// + /// Must be signed (permissionless). + /// + /// ## Details + /// + /// This call adds a spend that has matured (current block >= valid_from) to the end + /// of the payout queue. If the queue is empty, the spend becomes the `NextPayout` + /// with an expiration block set to `now + OrderExpirationPeriod`. + /// + /// The spend must not be expired (current block < expire_at) and must not already + /// be in the queue. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::SpendEnqueued`] if successful. + #[pallet::call_index(9)] + #[pallet::weight(100)] + pub fn enqueue_mature_spend(origin: OriginFor, index: SpendIndex) -> DispatchResult { + ensure_signed(origin)?; + + let spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + let now = T::BlockNumberProvider::current_block_number(); + + // Verify spend is in Pending status + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + Error::::AlreadyAttempted + ); + + // Verify spend has matured + ensure!(now >= spend.valid_from, Error::::EarlyPayout); + + // Verify spend has not expired + ensure!(spend.expire_at > now, Error::::SpendExpired); + + // Check if this is the current NextPayout + let queue = PayoutQueue::::get(); + ensure!(!queue.contains(&index), Error::::AlreadyQueued); + + // Check if this is the current NextPayout + if let Some((next_index, _)) = NextPayout::::get() { + ensure!(next_index != index, Error::::AlreadyQueued); + } + + // Add to queue + PayoutQueue::::try_append(index).map_err(|_| Error::::QueueFull)?; + + // If queue was empty (no NextPayout), set this as NextPayout + if NextPayout::::get().is_none() { + let expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::put((index, expire_at)); + } + + Self::deposit_event(Event::::SpendEnqueued { index }); + Ok(()) + } + + /// Rotate the payout queue when the current order expires. + /// + /// ## Dispatch Origin + /// + /// Must be signed (permissionless). + /// + /// ## Details + /// + /// This call moves the current `NextPayout` to the end of the queue when its order + /// has expired (expire_at < now). The next spend in the queue becomes the new + /// `NextPayout` with a fresh expiration period. + /// + /// This prevents a single spend from permanently blocking the queue when it cannot + /// be paid out (e.g., due to insufficient funds). + /// + /// ## Events + /// + /// /// Emits [`Event::PayoutQueueRotated`] if successful. + #[pallet::call_index(10)] + #[pallet::weight(100)] + pub fn rotate_payout_queue(origin: OriginFor) -> DispatchResult { + ensure_signed(origin)?; + + let (current_index, expire_at) = + NextPayout::::get().ok_or(Error::::NotNextPayout)?; + + let now = T::BlockNumberProvider::current_block_number(); + + // Ensure the order has expired + ensure!(expire_at < now, Error::::NotNextPayout); + + // Remove first element from queue + let mut queue = PayoutQueue::::get(); + if !queue.is_empty() { + queue.remove(0); + } + + // Push expired index to end of queue + queue.try_push(current_index).map_err(|_| Error::::QueueFull)?; + + // Set new NextPayout if queue is not empty + if let Some(&new_next) = queue.first() { + let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::put((new_next, new_expire_at)); + } else { + NextPayout::::kill(); + } + + PayoutQueue::::put(queue); + + Self::deposit_event(Event::::PayoutQueueRotated { index: current_index }); + Ok(()) + } } } @@ -903,6 +1062,30 @@ impl, I: 'static> Pallet { Approvals::::get() } + /// Remove a spend from the payout queue and update NextPayout. + /// Called when a spend is successfully paid out, expired, or voided. + fn remove_from_queue(index: SpendIndex) { + if let Some((next_index, _)) = NextPayout::::get() { + if next_index == index { + // This was the next payout, move to the next one + let mut queue = PayoutQueue::::get(); + if !queue.is_empty() { + queue.remove(0); + } + + if let Some(&new_next) = queue.first() { + let now = T::BlockNumberProvider::current_block_number(); + let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::put((new_next, new_expire_at)); + } else { + NextPayout::::kill(); + } + + PayoutQueue::::put(queue); + } + } + } + /// Spend some money! returns number of approvals before spend. pub fn spend_funds( spend_periods_passed: BlockNumberFor, @@ -1007,6 +1190,7 @@ impl, I: 'static> Pallet { fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { Self::try_state_proposals()?; Self::try_state_spends()?; + Self::try_state_payout_queue()?; Ok(()) } @@ -1080,6 +1264,42 @@ impl, I: 'static> Pallet { Ok(()) } + + /// ## Invariants of payout queue storage items + /// + /// 1. If [`NextPayout`] is Some, the first element of [`PayoutQueue`] must match the spend + /// index in [`NextPayout`]. + /// 2. All spend indices in [`PayoutQueue`] must exist in [`Spends`] and have Pending or Failed + /// status. + /// 3. The length of [`PayoutQueue`] must not exceed [`Config::MaxQueuedSpends`]. + #[cfg(any(feature = "try-runtime", test))] + fn try_state_payout_queue() -> Result<(), sp_runtime::TryRuntimeError> { + let queue = PayoutQueue::::get(); + + ensure!( + queue.len() as u32 <= T::MaxQueuedSpends::get(), + "Payout queue length exceeds MaxQueuedSpends." + ); + + if let Some((next_index, _)) = NextPayout::::get() { + ensure!( + queue.first() == Some(&next_index), + "NextPayout must match the first element of PayoutQueue." + ); + } + + for spend_index in queue.iter() { + let spend = Spends::::get(spend_index).ok_or( + sp_runtime::TryRuntimeError::Other("Spend in queue must exist in Spends."), + )?; + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + "Spend in queue must have Pending or Failed status." + ); + } + + Ok(()) + } } impl, I: 'static> OnUnbalanced> for Pallet { From 610482237f0ad864f19665923e1d0b09ee2c643f Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Mon, 6 Apr 2026 21:14:21 +0100 Subject: [PATCH 02/18] solution 1.2 --- .../asset-hub-westend/src/governance/mod.rs | 4 + .../collectives-westend/src/fellowship/mod.rs | 2 + polkadot/runtime/rococo/src/lib.rs | 5 + polkadot/runtime/westend/src/lib.rs | 6 +- substrate/bin/node/runtime/src/lib.rs | 4 + substrate/frame/treasury/src/lib.rs | 456 +++++++++++------- substrate/frame/treasury/src/migration.rs | 205 +++++++- substrate/frame/treasury/src/tests.rs | 380 +++++++++++++++ 8 files changed, 874 insertions(+), 188 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 9c00123b55c8..a74db01146dd 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -119,6 +119,8 @@ parameter_types! { pub const MaxPeerInHeartbeats: u32 = 10_000; pub const MaxBalance: Balance = Balance::max_value(); pub TreasuryAccount: AccountId = Treasury::account_id(); + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; } pub type TreasurySpender = EitherOf, Spender>; @@ -151,6 +153,8 @@ impl pallet_treasury::Config for Runtime { type BalanceConverter = TreasuryBalanceConverter; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = RelaychainDataProvider; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = parachains_common::pay::benchmarks::LocalPayArguments< xcm_config::TrustBackedAssetsPalletIndex, diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs index 50d0342f65a3..02b057c678dd 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs @@ -329,6 +329,8 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = ConstU32<{ 30 * DAYS }>; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU32<{ 3 * DAYS }>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments< sp_core::ConstU8<1>, diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index ed9d4a50587b..44d8069a3cb4 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -503,6 +503,7 @@ impl pallet_session::historical::Config for Runtime { parameter_types! { pub const SessionsPerEra: SessionIndex = 6; pub const BondingDuration: sp_staking::EraIndex = 28; + } parameter_types! { @@ -523,6 +524,8 @@ parameter_types! { pub const MaxKeys: u32 = 10_000; pub const MaxPeerInHeartbeats: u32 = 10_000; pub const MaxBalance: Balance = Balance::max_value(); + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; } impl pallet_treasury::Config for Runtime { @@ -561,6 +564,8 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index c77ec069969c..1c8c1fc5d0b3 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -937,6 +937,8 @@ parameter_types! { pub const MaxKeys: u32 = 10_000; pub const MaxPeerInHeartbeats: u32 = 10_000; pub const MaxBalance: Balance = Balance::max_value(); + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; } impl pallet_treasury::Config for Runtime { @@ -974,7 +976,9 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = PayoutSpendPeriod; - type BlockNumberProvider = System; + type BlockNumberProvider = System; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 8238a593d76f..9555b36c7713 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1308,6 +1308,8 @@ parameter_types! { pub const MaxApprovals: u32 = 100; pub const MaxBalance: Balance = Balance::max_value(); pub const SpendPayoutPeriod: BlockNumber = 30 * DAYS; + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 10 * HOURS; } impl pallet_treasury::Config for Runtime { @@ -1332,6 +1334,8 @@ impl pallet_treasury::Config for Runtime { type BalanceConverter = AssetRate; type PayoutPeriod = SpendPayoutPeriod; type BlockNumberProvider = System; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = PalletTreasuryArguments; } diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 761e2acb027a..a748a6be027b 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -67,6 +67,25 @@ //! [`pallet::Config::Paymaster`]. To claim these spends, the `payout` dispatchable should be called //! within some temporal bounds, starting from the moment they become valid and within one //! [`pallet::Config::PayoutPeriod`]. +//! +//! ## FIFO Payout Queue +//! +//! The treasury implements a FIFO (First-In-First-Out) payout queue per asset kind to ensure that +//! spends are paid out in the order they were approved. This prevents smaller, later-approved +//! spends from draining the treasury before larger, earlier-approved spends can be paid. +//! +//! When a spend is approved via the `spend` call, it is automatically inserted into the payout +//! queue for its asset kind in sorted order by `valid_from`. Spends are then paid out in order +//! via the `payout` call, which only processes the spend at the head of the queue for that +//! asset kind. +//! +//! If a spend at the head of the queue cannot be paid out (e.g., due to insufficient funds), +//! its order will expire after [`Config::OrderExpirationPeriod`] blocks. The `check_status` call +//! will then rotate it to the back of the queue, allowing other spends of the same asset kind +//! to be processed. +//! +//! Payout ordering is managed independently per asset kind - a spend of one asset cannot block +//! payouts of a different asset. #![cfg_attr(not(feature = "std"), no_std)] @@ -262,7 +281,7 @@ pub mod pallet { type SpendOrigin: EnsureOrigin>; /// Type parameter representing the asset kinds to be spent from the treasury. - type AssetKind: Parameter + MaxEncodedLen; + type AssetKind: Parameter + MaxEncodedLen + Clone; /// Type parameter used to identify the beneficiaries eligible to receive treasury spends. type Beneficiary: Parameter + MaxEncodedLen; @@ -286,7 +305,7 @@ pub mod pallet { #[pallet::constant] type PayoutPeriod: Get>; - /// Maximum number of spends in the payout queue. + /// Maximum number of spends in the payout queue per asset kind. #[pallet::constant] type MaxQueuedSpends: Get; @@ -368,15 +387,21 @@ pub mod pallet { #[pallet::storage] pub type LastSpendPeriod = StorageValue<_, BlockNumberFor, OptionQuery>; - /// The next spend to be paid out, with its order expiration block. + /// The next spend to be paid out for each asset kind, with its order expiration block. #[pallet::storage] pub type NextPayout, I: 'static = ()> = - StorageValue<_, (SpendIndex, BlockNumberFor), OptionQuery>; + StorageMap<_, Twox64Concat, T::AssetKind, (SpendIndex, BlockNumberFor), OptionQuery>; - /// The queue of mature spends in payout order. + /// The queue of spends in payout order for each asset kind. + /// Each entry contains (spend_index, valid_from) for sorting and maturity checking. #[pallet::storage] - pub type PayoutQueue, I: 'static = ()> = - StorageValue<_, BoundedVec, ValueQuery>; + pub type PayoutQueue, I: 'static = ()> = StorageMap< + _, + Twox64Concat, + T::AssetKind, + BoundedVec<(SpendIndex, BlockNumberFor), T::MaxQueuedSpends>, + ValueQuery, + >; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] @@ -436,10 +461,8 @@ pub mod pallet { /// A spend was processed and removed from the storage. It might have been successfully /// paid or it may have expired. SpendProcessed { index: SpendIndex }, - /// A mature spend has been added to the payout queue. - SpendEnqueued { index: SpendIndex }, - /// The payout queue has been rotated due to order expiration. - PayoutQueueRotated { index: SpendIndex }, + /// The payout queue was rotated for an asset kind due to order expiration. + PayoutQueueRotated { asset_kind: T::AssetKind, index: SpendIndex }, } /// Error for the treasury pallet. @@ -468,11 +491,9 @@ pub mod pallet { NotAttempted, /// The payment has neither failed nor succeeded yet. Inconclusive, - /// Spend is not the next in the payout queue. + /// Spend is not the next in the payout queue for its asset kind. NotNextPayout, - /// Spend is already in the payout queue. - AlreadyQueued, - /// Payout queue is full. + /// Payout queue is full for this asset kind. QueueFull, } @@ -662,6 +683,10 @@ pub mod pallet { /// designated beneficiary. The spend must be claimed using the `payout` dispatchable within /// the [`Config::PayoutPeriod`]. /// + /// The spend is automatically inserted into the payout queue for its asset kind in sorted + /// order by `valid_from`. If there is no current `NextPayout` for this asset kind, the + /// spend becomes the `NextPayout` with an expiration of `now + OrderExpirationPeriod`. + /// /// ### Parameters /// - `asset_kind`: An indicator of the specific asset class to be spent. /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. @@ -729,6 +754,9 @@ pub mod pallet { ); SpendCount::::put(index + 1); + // Insert into payout queue for this asset kind + Self::insert_into_payout_queue(*asset_kind.clone(), index, valid_from)?; + Self::deposit_event(Event::AssetSpendApproved { index, asset_kind: *asset_kind, @@ -753,8 +781,8 @@ pub mod pallet { /// In case of a payout failure, the spend status must be updated with the `check_status` /// dispatchable before retrying with the current function. /// - /// Only the spend designated as `NextPayout` can be claimed. This ensures FIFO ordering - /// of mature spends. + /// Only the spend designated as `NextPayout` for its asset kind can be claimed. This + /// ensures FIFO ordering of spends per asset kind. /// /// ### Parameters /// - `index`: The spend index. @@ -767,11 +795,14 @@ pub mod pallet { pub fn payout(origin: OriginFor, index: SpendIndex) -> DispatchResult { ensure_signed(origin)?; - // Ensure this is the next payout in the queue - let next_payout = NextPayout::::get().ok_or(Error::::NotNextPayout)?; + let spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + let asset_kind = spend.asset_kind.clone(); + + // Ensure this is the next payout for its asset kind + let next_payout = + NextPayout::::get(&asset_kind).ok_or(Error::::NotNextPayout)?; ensure!(next_payout.0 == index, Error::::NotNextPayout); - let mut spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; let now = T::BlockNumberProvider::current_block_number(); ensure!(now >= spend.valid_from, Error::::EarlyPayout); ensure!(spend.expire_at > now, Error::::SpendExpired); @@ -783,9 +814,11 @@ pub mod pallet { let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount) .map_err(|_| Error::::PayoutError)?; - spend.status = PaymentState::Attempted { id }; - spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); - Spends::::insert(index, spend); + // Update spend status + let mut updated_spend = spend; + updated_spend.status = PaymentState::Attempted { id }; + updated_spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); + Spends::::insert(index, updated_spend); Self::deposit_event(Event::::Paid { index, payment_id: id }); @@ -804,8 +837,11 @@ pub mod pallet { /// If a spend has either succeeded or expired, it is removed from the storage by this /// function. In such instances, transaction fees are refunded. /// - /// This function also updates the payout queue, removing completed/expired spends - /// and setting the next payout if available. + /// This function also manages the payout order: + /// - If the `NextPayout` order has expired and the spend is still Pending/Failed, it is + /// rotated to the back of the queue. + /// - If a spend has been successfully paid out or has fully expired, it is removed from the + /// queue and the next entry is promoted. /// /// ### Parameters /// - `index`: The spend index. @@ -814,6 +850,7 @@ pub mod pallet { /// /// Emits [`Event::PaymentFailed`] if the spend payout has failed. /// Emits [`Event::SpendProcessed`] if the spend payout has succeed. + /// Emits [`Event::PayoutQueueRotated`] if the queue was rotated due to expiration. #[pallet::call_index(7)] #[pallet::weight(T::WeightInfo::check_status())] pub fn check_status(origin: OriginFor, index: SpendIndex) -> DispatchResultWithPostInfo { @@ -821,17 +858,35 @@ pub mod pallet { use PaymentStatus as Status; ensure_signed(origin)?; - let mut spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + let spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + let asset_kind = spend.asset_kind.clone(); let now = T::BlockNumberProvider::current_block_number(); + // Handle spend expiration (not order expiration) if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) { - // spend has expired and no further status update is expected. - Self::remove_from_queue(index); + // Spend has expired and no further status update is expected. + Self::remove_from_queue(&asset_kind, index); Spends::::remove(index); Self::deposit_event(Event::::SpendProcessed { index }); return Ok(Pays::No.into()); } + // Check if this spend is the NextPayout and if its order has expired + if let Some((next_index, expire_at)) = NextPayout::::get(&asset_kind) { + if next_index == index && expire_at < now { + // Order has expired - rotate the queue if spend is still Pending/Failed + if matches!(spend.status, State::Pending | State::Failed) { + Self::rotate_payout_queue(&asset_kind)?; + Self::deposit_event(Event::::PayoutQueueRotated { + asset_kind: asset_kind.clone(), + index, + }); + // Return early - the spend was rotated, not processed + return Ok(Pays::No.into()); + } + } + } + let payment_id = match spend.status { State::Attempted { id } => id, _ => return Err(Error::::NotAttempted.into()), @@ -839,12 +894,13 @@ pub mod pallet { match T::Paymaster::check_payment(payment_id) { Status::Failure => { - spend.status = PaymentState::Failed; - Spends::::insert(index, spend); + let mut updated_spend = spend; + updated_spend.status = PaymentState::Failed; + Spends::::insert(index, updated_spend); Self::deposit_event(Event::::PaymentFailed { index, payment_id }); }, Status::Success | Status::Unknown => { - Self::remove_from_queue(index); + Self::remove_from_queue(&asset_kind, index); Spends::::remove(index); Self::deposit_event(Event::::SpendProcessed { index }); return Ok(Pays::No.into()); @@ -863,6 +919,7 @@ pub mod pallet { /// ## Details /// /// A spend void is only possible if the payout has not been attempted yet. + /// The spend is also removed from the payout queue for its asset kind. /// /// ### Parameters /// - `index`: The spend index. @@ -880,128 +937,12 @@ pub mod pallet { Error::::AlreadyAttempted ); - Self::remove_from_queue(index); + let asset_kind = spend.asset_kind.clone(); + Self::remove_from_queue(&asset_kind, index); Spends::::remove(index); Self::deposit_event(Event::::AssetSpendVoided { index }); Ok(()) } - - /// Add a matured spend to the payout queue. - /// - /// ## Dispatch Origin - /// - /// Must be signed (permissionless). - /// - /// ## Details - /// - /// This call adds a spend that has matured (current block >= valid_from) to the end - /// of the payout queue. If the queue is empty, the spend becomes the `NextPayout` - /// with an expiration block set to `now + OrderExpirationPeriod`. - /// - /// The spend must not be expired (current block < expire_at) and must not already - /// be in the queue. - /// - /// ### Parameters - /// - `index`: The spend index. - /// - /// ## Events - /// - /// Emits [`Event::SpendEnqueued`] if successful. - #[pallet::call_index(9)] - #[pallet::weight(100)] - pub fn enqueue_mature_spend(origin: OriginFor, index: SpendIndex) -> DispatchResult { - ensure_signed(origin)?; - - let spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; - let now = T::BlockNumberProvider::current_block_number(); - - // Verify spend is in Pending status - ensure!( - matches!(spend.status, PaymentState::Pending | PaymentState::Failed), - Error::::AlreadyAttempted - ); - - // Verify spend has matured - ensure!(now >= spend.valid_from, Error::::EarlyPayout); - - // Verify spend has not expired - ensure!(spend.expire_at > now, Error::::SpendExpired); - - // Check if this is the current NextPayout - let queue = PayoutQueue::::get(); - ensure!(!queue.contains(&index), Error::::AlreadyQueued); - - // Check if this is the current NextPayout - if let Some((next_index, _)) = NextPayout::::get() { - ensure!(next_index != index, Error::::AlreadyQueued); - } - - // Add to queue - PayoutQueue::::try_append(index).map_err(|_| Error::::QueueFull)?; - - // If queue was empty (no NextPayout), set this as NextPayout - if NextPayout::::get().is_none() { - let expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::put((index, expire_at)); - } - - Self::deposit_event(Event::::SpendEnqueued { index }); - Ok(()) - } - - /// Rotate the payout queue when the current order expires. - /// - /// ## Dispatch Origin - /// - /// Must be signed (permissionless). - /// - /// ## Details - /// - /// This call moves the current `NextPayout` to the end of the queue when its order - /// has expired (expire_at < now). The next spend in the queue becomes the new - /// `NextPayout` with a fresh expiration period. - /// - /// This prevents a single spend from permanently blocking the queue when it cannot - /// be paid out (e.g., due to insufficient funds). - /// - /// ## Events - /// - /// /// Emits [`Event::PayoutQueueRotated`] if successful. - #[pallet::call_index(10)] - #[pallet::weight(100)] - pub fn rotate_payout_queue(origin: OriginFor) -> DispatchResult { - ensure_signed(origin)?; - - let (current_index, expire_at) = - NextPayout::::get().ok_or(Error::::NotNextPayout)?; - - let now = T::BlockNumberProvider::current_block_number(); - - // Ensure the order has expired - ensure!(expire_at < now, Error::::NotNextPayout); - - // Remove first element from queue - let mut queue = PayoutQueue::::get(); - if !queue.is_empty() { - queue.remove(0); - } - - // Push expired index to end of queue - queue.try_push(current_index).map_err(|_| Error::::QueueFull)?; - - // Set new NextPayout if queue is not empty - if let Some(&new_next) = queue.first() { - let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::put((new_next, new_expire_at)); - } else { - NextPayout::::kill(); - } - - PayoutQueue::::put(queue); - - Self::deposit_event(Event::::PayoutQueueRotated { index: current_index }); - Ok(()) - } } } @@ -1062,30 +1003,100 @@ impl, I: 'static> Pallet { Approvals::::get() } - /// Remove a spend from the payout queue and update NextPayout. + /// Insert a spend into the payout queue for the given asset kind in sorted order by valid_from. + /// If there is no NextPayout for this asset kind, the spend becomes the NextPayout. + fn insert_into_payout_queue( + asset_kind: T::AssetKind, + index: SpendIndex, + valid_from: BlockNumberFor, + ) -> DispatchResult { + // Check if there's already a NextPayout for this asset kind + if NextPayout::::get(&asset_kind).is_some() { + // There's already a NextPayout, add to queue in sorted order + let mut queue = PayoutQueue::::get(&asset_kind); + + // Find the correct position to insert (maintain sorted order by valid_from) + let insert_pos = + queue.iter().position(|(_, vf)| *vf > valid_from).unwrap_or(queue.len()); + + // Insert at the found position + if queue.try_insert(insert_pos, (index, valid_from)).is_err() { + return Err(Error::::QueueFull.into()); + } + + PayoutQueue::::insert(&asset_kind, queue); + } else { + // No NextPayout for this asset kind, set this as NextPayout + let now = T::BlockNumberProvider::current_block_number(); + let expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert(&asset_kind, (index, expire_at)); + } + + Ok(()) + } + + /// Remove a spend from the payout queue for the given asset kind and update NextPayout. /// Called when a spend is successfully paid out, expired, or voided. - fn remove_from_queue(index: SpendIndex) { - if let Some((next_index, _)) = NextPayout::::get() { + fn remove_from_queue(asset_kind: &T::AssetKind, index: SpendIndex) { + if let Some((next_index, _)) = NextPayout::::get(asset_kind) { if next_index == index { - // This was the next payout, move to the next one - let mut queue = PayoutQueue::::get(); - if !queue.is_empty() { - queue.remove(0); - } + // This was the next payout, promote the next one from the queue + let mut queue = PayoutQueue::::get(asset_kind); - if let Some(&new_next) = queue.first() { + // Check what to promote BEFORE removing + if let Some(&(new_next_index, _)) = queue.first() { + // Promote to NextPayout let now = T::BlockNumberProvider::current_block_number(); let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::put((new_next, new_expire_at)); + NextPayout::::insert(asset_kind, (new_next_index, new_expire_at)); + + // Remove it from queue + queue.remove(0); + PayoutQueue::::insert(asset_kind, queue); } else { - NextPayout::::kill(); + // Queue is empty, clear NextPayout + NextPayout::::remove(asset_kind); + } + } else { + // This spend is in the queue but not at the head + let mut queue = PayoutQueue::::get(asset_kind); + if let Some(pos) = queue.iter().position(|(idx, _)| *idx == index) { + queue.remove(pos); + PayoutQueue::::insert(asset_kind, queue); } - - PayoutQueue::::put(queue); } } } + /// Rotate the payout queue for the given asset kind when the current order expires. + /// Moves the current NextPayout to the back of the queue and promotes the next entry. + fn rotate_payout_queue(asset_kind: &T::AssetKind) -> DispatchResult { + let (current_index, _) = + NextPayout::::get(asset_kind).ok_or(Error::::NotNextPayout)?; + let mut queue = PayoutQueue::::get(asset_kind); + + // Push the expired spend to the back of the queue + let spend = Spends::::get(current_index).ok_or(Error::::InvalidIndex)?; + queue + .try_push((current_index, spend.valid_from)) + .map_err(|_| Error::::QueueFull)?; + + // Promote the front of queue to NextPayout and remove it from queue + if let Some(&(new_next, _new_valid_from)) = queue.first() { + let now = T::BlockNumberProvider::current_block_number(); + let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert(asset_kind, (new_next, new_expire_at)); + // Remove the promoted entry from queue since it's now NextPayout + queue.remove(0); + } else { + // Queue was empty after pushing, clear NextPayout + NextPayout::::remove(asset_kind); + } + + PayoutQueue::::insert(asset_kind, queue); + Ok(()) + } + /// Spend some money! returns number of approvals before spend. pub fn spend_funds( spend_periods_passed: BlockNumberFor, @@ -1265,39 +1276,112 @@ impl, I: 'static> Pallet { Ok(()) } - /// ## Invariants of payout queue storage items - /// - /// 1. If [`NextPayout`] is Some, the first element of [`PayoutQueue`] must match the spend - /// index in [`NextPayout`]. - /// 2. All spend indices in [`PayoutQueue`] must exist in [`Spends`] and have Pending or Failed - /// status. - /// 3. The length of [`PayoutQueue`] must not exceed [`Config::MaxQueuedSpends`]. + // ## Invariants of payout queue storage items + // + // 1. If [`NextPayout`] for an asset kind is Some, the first element of [`PayoutQueue`] for + // that asset kind must match the spend index in [`NextPayout`]. + // 2. All spend indices in [`PayoutQueue`] for an asset kind must exist in [`Spends`] and + // have Pending or Failed status. + // 3. The length of each [`PayoutQueue`] must not exceed [`Config::MaxQueuedSpends`]. + // 4. No duplicate spend indices in any [`PayoutQueue`]. + // 5. The queue for each asset kind must be sorted by valid_from. + // #[cfg(any(feature = "try-runtime", test))] + // fn try_state_payout_queue() -> Result<(), sp_runtime::TryRuntimeError> { + // use alloc::collections::btree_set::BTreeSet; + // + // for (asset_kind, queue) in PayoutQueue::::iter() { + // ensure!( + // queue.len() as u32 <= T::MaxQueuedSpends::get(), + // "Payout queue length exceeds MaxQueuedSpends." + // ); + // + // Check no duplicates + // let unique_indices: BTreeSet<_> = queue.iter().map(|(idx, _)| *idx).collect(); + // ensure!( + // unique_indices.len() == queue.len() as usize, + // "Payout queue contains duplicate spend indices." + // ); + // + // Check sorted by valid_from + // let mut prev_valid_from: Option> = None; + // for (_, valid_from) in queue.iter() { + // if let Some(prev) = prev_valid_from { + // ensure!( + // prev <= *valid_from, + // "Payout queue is not sorted by valid_from." + // ); + // } + // prev_valid_from = Some(*valid_from); + // } + // + // if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { + // ensure!( + // queue.first().map(|(idx, _)| *idx) == Some(next_index), + // "NextPayout must match the first element of PayoutQueue." + // ); + // } + // + // for (spend_index, _) in queue.iter() { + // let spend = Spends::::get(spend_index).ok_or( + // sp_runtime::TryRuntimeError::Other("Spend in queue must exist in Spends."), + // )?; + // ensure!( + // matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + // "Spend in queue must have Pending or Failed status." + // ); + // } + // } + // + // Ok(()) + // } + #[cfg(any(feature = "try-runtime", test))] fn try_state_payout_queue() -> Result<(), sp_runtime::TryRuntimeError> { - let queue = PayoutQueue::::get(); - - ensure!( - queue.len() as u32 <= T::MaxQueuedSpends::get(), - "Payout queue length exceeds MaxQueuedSpends." - ); + use alloc::collections::btree_set::BTreeSet; - if let Some((next_index, _)) = NextPayout::::get() { + for (asset_kind, queue) in PayoutQueue::::iter() { ensure!( - queue.first() == Some(&next_index), - "NextPayout must match the first element of PayoutQueue." + queue.len() as u32 <= T::MaxQueuedSpends::get(), + "Payout queue length exceeds MaxQueuedSpends." ); - } - for spend_index in queue.iter() { - let spend = Spends::::get(spend_index).ok_or( - sp_runtime::TryRuntimeError::Other("Spend in queue must exist in Spends."), - )?; + // Check no duplicates + let unique_indices: BTreeSet<_> = queue.iter().map(|(idx, _)| *idx).collect(); ensure!( - matches!(spend.status, PaymentState::Pending | PaymentState::Failed), - "Spend in queue must have Pending or Failed status." + unique_indices.len() == queue.len(), + "Payout queue contains duplicate spend indices." ); - } + // Check sorted by valid_from + let mut prev_valid_from: Option> = None; + for (_, valid_from) in queue.iter() { + if let Some(prev) = prev_valid_from { + ensure!(prev <= *valid_from, "Payout queue is not sorted by valid_from."); + } + prev_valid_from = Some(*valid_from); + } + + // Check that NextPayout is not also in the queue (they're separate) + if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { + ensure!( + !queue.iter().any(|(idx, _)| *idx == next_index), + "NextPayout should not be in PayoutQueue." + ); + } + + // All items in queue must be valid pending spends + for (spend_index, _) in queue.iter() { + let spend = Spends::::get(spend_index).ok_or( + sp_runtime::TryRuntimeError::Other("Spend in queue must exist in Spends."), + )?; + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + "Spend in queue must have Pending or Failed status." + ); + // Verify asset kind matches + ensure!(spend.asset_kind == asset_kind, "Spend in queue has wrong asset kind."); + } + } Ok(()) } } diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index 7c8c587f1664..d791e2c41792 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -19,7 +19,6 @@ use super::*; use alloc::collections::BTreeSet; -#[cfg(feature = "try-runtime")] use alloc::vec::Vec; use core::marker::PhantomData; use frame_support::{defensive, traits::OnRuntimeUpgrade}; @@ -133,3 +132,207 @@ pub mod cleanup_proposals { } } } + +/// Migration to initialize the payout queue for existing spends (Solution 1.2). +/// +/// This migration identifies all pending/failed spends that have not yet expired, +/// groups them by asset kind, sorts them by valid_from (FIFO order), +/// and initializes the PayoutQueue and NextPayout for each asset kind. +pub mod migrate_to_ordered_payouts { + use super::*; + + /// Migration to initialize the payout queue for existing spends. + pub struct MigrateToOrderedPayouts(PhantomData<(T, I)>); + + impl, I: 'static> OnRuntimeUpgrade for MigrateToOrderedPayouts { + fn on_runtime_upgrade() -> Weight { + log::info!( + target: LOG_TARGET, + "Running migration to initialize ordered payouts", + ); + + let now = T::BlockNumberProvider::current_block_number(); + + // Collect all pending/failed spends that haven't expired. + let mut spends_vec: Vec<(T::AssetKind, SpendIndex, BlockNumberFor)> = Vec::new(); + + for (index, spend) in Spends::::iter() { + match spend.status { + PaymentState::Pending | PaymentState::Failed => { + // Only include spends that haven't expired + if spend.expire_at > now { + spends_vec.push((spend.asset_kind, index, spend.valid_from)); + } else { + log::debug!( + target: LOG_TARGET, + "Skipping expired spend {} (expire_at: {:?}, now: {:?})", + index, + spend.expire_at, + now, + ); + } + }, + PaymentState::Attempted { .. } => { + log::debug!( + target: LOG_TARGET, + "Skipping attempted spend {}", + index, + ); + }, + } + } + + // Group by encoded AssetKind + let mut spends_by_asset: BTreeMap< + Vec, + (T::AssetKind, Vec<(SpendIndex, BlockNumberFor)>), + > = BTreeMap::new(); + + for (asset_kind, index, valid_from) in spends_vec { + let key = asset_kind.encode(); + spends_by_asset + .entry(key) + .or_insert_with(|| (asset_kind, Vec::new())) + .1 + .push((index, valid_from)); + } + + let mut total_spends_processed = 0u32; + let mut total_assets_processed = 0u32; + + // Process each AssetKind + for (_, (asset_kind, mut spends)) in spends_by_asset { + // Sort by valid_from, then by index for deterministic ordering (consensus safety) + spends.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0))); + + let spend_count = spends.len() as u32; + total_spends_processed += spend_count; + total_assets_processed += 1; + + log::info!( + target: LOG_TARGET, + "Processing asset kind with {} spends", + spend_count, + ); + + // Build the payout queue (bounded by MaxQueuedSpends) + let mut queue = + BoundedVec::<(SpendIndex, BlockNumberFor), T::MaxQueuedSpends>::default(); + let mut next_payout_set = false; + + for (index, valid_from) in spends { + if !next_payout_set { + // First spend becomes NextPayout + let expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert(&asset_kind, (index, expire_at)); + next_payout_set = true; + log::debug!( + target: LOG_TARGET, + "Set NextPayout for asset to {} with expiration at {:?}", + index, + expire_at, + ); + } else if queue.len() < T::MaxQueuedSpends::get() as usize { + // Add to queue + if let Err(_) = queue.try_push((index, valid_from)) { + log::warn!( + target: LOG_TARGET, + "Failed to push spend {} to queue (queue full)", + index + ); + } + } else { + log::warn!( + target: LOG_TARGET, + "Payout queue is full, skipping spend {}", + index + ); + } + } + + // Set the payout queue + PayoutQueue::::insert(&asset_kind, queue); + } + + log::info!( + target: LOG_TARGET, + "Migration complete: processed {} spends across {} asset kinds", + total_spends_processed, + total_assets_processed, + ); + + let reads = total_spends_processed as u64 + 1; // Spends reads + block number read + let writes = total_assets_processed as u64 * 2; // PayoutQueue + NextPayout per asset + T::DbWeight::get().reads_writes(reads, writes) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + let pending_spends: Vec<(T::AssetKind, SpendIndex)> = Spends::::iter() + .filter_map(|(index, spend)| match spend.status { + PaymentState::Pending | PaymentState::Failed => Some((spend.asset_kind, index)), + _ => None, + }) + .collect(); + + log::info!( + target: LOG_TARGET, + "Pre-upgrade: {} pending/failed spends", + pending_spends.len() + ); + + Ok(pending_spends.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + let pre_pending_spends: Vec<(T::AssetKind, SpendIndex)> = + Vec::decode(&mut &state[..]).expect("Known good"); + + let mut post_queue_count = 0usize; + for (_, queue) in PayoutQueue::::iter() { + post_queue_count += queue.len(); + } + + log::info!( + target: LOG_TARGET, + "Post-upgrade: {} spends in queues (pre-upgrade had {} pending)", + post_queue_count, + pre_pending_spends.len() + ); + + // Verify queue invariants for each asset kind + for (asset_kind, queue) in PayoutQueue::::iter() { + ensure!( + queue.len() as u32 <= T::MaxQueuedSpends::get(), + "Queue length exceeds MaxQueuedSpends" + ); + + // Verify all items in queue are valid pending spends + for (index, _) in queue.iter() { + let spend = Spends::::get(index) + .ok_or(sp_runtime::TryRuntimeError::Other("Spend in queue not found"))?; + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + "Spend in queue has invalid status" + ); + ensure!(spend.asset_kind == asset_kind, "Spend in queue has wrong asset kind"); + } + + // Verify NextPayout is NOT in queue (they are separate storage items in Solution + // 1.2) + if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { + ensure!( + !queue.iter().any(|(idx, _)| *idx == next_index), + "NextPayout should not be in the queue" + ); + } + } + + Ok(()) + } + } +} + +pub use cleanup_proposals::Migration as CleanupProposalsMigration; +pub use migrate_to_ordered_payouts::MigrateToOrderedPayouts; diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 41a37cf98918..298412aa9970 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -139,6 +139,8 @@ parameter_types! { pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub TreasuryAccount: u128 = Treasury::account_id(); pub const SpendPayoutPeriod: u64 = 5; + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: u64 = 2; } pub struct TestSpendOrigin; @@ -195,6 +197,8 @@ impl Config for Test { type Paymaster = TestPay; type BalanceConverter = MulBy>; type PayoutPeriod = SpendPayoutPeriod; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; type BlockNumberProvider = System; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -655,6 +659,7 @@ fn spend_payout_works() { System::set_block_number(1); // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + // spend is automatically added to payout queue // payout the spend. assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); // beneficiary received `2` coins of asset `1`. @@ -743,6 +748,11 @@ fn spend_valid_from_works() { System::set_block_number(2); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + // Complete spend 0 so spend 1 can become NextPayout + let payment_id = get_payment_id(0).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Success); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + System::set_block_number(5); // spend approved even if `valid_from` in the past since the payout period has not passed. assert_ok!(Treasury::spend( @@ -838,6 +848,10 @@ fn check_status_works() { Error::::Inconclusive ); + // Complete spend 3 so spend 4 can become NextPayout + set_status(payment_id, PaymentStatus::Success); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 3)); + // spend `4` removed since the payment status is unknown. assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 4)); @@ -1046,3 +1060,369 @@ fn multiple_spend_periods_work() { assert_eq!(LastSpendPeriod::::get(), Some(8)); }); } + +#[test] +fn spend_auto_enqueues_to_payout_queue() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + // Create a spend - should be automatically added to payout queue + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None)); + // Check queue state - spend should be NextPayout since queue was empty + assert_eq!(NextPayout::::get(1u32), Some((0, 3))); // now + OrderExpirationPeriod + + // Queue should be empty since this was the first spend + assert_eq!(PayoutQueue::::get(1u32), vec![]); + }); +} + +#[test] +fn multiple_spends_auto_enqueue_in_sorted_order() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create multiple spends with different valid_from values + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(5) + )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(200), + Some(3) + )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(300), + Some(7) + )); + + // First spend should be NextPayout + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + + let queue = PayoutQueue::::get(1u32); + assert_eq!(queue, vec![(1, 3), (2, 7)]); + }); +} + +#[test] +fn payout_only_works_for_next_payout_per_asset() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create two spends for same asset + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(7), None)); + + // Payout spend 1 (not next) should fail + assert_noop!( + Treasury::payout(RuntimeOrigin::signed(1), 1), + Error::::NotNextPayout + ); + + // Payout spend 0 (next) should succeed + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + }); +} + +#[test] +fn different_assets_have_independent_queues() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create spends for different assets + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(2), 2, Box::new(200), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(150), None)); + + // Each asset should have its own NextPayout + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(2u32).map(|(idx, _)| idx), Some(1)); + + // Asset 1 queue should have spend 2 + assert_eq!(PayoutQueue::::get(1u32), vec![(2, 1)]); + + // Asset 2 queue should be empty + assert_eq!(PayoutQueue::::get(2u32), vec![]); + + // Can payout asset 2 even though asset 1 has earlier spends + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + assert_eq!(paid(200, 2), 2); + }); +} + +#[test] +fn check_status_rotates_expired_order() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create two spends for same asset + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + + // Verify initial state + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + + // Move past order expiration + System::set_block_number(4); + + // check_status should rotate the queue + let info = Treasury::check_status(RuntimeOrigin::signed(1), 0).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + + // Queue should be rotated: spend 0 moved to back + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![(0, 1)]); + + System::assert_last_event( + Event::::PayoutQueueRotated { asset_kind: 1, index: 0 }.into(), + ); + }); +} + +#[test] +fn check_status_does_not_rotate_if_not_expired() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create two spends + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + + // Try check_status before expiration - should fail with NotAttempted + assert_noop!( + Treasury::check_status(RuntimeOrigin::signed(1), 0), + Error::::NotAttempted + ); + + // State should be unchanged + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + }); +} + +#[test] +fn check_status_removes_completed_spend_and_promotes_next() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create two spends + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + + // Payout first spend + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + let payment_id = get_payment_id(0).unwrap(); + set_status(payment_id, PaymentStatus::Success); + + // check_status should remove completed spend and promote next + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + + // Spend 1 should now be NextPayout + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![]); + }); +} + +#[test] +fn void_spend_removes_from_queue_and_promotes_next() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create two spends + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + + // Void the first spend + assert_ok!(Treasury::void_spend(RuntimeOrigin::root(), 0)); + + // Spend 1 should now be NextPayout + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![]); + + // Spend 0 should be removed + assert_eq!(Spends::::get(0), None); + }); +} + +#[test] +fn fifo_ordering_enforced_per_asset() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create three spends in order for same asset + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(300), None)); + + // Payout should be in FIFO order + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + + // Payout spend 0 + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + let payment_id = get_payment_id(0).unwrap(); + set_status(payment_id, PaymentStatus::Success); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + + // Next should be spend 1 + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + + // Payout spend 1 + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + let payment_id = get_payment_id(1).unwrap(); + set_status(payment_id, PaymentStatus::Success); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 1)); + + // Next should be spend 2 + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(2)); + + // Verify beneficiaries received in order + assert_eq!(paid(100, 1), 1); + assert_eq!(paid(200, 1), 2); + }); +} + +#[test] +fn queue_full_scenario() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create spends up to MaxQueuedSpends for asset 1 + for i in 0..::MaxQueuedSpends::get().saturating_add(1) { + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(i as u128), + None + )); + } + + // Next spend should fail with QueueFull + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(999u128), None), + Error::::QueueFull + ); + + // But can still create spends for different asset + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(2), + 1, + Box::new(1000u128), + None + )); + }); +} + +#[test] +fn complex_scenario_with_rotation_and_completion() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create three spends + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(300), None)); + + // First payout succeeds + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + let payment_id = get_payment_id(0).unwrap(); + set_status(payment_id, PaymentStatus::Success); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + + // Second payout fails but stays in queue + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + let payment_id = get_payment_id(1).unwrap(); + set_status(payment_id, PaymentStatus::Failure); + unpay(200, 1, 200); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 1)); + + // Move past order expiration for spend 1 + System::set_block_number(4); + + // check_status should rotate the queue + let info = Treasury::check_status(RuntimeOrigin::signed(1), 1).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + + // Spend 2 should now be NextPayout + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(2)); + + // Payout spend 2 + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 2)); + let payment_id = get_payment_id(2).unwrap(); + set_status(payment_id, PaymentStatus::Success); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 2)); + + // Spend 1 should be back at head after rotation + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + + // Retry spend 1 + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + }); +} + +#[test] +fn try_state_payout_queue_invariants() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create some spends + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(5) + )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(200), + Some(3) + )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(300), + Some(7) + )); + + // Check invariants pass + assert!(Treasury::do_try_state().is_ok()); + + // Verify queue is sorted + let queue = PayoutQueue::::get(1u32); + assert_eq!(queue, vec![(1, 3), (2, 7)]); + }); +} + +#[test] +fn early_spend_cannot_be_paid_before_valid_from() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create a spend with future valid_from + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(10) + )); + + // Even though it's the NextPayout, it can't be paid before valid_from + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::EarlyPayout); + + // Move to valid_from block + System::set_block_number(10); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + }); +} From b00c9046af06e502e563fc2d8474bb7601936ef8 Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Fri, 17 Apr 2026 01:00:39 +0100 Subject: [PATCH 03/18] migration unit test --- polkadot/runtime/rococo/src/lib.rs | 2 +- substrate/frame/treasury/src/migration.rs | 269 +++++++++++++++++++++- 2 files changed, 261 insertions(+), 10 deletions(-) diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 44d8069a3cb4..88020d6ff58f 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1776,7 +1776,7 @@ pub mod migrations { pallet_elections_phragmen::migrations::unlock_and_unreserve_all_funds::UnlockAndUnreserveAllFunds, pallet_democracy::migrations::unlock_and_unreserve_all_funds::UnlockAndUnreserveAllFunds, pallet_tips::migrations::unreserve_deposits::UnreserveDeposits, - pallet_treasury::migration::cleanup_proposals::Migration, + pallet_treasury::migration::CleanupProposalsMigration, // Delete all Gov v1 pallet storage key/values. diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index d791e2c41792..3fc89a8efc59 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -18,15 +18,14 @@ //! Treasury pallet migrations. use super::*; -use alloc::collections::BTreeSet; -use alloc::vec::Vec; +use alloc::{collections::BTreeSet, vec::Vec}; use core::marker::PhantomData; use frame_support::{defensive, traits::OnRuntimeUpgrade}; /// The log target for this pallet. const LOG_TARGET: &str = "runtime::treasury"; -pub mod cleanup_proposals { +mod cleanup_proposals { use super::*; /// Migration to cleanup unapproved proposals to return the bonds back to the proposers. @@ -138,7 +137,7 @@ pub mod cleanup_proposals { /// This migration identifies all pending/failed spends that have not yet expired, /// groups them by asset kind, sorts them by valid_from (FIFO order), /// and initializes the PayoutQueue and NextPayout for each asset kind. -pub mod migrate_to_ordered_payouts { +mod migrate_to_ordered_payouts { use super::*; /// Migration to initialize the payout queue for existing spends. @@ -152,11 +151,13 @@ pub mod migrate_to_ordered_payouts { ); let now = T::BlockNumberProvider::current_block_number(); + let mut total_spends_read: u64 = 0; // Collect all pending/failed spends that haven't expired. let mut spends_vec: Vec<(T::AssetKind, SpendIndex, BlockNumberFor)> = Vec::new(); for (index, spend) in Spends::::iter() { + total_spends_read += 1; match spend.status { PaymentState::Pending | PaymentState::Failed => { // Only include spends that haven't expired @@ -250,7 +251,6 @@ pub mod migrate_to_ordered_payouts { } } - // Set the payout queue PayoutQueue::::insert(&asset_kind, queue); } @@ -261,8 +261,8 @@ pub mod migrate_to_ordered_payouts { total_assets_processed, ); - let reads = total_spends_processed as u64 + 1; // Spends reads + block number read - let writes = total_assets_processed as u64 * 2; // PayoutQueue + NextPayout per asset + let reads = total_spends_read + 1; + let writes = total_assets_processed as u64 * 2; T::DbWeight::get().reads_writes(reads, writes) } @@ -319,8 +319,7 @@ pub mod migrate_to_ordered_payouts { ensure!(spend.asset_kind == asset_kind, "Spend in queue has wrong asset kind"); } - // Verify NextPayout is NOT in queue (they are separate storage items in Solution - // 1.2) + // Verify NextPayout is NOT in queue if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { ensure!( !queue.iter().any(|(idx, _)| *idx == next_index), @@ -332,6 +331,258 @@ pub mod migrate_to_ordered_payouts { Ok(()) } } + + #[cfg(test)] + mod tests { + use super::*; + use crate::{ + pallet::Spends, + tests::{ExtBuilder, System, Test}, + }; + use frame_support::traits::OnRuntimeUpgrade; + + #[cfg(feature = "try-runtime")] + use frame_support::assert_ok; + + /// Helper to directly insert a spend into storage + fn insert_spend( + index: SpendIndex, + asset_kind: u32, + amount: u64, + beneficiary: u128, + valid_from: u64, + expire_at: u64, + status: PaymentState, + ) { + let spend = crate::SpendStatus { + asset_kind, + amount, + beneficiary, + valid_from, + expire_at, + status, + }; + + crate::pallet::Spends::::insert(index, spend); + + let current = crate::pallet::SpendCount::::get(); + + if index >= current { + crate::pallet::SpendCount::::put(index + 1); + } + } + + #[test] + fn migration_empty_state() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + assert_eq!(Spends::::iter().count(), 0); + + let weight = MigrateToOrderedPayouts::::on_runtime_upgrade(); + + assert!(weight.ref_time() == 0); + assert!(NextPayout::::iter().next().is_none()); + assert!(PayoutQueue::::iter().next().is_none()); + }); + } + + #[test] + fn migration_skips_attempted_spends() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Attempted { id: 123u64 }); + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + assert!(NextPayout::::get(1u32).is_none()); + assert_eq!(PayoutQueue::::get(1u32).len(), 0); + }); + } + + #[test] + fn migration_skips_expired_spends() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + // expire_at (99) < now (100) + insert_spend(0, 1, 100, 1000, 50, 99, PaymentState::Pending); + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + assert!(NextPayout::::get(1u32).is_none()); + }); + } + + #[test] + fn migration_groups_by_asset() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + // Asset 1: 2 spends + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); + insert_spend(1, 1, 200, 1001, 60, 200, PaymentState::Pending); + // Asset 2: 1 spend + insert_spend(2, 2, 300, 1002, 40, 200, PaymentState::Failed); + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + // Asset 1: First spend is NextPayout + let (next_idx, expire_at) = NextPayout::::get(1u32).unwrap(); + assert_eq!(next_idx, 0); + assert_eq!(expire_at, 102); // 100 + OrderExpirationPeriod(2) + + // Asset 1 queue: spend 1 + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 60)]); + + // Asset 2: Spend 2 is NextPayout + assert_eq!(NextPayout::::get(2u32).map(|(idx, _)| idx), Some(2)); + assert_eq!(PayoutQueue::::get(2u32).len(), 0); + }); + } + + #[test] + fn migration_sorts_by_valid_from() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + // Insert out of order + insert_spend(0, 1, 100, 1000, 100, 200, PaymentState::Pending); // latest + insert_spend(1, 1, 100, 1001, 50, 200, PaymentState::Pending); // earliest + insert_spend(2, 1, 100, 1002, 75, 200, PaymentState::Pending); // middle + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + // Sorted: 1 (50), 2 (75), 0 (100) + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![(2, 75), (0, 100)]); + }); + } + + #[test] + fn migration_tie_breaks_by_index() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + // Same valid_from, different indices (inserted out of order) + insert_spend(5, 1, 100, 1000, 50, 200, PaymentState::Pending); + insert_spend(3, 1, 100, 1001, 50, 200, PaymentState::Pending); + insert_spend(4, 1, 100, 1002, 50, 200, PaymentState::Pending); + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + // Sorted by index: 3, 4, 5 + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(3)); + assert_eq!(PayoutQueue::::get(1u32), vec![(4, 50), (5, 50)]); + }); + } + + #[test] + fn migration_respects_max_queue() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + // Create 105 spends (MaxQueuedSpends = 100) + for i in 0..105u32 { + insert_spend( + i, + 1, + 100, + 1000 + i as u128, + 50 + i as u64, + 200, + PaymentState::Pending, + ); + } + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + // NextPayout is index 0 + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + + // Queue capped at 100 + let queue = PayoutQueue::::get(1u32); + assert_eq!(queue.len(), 100); + assert_eq!(queue[0].0, 1); + assert_eq!(queue[99].0, 100); + }); + } + + #[test] + fn migration_mixed_statuses() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); + insert_spend(1, 1, 100, 1001, 51, 200, PaymentState::Failed); + insert_spend(2, 1, 100, 1002, 52, 200, PaymentState::Attempted { id: 1 }); + insert_spend(3, 1, 100, 1003, 53, 99, PaymentState::Pending); // expired + + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + // Only 0 and 1 in queue + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 51)]); + }); + } + + #[test] + #[cfg(feature = "try-runtime")] + fn pre_upgrade_captures_state() { + ExtBuilder::default().build().execute_with(|| { + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); + insert_spend(1, 1, 100, 1001, 51, 200, PaymentState::Failed); + insert_spend(2, 1, 100, 1002, 52, 200, PaymentState::Attempted { id: 1 }); + + let state = MigrateToOrderedPayouts::::pre_upgrade().unwrap(); + let decoded: Vec<(u32, u32)> = Vec::decode(&mut &state[..]).unwrap(); + + assert_eq!(decoded.len(), 2); + assert!(decoded.contains(&(1, 0))); + assert!(decoded.contains(&(1, 1))); + }); + } + + #[test] + #[cfg(feature = "try-runtime")] + fn post_upgrade_validates() { + ExtBuilder::default().build().execute_with(|| { + let pre_state = { + let mut spends = Vec::new(); + spends.push((1u32, 0u32)); + spends.push((1u32, 1u32)); + spends.encode() + }; + + System::set_block_number(100); + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); + insert_spend(1, 1, 100, 1001, 51, 200, PaymentState::Pending); + MigrateToOrderedPayouts::::on_runtime_upgrade(); + + assert_ok!(MigrateToOrderedPayouts::::post_upgrade(pre_state)); + }); + } + + #[test] + #[cfg(feature = "try-runtime")] + fn post_upgrade_detects_invalid() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + + // Create invalid state: NextPayout in queue + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); + NextPayout::::insert(1u32, (0u32, 102u64)); + + let bounded_vec: BoundedVec<(u32, u64), crate::tests::MaxQueuedSpends> = + vec![(0u32, 50u64)].try_into().unwrap(); + crate::pallet::PayoutQueue::::insert(1u32, bounded_vec); + + let pre_state = vec![(1u32, 0u32)].encode(); + + assert!(MigrateToOrderedPayouts::::post_upgrade(pre_state).is_err()); + }); + } + } } pub use cleanup_proposals::Migration as CleanupProposalsMigration; From 0454019623738e2258f54771017ff8ea803865d1 Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Sat, 18 Apr 2026 02:07:46 +0100 Subject: [PATCH 04/18] add migration to westend --- polkadot/runtime/westend/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 1c8c1fc5d0b3..a149ebd0bb4d 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -2101,6 +2101,7 @@ pub mod migrations { >, // permanent pallet_xcm::migration::MigrateToLatestXcmVersion, + pallet_treasury::migration::MigrateToOrderedPayouts, ); } From 413097480e84a8c4ee6574e5d544e95bc94a403b Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Fri, 12 Jun 2026 21:57:30 +0100 Subject: [PATCH 05/18] OrderKey --- prdoc/pr_11603.prdoc | 44 ++++++ substrate/frame/treasury/src/benchmarking.rs | 28 ++++ substrate/frame/treasury/src/lib.rs | 143 ++++++++----------- substrate/frame/treasury/src/tests.rs | 43 +++++- 4 files changed, 169 insertions(+), 89 deletions(-) create mode 100644 prdoc/pr_11603.prdoc diff --git a/prdoc/pr_11603.prdoc b/prdoc/pr_11603.prdoc new file mode 100644 index 000000000000..f8db7123a24d --- /dev/null +++ b/prdoc/pr_11603.prdoc @@ -0,0 +1,44 @@ +title: 'pallet-treasury: Ordered Payouts' +doc: +- audience: Runtime Dev + description: |- + Implements ordered spend payouts (Solution 1.2 of https://github.com/paritytech/polkadot-sdk/issues/11100) + for `pallet-treasury`. Spends are now paid out in order per asset kind, preventing smaller, + later-approved spends from draining the treasury before earlier-approved spends can be paid. + + Two new storage items are introduced: `NextPayout`, holding the spend currently at the head of + the payout order for each asset kind together with its order expiration block, and + `PayoutQueue`, a bounded vector of `(spend_index, order_key)` entries per asset kind sorted by + order key. The order key is the spend's `valid_from` for newly inserted spends and the block + number of the rotation for spends rotated to the back after their order expired. + + No new extrinsics are added; ordering is enforced transparently through the existing calls: + + - `spend` inserts the approved spend into the payout queue for its asset kind, or sets it as + `NextPayout` if there is none, with an order expiration of + `max(now, valid_from) + OrderExpirationPeriod`. + - `payout` only succeeds for the spend designated as `NextPayout` for its asset kind and fails + with the new `NotNextPayout` error otherwise. + - `check_status` additionally rotates an expired head back into the queue (emitting the new + `PayoutQueueRotated` event) and promotes the next entry on removal of a paid-out or expired + spend. + - `void_spend` also removes the spend from the payout order and promotes the next entry. + + Runtimes adopting this need to configure two new `Config` items: `MaxQueuedSpends` (bounds the + queue per asset kind; spends fail with the new `QueueFull` error once reached) and + `OrderExpirationPeriod` (how long a spend may block the head of the order before it can be + rotated). A `MigrateToOrderedPayouts` migration is provided to populate the payout order from + the existing `Spends` storage. +crates: +- name: pallet-treasury + bump: major +- name: westend-runtime + bump: minor +- name: rococo-runtime + bump: minor +- name: asset-hub-westend-runtime + bump: minor +- name: collectives-westend-runtime + bump: minor +- name: kitchensink-runtime + bump: minor diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index a11723a27b2c..91a0b2829c5d 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -100,6 +100,20 @@ fn assert_last_event, I: 'static>(generic_event: >:: frame_system::Pallet::::assert_last_event(generic_event.into()); } +// Fill the payout queue for `asset_kind` with `n` entries so the benchmarked call hits the +// worst case: a full scan over (and re-encode of) a queue of up to `MaxQueuedSpends` entries. +// The seeded indices are offset to avoid colliding with spends created by the benchmark and +// use an order key of zero so that a subsequent sorted insert scans the entire queue. +fn fill_payout_queue, I: 'static>(asset_kind: T::AssetKind, n: u32) { + let order_key = BlockNumberFor::::zero(); + let queue: BoundedVec<_, T::MaxQueuedSpends> = (0..n) + .map(|i| (i + 1_000_000, order_key)) + .collect::>() + .try_into() + .expect("n is at most MaxQueuedSpends; qed"); + PayoutQueue::::insert(asset_kind, queue); +} + // Create the arguments for the `spend` dispatchable. fn create_spend_arguments, I: 'static>( seed: u32, @@ -189,6 +203,13 @@ mod benchmarks { create_spend_arguments::(SEED); T::BalanceConverter::ensure_successful(asset_kind.clone()); + // Worst case: `NextPayout` is already occupied and the new spend is inserted into an + // almost full payout queue, scanning all existing entries. + let now = T::BlockNumberProvider::current_block_number(); + let order_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert(asset_kind.clone(), (1_000_000u32, order_expire_at)); + fill_payout_queue::(asset_kind.clone(), T::MaxQueuedSpends::get().saturating_sub(1)); + #[extrinsic_call] _( origin as T::RuntimeOrigin, @@ -294,6 +315,10 @@ mod benchmarks { false }; + // Worst case: removing the spend promotes the next entry from a full payout queue. + let (asset_kind, ..) = create_spend_arguments::(SEED); + fill_payout_queue::(asset_kind, T::MaxQueuedSpends::get()); + #[block] { let res = @@ -335,6 +360,9 @@ mod benchmarks { let origin = T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + // Worst case: voiding the spend promotes the next entry from a full payout queue. + fill_payout_queue::(asset_kind.clone(), T::MaxQueuedSpends::get()); + #[block] { let res = Treasury::::void_spend(origin as T::RuntimeOrigin, 0u32); diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index a748a6be027b..03d4e44fd616 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -75,13 +75,14 @@ //! spends from draining the treasury before larger, earlier-approved spends can be paid. //! //! When a spend is approved via the `spend` call, it is automatically inserted into the payout -//! queue for its asset kind in sorted order by `valid_from`. Spends are then paid out in order -//! via the `payout` call, which only processes the spend at the head of the queue for that -//! asset kind. +//! queue for its asset kind, sorted by an order key — the spend's `valid_from` for newly +//! inserted spends. Spends are then paid out in order via the `payout` call, which only +//! processes the spend at the head of the queue for that asset kind. //! //! If a spend at the head of the queue cannot be paid out (e.g., due to insufficient funds), //! its order will expire after [`Config::OrderExpirationPeriod`] blocks. The `check_status` call -//! will then rotate it to the back of the queue, allowing other spends of the same asset kind +//! will then rotate it back into the queue with the current block number as its order key, +//! placing it behind every already-mature spend and allowing other spends of the same asset kind //! to be processed. //! //! Payout ordering is managed independently per asset kind - a spend of one asset cannot block @@ -393,7 +394,10 @@ pub mod pallet { StorageMap<_, Twox64Concat, T::AssetKind, (SpendIndex, BlockNumberFor), OptionQuery>; /// The queue of spends in payout order for each asset kind. - /// Each entry contains (spend_index, valid_from) for sorting and maturity checking. + /// + /// Each entry contains `(spend_index, order_key)` and the queue is sorted by the order key. + /// The order key is the spend's `valid_from` for newly inserted spends and the block number + /// of the rotation for spends that were rotated to the back after their order expired. #[pallet::storage] pub type PayoutQueue, I: 'static = ()> = StorageMap< _, @@ -684,8 +688,9 @@ pub mod pallet { /// the [`Config::PayoutPeriod`]. /// /// The spend is automatically inserted into the payout queue for its asset kind in sorted - /// order by `valid_from`. If there is no current `NextPayout` for this asset kind, the - /// spend becomes the `NextPayout` with an expiration of `now + OrderExpirationPeriod`. + /// order by its order key (`valid_from` for new spends). If there is no current + /// `NextPayout` for this asset kind, the spend becomes the `NextPayout` with an + /// expiration of `max(now, valid_from) + OrderExpirationPeriod`. /// /// ### Parameters /// - `asset_kind`: An indicator of the specific asset class to be spent. @@ -1003,8 +1008,10 @@ impl, I: 'static> Pallet { Approvals::::get() } - /// Insert a spend into the payout queue for the given asset kind in sorted order by valid_from. - /// If there is no NextPayout for this asset kind, the spend becomes the NextPayout. + /// Insert a newly approved spend into the payout queue for the given asset kind, keeping the + /// queue sorted by order key. For newly inserted spends the order key is the spend's + /// `valid_from`. If there is no NextPayout for this asset kind, the spend becomes the + /// NextPayout instead. fn insert_into_payout_queue( asset_kind: T::AssetKind, index: SpendIndex, @@ -1015,9 +1022,11 @@ impl, I: 'static> Pallet { // There's already a NextPayout, add to queue in sorted order let mut queue = PayoutQueue::::get(&asset_kind); - // Find the correct position to insert (maintain sorted order by valid_from) - let insert_pos = - queue.iter().position(|(_, vf)| *vf > valid_from).unwrap_or(queue.len()); + // Find the correct position to insert (maintain sorted order by order key) + let insert_pos = queue + .iter() + .position(|(_, order_key)| *order_key > valid_from) + .unwrap_or(queue.len()); // Insert at the found position if queue.try_insert(insert_pos, (index, valid_from)).is_err() { @@ -1026,9 +1035,10 @@ impl, I: 'static> Pallet { PayoutQueue::::insert(&asset_kind, queue); } else { - // No NextPayout for this asset kind, set this as NextPayout + // No NextPayout for this asset kind, set this as NextPayout. The order only starts + // counting down once the spend is mature. let now = T::BlockNumberProvider::current_block_number(); - let expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + let expire_at = now.max(valid_from).saturating_add(T::OrderExpirationPeriod::get()); NextPayout::::insert(&asset_kind, (index, expire_at)); } @@ -1044,10 +1054,13 @@ impl, I: 'static> Pallet { let mut queue = PayoutQueue::::get(asset_kind); // Check what to promote BEFORE removing - if let Some(&(new_next_index, _)) = queue.first() { - // Promote to NextPayout + if let Some(&(new_next_index, order_key)) = queue.first() { + // Promote to NextPayout. The order only starts counting down once the + // promoted spend is mature (its order key is `valid_from` for fresh entries + // and is never in the future for rotated ones). let now = T::BlockNumberProvider::current_block_number(); - let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + let new_expire_at = + now.max(order_key).saturating_add(T::OrderExpirationPeriod::get()); NextPayout::::insert(asset_kind, (new_next_index, new_expire_at)); // Remove it from queue @@ -1069,22 +1082,26 @@ impl, I: 'static> Pallet { } /// Rotate the payout queue for the given asset kind when the current order expires. - /// Moves the current NextPayout to the back of the queue and promotes the next entry. + /// Re-inserts the current NextPayout into the queue with an order key of `now`, keeping the + /// queue sorted by order key, and promotes the next entry. fn rotate_payout_queue(asset_kind: &T::AssetKind) -> DispatchResult { let (current_index, _) = NextPayout::::get(asset_kind).ok_or(Error::::NotNextPayout)?; let mut queue = PayoutQueue::::get(asset_kind); + let now = T::BlockNumberProvider::current_block_number(); - // Push the expired spend to the back of the queue - let spend = Spends::::get(current_index).ok_or(Error::::InvalidIndex)?; + // Re-insert the expired spend with `now` as its order key, keeping the queue sorted. + // Using `now` (rather than the spend's `valid_from`) sends the rotated spend behind + // every entry that is already mature, while preserving the sort order. + let insert_pos = + queue.iter().position(|(_, order_key)| *order_key > now).unwrap_or(queue.len()); queue - .try_push((current_index, spend.valid_from)) + .try_insert(insert_pos, (current_index, now)) .map_err(|_| Error::::QueueFull)?; // Promote the front of queue to NextPayout and remove it from queue - if let Some(&(new_next, _new_valid_from)) = queue.first() { - let now = T::BlockNumberProvider::current_block_number(); - let new_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); + if let Some(&(new_next, order_key)) = queue.first() { + let new_expire_at = now.max(order_key).saturating_add(T::OrderExpirationPeriod::get()); NextPayout::::insert(asset_kind, (new_next, new_expire_at)); // Remove the promoted entry from queue since it's now NextPayout queue.remove(0); @@ -1278,63 +1295,15 @@ impl, I: 'static> Pallet { // ## Invariants of payout queue storage items // - // 1. If [`NextPayout`] for an asset kind is Some, the first element of [`PayoutQueue`] for - // that asset kind must match the spend index in [`NextPayout`]. - // 2. All spend indices in [`PayoutQueue`] for an asset kind must exist in [`Spends`] and - // have Pending or Failed status. - // 3. The length of each [`PayoutQueue`] must not exceed [`Config::MaxQueuedSpends`]. - // 4. No duplicate spend indices in any [`PayoutQueue`]. - // 5. The queue for each asset kind must be sorted by valid_from. - // #[cfg(any(feature = "try-runtime", test))] - // fn try_state_payout_queue() -> Result<(), sp_runtime::TryRuntimeError> { - // use alloc::collections::btree_set::BTreeSet; - // - // for (asset_kind, queue) in PayoutQueue::::iter() { - // ensure!( - // queue.len() as u32 <= T::MaxQueuedSpends::get(), - // "Payout queue length exceeds MaxQueuedSpends." - // ); - // - // Check no duplicates - // let unique_indices: BTreeSet<_> = queue.iter().map(|(idx, _)| *idx).collect(); - // ensure!( - // unique_indices.len() == queue.len() as usize, - // "Payout queue contains duplicate spend indices." - // ); - // - // Check sorted by valid_from - // let mut prev_valid_from: Option> = None; - // for (_, valid_from) in queue.iter() { - // if let Some(prev) = prev_valid_from { - // ensure!( - // prev <= *valid_from, - // "Payout queue is not sorted by valid_from." - // ); - // } - // prev_valid_from = Some(*valid_from); - // } - // - // if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { - // ensure!( - // queue.first().map(|(idx, _)| *idx) == Some(next_index), - // "NextPayout must match the first element of PayoutQueue." - // ); - // } - // - // for (spend_index, _) in queue.iter() { - // let spend = Spends::::get(spend_index).ok_or( - // sp_runtime::TryRuntimeError::Other("Spend in queue must exist in Spends."), - // )?; - // ensure!( - // matches!(spend.status, PaymentState::Pending | PaymentState::Failed), - // "Spend in queue must have Pending or Failed status." - // ); - // } - // } - // - // Ok(()) - // } - + // 1. The length of each [`PayoutQueue`] must not exceed [`Config::MaxQueuedSpends`]. + // 2. No duplicate spend indices in any [`PayoutQueue`]. + // 3. The queue for each asset kind must be sorted by order key. The order key is + // `valid_from` for newly inserted spends and the block number of the rotation for rotated + // spends, so the sort order always holds. + // 4. If [`NextPayout`] for an asset kind is Some, its spend index must not also be present + // in the [`PayoutQueue`] for that asset kind. + // 5. All spend indices in [`PayoutQueue`] for an asset kind must exist in [`Spends`] with + // Pending or Failed status and a matching asset kind. #[cfg(any(feature = "try-runtime", test))] fn try_state_payout_queue() -> Result<(), sp_runtime::TryRuntimeError> { use alloc::collections::btree_set::BTreeSet; @@ -1352,13 +1321,13 @@ impl, I: 'static> Pallet { "Payout queue contains duplicate spend indices." ); - // Check sorted by valid_from - let mut prev_valid_from: Option> = None; - for (_, valid_from) in queue.iter() { - if let Some(prev) = prev_valid_from { - ensure!(prev <= *valid_from, "Payout queue is not sorted by valid_from."); + // Check sorted by order key + let mut prev_order_key: Option> = None; + for (_, order_key) in queue.iter() { + if let Some(prev) = prev_order_key { + ensure!(prev <= *order_key, "Payout queue is not sorted by order key."); } - prev_valid_from = Some(*valid_from); + prev_order_key = Some(*order_key); } // Check that NextPayout is not also in the queue (they're separate) diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 298412aa9970..0fc35ef72cef 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -1176,13 +1176,51 @@ fn check_status_rotates_expired_order() { let info = Treasury::check_status(RuntimeOrigin::signed(1), 0).unwrap(); assert_eq!(info.pays_fee, Pays::No); - // Queue should be rotated: spend 0 moved to back + // Queue should be rotated: spend 0 moved to back with `now` as its order key assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); - assert_eq!(PayoutQueue::::get(1u32), vec![(0, 1)]); + assert_eq!(PayoutQueue::::get(1u32), vec![(0, 4)]); System::assert_last_event( Event::::PayoutQueueRotated { asset_kind: 1, index: 0 }.into(), ); + + // The queue must still satisfy all invariants after the rotation + assert_ok!(Treasury::do_try_state()); + }); +} + +#[test] +fn rotated_spend_keeps_queue_sorted_and_passes_try_state() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Two immediately payable spends and one not yet mature + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(100), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(300), + Some(10) + )); + + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 1), (2, 10)]); + + // Move past the head's order expiration and rotate it + System::set_block_number(4); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 0).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + + // The rotated spend is re-inserted with `now` (4) as its order key, which places it + // behind every mature spend but ahead of the not-yet-mature one, keeping the queue + // sorted. Spend 1 is promoted to NextPayout. + assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![(0, 4), (2, 10)]); + + // The sortedness invariant must hold after the rotation + assert_ok!(Treasury::do_try_state()); }); } @@ -1352,6 +1390,7 @@ fn complex_scenario_with_rotation_and_completion() { // Spend 2 should now be NextPayout assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(2)); + assert_ok!(Treasury::do_try_state()); // Payout spend 2 assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 2)); From fb5c377f437775cfef8edeaaccedd8b065dffe72 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:35:42 +0000 Subject: [PATCH 06/18] Update from github-actions[bot] running command 'bench --pallet pallet_treasury' --- .../src/weights/pallet_treasury.rs | 240 ++++++++++-------- .../src/weights/pallet_treasury.rs | 94 ++++--- substrate/frame/treasury/src/weights.rs | 167 +++++++----- 3 files changed, 283 insertions(+), 218 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs index bd90482f8f47..e0827982161a 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs @@ -1,5 +1,4 @@ // Copyright (C) Parity Technologies (UK) Ltd. -// This file is part of Cumulus. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,25 +15,29 @@ //! Autogenerated weights for `pallet_treasury` //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-07, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `cob`, CPU: `` -//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 +//! HOSTNAME: `22cd7d3cce09`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: -// ./target/debug/polkadot +// frame-omni-bencher +// v1 // benchmark // pallet -// --chain=rococo-dev -// --steps=50 -// --repeat=2 -// --pallet=pallet_treasury // --extrinsic=* +// --runtime=target/production/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm +// --pallet=pallet_treasury +// --header=/__w/polkadot-sdk/polkadot-sdk/cumulus/file_header.txt +// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights // --wasm-execution=compiled +// --steps=50 +// --repeat=20 // --heap-pages=4096 -// --output=./runtime/rococo/src/weights/ -// --header=./file_header.txt +// --no-storage-info +// --no-min-squares +// --no-median-slopes #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -47,132 +50,153 @@ use core::marker::PhantomData; /// Weight functions for `pallet_treasury`. pub struct WeightInfo(PhantomData); impl pallet_treasury::WeightInfo for WeightInfo { - /// Storage: Treasury ProposalCount (r:1 w:1) - /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: Treasury Approvals (r:1 w:1) - /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// Storage: Treasury Proposals (r:0 w:1) - /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: `Treasury::ProposalCount` (r:1 w:1) + /// Proof: `Treasury::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Treasury::Approvals` (r:1 w:1) + /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) + /// Storage: `Treasury::Proposals` (r:0 w:1) + /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) fn spend_local() -> Weight { // Proof Size summary in bytes: - // Measured: `42` + // Measured: `76` // Estimated: `1887` - // Minimum execution time: 177_000_000 picoseconds. - Weight::from_parts(191_000_000, 0) + // Minimum execution time: 13_212_000 picoseconds. + Weight::from_parts(14_667_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) } - /// Storage: Treasury Approvals (r:1 w:1) - /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: `Treasury::Approvals` (r:1 w:1) + /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) fn remove_approval() -> Weight { // Proof Size summary in bytes: - // Measured: `127` + // Measured: `161` // Estimated: `1887` - // Minimum execution time: 80_000_000 picoseconds. - Weight::from_parts(82_000_000, 0) + // Minimum execution time: 7_147_000 picoseconds. + Weight::from_parts(7_886_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: Treasury Deactivated (r:1 w:1) - /// Proof: Treasury Deactivated (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) - /// Storage: Balances InactiveIssuance (r:1 w:1) - /// Proof: Balances InactiveIssuance (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) - /// Storage: Treasury Approvals (r:1 w:1) - /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// Storage: Treasury Proposals (r:99 w:99) - /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) - /// Storage: System Account (r:199 w:199) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Bounties BountyApprovals (r:1 w:1) - /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) + /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Treasury::Deactivated` (r:1 w:1) + /// Proof: `Treasury::Deactivated` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `Treasury::LastSpendPeriod` (r:1 w:1) + /// Proof: `Treasury::LastSpendPeriod` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `331 + p * (251 ±0)` - // Estimated: `3593 + p * (5206 ±0)` - // Minimum execution time: 887_000_000 picoseconds. - Weight::from_parts(828_616_021, 0) + // Measured: `1444` + // Estimated: `3593` + // Minimum execution time: 21_490_000 picoseconds. + Weight::from_parts(29_173_765, 0) .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 695_351 - .saturating_add(Weight::from_parts(566_114_524, 0).saturating_mul(p.into())) + // Standard Error: 1_677 + .saturating_add(Weight::from_parts(91_915, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(p.into()))) - .saturating_add(T::DbWeight::get().writes(5)) - .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(p.into()))) - .saturating_add(Weight::from_parts(0, 5206).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().writes(2)) } - /// Storage: AssetRate ConversionRateToNative (r:1 w:0) - /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) - /// Storage: Treasury SpendCount (r:1 w:1) - /// Proof: Treasury SpendCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: Treasury Spends (r:0 w:1) - /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) + /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `AssetRate::ConversionRateToNative` (r:1 w:0) + /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(1238), added: 3713, mode: `MaxEncodedLen`) + /// Storage: `Treasury::SpendCount` (r:1 w:1) + /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:0) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) + /// Storage: `Treasury::Spends` (r:0 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `114` - // Estimated: `4702` - // Minimum execution time: 208_000_000 picoseconds. - Weight::from_parts(222_000_000, 0) - .saturating_add(Weight::from_parts(0, 4702)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `1068` + // Estimated: `5481` + // Minimum execution time: 36_111_000 picoseconds. + Weight::from_parts(38_640_000, 0) + .saturating_add(Weight::from_parts(0, 5481)) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(3)) } - /// Storage: Treasury Spends (r:1 w:1) - /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) - /// Storage: XcmPallet QueryCounter (r:1 w:1) - /// Proof Skipped: XcmPallet QueryCounter (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Configuration ActiveConfig (r:1 w:0) - /// Proof Skipped: Configuration ActiveConfig (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Dmp DeliveryFeeFactor (r:1 w:0) - /// Proof Skipped: Dmp DeliveryFeeFactor (max_values: None, max_size: None, mode: Measured) - /// Storage: XcmPallet SupportedVersion (r:1 w:0) - /// Proof Skipped: XcmPallet SupportedVersion (max_values: None, max_size: None, mode: Measured) - /// Storage: XcmPallet VersionDiscoveryQueue (r:1 w:1) - /// Proof Skipped: XcmPallet VersionDiscoveryQueue (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: XcmPallet SafeXcmVersion (r:1 w:0) - /// Proof Skipped: XcmPallet SafeXcmVersion (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Dmp DownwardMessageQueues (r:1 w:1) - /// Proof Skipped: Dmp DownwardMessageQueues (max_values: None, max_size: None, mode: Measured) - /// Storage: Dmp DownwardMessageQueueHeads (r:1 w:1) - /// Proof Skipped: Dmp DownwardMessageQueueHeads (max_values: None, max_size: None, mode: Measured) - /// Storage: XcmPallet Queries (r:0 w:1) - /// Proof Skipped: XcmPallet Queries (max_values: None, max_size: None, mode: Measured) + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:0) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) + /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:2) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `AssetsHolder::BalancesOnHold` (r:1 w:1) + /// Proof: `AssetsHolder::BalancesOnHold` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1) + /// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `AssetsFreezer::Freezes` (r:1 w:1) + /// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Storage: `AssetsHolder::Holds` (r:1 w:1) + /// Proof: `AssetsHolder::Holds` (`max_values`: None, `max_size`: Some(411), added: 2886, mode: `MaxEncodedLen`) + /// Storage: `Revive::OriginalAccount` (r:1 w:1) + /// Proof: `Revive::OriginalAccount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `737` - // Estimated: `5313` - // Minimum execution time: 551_000_000 picoseconds. - Weight::from_parts(569_000_000, 0) - .saturating_add(Weight::from_parts(0, 5313)) - .saturating_add(T::DbWeight::get().reads(9)) - .saturating_add(T::DbWeight::get().writes(6)) + // Measured: `5033` + // Estimated: `6518` + // Minimum execution time: 141_509_000 picoseconds. + Weight::from_parts(149_841_000, 0) + .saturating_add(Weight::from_parts(0, 6518)) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(11)) } - /// Storage: Treasury Spends (r:1 w:1) - /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) - /// Storage: XcmPallet Queries (r:1 w:1) - /// Proof Skipped: XcmPallet Queries (max_values: None, max_size: None, mode: Measured) + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) + /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `442` - // Estimated: `5313` - // Minimum execution time: 245_000_000 picoseconds. - Weight::from_parts(281_000_000, 0) - .saturating_add(Weight::from_parts(0, 5313)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `1112` + // Estimated: `5921` + // Minimum execution time: 35_815_000 picoseconds. + Weight::from_parts(38_194_000, 0) + .saturating_add(Weight::from_parts(0, 5921)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) } - /// Storage: Treasury Spends (r:1 w:1) - /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) + /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `172` - // Estimated: `5313` - // Minimum execution time: 147_000_000 picoseconds. - Weight::from_parts(160_000_000, 0) - .saturating_add(Weight::from_parts(0, 5313)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) + // Measured: `1104` + // Estimated: `5921` + // Minimum execution time: 30_642_000 picoseconds. + Weight::from_parts(32_548_000, 0) + .saturating_add(Weight::from_parts(0, 5921)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) } } diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs index 5e4a5a8b1edc..e88bf1a6e130 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs @@ -16,9 +16,9 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `e0f303704c84`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `22cd7d3cce09`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -58,10 +58,10 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `FellowshipTreasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) fn spend_local() -> Weight { // Proof Size summary in bytes: - // Measured: `76` + // Measured: `109` // Estimated: `1887` - // Minimum execution time: 12_879_000 picoseconds. - Weight::from_parts(13_346_000, 0) + // Minimum execution time: 12_419_000 picoseconds. + Weight::from_parts(13_318_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) @@ -70,10 +70,10 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `FellowshipTreasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) fn remove_approval() -> Weight { // Proof Size summary in bytes: - // Measured: `161` + // Measured: `194` // Estimated: `1887` - // Minimum execution time: 6_978_000 picoseconds. - Weight::from_parts(7_278_000, 0) + // Minimum execution time: 6_650_000 picoseconds. + Weight::from_parts(7_208_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -87,13 +87,13 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `280 + p * (1 ±0)` + // Measured: `313 + p * (1 ±0)` // Estimated: `3593` - // Minimum execution time: 13_758_000 picoseconds. - Weight::from_parts(17_033_109, 0) + // Minimum execution time: 13_090_000 picoseconds. + Weight::from_parts(16_718_647, 0) .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 1_091 - .saturating_add(Weight::from_parts(70_962, 0).saturating_mul(p.into())) + // Standard Error: 405 + .saturating_add(Weight::from_parts(63_474, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -103,20 +103,26 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(1238), added: 3713, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::SpendCount` (r:1 w:1) /// Proof: `FellowshipTreasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:0) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) + /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::Spends` (r:0 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `219` - // Estimated: `4703` - // Minimum execution time: 24_150_000 picoseconds. - Weight::from_parts(24_739_000, 0) - .saturating_add(Weight::from_parts(0, 4703)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `1143` + // Estimated: `5481` + // Minimum execution time: 35_418_000 picoseconds. + Weight::from_parts(37_538_000, 0) + .saturating_add(Weight::from_parts(0, 5481)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: `FellowshipTreasury::Spends` (r:1 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:0) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `PolkadotXcm::QueryCounter` (r:1 w:1) @@ -125,48 +131,56 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `XcmpQueue::DeliveryFeeFactor` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0) /// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:1) + /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: Some(2306), added: 2801, mode: `MaxEncodedLen`) /// Storage: `ParachainSystem::RelevantMessagingState` (r:1 w:0) /// Proof: `ParachainSystem::RelevantMessagingState` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:1) - /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: Some(1282), added: 1777, mode: `MaxEncodedLen`) /// Storage: `XcmpQueue::OutboundXcmpMessages` (r:0 w:1) /// Proof: `XcmpQueue::OutboundXcmpMessages` (`max_values`: None, `max_size`: Some(105506), added: 107981, mode: `MaxEncodedLen`) /// Storage: `PolkadotXcm::Queries` (r:0 w:1) /// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `559` + // Measured: `564` // Estimated: `5318` - // Minimum execution time: 59_146_000 picoseconds. - Weight::from_parts(62_110_000, 0) + // Minimum execution time: 61_552_000 picoseconds. + Weight::from_parts(65_444_000, 0) .saturating_add(Weight::from_parts(0, 5318)) - .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(5)) } /// Storage: `FellowshipTreasury::Spends` (r:1 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:1) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) /// Storage: `PolkadotXcm::Queries` (r:1 w:1) /// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) + /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `240` - // Estimated: `5318` - // Minimum execution time: 25_460_000 picoseconds. - Weight::from_parts(26_237_000, 0) - .saturating_add(Weight::from_parts(0, 5318)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `1172` + // Estimated: `5481` + // Minimum execution time: 40_524_000 picoseconds. + Weight::from_parts(42_890_000, 0) + .saturating_add(Weight::from_parts(0, 5481)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `FellowshipTreasury::Spends` (r:1 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:1) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) + /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `212` - // Estimated: `5318` - // Minimum execution time: 15_357_000 picoseconds. - Weight::from_parts(15_787_000, 0) - .saturating_add(Weight::from_parts(0, 5318)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) + // Measured: `1144` + // Estimated: `5481` + // Minimum execution time: 28_453_000 picoseconds. + Weight::from_parts(30_117_000, 0) + .saturating_add(Weight::from_parts(0, 5481)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(3)) } } diff --git a/substrate/frame/treasury/src/weights.rs b/substrate/frame/treasury/src/weights.rs index b2c9da1ffddb..70abe9ab2a68 100644 --- a/substrate/frame/treasury/src/weights.rs +++ b/substrate/frame/treasury/src/weights.rs @@ -35,9 +35,9 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `4563561839a5`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `22cd7d3cce09`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -58,8 +58,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --genesis-builder-policy=none -// --exclude-pallets=pallet_xcm,pallet_xcm_benchmarks::fungible,pallet_xcm_benchmarks::generic,pallet_nomination_pools,pallet_remark,pallet_transaction_storage,pallet_election_provider_multi_block,pallet_election_provider_multi_block::signed,pallet_election_provider_multi_block::unsigned,pallet_election_provider_multi_block::verifier +// --exclude-pallets=pallet_xcm,pallet_xcm_benchmarks::fungible,pallet_xcm_benchmarks::generic,pallet_nomination_pools,pallet_remark,pallet_transaction_storage #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -92,10 +91,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) fn spend_local() -> Weight { // Proof Size summary in bytes: - // Measured: `0` + // Measured: `76` // Estimated: `1887` - // Minimum execution time: 9_084_000 picoseconds. - Weight::from_parts(9_260_000, 1887) + // Minimum execution time: 11_693_000 picoseconds. + Weight::from_parts(12_793_000, 1887) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -103,10 +102,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) fn remove_approval() -> Weight { // Proof Size summary in bytes: - // Measured: `69` + // Measured: `161` // Estimated: `1887` - // Minimum execution time: 5_149_000 picoseconds. - Weight::from_parts(5_358_000, 1887) + // Minimum execution time: 6_558_000 picoseconds. + Weight::from_parts(7_218_000, 1887) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -117,12 +116,12 @@ impl WeightInfo for SubstrateWeight { /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `78` + // Measured: `170` // Estimated: `1501` - // Minimum execution time: 8_211_000 picoseconds. - Weight::from_parts(11_324_784, 1501) - // Standard Error: 806 - .saturating_add(Weight::from_parts(45_246, 0).saturating_mul(p.into())) + // Minimum execution time: 11_225_000 picoseconds. + Weight::from_parts(13_618_454, 1501) + // Standard Error: 220 + .saturating_add(Weight::from_parts(34_323, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -130,19 +129,25 @@ impl WeightInfo for SubstrateWeight { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Treasury::SpendCount` (r:1 w:1) /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:0) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) /// Storage: `Treasury::Spends` (r:0 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `42` - // Estimated: `3502` - // Minimum execution time: 11_348_000 picoseconds. - Weight::from_parts(11_874_000, 3502) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Measured: `1016` + // Estimated: `4280` + // Minimum execution time: 27_510_000 picoseconds. + Weight::from_parts(29_474_000, 4280) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:0) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:1 w:1) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:2 w:2) @@ -151,34 +156,42 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `473` + // Measured: `820` // Estimated: `6208` - // Minimum execution time: 55_665_000 picoseconds. - Weight::from_parts(57_099_000, 6208) - .saturating_add(T::DbWeight::get().reads(5_u64)) + // Minimum execution time: 66_876_000 picoseconds. + Weight::from_parts(71_534_000, 6208) + .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `143` - // Estimated: `3539` - // Minimum execution time: 12_058_000 picoseconds. - Weight::from_parts(12_297_000, 3539) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) + // Measured: `1082` + // Estimated: `4280` + // Minimum execution time: 28_415_000 picoseconds. + Weight::from_parts(30_468_000, 4280) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `144` - // Estimated: `3539` - // Minimum execution time: 10_730_000 picoseconds. - Weight::from_parts(10_908_000, 3539) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) + // Measured: `1082` + // Estimated: `4280` + // Minimum execution time: 25_225_000 picoseconds. + Weight::from_parts(26_896_000, 4280) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } } @@ -192,10 +205,10 @@ impl WeightInfo for () { /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) fn spend_local() -> Weight { // Proof Size summary in bytes: - // Measured: `0` + // Measured: `76` // Estimated: `1887` - // Minimum execution time: 9_084_000 picoseconds. - Weight::from_parts(9_260_000, 1887) + // Minimum execution time: 11_693_000 picoseconds. + Weight::from_parts(12_793_000, 1887) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -203,10 +216,10 @@ impl WeightInfo for () { /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) fn remove_approval() -> Weight { // Proof Size summary in bytes: - // Measured: `69` + // Measured: `161` // Estimated: `1887` - // Minimum execution time: 5_149_000 picoseconds. - Weight::from_parts(5_358_000, 1887) + // Minimum execution time: 6_558_000 picoseconds. + Weight::from_parts(7_218_000, 1887) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -217,12 +230,12 @@ impl WeightInfo for () { /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `78` + // Measured: `170` // Estimated: `1501` - // Minimum execution time: 8_211_000 picoseconds. - Weight::from_parts(11_324_784, 1501) - // Standard Error: 806 - .saturating_add(Weight::from_parts(45_246, 0).saturating_mul(p.into())) + // Minimum execution time: 11_225_000 picoseconds. + Weight::from_parts(13_618_454, 1501) + // Standard Error: 220 + .saturating_add(Weight::from_parts(34_323, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -230,19 +243,25 @@ impl WeightInfo for () { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Treasury::SpendCount` (r:1 w:1) /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:0) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) /// Storage: `Treasury::Spends` (r:0 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `42` - // Estimated: `3502` - // Minimum execution time: 11_348_000 picoseconds. - Weight::from_parts(11_874_000, 3502) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + // Measured: `1016` + // Estimated: `4280` + // Minimum execution time: 27_510_000 picoseconds. + Weight::from_parts(29_474_000, 4280) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:0) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:1 w:1) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:2 w:2) @@ -251,33 +270,41 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `473` + // Measured: `820` // Estimated: `6208` - // Minimum execution time: 55_665_000 picoseconds. - Weight::from_parts(57_099_000, 6208) - .saturating_add(RocksDbWeight::get().reads(5_u64)) + // Minimum execution time: 66_876_000 picoseconds. + Weight::from_parts(71_534_000, 6208) + .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `143` - // Estimated: `3539` - // Minimum execution time: 12_058_000 picoseconds. - Weight::from_parts(12_297_000, 3539) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) + // Measured: `1082` + // Estimated: `4280` + // Minimum execution time: 28_415_000 picoseconds. + Weight::from_parts(30_468_000, 4280) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `144` - // Estimated: `3539` - // Minimum execution time: 10_730_000 picoseconds. - Weight::from_parts(10_908_000, 3539) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) + // Measured: `1082` + // Estimated: `4280` + // Minimum execution time: 25_225_000 picoseconds. + Weight::from_parts(26_896_000, 4280) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } } From 04238eb76165580a5dc6e0c017d2140460d80cf9 Mon Sep 17 00:00:00 2001 From: muharem Date: Thu, 25 Jun 2026 12:51:32 +0200 Subject: [PATCH 07/18] order bug fix --- substrate/frame/treasury/src/benchmarking.rs | 12 +- substrate/frame/treasury/src/lib.rs | 124 ++++++++++++------- substrate/frame/treasury/src/migration.rs | 14 ++- 3 files changed, 98 insertions(+), 52 deletions(-) diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index 91a0b2829c5d..9ddc0089a0f1 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -203,11 +203,15 @@ mod benchmarks { create_spend_arguments::(SEED); T::BalanceConverter::ensure_successful(asset_kind.clone()); - // Worst case: `NextPayout` is already occupied and the new spend is inserted into an - // almost full payout queue, scanning all existing entries. + // Worst case: `NextPayout` is already occupied by a later-maturing head, so the new spend + // (with `valid_from = now`) preempts it. The demoted head is re-inserted into an almost + // full payout queue, scanning all existing entries, and the head is rewritten. let now = T::BlockNumberProvider::current_block_number(); - let order_expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::insert(asset_kind.clone(), (1_000_000u32, order_expire_at)); + let head_order_key = now.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert( + asset_kind.clone(), + (1_000_000u32, head_order_key, head_order_key), + ); fill_payout_queue::(asset_kind.clone(), T::MaxQueuedSpends::get().saturating_sub(1)); #[extrinsic_call] diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 03d4e44fd616..3bd6bf0d374e 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -75,9 +75,10 @@ //! spends from draining the treasury before larger, earlier-approved spends can be paid. //! //! When a spend is approved via the `spend` call, it is automatically inserted into the payout -//! queue for its asset kind, sorted by an order key — the spend's `valid_from` for newly -//! inserted spends. Spends are then paid out in order via the `payout` call, which only -//! processes the spend at the head of the queue for that asset kind. +//! queue for its asset kind, sorted by an order key — `max(now, valid_from)` for newly +//! inserted spends, so a later approval never overtakes an already-mature spend. Spends are then +//! paid out in order via the `payout` call, which only processes the spend at the head of the +//! queue for that asset kind. //! //! If a spend at the head of the queue cannot be paid out (e.g., due to insufficient funds), //! its order will expire after [`Config::OrderExpirationPeriod`] blocks. The `check_status` call @@ -388,15 +389,24 @@ pub mod pallet { #[pallet::storage] pub type LastSpendPeriod = StorageValue<_, BlockNumberFor, OptionQuery>; - /// The next spend to be paid out for each asset kind, with its order expiration block. + /// The next spend to be paid out for each asset kind, as `(spend_index, order_key, + /// expire_at)`. `order_key` is the head's order key (`max(now, valid_from)` for a freshly + /// inserted spend, or the rotation block for a rotated one) and is used to keep the head ahead + /// of every queued spend. `expire_at` is the block at which its position at the head expires + /// and it can be rotated to the back. #[pallet::storage] - pub type NextPayout, I: 'static = ()> = - StorageMap<_, Twox64Concat, T::AssetKind, (SpendIndex, BlockNumberFor), OptionQuery>; + pub type NextPayout, I: 'static = ()> = StorageMap< + _, + Twox64Concat, + T::AssetKind, + (SpendIndex, BlockNumberFor, BlockNumberFor), + OptionQuery, + >; /// The queue of spends in payout order for each asset kind. /// /// Each entry contains `(spend_index, order_key)` and the queue is sorted by the order key. - /// The order key is the spend's `valid_from` for newly inserted spends and the block number + /// The order key is `max(now, valid_from)` for newly inserted spends and the block number /// of the rotation for spends that were rotated to the back after their order expired. #[pallet::storage] pub type PayoutQueue, I: 'static = ()> = StorageMap< @@ -688,7 +698,7 @@ pub mod pallet { /// the [`Config::PayoutPeriod`]. /// /// The spend is automatically inserted into the payout queue for its asset kind in sorted - /// order by its order key (`valid_from` for new spends). If there is no current + /// order by its order key (`max(now, valid_from)` for new spends). If there is no current /// `NextPayout` for this asset kind, the spend becomes the `NextPayout` with an /// expiration of `max(now, valid_from) + OrderExpirationPeriod`. /// @@ -877,7 +887,7 @@ pub mod pallet { } // Check if this spend is the NextPayout and if its order has expired - if let Some((next_index, expire_at)) = NextPayout::::get(&asset_kind) { + if let Some((next_index, _, expire_at)) = NextPayout::::get(&asset_kind) { if next_index == index && expire_at < now { // Order has expired - rotate the queue if spend is still Pending/Failed if matches!(spend.status, State::Pending | State::Failed) { @@ -942,8 +952,8 @@ pub mod pallet { Error::::AlreadyAttempted ); - let asset_kind = spend.asset_kind.clone(); - Self::remove_from_queue(&asset_kind, index); + // let asset_kind = spend.asset_kind.clone(); + Self::remove_from_queue(&spend.asset_kind, index); Spends::::remove(index); Self::deposit_event(Event::::AssetSpendVoided { index }); Ok(()) @@ -1008,38 +1018,63 @@ impl, I: 'static> Pallet { Approvals::::get() } - /// Insert a newly approved spend into the payout queue for the given asset kind, keeping the - /// queue sorted by order key. For newly inserted spends the order key is the spend's - /// `valid_from`. If there is no NextPayout for this asset kind, the spend becomes the - /// NextPayout instead. + /// Insert a newly approved spend into the payout order for the given asset kind. For newly + /// inserted spends the order key is `max(now, valid_from)` — `valid_from` clamped to the + /// approval block, so a later approval cannot overtake an already-mature spend. + /// + /// The head ([`NextPayout`]) is always the earliest-maturing spend: + /// - if there is no head yet, the spend becomes the head; + /// - if the spend matures strictly earlier than the current head, it preempts it: the current + /// head is demoted into the queue (kept sorted by its own order key) and the new spend + /// becomes the head; + /// - otherwise the spend is inserted into the queue in sorted order. fn insert_into_payout_queue( asset_kind: T::AssetKind, index: SpendIndex, valid_from: BlockNumberFor, ) -> DispatchResult { - // Check if there's already a NextPayout for this asset kind - if NextPayout::::get(&asset_kind).is_some() { - // There's already a NextPayout, add to queue in sorted order - let mut queue = PayoutQueue::::get(&asset_kind); - - // Find the correct position to insert (maintain sorted order by order key) - let insert_pos = queue - .iter() - .position(|(_, order_key)| *order_key > valid_from) - .unwrap_or(queue.len()); - - // Insert at the found position - if queue.try_insert(insert_pos, (index, valid_from)).is_err() { - return Err(Error::::QueueFull.into()); - } - - PayoutQueue::::insert(&asset_kind, queue); - } else { - // No NextPayout for this asset kind, set this as NextPayout. The order only starts - // counting down once the spend is mature. - let now = T::BlockNumberProvider::current_block_number(); - let expire_at = now.max(valid_from).saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::insert(&asset_kind, (index, expire_at)); + let now = T::BlockNumberProvider::current_block_number(); + // The order key is clamped to `now` so that a spend approved later never overtakes an + // already-mature spend just because its `valid_from` lies further in the past. Spends that + // are already mature on approval are ordered by approval time, while not-yet-mature spends + // are ordered by their maturity (`valid_from`). + let order_key = now.max(valid_from); + // The order only starts counting down once the spend is mature. + let expire_at = order_key.saturating_add(T::OrderExpirationPeriod::get()); + + match NextPayout::::get(&asset_kind) { + // No head for this asset kind yet, set this spend as the head. + None => { + NextPayout::::insert(&asset_kind, (index, order_key, expire_at)); + }, + // The new spend matures strictly earlier than the current head: it preempts the head. + // Demote the current head into the queue (keeping it sorted by its own order key) and + // install the new spend as the head. + Some((head_index, head_order_key, _)) if order_key < head_order_key => { + let mut queue = PayoutQueue::::get(&asset_kind); + let insert_pos = queue + .iter() + .position(|(_, order_key)| *order_key > head_order_key) + .unwrap_or(queue.len()); + queue + .try_insert(insert_pos, (head_index, head_order_key)) + .map_err(|_| Error::::QueueFull)?; + PayoutQueue::::insert(&asset_kind, queue); + NextPayout::::insert(&asset_kind, (index, order_key, expire_at)); + }, + // The head already matures no later than the new spend, add it to the queue in sorted + // order. + Some(_) => { + let mut queue = PayoutQueue::::get(&asset_kind); + let insert_pos = queue + .iter() + .position(|(_, queue_order_key)| *queue_order_key > order_key) + .unwrap_or(queue.len()); + queue + .try_insert(insert_pos, (index, order_key)) + .map_err(|_| Error::::QueueFull)?; + PayoutQueue::::insert(&asset_kind, queue); + }, } Ok(()) @@ -1048,7 +1083,7 @@ impl, I: 'static> Pallet { /// Remove a spend from the payout queue for the given asset kind and update NextPayout. /// Called when a spend is successfully paid out, expired, or voided. fn remove_from_queue(asset_kind: &T::AssetKind, index: SpendIndex) { - if let Some((next_index, _)) = NextPayout::::get(asset_kind) { + if let Some((next_index, _, _)) = NextPayout::::get(asset_kind) { if next_index == index { // This was the next payout, promote the next one from the queue let mut queue = PayoutQueue::::get(asset_kind); @@ -1061,7 +1096,10 @@ impl, I: 'static> Pallet { let now = T::BlockNumberProvider::current_block_number(); let new_expire_at = now.max(order_key).saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::insert(asset_kind, (new_next_index, new_expire_at)); + NextPayout::::insert( + asset_kind, + (new_next_index, order_key, new_expire_at), + ); // Remove it from queue queue.remove(0); @@ -1085,7 +1123,7 @@ impl, I: 'static> Pallet { /// Re-inserts the current NextPayout into the queue with an order key of `now`, keeping the /// queue sorted by order key, and promotes the next entry. fn rotate_payout_queue(asset_kind: &T::AssetKind) -> DispatchResult { - let (current_index, _) = + let (current_index, _, _) = NextPayout::::get(asset_kind).ok_or(Error::::NotNextPayout)?; let mut queue = PayoutQueue::::get(asset_kind); let now = T::BlockNumberProvider::current_block_number(); @@ -1102,7 +1140,7 @@ impl, I: 'static> Pallet { // Promote the front of queue to NextPayout and remove it from queue if let Some(&(new_next, order_key)) = queue.first() { let new_expire_at = now.max(order_key).saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::insert(asset_kind, (new_next, new_expire_at)); + NextPayout::::insert(asset_kind, (new_next, order_key, new_expire_at)); // Remove the promoted entry from queue since it's now NextPayout queue.remove(0); } else { @@ -1331,7 +1369,7 @@ impl, I: 'static> Pallet { } // Check that NextPayout is not also in the queue (they're separate) - if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { + if let Some((next_index, _, _)) = NextPayout::::get(&asset_kind) { ensure!( !queue.iter().any(|(idx, _)| *idx == next_index), "NextPayout should not be in PayoutQueue." diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index 3fc89a8efc59..0b4c37d7c728 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -222,10 +222,14 @@ mod migrate_to_ordered_payouts { let mut next_payout_set = false; for (index, valid_from) in spends { + // The order key is clamped to `now` so that spends already mature at migration + // are ordered by approval (index), while not-yet-mature spends keep their + // maturity order. Clamping is monotonic, so the pre-sorted order is preserved. + let order_key = now.max(valid_from); if !next_payout_set { - // First spend becomes NextPayout - let expire_at = now.saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::insert(&asset_kind, (index, expire_at)); + // First spend (earliest-maturing) becomes NextPayout. + let expire_at = order_key.saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert(&asset_kind, (index, order_key, expire_at)); next_payout_set = true; log::debug!( target: LOG_TARGET, @@ -235,7 +239,7 @@ mod migrate_to_ordered_payouts { ); } else if queue.len() < T::MaxQueuedSpends::get() as usize { // Add to queue - if let Err(_) = queue.try_push((index, valid_from)) { + if let Err(_) = queue.try_push((index, order_key)) { log::warn!( target: LOG_TARGET, "Failed to push spend {} to queue (queue full)", @@ -320,7 +324,7 @@ mod migrate_to_ordered_payouts { } // Verify NextPayout is NOT in queue - if let Some((next_index, _)) = NextPayout::::get(&asset_kind) { + if let Some((next_index, _, _)) = NextPayout::::get(&asset_kind) { ensure!( !queue.iter().any(|(idx, _)| *idx == next_index), "NextPayout should not be in the queue" From b2eaa46036c646e30e1aa3419ddcc565bc00f1ab Mon Sep 17 00:00:00 2001 From: muharem Date: Thu, 25 Jun 2026 15:32:58 +0200 Subject: [PATCH 08/18] fix migration --- substrate/frame/treasury/src/migration.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index 0b4c37d7c728..8a2b6d8697a7 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -203,8 +203,12 @@ mod migrate_to_ordered_payouts { // Process each AssetKind for (_, (asset_kind, mut spends)) in spends_by_asset { - // Sort by valid_from, then by index for deterministic ordering (consensus safety) - spends.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0))); + // Sort by the clamped order key (`max(now, valid_from)`), then by index for + // deterministic ordering (consensus safety). Clamping is what keeps spends already + // mature at migration ordered by approval (index) rather than by a back-dated + // `valid_from`, matching the runtime insertion rule; not-yet-mature spends still + // order by their maturity. + spends.sort_by(|a, b| now.max(a.1).cmp(&now.max(b.1)).then_with(|| a.0.cmp(&b.0))); let spend_count = spends.len() as u32; total_spends_processed += spend_count; From 7aac659c60eb684cc489d4a227bc4aa3207d8acc Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Mon, 29 Jun 2026 10:52:09 +0100 Subject: [PATCH 09/18] fix full-queue rotation deadlock and equal-order-key demotion ordering --- substrate/frame/treasury/src/benchmarking.rs | 44 +++ substrate/frame/treasury/src/lib.rs | 44 +-- substrate/frame/treasury/src/migration.rs | 147 ++++++---- substrate/frame/treasury/src/tests.rs | 282 +++++++++++++++++-- 4 files changed, 430 insertions(+), 87 deletions(-) diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index 9ddc0089a0f1..693390498a0f 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -342,6 +342,50 @@ mod benchmarks { Ok(()) } + // The rotation branch of `check_status`: a head whose payout order has expired is rotated to + // the back of a (near) full queue and the front entry is promoted. Benchmarked separately so + // `check_status`'s declared weight can be `check_status().max(check_status_rotation())`. + #[benchmark] + fn check_status_rotation() -> Result<(), BenchmarkError> { + let origin = + T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let (asset_kind, amount, _, beneficiary_lookup) = create_spend_arguments::(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + let caller: T::AccountId = account("caller", 0, SEED); + + // The spend becomes NextPayout (queue empty), status Pending. + Treasury::::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + + // Force the head's payout order to be already expired (order `expire_at` of zero is `< + // now`) while its spend payout window stays open, so `check_status` takes the rotation + // branch. + NextPayout::::mutate(&asset_kind, |entry| { + if let Some((_, _, expire_at)) = entry { + *expire_at = BlockNumberFor::::zero(); + } + }); + + // Worst case: a near-full queue, so rotation scans it, re-inserts the expired head, and + // promotes the front. Leave one slot free for the re-insert. + fill_payout_queue::(asset_kind.clone(), T::MaxQueuedSpends::get().saturating_sub(1)); + + #[block] + { + assert_ok!(Treasury::::check_status(RawOrigin::Signed(caller).into(), 0u32)); + } + + // The expired head (index 0) was rotated to the back; a queued spend is now NextPayout. + assert!(matches!(NextPayout::::get(&asset_kind), Some((idx, _, _)) if idx != 0)); + assert_last_event::(Event::PayoutQueueRotated { asset_kind, index: 0 }.into()); + Ok(()) + } + #[benchmark] fn void_spend() -> Result<(), BenchmarkError> { let (asset_kind, amount, _, beneficiary_lookup) = create_spend_arguments::(SEED); diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 3bd6bf0d374e..20fa3a741d94 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -1052,9 +1052,11 @@ impl, I: 'static> Pallet { // install the new spend as the head. Some((head_index, head_order_key, _)) if order_key < head_order_key => { let mut queue = PayoutQueue::::get(&asset_kind); + // Insert the demoted head ahead of queue entries that share its order key: it was + // approved before them, so it keeps its place among equal-maturity spends. let insert_pos = queue .iter() - .position(|(_, order_key)| *order_key > head_order_key) + .position(|(_, order_key)| *order_key >= head_order_key) .unwrap_or(queue.len()); queue .try_insert(insert_pos, (head_index, head_order_key)) @@ -1125,29 +1127,28 @@ impl, I: 'static> Pallet { fn rotate_payout_queue(asset_kind: &T::AssetKind) -> DispatchResult { let (current_index, _, _) = NextPayout::::get(asset_kind).ok_or(Error::::NotNextPayout)?; - let mut queue = PayoutQueue::::get(asset_kind); let now = T::BlockNumberProvider::current_block_number(); + // Rotation is size-neutral: it re-inserts the expired head and promotes one entry from the + // front, so the queue length never changes. + let mut queue = PayoutQueue::::get(asset_kind).into_inner(); + // Re-insert the expired spend with `now` as its order key, keeping the queue sorted. // Using `now` (rather than the spend's `valid_from`) sends the rotated spend behind // every entry that is already mature, while preserving the sort order. let insert_pos = queue.iter().position(|(_, order_key)| *order_key > now).unwrap_or(queue.len()); - queue - .try_insert(insert_pos, (current_index, now)) - .map_err(|_| Error::::QueueFull)?; + queue.insert(insert_pos, (current_index, now)); - // Promote the front of queue to NextPayout and remove it from queue - if let Some(&(new_next, order_key)) = queue.first() { - let new_expire_at = now.max(order_key).saturating_add(T::OrderExpirationPeriod::get()); - NextPayout::::insert(asset_kind, (new_next, order_key, new_expire_at)); - // Remove the promoted entry from queue since it's now NextPayout - queue.remove(0); - } else { - // Queue was empty after pushing, clear NextPayout - NextPayout::::remove(asset_kind); - } + // Promote the front to NextPayout and remove it. The queue is guaranteed non-empty here + // because we just inserted into it, so there is always something to promote. + let (new_next, order_key) = queue.remove(0); + let new_expire_at = now.max(order_key).saturating_add(T::OrderExpirationPeriod::get()); + NextPayout::::insert(asset_kind, (new_next, order_key, new_expire_at)); + // Net length is unchanged (one inserted, one removed), so it still fits the bound. + let queue = BoundedVec::<_, T::MaxQueuedSpends>::try_from(queue) + .map_err(|_| Error::::QueueFull)?; PayoutQueue::::insert(asset_kind, queue); Ok(()) } @@ -1376,14 +1377,21 @@ impl, I: 'static> Pallet { ); } - // All items in queue must be valid pending spends + // Every queued spend is awaiting payout. In steady state that means `Pending` or + // `Failed`, but the migration can also seed an in-flight `Attempted` spend into the + // queue (so a later payment failure can still be retried rather than lost), which + // `check_status` then resolves. for (spend_index, _) in queue.iter() { let spend = Spends::::get(spend_index).ok_or( sp_runtime::TryRuntimeError::Other("Spend in queue must exist in Spends."), )?; ensure!( - matches!(spend.status, PaymentState::Pending | PaymentState::Failed), - "Spend in queue must have Pending or Failed status." + matches!( + spend.status, + PaymentState::Pending | + PaymentState::Failed | PaymentState::Attempted { .. } + ), + "Spend in queue must have Pending, Failed or Attempted status." ); // Verify asset kind matches ensure!(spend.asset_kind == asset_kind, "Spend in queue has wrong asset kind."); diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index 8a2b6d8697a7..6a31728b6680 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -159,27 +159,26 @@ mod migrate_to_ordered_payouts { for (index, spend) in Spends::::iter() { total_spends_read += 1; match spend.status { - PaymentState::Pending | PaymentState::Failed => { - // Only include spends that haven't expired - if spend.expire_at > now { - spends_vec.push((spend.asset_kind, index, spend.valid_from)); - } else { - log::debug!( - target: LOG_TARGET, - "Skipping expired spend {} (expire_at: {:?}, now: {:?})", - index, - spend.expire_at, - now, - ); - } - }, - PaymentState::Attempted { .. } => { + // Expired `Pending`/`Failed` spends are dropped, matching `check_status`, which + // removes them once `now > expire_at`. + PaymentState::Pending | PaymentState::Failed if spend.expire_at <= now => { log::debug!( target: LOG_TARGET, - "Skipping attempted spend {}", + "Skipping expired spend {} (expire_at: {:?}, now: {:?})", index, + spend.expire_at, + now, ); }, + // Every other live spend is ordered, including in-flight `Attempted` ones. An + // `Attempted` spend whose payment later fails would otherwise be left out of + // the payout order and could never be retried (lost); keeping it ordered + // lets `check_status` resolve it and, on failure, promote/retry it. This + // mirrors `check_status`, which never drops an `Attempted` spend on + // expiration. + _ => { + spends_vec.push((spend.asset_kind, index, spend.valid_from)); + }, } } @@ -276,52 +275,89 @@ mod migrate_to_ordered_payouts { #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { - let pending_spends: Vec<(T::AssetKind, SpendIndex)> = Spends::::iter() - .filter_map(|(index, spend)| match spend.status { - PaymentState::Pending | PaymentState::Failed => Some((spend.asset_kind, index)), - _ => None, - }) - .collect(); + let now = T::BlockNumberProvider::current_block_number(); + + // Spends the migration will order, using the same inclusion rule as + // `on_runtime_upgrade` (every live spend, plus in-flight `Attempted` ones regardless + // of expiration), and the per-asset counts. + let mut migrated: Vec<(T::AssetKind, SpendIndex)> = Vec::new(); + let mut per_asset: BTreeMap, u32> = BTreeMap::new(); + for (index, spend) in Spends::::iter() { + let include = match spend.status { + PaymentState::Pending | PaymentState::Failed => spend.expire_at > now, + PaymentState::Attempted { .. } => true, + }; + if include { + *per_asset.entry(spend.asset_kind.encode()).or_default() += 1; + migrated.push((spend.asset_kind, index)); + } + } + + // The migration places one spend at `NextPayout` and the rest in the queue (bounded by + // `MaxQueuedSpends`). If an asset has more live spends than that capacity, the + // migration would silently drop the overflow — fail here so the runtime raises + // `MaxQueuedSpends` before deploying instead. + let capacity = T::MaxQueuedSpends::get().saturating_add(1); + for (_, count) in per_asset.iter() { + ensure!( + *count <= capacity, + "MaxQueuedSpends too small: an asset has more live spends than NextPayout + queue can hold." + ); + } log::info!( target: LOG_TARGET, - "Pre-upgrade: {} pending/failed spends", - pending_spends.len() + "Pre-upgrade: {} spends to order across {} asset kinds", + migrated.len(), + per_asset.len(), ); - Ok(pending_spends.encode()) + Ok(migrated.encode()) } #[cfg(feature = "try-runtime")] fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { - let pre_pending_spends: Vec<(T::AssetKind, SpendIndex)> = + let pre_migrated: Vec<(T::AssetKind, SpendIndex)> = Vec::decode(&mut &state[..]).expect("Known good"); - let mut post_queue_count = 0usize; + // Every spend the migration intended to order must land in exactly one of `NextPayout` + // or `PayoutQueue` — nothing silently dropped (the `pre_upgrade` capacity check + // guarantees this; verify it here). + let mut post_ordered_count = 0usize; for (_, queue) in PayoutQueue::::iter() { - post_queue_count += queue.len(); + post_ordered_count += queue.len(); } + post_ordered_count += NextPayout::::iter().count(); log::info!( target: LOG_TARGET, - "Post-upgrade: {} spends in queues (pre-upgrade had {} pending)", - post_queue_count, - pre_pending_spends.len() + "Post-upgrade: {} spends ordered (pre-upgrade had {} to order)", + post_ordered_count, + pre_migrated.len() + ); + ensure!( + post_ordered_count == pre_migrated.len(), + "Migration dropped spends: ordered count does not match pre-upgrade count." ); - // Verify queue invariants for each asset kind + // Verify queue invariants for each asset kind. for (asset_kind, queue) in PayoutQueue::::iter() { ensure!( queue.len() as u32 <= T::MaxQueuedSpends::get(), "Queue length exceeds MaxQueuedSpends" ); - // Verify all items in queue are valid pending spends + // A queued spend is awaiting payout, so it may be `Pending`, `Failed`, or an + // in-flight `Attempted` carried over by the migration. for (index, _) in queue.iter() { let spend = Spends::::get(index) .ok_or(sp_runtime::TryRuntimeError::Other("Spend in queue not found"))?; ensure!( - matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + matches!( + spend.status, + PaymentState::Pending | + PaymentState::Failed | PaymentState::Attempted { .. } + ), "Spend in queue has invalid status" ); ensure!(spend.asset_kind == asset_kind, "Spend in queue has wrong asset kind"); @@ -395,15 +431,18 @@ mod migrate_to_ordered_payouts { } #[test] - fn migration_skips_attempted_spends() { + fn migration_includes_attempted_spends() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(100); + // An in-flight `Attempted` spend must stay in the payout order so a later payment + // failure can be retried rather than lost. insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Attempted { id: 123u64 }); MigrateToOrderedPayouts::::on_runtime_upgrade(); - assert!(NextPayout::::get(1u32).is_none()); + // Only spend → becomes NextPayout (order key = max(now=100, valid_from=50) = 100). + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); assert_eq!(PayoutQueue::::get(1u32).len(), 0); }); } @@ -436,15 +475,15 @@ mod migrate_to_ordered_payouts { MigrateToOrderedPayouts::::on_runtime_upgrade(); // Asset 1: First spend is NextPayout - let (next_idx, expire_at) = NextPayout::::get(1u32).unwrap(); + let (next_idx, _order_key, expire_at) = NextPayout::::get(1u32).unwrap(); assert_eq!(next_idx, 0); assert_eq!(expire_at, 102); // 100 + OrderExpirationPeriod(2) - // Asset 1 queue: spend 1 - assert_eq!(PayoutQueue::::get(1u32), vec![(1, 60)]); + // Asset 1 queue: spend 1 (mature at now=100, so order key clamps to 100) + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 100)]); // Asset 2: Spend 2 is NextPayout - assert_eq!(NextPayout::::get(2u32).map(|(idx, _)| idx), Some(2)); + assert_eq!(NextPayout::::get(2u32).map(|(idx, _, _)| idx), Some(2)); assert_eq!(PayoutQueue::::get(2u32).len(), 0); }); } @@ -452,7 +491,9 @@ mod migrate_to_ordered_payouts { #[test] fn migration_sorts_by_valid_from() { ExtBuilder::default().build().execute_with(|| { - System::set_block_number(100); + // Block is before every `valid_from`, so the order-key clamp is a no-op and spends + // order by maturity (`valid_from`). + System::set_block_number(40); // Insert out of order insert_spend(0, 1, 100, 1000, 100, 200, PaymentState::Pending); // latest @@ -462,7 +503,7 @@ mod migrate_to_ordered_payouts { MigrateToOrderedPayouts::::on_runtime_upgrade(); // Sorted: 1 (50), 2 (75), 0 (100) - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); assert_eq!(PayoutQueue::::get(1u32), vec![(2, 75), (0, 100)]); }); } @@ -470,7 +511,9 @@ mod migrate_to_ordered_payouts { #[test] fn migration_tie_breaks_by_index() { ExtBuilder::default().build().execute_with(|| { - System::set_block_number(100); + // Block before `valid_from`, so order keys equal `valid_from` and only the + // index tie-break is exercised. + System::set_block_number(40); // Same valid_from, different indices (inserted out of order) insert_spend(5, 1, 100, 1000, 50, 200, PaymentState::Pending); @@ -480,7 +523,7 @@ mod migrate_to_ordered_payouts { MigrateToOrderedPayouts::::on_runtime_upgrade(); // Sorted by index: 3, 4, 5 - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(3)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(3)); assert_eq!(PayoutQueue::::get(1u32), vec![(4, 50), (5, 50)]); }); } @@ -506,7 +549,7 @@ mod migrate_to_ordered_payouts { MigrateToOrderedPayouts::::on_runtime_upgrade(); // NextPayout is index 0 - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); // Queue capped at 100 let queue = PayoutQueue::::get(1u32); @@ -528,9 +571,11 @@ mod migrate_to_ordered_payouts { MigrateToOrderedPayouts::::on_runtime_upgrade(); - // Only 0 and 1 in queue - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); - assert_eq!(PayoutQueue::::get(1u32), vec![(1, 51)]); + // Pending (0), Failed (1) and the in-flight Attempted (2) are ordered; the expired + // Pending (3) is dropped. All are mature at `now = 100`, so order keys clamp to 100 + // and ties break by index. + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 100), (2, 100)]); }); } @@ -545,9 +590,11 @@ mod migrate_to_ordered_payouts { let state = MigrateToOrderedPayouts::::pre_upgrade().unwrap(); let decoded: Vec<(u32, u32)> = Vec::decode(&mut &state[..]).unwrap(); - assert_eq!(decoded.len(), 2); + // All three are ordered by the migration, including the in-flight Attempted spend. + assert_eq!(decoded.len(), 3); assert!(decoded.contains(&(1, 0))); assert!(decoded.contains(&(1, 1))); + assert!(decoded.contains(&(1, 2))); }); } @@ -579,7 +626,7 @@ mod migrate_to_ordered_payouts { // Create invalid state: NextPayout in queue insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); - NextPayout::::insert(1u32, (0u32, 102u64)); + NextPayout::::insert(1u32, (0u32, 100u64, 102u64)); let bounded_vec: BoundedVec<(u32, u64), crate::tests::MaxQueuedSpends> = vec![(0u32, 50u64)].try_into().unwrap(); diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 0fc35ef72cef..57aae7354abf 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -1068,7 +1068,7 @@ fn spend_auto_enqueues_to_payout_queue() { // Create a spend - should be automatically added to payout queue assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None)); // Check queue state - spend should be NextPayout since queue was empty - assert_eq!(NextPayout::::get(1u32), Some((0, 3))); // now + OrderExpirationPeriod + assert_eq!(NextPayout::::get(1u32), Some((0, 1, 3))); // (index, order_key, expire_at = now + OrderExpirationPeriod) // Queue should be empty since this was the first spend assert_eq!(PayoutQueue::::get(1u32), vec![]); @@ -1103,11 +1103,12 @@ fn multiple_spends_auto_enqueue_in_sorted_order() { Some(7) )); - // First spend should be NextPayout - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + // Spend 1 has the earliest order key (3), so it preempts the head; spends 0 and 2 sit in + // the queue sorted by order key. + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); let queue = PayoutQueue::::get(1u32); - assert_eq!(queue, vec![(1, 3), (2, 7)]); + assert_eq!(queue, vec![(0, 5), (2, 7)]); }); } @@ -1142,8 +1143,8 @@ fn different_assets_have_independent_queues() { assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(150), None)); // Each asset should have its own NextPayout - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); - assert_eq!(NextPayout::::get(2u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(2u32).map(|(idx, _, _)| idx), Some(1)); // Asset 1 queue should have spend 2 assert_eq!(PayoutQueue::::get(1u32), vec![(2, 1)]); @@ -1167,7 +1168,7 @@ fn check_status_rotates_expired_order() { assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(200), None)); // Verify initial state - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); // Move past order expiration System::set_block_number(4); @@ -1177,7 +1178,7 @@ fn check_status_rotates_expired_order() { assert_eq!(info.pays_fee, Pays::No); // Queue should be rotated: spend 0 moved to back with `now` as its order key - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); assert_eq!(PayoutQueue::::get(1u32), vec![(0, 4)]); System::assert_last_event( @@ -1205,7 +1206,7 @@ fn rotated_spend_keeps_queue_sorted_and_passes_try_state() { Some(10) )); - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); assert_eq!(PayoutQueue::::get(1u32), vec![(1, 1), (2, 10)]); // Move past the head's order expiration and rotate it @@ -1216,7 +1217,7 @@ fn rotated_spend_keeps_queue_sorted_and_passes_try_state() { // The rotated spend is re-inserted with `now` (4) as its order key, which places it // behind every mature spend but ahead of the not-yet-mature one, keeping the queue // sorted. Spend 1 is promoted to NextPayout. - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); assert_eq!(PayoutQueue::::get(1u32), vec![(0, 4), (2, 10)]); // The sortedness invariant must hold after the rotation @@ -1240,7 +1241,7 @@ fn check_status_does_not_rotate_if_not_expired() { ); // State should be unchanged - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); }); } @@ -1262,7 +1263,7 @@ fn check_status_removes_completed_spend_and_promotes_next() { assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); // Spend 1 should now be NextPayout - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); assert_eq!(PayoutQueue::::get(1u32), vec![]); }); } @@ -1280,7 +1281,7 @@ fn void_spend_removes_from_queue_and_promotes_next() { assert_ok!(Treasury::void_spend(RuntimeOrigin::root(), 0)); // Spend 1 should now be NextPayout - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); assert_eq!(PayoutQueue::::get(1u32), vec![]); // Spend 0 should be removed @@ -1299,7 +1300,7 @@ fn fifo_ordering_enforced_per_asset() { assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(300), None)); // Payout should be in FIFO order - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(0)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); // Payout spend 0 assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); @@ -1308,7 +1309,7 @@ fn fifo_ordering_enforced_per_asset() { assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); // Next should be spend 1 - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); // Payout spend 1 assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); @@ -1317,7 +1318,7 @@ fn fifo_ordering_enforced_per_asset() { assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 1)); // Next should be spend 2 - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(2)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(2)); // Verify beneficiaries received in order assert_eq!(paid(100, 1), 1); @@ -1389,7 +1390,7 @@ fn complex_scenario_with_rotation_and_completion() { assert_eq!(info.pays_fee, Pays::No); // Spend 2 should now be NextPayout - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(2)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(2)); assert_ok!(Treasury::do_try_state()); // Payout spend 2 @@ -1399,7 +1400,7 @@ fn complex_scenario_with_rotation_and_completion() { assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 2)); // Spend 1 should be back at head after rotation - assert_eq!(NextPayout::::get(1u32).map(|(idx, _)| idx), Some(1)); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); // Retry spend 1 assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); @@ -1439,7 +1440,7 @@ fn try_state_payout_queue_invariants() { // Verify queue is sorted let queue = PayoutQueue::::get(1u32); - assert_eq!(queue, vec![(1, 3), (2, 7)]); + assert_eq!(queue, vec![(0, 5), (2, 7)]); }); } @@ -1465,3 +1466,246 @@ fn early_spend_cannot_be_paid_before_valid_from() { assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); }); } + +#[test] +fn preemption_earlier_maturing_spend_takes_head() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(10); + + // Spend 0 matures far in the future and, with an empty queue, becomes the head. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(100) + )); + // Spend 1 is approved later but matures now, so its earlier order key (10 < 100) preempts + // the head; the far-future spend is demoted into the queue. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(200), + Some(10) + )); + + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![(0, 100)]); + + // The preempting spend is already mature, so it is payable immediately. + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + assert_eq!(paid(200, 1), 1); + }); +} + +#[test] +fn order_key_clamp_prevents_backdated_overtaking() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(5); + + // Spend 0 is already mature; its order key clamps to now (5) and it becomes the head. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(3) + )); + // Spend 1 has an earlier valid_from (1) but is approved later (both still within the payout + // window). Its order key also clamps to 5, so it does NOT overtake the already-mature head + // — it queues behind it rather than back-dating its way to the front. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(200), + Some(1) + )); + + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 5)]); + }); +} + +#[test] +fn preemption_among_not_yet_mature_spends() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(5); + + // Spend 0 matures at 100 and becomes the head. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(100) + )); + // Spend 1 matures earlier (50); while both are still in the future it preempts the head. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(200), + Some(50) + )); + + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); + assert_eq!(PayoutQueue::::get(1u32), vec![(0, 100)]); + + // Neither is payable yet: the head has not matured and the other is not the head. + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 1), Error::::EarlyPayout); + assert_noop!( + Treasury::payout(RuntimeOrigin::signed(1), 0), + Error::::NotNextPayout + ); + }); +} + +#[test] +fn preempt_into_full_queue_fails_with_queue_full() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(10); + let max = ::MaxQueuedSpends::get(); + + // A head with a far-future order key, so a later spend can preempt it. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(0), + Some(2000) + )); + + // Fill the queue to capacity with spends that mature after the head, so they line up behind + // it without preempting. + for i in 0..max { + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new((i + 1) as u128), + Some(3000 + i as u64), + )); + } + + // A mature spend preempts the head, which demotes the old head into the queue. Unlike + // rotation, preemption adds a spend, so with the queue already full there is no room and + // the call must fail rather than exceed the bound. + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(9999), Some(10)), + Error::::QueueFull + ); + }); +} + +#[test] +fn mature_spend_is_paid_before_far_future_head() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(10); + + // A far-future spend is approved first and is initially the head. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(5000) + )); + // A later-approved but already-mature spend. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(200), + Some(10) + )); + + // The mature spend preempts the head and is paid first; the far-future spend waits in the + // queue rather than blocking it. + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + assert_eq!(paid(200, 1), 1); + assert_noop!( + Treasury::payout(RuntimeOrigin::signed(1), 0), + Error::::NotNextPayout + ); + }); +} + +// Rotation is size-neutral (demote the head, promote one entry), so it must succeed even when the +// queue is full. Otherwise the expired head can never be rotated and stays stuck as `NextPayout`. +#[test] +fn rotate_payout_queue_does_not_deadlock_when_queue_is_full() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let max = ::MaxQueuedSpends::get(); + // One head + `MaxQueuedSpends` queued fills the queue exactly. All created at block 1 with + // `valid_from = None`, so every order key is `1` and the head's order expires at + // `1 + OrderExpirationPeriod`. Root origin avoids the per-origin spend budget. + for i in 0..=max { + assert_ok!(Treasury::spend( + RuntimeOrigin::root(), + Box::new(1), + 1, + Box::new(i as u128), + None + )); + } + assert_eq!(PayoutQueue::::get(1u32).len() as u32, max); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); + + // Move past the head's order expiration so `check_status` rotates it. + System::set_block_number(4); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + + // Head rotated to the back, spend 1 promoted; queue size unchanged. + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); + let queue = PayoutQueue::::get(1u32); + assert_eq!(queue.len() as u32, max); + assert_eq!(queue.last(), Some(&(0, 4))); + assert_ok!(Treasury::do_try_state()); + }); +} + +// When a new spend preempts the head, the demoted head must be placed ahead of queue entries that +// share its order key, since it was approved before them. +#[test] +fn head_preemption_preserves_fifo_among_equal_order_keys() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Spend 0 becomes the head with order_key = max(1, 20) = 20. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(100), + Some(20) + )); + // Spend 1 has the same order_key 20 (not strictly earlier), so it joins the queue. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(200), + Some(20) + )); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); + assert_eq!(PayoutQueue::::get(1u32), vec![(1, 20)]); + + // Spend 2 matures strictly earlier (order_key = max(1, 15) = 15 < 20) and preempts the + // head. The demoted head (index 0) was approved before spend 1 and shares order_key 20, so + // FIFO requires it ahead of spend 1. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 1, + Box::new(300), + Some(15) + )); + + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(2)); + assert_eq!(PayoutQueue::::get(1u32), vec![(0, 20), (1, 20)]); + }); +} From f5945d30da245132832fa480afbe32aed778afbe Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Mon, 29 Jun 2026 10:56:00 +0100 Subject: [PATCH 10/18] include benchmark doc change --- substrate/frame/treasury/src/migration.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index 6a31728b6680..7c86afefb3a1 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -134,9 +134,10 @@ mod cleanup_proposals { /// Migration to initialize the payout queue for existing spends (Solution 1.2). /// -/// This migration identifies all pending/failed spends that have not yet expired, -/// groups them by asset kind, sorts them by valid_from (FIFO order), -/// and initializes the PayoutQueue and NextPayout for each asset kind. +/// Collects every live spend (dropping only `Pending`/`Failed` spends whose payout window has +/// already expired, and keeping in-flight `Attempted` ones), groups them by asset kind, sorts each +/// group by order key `max(now, valid_from)` then by index, and initializes `NextPayout` and +/// `PayoutQueue` for each asset kind. mod migrate_to_ordered_payouts { use super::*; From 062371499197f4cb943e274032259b10354e75bb Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:32:43 +0000 Subject: [PATCH 11/18] Update from github-actions[bot] running command 'bench --pallet pallet_treasury' --- .../src/weights/pallet_treasury.rs | 72 +++++---- substrate/frame/treasury/src/weights.rs | 139 +++++++++++------- 2 files changed, 129 insertions(+), 82 deletions(-) diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs index e88bf1a6e130..2a281e551e68 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/pallet_treasury.rs @@ -16,9 +16,9 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `22cd7d3cce09`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `1807571685ea`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -60,8 +60,8 @@ impl pallet_treasury::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `109` // Estimated: `1887` - // Minimum execution time: 12_419_000 picoseconds. - Weight::from_parts(13_318_000, 0) + // Minimum execution time: 12_453_000 picoseconds. + Weight::from_parts(13_366_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) @@ -72,8 +72,8 @@ impl pallet_treasury::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `194` // Estimated: `1887` - // Minimum execution time: 6_650_000 picoseconds. - Weight::from_parts(7_208_000, 0) + // Minimum execution time: 6_619_000 picoseconds. + Weight::from_parts(7_219_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -89,11 +89,11 @@ impl pallet_treasury::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `313 + p * (1 ±0)` // Estimated: `3593` - // Minimum execution time: 13_090_000 picoseconds. - Weight::from_parts(16_718_647, 0) + // Minimum execution time: 13_437_000 picoseconds. + Weight::from_parts(16_723_316, 0) .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 405 - .saturating_add(Weight::from_parts(63_474, 0).saturating_mul(p.into())) + // Standard Error: 398 + .saturating_add(Weight::from_parts(66_219, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -103,26 +103,26 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(1238), added: 3713, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::SpendCount` (r:1 w:1) /// Proof: `FellowshipTreasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:0) - /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:1) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::Spends` (r:0 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1143` + // Measured: `1147` // Estimated: `5481` - // Minimum execution time: 35_418_000 picoseconds. - Weight::from_parts(37_538_000, 0) + // Minimum execution time: 37_925_000 picoseconds. + Weight::from_parts(39_956_000, 0) .saturating_add(Weight::from_parts(0, 5481)) .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `FellowshipTreasury::Spends` (r:1 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:0) - /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `PolkadotXcm::QueryCounter` (r:1 w:1) @@ -141,10 +141,10 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `564` + // Measured: `568` // Estimated: `5318` - // Minimum execution time: 61_552_000 picoseconds. - Weight::from_parts(65_444_000, 0) + // Minimum execution time: 61_546_000 picoseconds. + Weight::from_parts(65_150_000, 0) .saturating_add(Weight::from_parts(0, 5318)) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(5)) @@ -152,33 +152,49 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Storage: `FellowshipTreasury::Spends` (r:1 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:1) - /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `PolkadotXcm::Queries` (r:1 w:1) /// Proof: `PolkadotXcm::Queries` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `1172` + // Measured: `1176` // Estimated: `5481` - // Minimum execution time: 40_524_000 picoseconds. - Weight::from_parts(42_890_000, 0) + // Minimum execution time: 40_938_000 picoseconds. + Weight::from_parts(43_298_000, 0) .saturating_add(Weight::from_parts(0, 5481)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } + /// Storage: `FellowshipTreasury::Spends` (r:1 w:0) + /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:1) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) + /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) + /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1140` + // Estimated: `5481` + // Minimum execution time: 30_501_000 picoseconds. + Weight::from_parts(32_604_000, 0) + .saturating_add(Weight::from_parts(0, 5481)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } /// Storage: `FellowshipTreasury::Spends` (r:1 w:1) /// Proof: `FellowshipTreasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::NextPayout` (r:1 w:1) - /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Proof: `FellowshipTreasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `FellowshipTreasury::PayoutQueue` (r:1 w:1) /// Proof: `FellowshipTreasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1144` + // Measured: `1148` // Estimated: `5481` - // Minimum execution time: 28_453_000 picoseconds. - Weight::from_parts(30_117_000, 0) + // Minimum execution time: 28_370_000 picoseconds. + Weight::from_parts(30_215_000, 0) .saturating_add(Weight::from_parts(0, 5481)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) diff --git a/substrate/frame/treasury/src/weights.rs b/substrate/frame/treasury/src/weights.rs index 70abe9ab2a68..9f45e73a3c7d 100644 --- a/substrate/frame/treasury/src/weights.rs +++ b/substrate/frame/treasury/src/weights.rs @@ -35,9 +35,9 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `22cd7d3cce09`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `1807571685ea`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -77,6 +77,7 @@ pub trait WeightInfo { fn spend() -> Weight; fn payout() -> Weight; fn check_status() -> Weight; + fn check_status_rotation() -> Weight; fn void_spend() -> Weight; } @@ -93,8 +94,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `76` // Estimated: `1887` - // Minimum execution time: 11_693_000 picoseconds. - Weight::from_parts(12_793_000, 1887) + // Minimum execution time: 11_815_000 picoseconds. + Weight::from_parts(12_913_000, 1887) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -104,8 +105,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `161` // Estimated: `1887` - // Minimum execution time: 6_558_000 picoseconds. - Weight::from_parts(7_218_000, 1887) + // Minimum execution time: 6_556_000 picoseconds. + Weight::from_parts(7_287_000, 1887) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -118,10 +119,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `170` // Estimated: `1501` - // Minimum execution time: 11_225_000 picoseconds. - Weight::from_parts(13_618_454, 1501) - // Standard Error: 220 - .saturating_add(Weight::from_parts(34_323, 0).saturating_mul(p.into())) + // Minimum execution time: 11_141_000 picoseconds. + Weight::from_parts(13_838_963, 1501) + // Standard Error: 251 + .saturating_add(Weight::from_parts(32_789, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -129,25 +130,25 @@ impl WeightInfo for SubstrateWeight { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Treasury::SpendCount` (r:1 w:1) /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Treasury::NextPayout` (r:1 w:0) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) /// Storage: `Treasury::Spends` (r:0 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1016` + // Measured: `1020` // Estimated: `4280` - // Minimum execution time: 27_510_000 picoseconds. - Weight::from_parts(29_474_000, 4280) + // Minimum execution time: 29_422_000 picoseconds. + Weight::from_parts(31_564_000, 4280) .saturating_add(T::DbWeight::get().reads(4_u64)) - .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:0) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:1 w:1) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:2 w:2) @@ -156,40 +157,55 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `820` + // Measured: `824` // Estimated: `6208` - // Minimum execution time: 66_876_000 picoseconds. - Weight::from_parts(71_534_000, 6208) + // Minimum execution time: 65_968_000 picoseconds. + Weight::from_parts(71_212_000, 6208) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:1) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `1082` + // Measured: `1086` // Estimated: `4280` - // Minimum execution time: 28_415_000 picoseconds. - Weight::from_parts(30_468_000, 4280) + // Minimum execution time: 28_581_000 picoseconds. + Weight::from_parts(30_728_000, 4280) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// Storage: `Treasury::Spends` (r:1 w:0) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1078` + // Estimated: `4280` + // Minimum execution time: 26_579_000 picoseconds. + Weight::from_parts(28_594_000, 4280) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:1) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1082` + // Measured: `1086` // Estimated: `4280` - // Minimum execution time: 25_225_000 picoseconds. - Weight::from_parts(26_896_000, 4280) + // Minimum execution time: 25_244_000 picoseconds. + Weight::from_parts(27_207_000, 4280) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -207,8 +223,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `76` // Estimated: `1887` - // Minimum execution time: 11_693_000 picoseconds. - Weight::from_parts(12_793_000, 1887) + // Minimum execution time: 11_815_000 picoseconds. + Weight::from_parts(12_913_000, 1887) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -218,8 +234,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `161` // Estimated: `1887` - // Minimum execution time: 6_558_000 picoseconds. - Weight::from_parts(7_218_000, 1887) + // Minimum execution time: 6_556_000 picoseconds. + Weight::from_parts(7_287_000, 1887) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -232,10 +248,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `170` // Estimated: `1501` - // Minimum execution time: 11_225_000 picoseconds. - Weight::from_parts(13_618_454, 1501) - // Standard Error: 220 - .saturating_add(Weight::from_parts(34_323, 0).saturating_mul(p.into())) + // Minimum execution time: 11_141_000 picoseconds. + Weight::from_parts(13_838_963, 1501) + // Standard Error: 251 + .saturating_add(Weight::from_parts(32_789, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -243,25 +259,25 @@ impl WeightInfo for () { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Treasury::SpendCount` (r:1 w:1) /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Treasury::NextPayout` (r:1 w:0) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) /// Storage: `Treasury::Spends` (r:0 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1016` + // Measured: `1020` // Estimated: `4280` - // Minimum execution time: 27_510_000 picoseconds. - Weight::from_parts(29_474_000, 4280) + // Minimum execution time: 29_422_000 picoseconds. + Weight::from_parts(31_564_000, 4280) .saturating_add(RocksDbWeight::get().reads(4_u64)) - .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:0) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:1 w:1) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:2 w:2) @@ -270,40 +286,55 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `820` + // Measured: `824` // Estimated: `6208` - // Minimum execution time: 66_876_000 picoseconds. - Weight::from_parts(71_534_000, 6208) + // Minimum execution time: 65_968_000 picoseconds. + Weight::from_parts(71_212_000, 6208) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:1) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `1082` + // Measured: `1086` // Estimated: `4280` - // Minimum execution time: 28_415_000 picoseconds. - Weight::from_parts(30_468_000, 4280) + // Minimum execution time: 28_581_000 picoseconds. + Weight::from_parts(30_728_000, 4280) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + /// Storage: `Treasury::Spends` (r:1 w:0) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1078` + // Estimated: `4280` + // Minimum execution time: 26_579_000 picoseconds. + Weight::from_parts(28_594_000, 4280) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:1) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(25), added: 2500, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(815), added: 3290, mode: `MaxEncodedLen`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1082` + // Measured: `1086` // Estimated: `4280` - // Minimum execution time: 25_225_000 picoseconds. - Weight::from_parts(26_896_000, 4280) + // Minimum execution time: 25_244_000 picoseconds. + Weight::from_parts(27_207_000, 4280) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } From e69104059c3298eae73e33a5fb398fea2dddf0ae Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Mon, 29 Jun 2026 14:30:23 +0100 Subject: [PATCH 12/18] propagate feature runtime wide --- .../assets/asset-hub-westend/src/governance/mod.rs | 8 ++++---- .../asset-hub-westend/src/weights/pallet_treasury.rs | 11 ++++++++++- polkadot/runtime/common/src/impls.rs | 2 ++ polkadot/runtime/rococo/src/lib.rs | 8 ++++---- .../runtime/rococo/src/weights/pallet_treasury.rs | 11 ++++++++++- prdoc/pr_11603.prdoc | 9 +++++++-- substrate/frame/bounties/src/tests.rs | 4 ++++ substrate/frame/child-bounties/src/tests.rs | 2 ++ .../runtimes/parachain/src/governance/mod.rs | 2 ++ .../runtimes/parachain/src/weights/pallet_treasury.rs | 11 ++++++++++- substrate/frame/staking-async/runtimes/rc/src/lib.rs | 2 ++ .../runtimes/rc/src/weights/pallet_treasury.rs | 11 ++++++++++- substrate/frame/tips/src/tests.rs | 4 ++++ substrate/frame/treasury/src/lib.rs | 2 +- 14 files changed, 72 insertions(+), 15 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 6a48ebac6764..de78199b5b79 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -117,8 +117,8 @@ parameter_types! { pub const MaxPeerInHeartbeats: u32 = 10_000; pub const MaxBalance: Balance = Balance::max_value(); pub TreasuryAccount: AccountId = Treasury::account_id(); - pub const MaxQueuedSpends: u32 = 100; - pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; } pub type TreasurySpender = EitherOf, Spender>; @@ -153,8 +153,8 @@ impl pallet_treasury::Config for Runtime { type BalanceConverter = TreasuryBalanceConverter; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = RelaychainDataProvider; - type MaxQueuedSpends = MaxQueuedSpends; - type OrderExpirationPeriod = OrderExpirationPeriod; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = parachains_common::pay::benchmarks::LocalPayArguments< xcm_config::TrustBackedAssetsPalletIndex, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs index e0827982161a..1d4a2b9ccccb 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs @@ -18,7 +18,6 @@ //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 //! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `22cd7d3cce09`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -179,6 +178,16 @@ impl pallet_treasury::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `1112` + // Estimated: `5921` + // Minimum execution time: 35_815_000 picoseconds. + Weight::from_parts(38_194_000, 0) + .saturating_add(Weight::from_parts(0, 5921)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:1) diff --git a/polkadot/runtime/common/src/impls.rs b/polkadot/runtime/common/src/impls.rs index 3e9e5e6c04c7..96f75d427f4f 100644 --- a/polkadot/runtime/common/src/impls.rs +++ b/polkadot/runtime/common/src/impls.rs @@ -400,6 +400,8 @@ mod tests { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<0>; type BlockNumberProvider = System; + type MaxQueuedSpends = frame_support::traits::ConstU32<100>; + type OrderExpirationPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 475a4eac19aa..0f02c3b867ea 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -533,8 +533,8 @@ parameter_types! { pub const MaxKeys: u32 = 10_000; pub const MaxPeerInHeartbeats: u32 = 10_000; pub const MaxBalance: Balance = Balance::max_value(); - pub const MaxQueuedSpends: u32 = 100; - pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; } impl pallet_treasury::Config for Runtime { @@ -573,8 +573,8 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; - type MaxQueuedSpends = MaxQueuedSpends; - type OrderExpirationPeriod = OrderExpirationPeriod; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/polkadot/runtime/rococo/src/weights/pallet_treasury.rs b/polkadot/runtime/rococo/src/weights/pallet_treasury.rs index c875202a22fe..7c6549de45cf 100644 --- a/polkadot/runtime/rococo/src/weights/pallet_treasury.rs +++ b/polkadot/runtime/rococo/src/weights/pallet_treasury.rs @@ -19,7 +19,6 @@ //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 //! DATE: 2025-02-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `d3a9aad6f7a3`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -152,6 +151,16 @@ impl pallet_treasury::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `305` + // Estimated: `5318` + // Minimum execution time: 25_238_000 picoseconds. + Weight::from_parts(25_654_000, 0) + .saturating_add(Weight::from_parts(0, 5318)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) fn void_spend() -> Weight { diff --git a/prdoc/pr_11603.prdoc b/prdoc/pr_11603.prdoc index f8db7123a24d..9a5385846a18 100644 --- a/prdoc/pr_11603.prdoc +++ b/prdoc/pr_11603.prdoc @@ -9,8 +9,9 @@ doc: Two new storage items are introduced: `NextPayout`, holding the spend currently at the head of the payout order for each asset kind together with its order expiration block, and `PayoutQueue`, a bounded vector of `(spend_index, order_key)` entries per asset kind sorted by - order key. The order key is the spend's `valid_from` for newly inserted spends and the block - number of the rotation for spends rotated to the back after their order expired. + order key. The order key is `max(now, valid_from)` for newly inserted spends, so a later + approval never overtakes an already-mature spend, and the block number of the rotation for + spends rotated to the back after their order expired. No new extrinsics are added; ordering is enforced transparently through the existing calls: @@ -42,3 +43,7 @@ crates: bump: minor - name: kitchensink-runtime bump: minor +- name: pallet-staking-async-parachain-runtime + bump: minor +- name: pallet-staking-async-rc-runtime + bump: minor diff --git a/substrate/frame/bounties/src/tests.rs b/substrate/frame/bounties/src/tests.rs index 812ae5b8e10a..cc3fca6430b7 100644 --- a/substrate/frame/bounties/src/tests.rs +++ b/substrate/frame/bounties/src/tests.rs @@ -113,6 +113,8 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -136,6 +138,8 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/child-bounties/src/tests.rs b/substrate/frame/child-bounties/src/tests.rs index e0bb1ac6f75a..6c2697e842ea 100644 --- a/substrate/frame/child-bounties/src/tests.rs +++ b/substrate/frame/child-bounties/src/tests.rs @@ -111,6 +111,8 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs index 476e3897b269..21c130a199fd 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -166,6 +166,8 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = RelayChainBlockNumberProvider; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU32<{ 2 * DAYS }>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_treasury.rs b/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_treasury.rs index 80fd80017608..f1503790dbcf 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_treasury.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_treasury.rs @@ -19,7 +19,6 @@ //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev //! DATE: 2023-07-07, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `cob`, CPU: `` //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 // Executed Command: @@ -163,6 +162,16 @@ impl pallet_treasury::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `442` + // Estimated: `5313` + // Minimum execution time: 245_000_000 picoseconds. + Weight::from_parts(281_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } /// Storage: Treasury Spends (r:1 w:1) /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) fn void_spend() -> Weight { diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index f2e37f4d2644..1255ef23c51a 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -1053,6 +1053,8 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU32<{ 2 * DAYS }>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_treasury.rs b/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_treasury.rs index 2ff1b7ec304f..0f84034e55bb 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_treasury.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_treasury.rs @@ -19,7 +19,6 @@ //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 //! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `3a2e9ae8a8f5`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -154,6 +153,16 @@ impl pallet_treasury::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } + fn check_status_rotation() -> Weight { + // Proof Size summary in bytes: + // Measured: `305` + // Estimated: `5318` + // Minimum execution time: 28_594_000 picoseconds. + Weight::from_parts(29_512_000, 0) + .saturating_add(Weight::from_parts(0, 5318)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(1853), added: 4328, mode: `MaxEncodedLen`) fn void_spend() -> Weight { diff --git a/substrate/frame/tips/src/tests.rs b/substrate/frame/tips/src/tests.rs index 47388a108622..f0446a5e4145 100644 --- a/substrate/frame/tips/src/tests.rs +++ b/substrate/frame/tips/src/tests.rs @@ -120,6 +120,8 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -143,6 +145,8 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 20fa3a741d94..96255558e5ec 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -867,7 +867,7 @@ pub mod pallet { /// Emits [`Event::SpendProcessed`] if the spend payout has succeed. /// Emits [`Event::PayoutQueueRotated`] if the queue was rotated due to expiration. #[pallet::call_index(7)] - #[pallet::weight(T::WeightInfo::check_status())] + #[pallet::weight(T::WeightInfo::check_status().max(T::WeightInfo::check_status_rotation()))] pub fn check_status(origin: OriginFor, index: SpendIndex) -> DispatchResultWithPostInfo { use PaymentState as State; use PaymentStatus as Status; From e92c3e352fe2f5af219c1fe5d122b419a2ddbe60 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:47:23 +0000 Subject: [PATCH 13/18] Update from github-actions[bot] running command 'fmt' --- .../collectives/collectives-westend/src/fellowship/mod.rs | 4 ++-- polkadot/runtime/westend/src/lib.rs | 8 ++++---- substrate/bin/node/runtime/src/lib.rs | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs index a3353adf3c6c..cd0ad5b6b6ba 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs @@ -333,8 +333,8 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = ConstU32<{ 30 * DAYS }>; - type MaxQueuedSpends = ConstU32<100>; - type OrderExpirationPeriod = ConstU32<{ 3 * DAYS }>; + type MaxQueuedSpends = ConstU32<100>; + type OrderExpirationPeriod = ConstU32<{ 3 * DAYS }>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments< sp_core::ConstU8<1>, diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 4d467e7421de..c5b967626241 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -986,9 +986,9 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = PayoutSpendPeriod; - type BlockNumberProvider = System; - type MaxQueuedSpends = MaxQueuedSpends; - type OrderExpirationPeriod = OrderExpirationPeriod; + type BlockNumberProvider = System; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } @@ -2208,7 +2208,7 @@ pub mod migrations { >, // permanent pallet_xcm::migration::MigrateToLatestXcmVersion, - pallet_treasury::migration::MigrateToOrderedPayouts, + pallet_treasury::migration::MigrateToOrderedPayouts, ); } diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 4ca45eae8b78..efd364ce519e 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1328,8 +1328,8 @@ parameter_types! { pub const MaxApprovals: u32 = 100; pub const MaxBalance: Balance = Balance::max_value(); pub const SpendPayoutPeriod: BlockNumber = 30 * DAYS; - pub const MaxQueuedSpends: u32 = 100; - pub const OrderExpirationPeriod: BlockNumber = 10 * HOURS; + pub const MaxQueuedSpends: u32 = 100; + pub const OrderExpirationPeriod: BlockNumber = 10 * HOURS; } impl pallet_treasury::Config for Runtime { @@ -1354,8 +1354,8 @@ impl pallet_treasury::Config for Runtime { type BalanceConverter = AssetRate; type PayoutPeriod = SpendPayoutPeriod; type BlockNumberProvider = System; - type MaxQueuedSpends = MaxQueuedSpends; - type OrderExpirationPeriod = OrderExpirationPeriod; + type MaxQueuedSpends = MaxQueuedSpends; + type OrderExpirationPeriod = OrderExpirationPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = PalletTreasuryArguments; } From 49509a8e2efe6d7347a2f4700c1fb3fbc58997a7 Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Mon, 29 Jun 2026 18:24:27 +0100 Subject: [PATCH 14/18] revert westend, no pallet_treasury --- polkadot/runtime/westend/src/lib.rs | 48 ----------------------------- prdoc/pr_11603.prdoc | 2 -- 2 files changed, 50 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index c5b967626241..1153311af051 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -944,53 +944,6 @@ impl pallet_fast_unstake::Config for Runtime { parameter_types! { pub const MaxAuthorities: u32 = 100_000; - pub const MaxKeys: u32 = 10_000; - pub const MaxPeerInHeartbeats: u32 = 10_000; - pub const MaxBalance: Balance = Balance::max_value(); - pub const MaxQueuedSpends: u32 = 100; - pub const OrderExpirationPeriod: BlockNumber = 2 * DAYS; -} - -impl pallet_treasury::Config for Runtime { - type PalletId = TreasuryPalletId; - type Currency = Balances; - type RejectOrigin = EitherOfDiverse, Treasurer>; - type RuntimeEvent = RuntimeEvent; - type SpendPeriod = SpendPeriod; - type Burn = (); - type BurnDestination = (); - type MaxApprovals = MaxApprovals; - type WeightInfo = weights::pallet_treasury::WeightInfo; - type SpendFunds = (); - type SpendOrigin = TreasurySpender; - type AssetKind = VersionedLocatableAsset; - type Beneficiary = VersionedLocation; - type BeneficiaryLookup = IdentityLookup; - type Paymaster = PayOverXcm< - TreasuryInteriorLocation, - crate::xcm_config::XcmConfig, - crate::XcmPallet, - ConstU32<{ 6 * HOURS }>, - Self::Beneficiary, - Self::AssetKind, - LocatableAssetConverter, - VersionedLocationConverter, - >; - type BalanceConverter = UnityOrOuterConversion< - ContainsParts< - FromContains< - xcm_builder::IsChildSystemParachain, - xcm_builder::IsParentsOnly>, - >, - >, - AssetRate, - >; - type PayoutPeriod = PayoutSpendPeriod; - type BlockNumberProvider = System; - type MaxQueuedSpends = MaxQueuedSpends; - type OrderExpirationPeriod = OrderExpirationPeriod; - #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } impl pallet_offences::Config for Runtime { @@ -2208,7 +2161,6 @@ pub mod migrations { >, // permanent pallet_xcm::migration::MigrateToLatestXcmVersion, - pallet_treasury::migration::MigrateToOrderedPayouts, ); } diff --git a/prdoc/pr_11603.prdoc b/prdoc/pr_11603.prdoc index 9a5385846a18..90175fd69828 100644 --- a/prdoc/pr_11603.prdoc +++ b/prdoc/pr_11603.prdoc @@ -33,8 +33,6 @@ doc: crates: - name: pallet-treasury bump: major -- name: westend-runtime - bump: minor - name: rococo-runtime bump: minor - name: asset-hub-westend-runtime From 91fa02215e06bf3c92a75a345b5b17c2a5f7c0b6 Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Mon, 29 Jun 2026 22:57:13 +0100 Subject: [PATCH 15/18] zeroed order expiration --- substrate/frame/treasury/src/benchmarking.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index 693390498a0f..c1e9d9425db4 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -370,6 +370,9 @@ mod benchmarks { *expire_at = BlockNumberFor::::zero(); } }); + // Relay-block providers (e.g. asset-hub) report 0 at genesis; advance so the zeroed order + // expiration is in the past. + T::BlockNumberProvider::set_block_number(One::one()); // Worst case: a near-full queue, so rotation scans it, re-inserts the expired head, and // promotes the front. Leave one slot free for the re-insert. From ed82a0a4676aa16dba81f319bbe21d369c021fbd Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:36:17 +0000 Subject: [PATCH 16/18] Update from github-actions[bot] running command 'bench --runtime asset-hub-westend --pallet pallet_treasury' --- .../src/weights/pallet_treasury.rs | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs index 1d4a2b9ccccb..48ec063be7b5 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_treasury.rs @@ -16,8 +16,9 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2026-06-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `c37d1d4d187b`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -59,8 +60,8 @@ impl pallet_treasury::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `76` // Estimated: `1887` - // Minimum execution time: 13_212_000 picoseconds. - Weight::from_parts(14_667_000, 0) + // Minimum execution time: 13_106_000 picoseconds. + Weight::from_parts(14_516_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) @@ -71,8 +72,8 @@ impl pallet_treasury::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `161` // Estimated: `1887` - // Minimum execution time: 7_147_000 picoseconds. - Weight::from_parts(7_886_000, 0) + // Minimum execution time: 7_279_000 picoseconds. + Weight::from_parts(8_029_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -92,11 +93,11 @@ impl pallet_treasury::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1444` // Estimated: `3593` - // Minimum execution time: 21_490_000 picoseconds. - Weight::from_parts(29_173_765, 0) + // Minimum execution time: 21_280_000 picoseconds. + Weight::from_parts(29_151_412, 0) .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 1_677 - .saturating_add(Weight::from_parts(91_915, 0).saturating_mul(p.into())) + // Standard Error: 1_379 + .saturating_add(Weight::from_parts(74_362, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -108,26 +109,26 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(1238), added: 3713, mode: `MaxEncodedLen`) /// Storage: `Treasury::SpendCount` (r:1 w:1) /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Treasury::NextPayout` (r:1 w:0) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) /// Storage: `Treasury::Spends` (r:0 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) fn spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1068` + // Measured: `1072` // Estimated: `5481` - // Minimum execution time: 36_111_000 picoseconds. - Weight::from_parts(38_640_000, 0) + // Minimum execution time: 39_144_000 picoseconds. + Weight::from_parts(41_448_000, 0) .saturating_add(Weight::from_parts(0, 5481)) .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:0) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) @@ -150,11 +151,11 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `Revive::OriginalAccount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) fn payout() -> Weight { // Proof Size summary in bytes: - // Measured: `5033` - // Estimated: `6518` - // Minimum execution time: 141_509_000 picoseconds. - Weight::from_parts(149_841_000, 0) - .saturating_add(Weight::from_parts(0, 6518)) + // Measured: `5037` + // Estimated: `6522` + // Minimum execution time: 141_719_000 picoseconds. + Weight::from_parts(150_487_000, 0) + .saturating_add(Weight::from_parts(0, 6522)) .saturating_add(T::DbWeight::get().reads(14)) .saturating_add(T::DbWeight::get().writes(11)) } @@ -165,33 +166,41 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `Treasury::NextPayout` (r:1 w:1) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn check_status() -> Weight { // Proof Size summary in bytes: - // Measured: `1112` + // Measured: `1116` // Estimated: `5921` - // Minimum execution time: 35_815_000 picoseconds. - Weight::from_parts(38_194_000, 0) + // Minimum execution time: 36_048_000 picoseconds. + Weight::from_parts(38_142_000, 0) .saturating_add(Weight::from_parts(0, 5921)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: `Treasury::Spends` (r:1 w:0) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Treasury::NextPayout` (r:1 w:1) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) + /// Storage: `Treasury::PayoutQueue` (r:1 w:1) + /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) fn check_status_rotation() -> Weight { // Proof Size summary in bytes: - // Measured: `1112` + // Measured: `1129` // Estimated: `5921` - // Minimum execution time: 35_815_000 picoseconds. - Weight::from_parts(38_194_000, 0) + // Minimum execution time: 33_764_000 picoseconds. + Weight::from_parts(35_650_000, 0) .saturating_add(Weight::from_parts(0, 5921)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Treasury::Spends` (r:1 w:1) /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(2456), added: 4931, mode: `MaxEncodedLen`) /// Storage: `Treasury::NextPayout` (r:1 w:1) - /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1222), added: 3697, mode: `MaxEncodedLen`) + /// Proof: `Treasury::NextPayout` (`max_values`: None, `max_size`: Some(1226), added: 3701, mode: `MaxEncodedLen`) /// Storage: `Treasury::PayoutQueue` (r:1 w:1) /// Proof: `Treasury::PayoutQueue` (`max_values`: None, `max_size`: Some(2016), added: 4491, mode: `MaxEncodedLen`) /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) @@ -200,10 +209,10 @@ impl pallet_treasury::WeightInfo for WeightInfo { /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn void_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `1104` + // Measured: `1108` // Estimated: `5921` - // Minimum execution time: 30_642_000 picoseconds. - Weight::from_parts(32_548_000, 0) + // Minimum execution time: 30_391_000 picoseconds. + Weight::from_parts(32_216_000, 0) .saturating_add(Weight::from_parts(0, 5921)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) From 891a223b67979acce4c5e2f6e5d17b2fe1b2d9b8 Mon Sep 17 00:00:00 2001 From: jessechejieh Date: Sat, 11 Jul 2026 01:13:04 +0100 Subject: [PATCH 17/18] Versioned Migration --- .../assets/asset-hub-westend/src/lib.rs | 2 + .../collectives-westend/src/lib.rs | 5 ++ polkadot/runtime/rococo/src/lib.rs | 2 + prdoc/pr_11603.prdoc | 11 ++- substrate/bin/node/runtime/src/lib.rs | 1 + substrate/frame/treasury/src/lib.rs | 4 ++ substrate/frame/treasury/src/migration.rs | 72 ++++++++++++++----- 7 files changed, 79 insertions(+), 18 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 0c3ad4ddcee0..53e03d21525f 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -1998,6 +1998,8 @@ pub type Migrations = ( Runtime, TrappedBalanceMember, >, + // unreleased + pallet_treasury::migration::MigrateToOrderedPayouts, // permanent pallet_xcm::migration::MigrateToLatestXcmVersion, cumulus_pallet_aura_ext::migration::MigrateV0ToV1, diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index 5997d58fac14..31a77571fab0 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -864,6 +864,11 @@ type Migrations = ( pallet_session::migrations::v1::InitOffenceSeverity, >, cumulus_pallet_parachain_system::migration::Migration, + // unreleased + pallet_treasury::migration::MigrateToOrderedPayouts< + Runtime, + fellowship::FellowshipTreasuryInstance, + >, ); /// Executive: handles dispatch to the various modules. diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 0f02c3b867ea..ad2e9d71f858 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1781,6 +1781,8 @@ pub mod migrations { parachains_configuration::migration::v13::MigrateToV13, parachains_shared::migration::MigrateToV2, + pallet_treasury::migration::MigrateToOrderedPayouts, + // permanent pallet_xcm::migration::MigrateToLatestXcmVersion, parachains_inclusion::migration::MigrateToV1, diff --git a/prdoc/pr_11603.prdoc b/prdoc/pr_11603.prdoc index 90175fd69828..03ffae4eeca3 100644 --- a/prdoc/pr_11603.prdoc +++ b/prdoc/pr_11603.prdoc @@ -29,7 +29,8 @@ doc: queue per asset kind; spends fail with the new `QueueFull` error once reached) and `OrderExpirationPeriod` (how long a spend may block the head of the order before it can be rotated). A `MigrateToOrderedPayouts` migration is provided to populate the payout order from - the existing `Spends` storage. + the existing `Spends` storage; it is a `VersionedMigration` bumping the pallet storage version + from 0 to 1. crates: - name: pallet-treasury bump: major @@ -45,3 +46,11 @@ crates: bump: minor - name: pallet-staking-async-rc-runtime bump: minor +- name: polkadot-runtime-common + bump: none +- name: pallet-bounties + bump: none +- name: pallet-child-bounties + bump: none +- name: pallet-tips + bump: none diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index efd364ce519e..0d491ff749e5 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -3024,6 +3024,7 @@ type Migrations = ( pallet_alliance::migration::Migration, pallet_contracts::Migration, pallet_identity::migration::versioned::V0ToV1, + pallet_treasury::migration::MigrateToOrderedPayouts, ); type EventRecord = frame_system::EventRecord< diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 96255558e5ec..68d162951454 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -231,7 +231,11 @@ pub mod pallet { }; use frame_system::pallet_prelude::{ensure_signed, OriginFor}; + /// The in-code storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData<(T, I)>); #[pallet::config] diff --git a/substrate/frame/treasury/src/migration.rs b/substrate/frame/treasury/src/migration.rs index 7c86afefb3a1..cb72680b5622 100644 --- a/substrate/frame/treasury/src/migration.rs +++ b/substrate/frame/treasury/src/migration.rs @@ -20,7 +20,10 @@ use super::*; use alloc::{collections::BTreeSet, vec::Vec}; use core::marker::PhantomData; -use frame_support::{defensive, traits::OnRuntimeUpgrade}; +use frame_support::{ + defensive, + traits::{OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade}, +}; /// The log target for this pallet. const LOG_TARGET: &str = "runtime::treasury"; @@ -142,9 +145,14 @@ mod migrate_to_ordered_payouts { use super::*; /// Migration to initialize the payout queue for existing spends. - pub struct MigrateToOrderedPayouts(PhantomData<(T, I)>); + /// + /// Wrapped in [`MigrateToOrderedPayouts`](super::MigrateToOrderedPayouts) for the storage + /// version check. + pub struct UncheckedMigrateToOrderedPayouts(PhantomData<(T, I)>); - impl, I: 'static> OnRuntimeUpgrade for MigrateToOrderedPayouts { + impl, I: 'static> UncheckedOnRuntimeUpgrade + for UncheckedMigrateToOrderedPayouts + { fn on_runtime_upgrade() -> Weight { log::info!( target: LOG_TARGET, @@ -384,7 +392,7 @@ mod migrate_to_ordered_payouts { pallet::Spends, tests::{ExtBuilder, System, Test}, }; - use frame_support::traits::OnRuntimeUpgrade; + use frame_support::traits::{OnRuntimeUpgrade, StorageVersion, UncheckedOnRuntimeUpgrade}; #[cfg(feature = "try-runtime")] use frame_support::assert_ok; @@ -417,13 +425,34 @@ mod migrate_to_ordered_payouts { } } + #[test] + fn migration_runs_once() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(100); + insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); + + StorageVersion::new(0).put::>(); + crate::migration::MigrateToOrderedPayouts::::on_runtime_upgrade(); + + assert_eq!(StorageVersion::get::>(), 1); + assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); + + // Re-run is a no-op: the version gate skips the migration. + NextPayout::::remove(1u32); + crate::migration::MigrateToOrderedPayouts::::on_runtime_upgrade(); + + assert_eq!(StorageVersion::get::>(), 1); + assert!(NextPayout::::get(1u32).is_none()); + }); + } + #[test] fn migration_empty_state() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(100); assert_eq!(Spends::::iter().count(), 0); - let weight = MigrateToOrderedPayouts::::on_runtime_upgrade(); + let weight = UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); assert!(weight.ref_time() == 0); assert!(NextPayout::::iter().next().is_none()); @@ -440,7 +469,7 @@ mod migrate_to_ordered_payouts { // failure can be retried rather than lost. insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Attempted { id: 123u64 }); - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); // Only spend → becomes NextPayout (order key = max(now=100, valid_from=50) = 100). assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); @@ -456,7 +485,7 @@ mod migrate_to_ordered_payouts { // expire_at (99) < now (100) insert_spend(0, 1, 100, 1000, 50, 99, PaymentState::Pending); - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); assert!(NextPayout::::get(1u32).is_none()); }); @@ -473,7 +502,7 @@ mod migrate_to_ordered_payouts { // Asset 2: 1 spend insert_spend(2, 2, 300, 1002, 40, 200, PaymentState::Failed); - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); // Asset 1: First spend is NextPayout let (next_idx, _order_key, expire_at) = NextPayout::::get(1u32).unwrap(); @@ -501,7 +530,7 @@ mod migrate_to_ordered_payouts { insert_spend(1, 1, 100, 1001, 50, 200, PaymentState::Pending); // earliest insert_spend(2, 1, 100, 1002, 75, 200, PaymentState::Pending); // middle - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); // Sorted: 1 (50), 2 (75), 0 (100) assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(1)); @@ -521,7 +550,7 @@ mod migrate_to_ordered_payouts { insert_spend(3, 1, 100, 1001, 50, 200, PaymentState::Pending); insert_spend(4, 1, 100, 1002, 50, 200, PaymentState::Pending); - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); // Sorted by index: 3, 4, 5 assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(3)); @@ -547,7 +576,7 @@ mod migrate_to_ordered_payouts { ); } - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); // NextPayout is index 0 assert_eq!(NextPayout::::get(1u32).map(|(idx, _, _)| idx), Some(0)); @@ -570,7 +599,7 @@ mod migrate_to_ordered_payouts { insert_spend(2, 1, 100, 1002, 52, 200, PaymentState::Attempted { id: 1 }); insert_spend(3, 1, 100, 1003, 53, 99, PaymentState::Pending); // expired - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); // Pending (0), Failed (1) and the in-flight Attempted (2) are ordered; the expired // Pending (3) is dropped. All are mature at `now = 100`, so order keys clamp to 100 @@ -588,7 +617,7 @@ mod migrate_to_ordered_payouts { insert_spend(1, 1, 100, 1001, 51, 200, PaymentState::Failed); insert_spend(2, 1, 100, 1002, 52, 200, PaymentState::Attempted { id: 1 }); - let state = MigrateToOrderedPayouts::::pre_upgrade().unwrap(); + let state = UncheckedMigrateToOrderedPayouts::::pre_upgrade().unwrap(); let decoded: Vec<(u32, u32)> = Vec::decode(&mut &state[..]).unwrap(); // All three are ordered by the migration, including the in-flight Attempted spend. @@ -613,9 +642,9 @@ mod migrate_to_ordered_payouts { System::set_block_number(100); insert_spend(0, 1, 100, 1000, 50, 200, PaymentState::Pending); insert_spend(1, 1, 100, 1001, 51, 200, PaymentState::Pending); - MigrateToOrderedPayouts::::on_runtime_upgrade(); + UncheckedMigrateToOrderedPayouts::::on_runtime_upgrade(); - assert_ok!(MigrateToOrderedPayouts::::post_upgrade(pre_state)); + assert_ok!(UncheckedMigrateToOrderedPayouts::::post_upgrade(pre_state)); }); } @@ -635,11 +664,20 @@ mod migrate_to_ordered_payouts { let pre_state = vec![(1u32, 0u32)].encode(); - assert!(MigrateToOrderedPayouts::::post_upgrade(pre_state).is_err()); + assert!(UncheckedMigrateToOrderedPayouts::::post_upgrade(pre_state).is_err()); }); } } } pub use cleanup_proposals::Migration as CleanupProposalsMigration; -pub use migrate_to_ordered_payouts::MigrateToOrderedPayouts; + +/// Initialize the payout queue for existing spends, gated on storage version 0 and bumping it +/// to 1. +pub type MigrateToOrderedPayouts = frame_support::migrations::VersionedMigration< + 0, + 1, + migrate_to_ordered_payouts::UncheckedMigrateToOrderedPayouts, + Pallet, + ::DbWeight, +>; From 6cf321813fd9568fe68e95aa12df583504bd9d33 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:41:40 +0000 Subject: [PATCH 18/18] Update from github-actions[bot] running command 'fmt' --- .../staking-async/runtimes/parachain/src/governance/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs index a584de3b46b6..5fdb7f26eeab 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -166,7 +166,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = RelaychainDataProvider; - type MaxQueuedSpends = ConstU32<100>; + type MaxQueuedSpends = ConstU32<100>; type OrderExpirationPeriod = ConstU32<{ 2 * DAYS }>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments;