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