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