Skip to content

Commit f281b48

Browse files
sigurpolgithub-actions[bot]kianenigma
committed
bags-list: implement pending rebag (#9725)
Fix the issue where registering a nominator or validator while the bags list is locked would completely ignore the account, making it unreachable for on_idle rebagging. Changes: - Add PendingRebag storage to queue accounts that fail insertion during lock. - Modify on_idle to process pending accounts with priority before regular accounts. - Add cleanup logic for non-staking pending accounts. Related issue: paritytech-secops/srlabs_findings#567 --------- Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kian Paimani <[email protected]> (cherry picked from commit 5c63e5e)
1 parent 5ab4013 commit f281b48

File tree

8 files changed

+432
-85
lines changed

8 files changed

+432
-85
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_bags_list.rs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
//! Autogenerated weights for `pallet_bags_list`
1717
//!
1818
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0
19-
//! DATE: 2025-07-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
19+
//! DATE: 2025-09-12, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
2020
//! WORST CASE MAP SIZE: `1000000`
21-
//! HOSTNAME: `66f1737e2c94`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
21+
//! HOSTNAME: `157b7e44410a`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
2222
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024
2323
2424
// Executed Command:
@@ -52,6 +52,8 @@ pub struct WeightInfo<T>(PhantomData<T>);
5252
impl<T: frame_system::Config> pallet_bags_list::WeightInfo for WeightInfo<T> {
5353
/// Storage: `VoterList::Lock` (r:1 w:0)
5454
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
55+
/// Storage: `VoterList::PendingRebag` (r:1 w:0)
56+
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
5557
/// Storage: `VoterList::ListNodes` (r:4 w:4)
5658
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
5759
/// Storage: `Staking::Bonded` (r:1 w:0)
@@ -64,14 +66,16 @@ impl<T: frame_system::Config> pallet_bags_list::WeightInfo for WeightInfo<T> {
6466
// Proof Size summary in bytes:
6567
// Measured: `5012`
6668
// Estimated: `11506`
67-
// Minimum execution time: 149_768_000 picoseconds.
68-
Weight::from_parts(162_271_000, 0)
69+
// Minimum execution time: 154_457_000 picoseconds.
70+
Weight::from_parts(161_546_000, 0)
6971
.saturating_add(Weight::from_parts(0, 11506))
70-
.saturating_add(T::DbWeight::get().reads(8))
72+
.saturating_add(T::DbWeight::get().reads(9))
7173
.saturating_add(T::DbWeight::get().writes(5))
7274
}
7375
/// Storage: `VoterList::Lock` (r:1 w:0)
7476
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
77+
/// Storage: `VoterList::PendingRebag` (r:1 w:0)
78+
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
7579
/// Storage: `VoterList::ListNodes` (r:3 w:3)
7680
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
7781
/// Storage: `Staking::Bonded` (r:1 w:0)
@@ -84,10 +88,10 @@ impl<T: frame_system::Config> pallet_bags_list::WeightInfo for WeightInfo<T> {
8488
// Proof Size summary in bytes:
8589
// Measured: `4915`
8690
// Estimated: `8877`
87-
// Minimum execution time: 145_767_000 picoseconds.
88-
Weight::from_parts(157_284_000, 0)
91+
// Minimum execution time: 155_177_000 picoseconds.
92+
Weight::from_parts(159_976_000, 0)
8993
.saturating_add(Weight::from_parts(0, 8877))
90-
.saturating_add(T::DbWeight::get().reads(8))
94+
.saturating_add(T::DbWeight::get().reads(9))
9195
.saturating_add(T::DbWeight::get().writes(5))
9296
}
9397
/// Storage: `VoterList::Lock` (r:1 w:0)
@@ -106,34 +110,38 @@ impl<T: frame_system::Config> pallet_bags_list::WeightInfo for WeightInfo<T> {
106110
// Proof Size summary in bytes:
107111
// Measured: `7365`
108112
// Estimated: `11506`
109-
// Minimum execution time: 192_992_000 picoseconds.
110-
Weight::from_parts(200_226_000, 0)
113+
// Minimum execution time: 190_977_000 picoseconds.
114+
Weight::from_parts(206_642_000, 0)
111115
.saturating_add(Weight::from_parts(0, 11506))
112116
.saturating_add(T::DbWeight::get().reads(11))
113117
.saturating_add(T::DbWeight::get().writes(6))
114118
}
115-
/// Storage: `VoterList::CounterForListNodes` (r:1 w:0)
119+
/// Storage: `VoterList::CounterForListNodes` (r:1 w:1)
116120
/// Proof: `VoterList::CounterForListNodes` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
121+
/// Storage: `VoterList::CounterForPendingRebag` (r:1 w:1)
122+
/// Proof: `VoterList::CounterForPendingRebag` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
117123
/// Storage: `VoterList::Lock` (r:1 w:0)
118124
/// Proof: `VoterList::Lock` (`max_values`: Some(1), `max_size`: Some(0), added: 495, mode: `MaxEncodedLen`)
119125
/// Storage: `VoterList::NextNodeAutoRebagged` (r:1 w:1)
120126
/// Proof: `VoterList::NextNodeAutoRebagged` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
121-
/// Storage: `VoterList::ListBags` (r:200 w:4)
127+
/// Storage: `VoterList::PendingRebag` (r:12 w:3)
128+
/// Proof: `VoterList::PendingRebag` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
129+
/// Storage: `VoterList::ListBags` (r:199 w:3)
122130
/// Proof: `VoterList::ListBags` (`max_values`: None, `max_size`: Some(82), added: 2557, mode: `MaxEncodedLen`)
123-
/// Storage: `VoterList::ListNodes` (r:11 w:11)
131+
/// Storage: `VoterList::ListNodes` (r:11 w:10)
124132
/// Proof: `VoterList::ListNodes` (`max_values`: None, `max_size`: Some(154), added: 2629, mode: `MaxEncodedLen`)
125133
/// Storage: `Staking::Bonded` (r:10 w:0)
126134
/// Proof: `Staking::Bonded` (`max_values`: None, `max_size`: Some(72), added: 2547, mode: `MaxEncodedLen`)
127-
/// Storage: `Staking::Ledger` (r:10 w:0)
135+
/// Storage: `Staking::Ledger` (r:9 w:0)
128136
/// Proof: `Staking::Ledger` (`max_values`: None, `max_size`: Some(753), added: 3228, mode: `MaxEncodedLen`)
129137
fn on_idle() -> Weight {
130138
// Proof Size summary in bytes:
131-
// Measured: `24300`
132-
// Estimated: `512390`
133-
// Minimum execution time: 1_064_149_000 picoseconds.
134-
Weight::from_parts(1_090_878_000, 0)
135-
.saturating_add(Weight::from_parts(0, 512390))
136-
.saturating_add(T::DbWeight::get().reads(234))
137-
.saturating_add(T::DbWeight::get().writes(16))
139+
// Measured: `23853`
140+
// Estimated: `509833`
141+
// Minimum execution time: 1_173_907_000 picoseconds.
142+
Weight::from_parts(1_196_347_000, 0)
143+
.saturating_add(Weight::from_parts(0, 509833))
144+
.saturating_add(T::DbWeight::get().reads(245))
145+
.saturating_add(T::DbWeight::get().writes(19))
138146
}
139147
}

prdoc/pr_9725.prdoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
title: 'bags-list: implement pending rebag'
2+
doc:
3+
- audience: Runtime Dev
4+
description: |-
5+
Fix the issue where registering a nominator or validator while the bags list is locked would completely ignore the account, making it unreachable for on_idle rebagging.
6+
7+
Changes:
8+
- Add PendingRebag storage to queue accounts that fail insertion during lock.
9+
- Modify on_idle to process pending accounts with priority before regular accounts.
10+
- Add cleanup logic for non-staking pending accounts.
11+
crates:
12+
- name: pallet-bags-list
13+
bump: minor
14+
- name: asset-hub-westend-runtime
15+
bump: patch

substrate/frame/bags-list/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pallet-balances = { default-features = true, workspace = true }
4040
sp-core = { default-features = true, workspace = true }
4141
sp-io = { default-features = true, workspace = true }
4242
sp-tracing = { default-features = true, workspace = true }
43+
substrate-test-utils = { workspace = true }
4344

4445
[features]
4546
default = ["std"]

substrate/frame/bags-list/src/benchmarks.rs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,8 @@ benchmarks_instance_pallet! {
352352
// This benchmark generates weights for `on_idle` based on runtime configuration.
353353
// The main input is the runtime's `MaxAutoRebagPerBlock` type, which defines how many
354354
// nodes can be rebagged per block.
355-
// This benchmark simulates a more realistic and fragmented rebag scenario.
355+
// This benchmark simulates a scenario with both pending rebag processing
356+
// and fragmented rebag scenario.
356357

357358
List::<T, _>::unsafe_clear();
358359

@@ -362,11 +363,14 @@ benchmarks_instance_pallet! {
362363
let high = bag_thresh[2];
363364

364365
let rebag_budget = <T as Config<I>>::MaxAutoRebagPerBlock::get();
365-
let n = rebag_budget + 5;
366366

367-
// Insert nodes with varying scores
368-
for i in 0..n {
369-
let node: T::AccountId = account("node", i, 0);
367+
// Adjust counts to ensure exact budget usage
368+
let pending_count = rebag_budget / 3; // Smaller portion for pending
369+
let regular_count = rebag_budget + 5;
370+
371+
// Insert regular nodes with varying scores
372+
for i in 0..regular_count {
373+
let node: T::AccountId = account("regular_node", i, 0);
370374
let score = match i % 3 {
371375
0 => low - One::one(),
372376
1 => mid - One::one(),
@@ -376,14 +380,38 @@ benchmarks_instance_pallet! {
376380
}
377381

378382
// Corrupt some nodes to simulate edge cases
379-
for i in (0..n).step_by(4) {
380-
let node: T::AccountId = account("node", i, 0);
383+
for i in (0..regular_count).step_by(4) {
384+
let node: T::AccountId = account("regular_node", i, 0);
381385
let _ = List::<T, _>::remove(&node); // orphan nodes
382386
}
383387

388+
// Lock the list and simulate pending rebag insertions
389+
<Pallet<T, I>>::lock();
390+
391+
// Create pending rebag entries (mix of valid and corrupted)
392+
for i in 0..pending_count {
393+
let pending_node: T::AccountId = account("pending_node", i, 0);
394+
let pending_score = match i % 3 {
395+
0 => mid,
396+
1 => high,
397+
_ => high + high,
398+
};
399+
400+
// Set score first for most nodes, but skip some to simulate cleanup scenarios
401+
if i % 7 != 0 {
402+
T::ScoreProvider::set_score_of(&pending_node, pending_score);
403+
}
404+
405+
let _ = <Pallet<T, I> as SortedListProvider<T::AccountId>>::on_insert(
406+
pending_node, pending_score
407+
);
408+
}
409+
410+
<Pallet<T, I>>::unlock();
411+
384412
// Now set new scores that will move nodes into higher bags
385-
for i in 0..n {
386-
let node: T::AccountId = account("node", i, 0);
413+
for i in 0..regular_count {
414+
let node: T::AccountId = account("regular_node", i, 0);
387415
let new_score = match i % 3 {
388416
0 => mid,
389417
1 => high,
@@ -392,6 +420,12 @@ benchmarks_instance_pallet! {
392420
T::ScoreProvider::set_score_of(&node, new_score);
393421
}
394422

423+
assert_eq!(
424+
PendingRebag::<T, I>::count(),
425+
pending_count,
426+
"Expected exactly {} pending rebag entries",
427+
pending_count
428+
);
395429
// Ensure we have at least three bags populated before rebag
396430
assert!(List::<T, _>::get_bags().len() >= 2);
397431
}
@@ -400,6 +434,11 @@ benchmarks_instance_pallet! {
400434
<Pallet<T, I> as Hooks<_>>::on_idle(Default::default(), Weight::MAX);
401435
}
402436
verify {
437+
// Verify all pending rebag entries were processed.
438+
// This should always be true since pending_count = rebag_budget / 3 < rebag_budget,
439+
// and pending accounts are processed first so all pending entries fit within the budget.
440+
assert_eq!(PendingRebag::<T, I>::count(), 0, "All pending rebag entries should be processed");
441+
403442
// Count how many nodes ended up in higher bags
404443
let total_rebagged: usize = List::<T, _>::get_bags()
405444
.iter()
@@ -408,7 +447,7 @@ benchmarks_instance_pallet! {
408447
.sum();
409448

410449
let expected = <T as Config<I>>::MaxAutoRebagPerBlock::get() as usize;
411-
assert_eq!(total_rebagged, expected,"Expected exactly{:?} rebagged nodes, found {:?}", expected, total_rebagged);
450+
assert_eq!(total_rebagged, expected, "Expected exactly {:?} rebagged nodes, found {:?}", expected, total_rebagged);
412451
}
413452

414453
impl_benchmark_test_suite!(

0 commit comments

Comments
 (0)