Skip to content

Commit cbf661d

Browse files
committed
fix(predict): used bags and voterlist for fetching staking data
1 parent aaeb8f5 commit cbf661d

File tree

6 files changed

+486
-201
lines changed

6 files changed

+486
-201
lines changed

src/commands/predict.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use polkadot_sdk::pallet_election_provider_multi_block::unsigned::miner::MinerCo
33

44
use crate::{
55
client::Client,
6-
commands::types::{CustomElectionFile, ElectionDataSource, PredictConfig},
6+
commands::types::{
7+
CustomElectionFile, ElectionDataSource, NominatorData, PredictConfig, ValidatorData,
8+
},
79
dynamic::{
810
election_data::{
911
PredictionContext, build_predictions_from_solution, convert_staking_data_to_snapshots,
@@ -84,7 +86,7 @@ where
8486

8587
log::info!(
8688
target: LOG_TARGET,
87-
"Mining solution with desired_targets={}, candidates={}, nominators={}",
89+
"Mining solution with desired_targets={}, candidates={}, voter pages={}",
8890
desired_targets,
8991
targets.len(),
9092
voters.len()
@@ -156,7 +158,7 @@ where
156158

157159
async fn load_custom_file(
158160
custom_file_path: &str,
159-
) -> Result<(Vec<(AccountId, u128)>, Vec<(AccountId, u64, Vec<AccountId>)>), Error> {
161+
) -> Result<(Vec<ValidatorData>, Vec<NominatorData>), Error> {
160162
use std::path::PathBuf;
161163

162164
// Resolve relative path → absolute

src/commands/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use polkadot_sdk::{sp_npos_elections::ElectionScore, sp_runtime::Perbill};
22
use serde::{Deserialize, Serialize};
33

4+
use crate::prelude::AccountId;
5+
46
/// Submission strategy to use.
57
#[derive(Debug, Copy, Clone)]
68
#[cfg_attr(test, derive(PartialEq))]
@@ -159,6 +161,9 @@ pub(crate) struct ValidatorStakeAllocation {
159161
pub(crate) allocated_stake: String, // Token amount as string
160162
}
161163

164+
pub(crate) type NominatorData = (AccountId, u64, Vec<AccountId>);
165+
pub(crate) type ValidatorData = (AccountId, u128);
166+
162167
// ============================================================================
163168
// Custom File Format Types
164169
// ============================================================================

src/dynamic/election_data.rs

Lines changed: 99 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Helpers for fetching and shaping election data shared by CLI commands
22
33
use polkadot_sdk::{
4-
frame_election_provider_support::BoundedSupports,
4+
frame_election_provider_support::{BoundedSupports, Get},
55
frame_support::BoundedVec,
66
pallet_election_provider_multi_block::{
77
PagedRawSolution,
@@ -22,8 +22,9 @@ use crate::{
2222
commands::{
2323
multi_block::types::{TargetSnapshotPageOf, Voter, VoterSnapshotPageOf},
2424
types::{
25-
ElectionDataSource, NominatorPrediction, NominatorsPrediction, PredictionMetadata,
26-
ValidatorInfo, ValidatorStakeAllocation, ValidatorsPrediction,
25+
ElectionDataSource, NominatorData, NominatorPrediction, NominatorsPrediction,
26+
PredictionMetadata, ValidatorData, ValidatorInfo, ValidatorStakeAllocation,
27+
ValidatorsPrediction,
2728
},
2829
},
2930
dynamic::staking::{fetch_candidates, fetch_nominators},
@@ -46,67 +47,82 @@ pub struct PredictionContext<'a> {
4647
pub data_source: ElectionDataSource,
4748
}
4849

49-
/// Merge on-chain nominators with validator self votes to ensure every candidate
50-
/// backs itself when generating synthetic election snapshots.
50+
/// Inject validator self-votes to ensure every candidate backs itself when generating
51+
/// synthetic election snapshots.
52+
///
53+
/// - If an account is a nominator, use their nominator data BUT ensure they vote for themselves
54+
/// - If an account is a validator but NOT a nominator, inject them as voting for themselves
55+
///
56+
/// We do NOT merge stakes - we respect the nominator data if it exists.
5157
pub(crate) fn inject_self_votes(
52-
candidates: &[(AccountId, u128)],
53-
nominators: Vec<(AccountId, u64, Vec<AccountId>)>,
54-
) -> Vec<(AccountId, u64, Vec<AccountId>)> {
55-
let mut nominator_map: HashMap<AccountId, (u64, Vec<AccountId>)> =
56-
HashMap::with_capacity(nominators.len());
57-
58-
for (account, stake, targets) in nominators {
59-
nominator_map
60-
.entry(account)
61-
.and_modify(|(existing_stake, existing_targets)| {
62-
if stake > *existing_stake {
63-
*existing_stake = stake;
64-
}
65-
existing_targets.extend(targets.clone());
66-
})
67-
.or_insert((stake, targets));
68-
}
69-
70-
let mut combined: Vec<(AccountId, u64, Vec<AccountId>)> =
71-
Vec::with_capacity(nominator_map.len() + candidates.len());
72-
73-
let mut injected = 0usize;
74-
for (account, stake) in candidates {
75-
let (stake_u64, truncated) =
76-
if *stake > u64::MAX as u128 { (u64::MAX, true) } else { (*stake as u64, false) };
77-
78-
if truncated {
79-
log::warn!(
80-
target: LOG_TARGET,
81-
"Candidate {:?} stake {} exceeds u64::MAX; truncating to {} for snapshot conversion",
82-
account,
83-
stake,
58+
candidates: &[ValidatorData],
59+
nominators: Vec<NominatorData>,
60+
) -> Vec<NominatorData> {
61+
// Build a map of validator accounts to their stakes for lookup
62+
let validator_stakes: HashMap<AccountId, u64> = candidates
63+
.iter()
64+
.map(|(account, stake)| {
65+
let stake_u64 = if *stake > u64::MAX as u128 {
66+
log::warn!(
67+
target: LOG_TARGET,
68+
"Validator {:?} stake {} exceeds u64::MAX; truncating to {}",
69+
account,
70+
stake,
71+
u64::MAX
72+
);
8473
u64::MAX
85-
);
86-
}
87-
88-
if let Some((existing_stake, mut targets)) = nominator_map.remove(account) {
89-
if !targets.iter().any(|t| t == account) {
74+
} else {
75+
*stake as u64
76+
};
77+
(account.clone(), stake_u64)
78+
})
79+
.collect();
80+
81+
// Build a set of all validator accounts for fast lookup
82+
let validator_accounts: HashSet<AccountId> =
83+
candidates.iter().map(|(account, _)| account.clone()).collect();
84+
85+
let mut combined: Vec<NominatorData> = Vec::with_capacity(nominators.len() + candidates.len());
86+
87+
let mut self_votes_added = 0usize;
88+
89+
// Process all nominators, ensuring validators vote for themselves
90+
for (account, stake, mut targets) in nominators {
91+
// If this nominator is also a validator, ensure they vote for themselves
92+
if validator_accounts.contains(&account) {
93+
// Add self-target if not already present
94+
if !targets.iter().any(|t| t == &account) {
9095
targets.push(account.clone());
96+
self_votes_added += 1;
9197
}
92-
let merged_stake = existing_stake.max(stake_u64);
93-
combined.push((account.clone(), merged_stake, targets));
94-
} else {
95-
combined.push((account.clone(), stake_u64, vec![account.clone()]));
96-
injected += 1;
9798
}
99+
combined.push((account, stake, targets));
98100
}
99101

100-
let remaining = nominator_map.len();
101-
combined.extend(
102-
nominator_map
103-
.into_iter()
104-
.map(|(account, (stake, targets))| (account, stake, targets)),
105-
);
102+
// Then, for validators that are NOT nominators, inject them as self-voters
103+
let mut injected = 0usize;
104+
for (account, _stake) in candidates {
105+
// Skip if this validator is already a nominator (we already processed them above)
106+
if combined.iter().any(|(acc, _, _)| acc == account) {
107+
continue;
108+
}
109+
110+
// Get the validator stake (already converted to u64 in validator_stakes map)
111+
let stake_u64 = validator_stakes
112+
.get(account)
113+
.copied()
114+
.expect("Validator stake should exist in map");
106115

116+
// Add validator as a self-voter (voting only for themselves)
117+
combined.push((account.clone(), stake_u64, vec![account.clone()]));
118+
injected += 1;
119+
}
107120
log::info!(
108121
target: LOG_TARGET,
109-
"Prepared nominators for snapshot: {injected} injected self votes, {remaining} existing nominators appended"
122+
"Ensured {} validator self-votes in nominators, injected {} new validator self-voters (total: {} voters)",
123+
self_votes_added,
124+
injected,
125+
combined.len()
110126
);
111127

112128
combined
@@ -118,8 +134,8 @@ pub(crate) fn inject_self_votes(
118134
/// dropped). The miner will use all pages when solving; the solution is then trimmed per the
119135
/// chain's limits.
120136
pub(crate) fn convert_staking_data_to_snapshots<T>(
121-
candidates: Vec<(AccountId, u128)>,
122-
nominators: Vec<(AccountId, u64, Vec<AccountId>)>,
137+
candidates: Vec<ValidatorData>,
138+
nominators: Vec<NominatorData>,
123139
) -> Result<(TargetSnapshotPageOf<T>, Vec<VoterSnapshotPageOf<T>>), Error>
124140
where
125141
T: MinerConfig<AccountId = AccountId>,
@@ -263,6 +279,24 @@ where
263279
let active_set: HashSet<AccountId> =
264280
winners_sorted.iter().map(|(validator, _)| validator.clone()).collect();
265281

282+
// Flatten voters from paged snapshot for nominator perspective.
283+
let all_voters: Vec<Voter<T>> =
284+
voter_snapshot_paged.iter().flat_map(|page| page.iter().cloned()).collect();
285+
286+
// Identify validators who only have self-votes
287+
let validators_with_only_self_vote: HashSet<AccountId> = all_voters
288+
.iter()
289+
.filter(|(nominator, _, targets)| {
290+
// validator has only self-vote if:
291+
// 1. They are a validator (in active_set)
292+
// 2. Their only target is themselves
293+
// NOTE: Reverted to your original logic as requested, assuming you want strictly this
294+
// behavior.
295+
active_set.contains(nominator) || (targets.len() == 1 && targets[0] == *nominator)
296+
})
297+
.map(|(nominator, _, _)| nominator.clone())
298+
.collect();
299+
266300
let mut validator_infos: Vec<ValidatorInfo> = Vec::new();
267301
for (validator, support) in winners_sorted.iter() {
268302
let self_stake = support
@@ -302,13 +336,15 @@ where
302336

303337
let validators_prediction = ValidatorsPrediction { metadata, results: validator_infos };
304338

305-
// Flatten voters from paged snapshot for nominator perspective.
306-
let all_voters: Vec<Voter<T>> =
307-
voter_snapshot_paged.iter().flat_map(|page| page.iter().cloned()).collect();
308-
339+
// Build nominator predictions, excluding validators who only have self-votes
309340
let mut nominator_predictions: Vec<NominatorPrediction> = Vec::new();
310341

311342
for (nominator, stake, nominated_targets) in all_voters {
343+
// Skip validators who only have self-votes
344+
if validators_with_only_self_vote.contains(&nominator) {
345+
continue;
346+
}
347+
312348
let nominator_encoded = encode_account_id(&nominator, ctx.ss58_prefix);
313349
let allocations = allocation_map.get(&nominator);
314350

@@ -383,7 +419,9 @@ where
383419
.await
384420
.map_err(|e| Error::Other(format!("Failed to fetch candidates: {e}")))?;
385421

386-
let nominators = fetch_nominators(client)
422+
let voter_limit = (T::Pages::get() * T::VoterSnapshotPerBlock::get()) as usize;
423+
424+
let nominators = fetch_nominators(client, voter_limit)
387425
.await
388426
.map_err(|e| Error::Other(format!("Failed to fetch nominators: {e}")))?;
389427

src/dynamic/multi_block.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ where
249249
T::MaxVotesPerVoter: Send,
250250
{
251251
// Validate n_pages
252-
if n_pages != static_types::multi_block::Pages::get() {
253-
return Err(Error::Other("n_pages must be > 0".into()));
252+
let chain_pages = static_types::multi_block::Pages::get();
253+
if n_pages != chain_pages {
254+
return Err(Error::Other(format!("n_pages must be equal to {chain_pages}")));
254255
}
255256

256257
// Fetch the (single) target snapshot. Use the last page index
@@ -264,7 +265,7 @@ where
264265
Vec::with_capacity(n_pages as usize);
265266
for page in 0..n_pages {
266267
let voter_page = paged_voter_snapshot::<T>(page, round, storage).await?;
267-
log::info!(target: LOG_TARGET, "Fetched {page}/{n_pages} pages from snapshot");
268+
log::info!(target: LOG_TARGET, "Fetched {page}/{n_pages} pages of voter snapshot");
268269
voter_snapshot_paged.push(voter_page);
269270
}
270271

src/dynamic/pallet_api.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,19 @@ pub mod staking {
158158

159159
pub mod storage {
160160
use super::{super::*, *};
161-
pub const _ACTIVE_ERA: PalletItem = PalletItem::new(NAME, "ActiveEra");
162-
pub const _VALIDATOR_COUNT: PalletItem = PalletItem::new(NAME, "ValidatorCount");
163161
pub const VALIDATORS: PalletItem = PalletItem::new(NAME, "Validators");
164162
pub const LEDGER: PalletItem = PalletItem::new(NAME, "Ledger");
165163
pub const NOMINATORS: PalletItem = PalletItem::new(NAME, "Nominators");
166164
}
167165
}
166+
167+
pub mod voter_list {
168+
pub const NAME: &str = "VoterList";
169+
170+
pub mod storage {
171+
use super::{super::*, *};
172+
pub const LIST_NODES: PalletItem = PalletItem::new(NAME, "ListNodes");
173+
pub const LIST_BAGS: PalletItem = PalletItem::new(NAME, "ListBags");
174+
pub const COUNTER_FOR_LIST_NODES: PalletItem = PalletItem::new(NAME, "CounterForListNodes");
175+
}
176+
}

0 commit comments

Comments
 (0)