@@ -8,7 +8,7 @@ use crate::{
88} ;
99use codec:: { Decode , Encode } ;
1010use scale_value:: At ;
11- use std:: time:: Duration ;
11+ use std:: { collections :: HashSet , time:: Duration } ;
1212use subxt:: dynamic:: Value ;
1313
1414/// Fetch all candidate validators (stash AccountId) with their active stake
@@ -238,9 +238,15 @@ pub(crate) async fn fetch_stakes_batch_static(
238238 Ok ( stakes)
239239}
240240
241- /// Fetch all nominators (stash, stake, targets) from Staking::Nominators
241+ /// Fetch all nominators (stash, stake, targets) from `Staking::Nominators`.
242+ ///
243+ /// `candidate_accounts` is the set of validator candidates for the election. We
244+ /// use this to filter out "pure self" validator nominators (validators whose
245+ /// only nomination target is themselves), since those will be injected as
246+ /// self-votes separately when building the synthetic snapshots.
242247pub ( crate ) async fn fetch_nominators (
243248 client : & Client ,
249+ candidate_accounts : & [ AccountId ] ,
244250) -> Result < Vec < ( AccountId , u64 , Vec < AccountId > ) > , Error > {
245251 /// Nominations data structure from the blockchain
246252 #[ derive( Debug , Clone , Decode ) ]
@@ -289,6 +295,11 @@ pub(crate) async fn fetch_nominators(
289295 log:: info!( target: LOG_TARGET , "Processed {count} nominators..." ) ;
290296 log:: info!( target: LOG_TARGET , "Fetching stakes of nominators" ) ;
291297
298+ // Build a fast lookup set of validator candidates to detect validators that
299+ // also appear as nominators.
300+ let candidate_set: HashSet < AccountId > =
301+ candidate_accounts. iter ( ) . cloned ( ) . collect ( ) ;
302+
292303 let nominator_stashes: Vec < AccountId > = nominators. iter ( ) . map ( |e| e. 0 . clone ( ) ) . collect ( ) ;
293304 let nominator_stakes_u128 = fetch_stakes_in_batches ( client, & nominator_stashes, true ) . await ?;
294305
@@ -297,7 +308,16 @@ pub(crate) async fn fetch_nominators(
297308 let mut filtered_count = 0usize ;
298309 for ( ( stash, targets) , stake_u128) in nominators. into_iter ( ) . zip ( nominator_stakes_u128) {
299310 // Filter out nominators with zero stake or empty targets (matching snapshot behavior)
300- if stake_u128 == 0 || targets. is_empty ( ) {
311+ // and validators that only self-nominate (pure self-nominators). The latter would
312+ // otherwise show up as duplicate "validator-as-nominator" entries in the exported
313+ // nominators JSON, even though they are handled as self-votes in the snapshots.
314+ let is_candidate = candidate_set. contains ( & stash) ;
315+ let is_pure_self_nominator =
316+ is_candidate &&
317+ !targets. is_empty ( ) &&
318+ targets. iter ( ) . all ( |t| t == & stash) ;
319+
320+ if stake_u128 == 0 || targets. is_empty ( ) || is_pure_self_nominator {
301321 filtered_count += 1 ;
302322 continue ;
303323 }
0 commit comments