Skip to main content

ic_query/nns/neuron/report/distribution/
mod.rs

1//! Module: nns::neuron::report::distribution
2//!
3//! Responsibility: validate and aggregate complete caller-retained public NNS neuron collections.
4//! Does not own: collection transport, persistence, cache policy, or process output.
5//! Boundary: projects complete public `NeuronInfo` evidence into portable distributions.
6
7use super::{
8    NnsNeuronCollectionState, collection::validate_collection_state, model::NnsNeuronRow,
9    source::validate_neuron_rows,
10};
11use crate::{
12    nns::{
13        MAINNET_GOVERNANCE_CANISTER_ID,
14        governance::{
15            NnsGovernanceCollectionStatus, NnsGovernanceSourceProvenance,
16            validate_governance_report_source, validate_governance_time_interval,
17        },
18    },
19    subnet_catalog::MAINNET_NETWORK,
20};
21use serde::{Deserialize, Serialize};
22use std::collections::BTreeMap;
23use thiserror::Error as ThisError;
24
25/// Version of the portable NNS public-neuron distribution report schema.
26pub const NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION: u32 = 1;
27
28///
29/// NnsNeuronStateDistribution
30///
31/// Public neuron count and effective stake for one raw Governance state code.
32///
33
34#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
35#[serde(deny_unknown_fields)]
36pub struct NnsNeuronStateDistribution {
37    /// Raw native Governance state code.
38    pub state: i32,
39    /// Number of collected neurons with this state code.
40    pub neuron_count: u64,
41    /// Sum of public effective stake for neurons with this state code.
42    pub effective_stake_e8s: u64,
43}
44
45///
46/// NnsNeuronVisibilityDistribution
47///
48/// Public neuron count and effective stake for one optional Governance visibility code.
49///
50
51#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
52#[serde(deny_unknown_fields)]
53pub struct NnsNeuronVisibilityDistribution {
54    /// Raw optional native Governance visibility code.
55    pub visibility: Option<i32>,
56    /// Number of collected neurons with this visibility code or absence.
57    pub neuron_count: u64,
58    /// Sum of public effective stake for neurons with this visibility code or absence.
59    pub effective_stake_e8s: u64,
60}
61
62///
63/// NnsNeuronTypeDistribution
64///
65/// Public neuron count and effective stake for one optional Governance neuron-type code.
66///
67
68#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
69#[serde(deny_unknown_fields)]
70pub struct NnsNeuronTypeDistribution {
71    /// Raw optional native Governance neuron-type code.
72    pub neuron_type: Option<i32>,
73    /// Number of collected neurons with this type code or absence.
74    pub neuron_count: u64,
75    /// Sum of public effective stake for neurons with this type code or absence.
76    pub effective_stake_e8s: u64,
77}
78
79///
80/// NnsNeuronDistributionReport
81///
82/// Deterministic local distribution over one complete public NNS neuron collection.
83///
84
85#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
86pub struct NnsNeuronDistributionReport {
87    /// Report schema version.
88    pub schema_version: u32,
89    /// Network identity retained by the complete collection.
90    pub network: String,
91    /// Fixed NNS Governance canister principal retained by the collection.
92    pub governance_canister_id: String,
93    /// Concrete transport and collector provenance retained by the collection.
94    pub source: NnsGovernanceSourceProvenance,
95    /// Caller-supplied time attached to collection creation.
96    pub collection_started_at: String,
97    /// Caller-supplied time attached to the final admitted page.
98    pub collection_updated_at: String,
99    /// Number of admitted source pages in the complete collection.
100    pub collection_page_count: u32,
101    /// Number of admitted public neuron rows in the complete collection.
102    pub collected_neuron_count: u64,
103    /// Whether the sequential collection is guaranteed to represent one point in time.
104    pub point_in_time_guaranteed: bool,
105    /// Earliest raw Governance retrieval timestamp among collected rows.
106    pub earliest_retrieved_at_timestamp_seconds: Option<u64>,
107    /// Latest raw Governance retrieval timestamp among collected rows.
108    pub latest_retrieved_at_timestamp_seconds: Option<u64>,
109    /// Sum of public effective stake across every collected neuron.
110    pub total_effective_stake_e8s: u64,
111    /// Rows carrying a public staked-maturity value.
112    pub reported_staked_maturity_neuron_count: u64,
113    /// Rows without a public staked-maturity value.
114    pub unreported_staked_maturity_neuron_count: u64,
115    /// Sum of reported staked maturity values only.
116    pub total_reported_staked_maturity_e8s_equivalent: u64,
117    /// Rows carrying current deciding voting power.
118    pub reported_deciding_voting_power_neuron_count: u64,
119    /// Rows without current deciding voting power.
120    pub unreported_deciding_voting_power_neuron_count: u64,
121    /// Sum of reported current deciding voting power values only.
122    pub total_reported_deciding_voting_power: u64,
123    /// Rows carrying current potential voting power.
124    pub reported_potential_voting_power_neuron_count: u64,
125    /// Rows without current potential voting power.
126    pub unreported_potential_voting_power_neuron_count: u64,
127    /// Sum of reported current potential voting power values only.
128    pub total_reported_potential_voting_power: u64,
129    /// Rows carrying registered known-neuron metadata.
130    pub known_neuron_metadata_count: u64,
131    /// Rows carrying a public Neurons' Fund join timestamp.
132    pub neurons_fund_join_timestamp_present_count: u64,
133    /// Canonically raw-code-ordered distribution by neuron state.
134    pub state_distribution: Vec<NnsNeuronStateDistribution>,
135    /// Canonically optional-raw-code-ordered distribution by visibility.
136    pub visibility_distribution: Vec<NnsNeuronVisibilityDistribution>,
137    /// Canonically optional-raw-code-ordered distribution by neuron type.
138    pub neuron_type_distribution: Vec<NnsNeuronTypeDistribution>,
139}
140
141///
142/// NnsNeuronDistributionValidationError
143///
144/// Pure validation failure for an untrusted serialized or in-memory distribution report.
145///
146
147#[derive(Debug, Eq, PartialEq, ThisError)]
148#[error("invalid NNS neuron distribution report: {reason}")]
149pub struct NnsNeuronDistributionValidationError {
150    /// Deterministic invariant failure.
151    pub reason: String,
152}
153
154///
155/// NnsNeuronDistributionError
156///
157/// Deterministic validation or accounting failure from local neuron distribution projection.
158///
159
160#[derive(Debug, ThisError)]
161pub enum NnsNeuronDistributionError {
162    /// The supplied collection state failed its shared continuation invariants.
163    #[error("invalid NNS neuron collection state for distribution projection: {reason}")]
164    InvalidCollectionState {
165        /// Deterministic collection invariant failure.
166        reason: String,
167    },
168
169    /// The collection stopped without observing Governance API exhaustion.
170    #[error("NNS neuron distribution requires a complete collection; state is {status}")]
171    CollectionNotComplete {
172        /// Current lifecycle of the otherwise valid collection state.
173        status: NnsGovernanceCollectionStatus,
174    },
175
176    /// The supplied rows do not match the collection's admitted-row accounting.
177    #[error(
178        "NNS neuron distribution received {actual} rows; complete collection accounts for {expected}"
179    )]
180    NeuronCountMismatch {
181        /// Neuron rows accounted for by the collection state.
182        expected: u64,
183        /// Neuron rows supplied to the builder.
184        actual: u64,
185    },
186
187    /// Supplied rows failed the shared public-neuron response contract.
188    #[error("invalid NNS neuron rows for distribution projection: {reason}")]
189    InvalidNeuronRows {
190        /// Deterministic row invariant failure.
191        reason: String,
192    },
193
194    /// A count conversion, increment, or numeric sum exceeded `u64`.
195    #[error("NNS neuron distribution accounting overflow while updating {field}")]
196    AccountingOverflow {
197        /// Count or sum that exceeded its representation.
198        field: &'static str,
199    },
200}
201
202/// Build one deterministic distribution from a complete caller-retained public-neuron collection.
203pub fn build_nns_neuron_distribution_report(
204    collection: &NnsNeuronCollectionState,
205    neurons: &[NnsNeuronRow],
206) -> Result<NnsNeuronDistributionReport, NnsNeuronDistributionError> {
207    validate_collection_state(collection).map_err(|error| {
208        NnsNeuronDistributionError::InvalidCollectionState {
209            reason: error.to_string(),
210        }
211    })?;
212    if !collection.is_complete() {
213        return Err(NnsNeuronDistributionError::CollectionNotComplete {
214            status: collection.status(),
215        });
216    }
217    let source = collection.source().cloned().ok_or_else(|| {
218        NnsNeuronDistributionError::InvalidCollectionState {
219            reason: "complete collection has no concrete source provenance".to_string(),
220        }
221    })?;
222
223    let expected = collection.neurons_fetched();
224    let actual = u64::try_from(neurons.len()).map_err(|_| {
225        NnsNeuronDistributionError::AccountingOverflow {
226            field: "supplied_neuron_count",
227        }
228    })?;
229    if actual != expected {
230        return Err(NnsNeuronDistributionError::NeuronCountMismatch { expected, actual });
231    }
232    validate_neuron_rows(neurons).map_err(|error| {
233        NnsNeuronDistributionError::InvalidNeuronRows {
234            reason: error.to_string(),
235        }
236    })?;
237
238    let mut distribution = DistributionAccumulator::default();
239    for neuron in neurons {
240        distribution.observe(neuron)?;
241    }
242    Ok(distribution.into_report(collection, expected, source))
243}
244
245/// Validate every distribution-report invariant available without source rows or live host calls.
246pub fn validate_nns_neuron_distribution_report(
247    report: &NnsNeuronDistributionReport,
248) -> Result<(), NnsNeuronDistributionValidationError> {
249    validate_distribution_header(report)?;
250    validate_distribution_summary(report)?;
251    validate_distribution(
252        report,
253        "state",
254        "raw-code",
255        report
256            .state_distribution
257            .iter()
258            .map(|row| (row.state, row.neuron_count, row.effective_stake_e8s)),
259    )?;
260    validate_distribution(
261        report,
262        "visibility",
263        "optional-raw-code",
264        report
265            .visibility_distribution
266            .iter()
267            .map(|row| (row.visibility, row.neuron_count, row.effective_stake_e8s)),
268    )?;
269    validate_distribution(
270        report,
271        "neuron-type",
272        "optional-raw-code",
273        report
274            .neuron_type_distribution
275            .iter()
276            .map(|row| (row.neuron_type, row.neuron_count, row.effective_stake_e8s)),
277    )
278}
279
280fn validate_distribution_header(
281    report: &NnsNeuronDistributionReport,
282) -> Result<(), NnsNeuronDistributionValidationError> {
283    if report.schema_version != NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION {
284        return Err(invalid_validation(format!(
285            "schema version {} does not equal {}",
286            report.schema_version, NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION
287        )));
288    }
289    if report.network != MAINNET_NETWORK {
290        return Err(invalid_validation(format!(
291            "network is {}, expected {MAINNET_NETWORK}",
292            report.network
293        )));
294    }
295    if report.governance_canister_id != MAINNET_GOVERNANCE_CANISTER_ID {
296        return Err(invalid_validation(format!(
297            "governance_canister_id is {}, expected {MAINNET_GOVERNANCE_CANISTER_ID}",
298            report.governance_canister_id
299        )));
300    }
301    if report.collection_page_count == 0 {
302        return Err(invalid_validation(
303            "complete distribution report must retain at least one collection page",
304        ));
305    }
306    let minimum_neuron_count = u64::from(report.collection_page_count - 1);
307    if report.collected_neuron_count < minimum_neuron_count {
308        return Err(invalid_validation(format!(
309            "collection_page_count {} requires at least {minimum_neuron_count} collected neurons, found {}",
310            report.collection_page_count, report.collected_neuron_count
311        )));
312    }
313    validate_governance_time_interval(
314        "collection_started_at",
315        &report.collection_started_at,
316        "collection_updated_at",
317        &report.collection_updated_at,
318    )
319    .map_err(|error| invalid_validation(error.to_string()))?;
320    if report.point_in_time_guaranteed {
321        return Err(invalid_validation(
322            "sequential public-neuron collection cannot claim a point-in-time snapshot",
323        ));
324    }
325    validate_governance_report_source(&report.network, &report.source).map_err(|error| {
326        let context = match &report.source {
327            NnsGovernanceSourceProvenance::ReplicaQuery { .. } => "source",
328            NnsGovernanceSourceProvenance::ReplicatedInterCanisterCall { .. } => "provenance",
329        };
330        invalid_validation(format!("invalid collection {context}: {error}"))
331    })?;
332    validate_retrieval_range(report)
333}
334
335fn validate_retrieval_range(
336    report: &NnsNeuronDistributionReport,
337) -> Result<(), NnsNeuronDistributionValidationError> {
338    match (
339        report.earliest_retrieved_at_timestamp_seconds,
340        report.latest_retrieved_at_timestamp_seconds,
341    ) {
342        (None, None) if report.collected_neuron_count == 0 => Ok(()),
343        (Some(earliest), Some(latest))
344            if report.collected_neuron_count > 0 && earliest <= latest =>
345        {
346            Ok(())
347        }
348        _ => Err(invalid_validation(
349            "retrieval timestamp range disagrees with collected_neuron_count or is reversed",
350        )),
351    }
352}
353
354fn validate_distribution_summary(
355    report: &NnsNeuronDistributionReport,
356) -> Result<(), NnsNeuronDistributionValidationError> {
357    validate_optional_summary(
358        report.reported_staked_maturity_neuron_count,
359        report.unreported_staked_maturity_neuron_count,
360        report.total_reported_staked_maturity_e8s_equivalent,
361        report.collected_neuron_count,
362        "staked maturity",
363    )?;
364    validate_optional_summary(
365        report.reported_deciding_voting_power_neuron_count,
366        report.unreported_deciding_voting_power_neuron_count,
367        report.total_reported_deciding_voting_power,
368        report.collected_neuron_count,
369        "deciding voting power",
370    )?;
371    validate_optional_summary(
372        report.reported_potential_voting_power_neuron_count,
373        report.unreported_potential_voting_power_neuron_count,
374        report.total_reported_potential_voting_power,
375        report.collected_neuron_count,
376        "potential voting power",
377    )?;
378    for (field, count) in [
379        (
380            "known_neuron_metadata_count",
381            report.known_neuron_metadata_count,
382        ),
383        (
384            "neurons_fund_join_timestamp_present_count",
385            report.neurons_fund_join_timestamp_present_count,
386        ),
387    ] {
388        if count > report.collected_neuron_count {
389            return Err(invalid_validation(format!(
390                "{field} {count} exceeds collected_neuron_count {}",
391                report.collected_neuron_count
392            )));
393        }
394    }
395    Ok(())
396}
397
398fn validate_optional_summary(
399    reported: u64,
400    unreported: u64,
401    total: u64,
402    collected: u64,
403    field: &'static str,
404) -> Result<(), NnsNeuronDistributionValidationError> {
405    let accounted = reported
406        .checked_add(unreported)
407        .ok_or_else(|| invalid_validation(format!("{field} coverage count overflow")))?;
408    if accounted != collected {
409        return Err(invalid_validation(format!(
410            "{field} coverage accounts for {accounted} neurons, expected {collected}"
411        )));
412    }
413    if reported == 0 && total != 0 {
414        return Err(invalid_validation(format!(
415            "{field} total must be zero when no rows report the field"
416        )));
417    }
418    Ok(())
419}
420
421fn validate_distribution<Key: Copy + Ord + std::fmt::Debug>(
422    report: &NnsNeuronDistributionReport,
423    dimension: &'static str,
424    order: &'static str,
425    rows: impl Iterator<Item = (Key, u64, u64)>,
426) -> Result<(), NnsNeuronDistributionValidationError> {
427    let mut previous = None;
428    let mut neuron_count = 0_u64;
429    let mut stake = 0_u64;
430    for (key, count, effective_stake) in rows {
431        if previous.is_some_and(|previous| previous >= key) {
432            return Err(invalid_validation(format!(
433                "{dimension} distribution is not strictly {order} ordered"
434            )));
435        }
436        neuron_count = add_distribution_count(neuron_count, count, dimension)?;
437        stake = stake
438            .checked_add(effective_stake)
439            .ok_or_else(|| invalid_validation(format!("{dimension} stake total overflow")))?;
440        previous = Some(key);
441    }
442    validate_distribution_totals(report, neuron_count, stake, dimension)
443}
444
445fn add_distribution_count(
446    total: u64,
447    count: u64,
448    dimension: &'static str,
449) -> Result<u64, NnsNeuronDistributionValidationError> {
450    if count == 0 {
451        return Err(invalid_validation(format!(
452            "{dimension} distribution row must contain at least one neuron"
453        )));
454    }
455    add_validation_total(total, count, dimension)
456}
457
458fn add_validation_total(
459    total: u64,
460    value: u64,
461    field: &'static str,
462) -> Result<u64, NnsNeuronDistributionValidationError> {
463    total
464        .checked_add(value)
465        .ok_or_else(|| invalid_validation(format!("{field} total overflow")))
466}
467
468fn validate_distribution_totals(
469    report: &NnsNeuronDistributionReport,
470    neuron_count: u64,
471    stake: u64,
472    dimension: &'static str,
473) -> Result<(), NnsNeuronDistributionValidationError> {
474    if neuron_count != report.collected_neuron_count {
475        return Err(invalid_validation(format!(
476            "{dimension} neuron counts sum to {neuron_count}, expected {}",
477            report.collected_neuron_count
478        )));
479    }
480    if stake != report.total_effective_stake_e8s {
481        return Err(invalid_validation(format!(
482            "{dimension} effective stake sums to {stake}, expected {}",
483            report.total_effective_stake_e8s
484        )));
485    }
486    Ok(())
487}
488
489fn invalid_validation(reason: impl Into<String>) -> NnsNeuronDistributionValidationError {
490    NnsNeuronDistributionValidationError {
491        reason: reason.into(),
492    }
493}
494
495#[derive(Clone, Copy, Default)]
496struct DimensionAccumulator {
497    neuron_count: u64,
498    effective_stake_e8s: u64,
499}
500
501impl DimensionAccumulator {
502    fn observe(
503        &mut self,
504        effective_stake_e8s: u64,
505        count_field: &'static str,
506        stake_field: &'static str,
507    ) -> Result<(), NnsNeuronDistributionError> {
508        increment(&mut self.neuron_count, count_field)?;
509        add(
510            &mut self.effective_stake_e8s,
511            effective_stake_e8s,
512            stake_field,
513        )
514    }
515}
516
517#[derive(Default)]
518struct OptionalValueAccumulator {
519    reported_neuron_count: u64,
520    unreported_neuron_count: u64,
521    total_reported_value: u64,
522}
523
524impl OptionalValueAccumulator {
525    fn observe(
526        &mut self,
527        value: Option<u64>,
528        reported_field: &'static str,
529        unreported_field: &'static str,
530        total_field: &'static str,
531    ) -> Result<(), NnsNeuronDistributionError> {
532        if let Some(value) = value {
533            increment(&mut self.reported_neuron_count, reported_field)?;
534            add(&mut self.total_reported_value, value, total_field)
535        } else {
536            increment(&mut self.unreported_neuron_count, unreported_field)
537        }
538    }
539}
540
541#[derive(Default)]
542struct DistributionAccumulator {
543    states: BTreeMap<i32, DimensionAccumulator>,
544    visibilities: BTreeMap<Option<i32>, DimensionAccumulator>,
545    neuron_types: BTreeMap<Option<i32>, DimensionAccumulator>,
546    total_effective_stake_e8s: u64,
547    staked_maturity: OptionalValueAccumulator,
548    deciding_voting_power: OptionalValueAccumulator,
549    potential_voting_power: OptionalValueAccumulator,
550    known_neuron_metadata_count: u64,
551    neurons_fund_join_timestamp_present_count: u64,
552    earliest_retrieved_at_timestamp_seconds: Option<u64>,
553    latest_retrieved_at_timestamp_seconds: Option<u64>,
554}
555
556impl DistributionAccumulator {
557    fn observe(&mut self, neuron: &NnsNeuronRow) -> Result<(), NnsNeuronDistributionError> {
558        add(
559            &mut self.total_effective_stake_e8s,
560            neuron.stake_e8s,
561            "total_effective_stake_e8s",
562        )?;
563        self.states.entry(neuron.state).or_default().observe(
564            neuron.stake_e8s,
565            "state_neuron_count",
566            "state_effective_stake_e8s",
567        )?;
568        self.visibilities
569            .entry(neuron.visibility)
570            .or_default()
571            .observe(
572                neuron.stake_e8s,
573                "visibility_neuron_count",
574                "visibility_effective_stake_e8s",
575            )?;
576        self.neuron_types
577            .entry(neuron.neuron_type)
578            .or_default()
579            .observe(
580                neuron.stake_e8s,
581                "neuron_type_neuron_count",
582                "neuron_type_effective_stake_e8s",
583            )?;
584        self.staked_maturity.observe(
585            neuron.staked_maturity_e8s_equivalent,
586            "reported_staked_maturity_neuron_count",
587            "unreported_staked_maturity_neuron_count",
588            "total_reported_staked_maturity_e8s_equivalent",
589        )?;
590        self.deciding_voting_power.observe(
591            neuron.deciding_voting_power,
592            "reported_deciding_voting_power_neuron_count",
593            "unreported_deciding_voting_power_neuron_count",
594            "total_reported_deciding_voting_power",
595        )?;
596        self.potential_voting_power.observe(
597            neuron.potential_voting_power,
598            "reported_potential_voting_power_neuron_count",
599            "unreported_potential_voting_power_neuron_count",
600            "total_reported_potential_voting_power",
601        )?;
602        if neuron.known_neuron_data.is_some() {
603            increment(
604                &mut self.known_neuron_metadata_count,
605                "known_neuron_metadata_count",
606            )?;
607        }
608        if neuron.joined_community_fund_timestamp_seconds.is_some() {
609            increment(
610                &mut self.neurons_fund_join_timestamp_present_count,
611                "neurons_fund_join_timestamp_present_count",
612            )?;
613        }
614        let retrieved_at = neuron.retrieved_at_timestamp_seconds;
615        self.earliest_retrieved_at_timestamp_seconds = Some(
616            self.earliest_retrieved_at_timestamp_seconds
617                .map_or(retrieved_at, |earliest| earliest.min(retrieved_at)),
618        );
619        self.latest_retrieved_at_timestamp_seconds = Some(
620            self.latest_retrieved_at_timestamp_seconds
621                .map_or(retrieved_at, |latest| latest.max(retrieved_at)),
622        );
623        Ok(())
624    }
625
626    fn into_report(
627        self,
628        collection: &NnsNeuronCollectionState,
629        collected_neuron_count: u64,
630        source: NnsGovernanceSourceProvenance,
631    ) -> NnsNeuronDistributionReport {
632        NnsNeuronDistributionReport {
633            schema_version: NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION,
634            network: collection.network().to_string(),
635            governance_canister_id: collection.governance_canister_id().to_string(),
636            source,
637            collection_started_at: collection.started_at().to_string(),
638            collection_updated_at: collection.updated_at().to_string(),
639            collection_page_count: collection.pages_fetched(),
640            collected_neuron_count,
641            point_in_time_guaranteed: false,
642            earliest_retrieved_at_timestamp_seconds: self.earliest_retrieved_at_timestamp_seconds,
643            latest_retrieved_at_timestamp_seconds: self.latest_retrieved_at_timestamp_seconds,
644            total_effective_stake_e8s: self.total_effective_stake_e8s,
645            reported_staked_maturity_neuron_count: self.staked_maturity.reported_neuron_count,
646            unreported_staked_maturity_neuron_count: self.staked_maturity.unreported_neuron_count,
647            total_reported_staked_maturity_e8s_equivalent: self
648                .staked_maturity
649                .total_reported_value,
650            reported_deciding_voting_power_neuron_count: self
651                .deciding_voting_power
652                .reported_neuron_count,
653            unreported_deciding_voting_power_neuron_count: self
654                .deciding_voting_power
655                .unreported_neuron_count,
656            total_reported_deciding_voting_power: self.deciding_voting_power.total_reported_value,
657            reported_potential_voting_power_neuron_count: self
658                .potential_voting_power
659                .reported_neuron_count,
660            unreported_potential_voting_power_neuron_count: self
661                .potential_voting_power
662                .unreported_neuron_count,
663            total_reported_potential_voting_power: self.potential_voting_power.total_reported_value,
664            known_neuron_metadata_count: self.known_neuron_metadata_count,
665            neurons_fund_join_timestamp_present_count: self
666                .neurons_fund_join_timestamp_present_count,
667            state_distribution: self
668                .states
669                .into_iter()
670                .map(|(state, distribution)| NnsNeuronStateDistribution {
671                    state,
672
673                    neuron_count: distribution.neuron_count,
674                    effective_stake_e8s: distribution.effective_stake_e8s,
675                })
676                .collect(),
677            visibility_distribution: self
678                .visibilities
679                .into_iter()
680                .map(
681                    |(visibility, distribution)| NnsNeuronVisibilityDistribution {
682                        visibility,
683
684                        neuron_count: distribution.neuron_count,
685                        effective_stake_e8s: distribution.effective_stake_e8s,
686                    },
687                )
688                .collect(),
689            neuron_type_distribution: self
690                .neuron_types
691                .into_iter()
692                .map(|(neuron_type, distribution)| NnsNeuronTypeDistribution {
693                    neuron_type,
694
695                    neuron_count: distribution.neuron_count,
696                    effective_stake_e8s: distribution.effective_stake_e8s,
697                })
698                .collect(),
699        }
700    }
701}
702
703fn increment(value: &mut u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
704    *value = value
705        .checked_add(1)
706        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
707    Ok(())
708}
709
710fn add(total: &mut u64, value: u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
711    *total = total
712        .checked_add(value)
713        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
714    Ok(())
715}
716
717#[cfg(test)]
718mod tests;