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 = u64::try_from(collection.neurons_fetched()).map_err(|_| {
231        NnsNeuronDistributionError::AccountingOverflow {
232            field: "collected_neuron_count",
233        }
234    })?;
235    let actual = u64::try_from(neurons.len()).map_err(|_| {
236        NnsNeuronDistributionError::AccountingOverflow {
237            field: "supplied_neuron_count",
238        }
239    })?;
240    if actual != expected {
241        return Err(NnsNeuronDistributionError::NeuronCountMismatch { expected, actual });
242    }
243    validate_neuron_rows(neurons).map_err(|error| {
244        NnsNeuronDistributionError::InvalidNeuronRows {
245            reason: error.to_string(),
246        }
247    })?;
248
249    let mut distribution = DistributionAccumulator::default();
250    for neuron in neurons {
251        distribution.observe(neuron)?;
252    }
253    let report = distribution.into_report(collection, expected, source);
254    validate_nns_neuron_distribution_report(&report)?;
255    Ok(report)
256}
257
258/// Validate every distribution-report invariant available without source rows or live host calls.
259pub fn validate_nns_neuron_distribution_report(
260    report: &NnsNeuronDistributionReport,
261) -> Result<(), NnsNeuronDistributionValidationError> {
262    validate_distribution_header(report)?;
263    validate_distribution_summary(report)?;
264    validate_state_distribution(report)?;
265    validate_visibility_distribution(report)?;
266    validate_neuron_type_distribution(report)
267}
268
269fn validate_distribution_header(
270    report: &NnsNeuronDistributionReport,
271) -> Result<(), NnsNeuronDistributionValidationError> {
272    if report.schema_version != NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION {
273        return Err(invalid_validation(format!(
274            "schema version {} does not equal {}",
275            report.schema_version, NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION
276        )));
277    }
278    if report.network != MAINNET_NETWORK {
279        return Err(invalid_validation(format!(
280            "network is {}, expected {MAINNET_NETWORK}",
281            report.network
282        )));
283    }
284    if report.governance_canister_id != MAINNET_GOVERNANCE_CANISTER_ID {
285        return Err(invalid_validation(format!(
286            "governance_canister_id is {}, expected {MAINNET_GOVERNANCE_CANISTER_ID}",
287            report.governance_canister_id
288        )));
289    }
290    if report.collection_page_count == 0 {
291        return Err(invalid_validation(
292            "complete distribution report must retain at least one collection page",
293        ));
294    }
295    let minimum_neuron_count = u64::from(report.collection_page_count - 1);
296    if report.collected_neuron_count < minimum_neuron_count {
297        return Err(invalid_validation(format!(
298            "collection_page_count {} requires at least {minimum_neuron_count} collected neurons, found {}",
299            report.collection_page_count, report.collected_neuron_count
300        )));
301    }
302    if report.point_in_time_guaranteed {
303        return Err(invalid_validation(
304            "sequential public-neuron collection cannot claim a point-in-time snapshot",
305        ));
306    }
307    validate_governance_report_source(&report.network, &report.source).map_err(|error| {
308        let context = match &report.source {
309            NnsGovernanceSourceProvenance::ReplicaQuery { .. } => "source",
310            NnsGovernanceSourceProvenance::ReplicatedInterCanisterCall { .. } => "provenance",
311        };
312        invalid_validation(format!("invalid collection {context}: {error}"))
313    })?;
314    validate_retrieval_range(report)
315}
316
317fn validate_retrieval_range(
318    report: &NnsNeuronDistributionReport,
319) -> Result<(), NnsNeuronDistributionValidationError> {
320    match (
321        report.earliest_retrieved_at_timestamp_seconds,
322        report.latest_retrieved_at_timestamp_seconds,
323    ) {
324        (None, None) if report.collected_neuron_count == 0 => Ok(()),
325        (Some(earliest), Some(latest))
326            if report.collected_neuron_count > 0 && earliest <= latest =>
327        {
328            Ok(())
329        }
330        _ => Err(invalid_validation(
331            "retrieval timestamp range disagrees with collected_neuron_count or is reversed",
332        )),
333    }
334}
335
336fn validate_distribution_summary(
337    report: &NnsNeuronDistributionReport,
338) -> Result<(), NnsNeuronDistributionValidationError> {
339    validate_optional_summary(
340        report.reported_staked_maturity_neuron_count,
341        report.unreported_staked_maturity_neuron_count,
342        report.total_reported_staked_maturity_e8s_equivalent,
343        report.collected_neuron_count,
344        "staked maturity",
345    )?;
346    validate_optional_summary(
347        report.reported_deciding_voting_power_neuron_count,
348        report.unreported_deciding_voting_power_neuron_count,
349        report.total_reported_deciding_voting_power,
350        report.collected_neuron_count,
351        "deciding voting power",
352    )?;
353    validate_optional_summary(
354        report.reported_potential_voting_power_neuron_count,
355        report.unreported_potential_voting_power_neuron_count,
356        report.total_reported_potential_voting_power,
357        report.collected_neuron_count,
358        "potential voting power",
359    )?;
360    for (field, count) in [
361        (
362            "known_neuron_metadata_count",
363            report.known_neuron_metadata_count,
364        ),
365        (
366            "neurons_fund_join_timestamp_present_count",
367            report.neurons_fund_join_timestamp_present_count,
368        ),
369    ] {
370        if count > report.collected_neuron_count {
371            return Err(invalid_validation(format!(
372                "{field} {count} exceeds collected_neuron_count {}",
373                report.collected_neuron_count
374            )));
375        }
376    }
377    Ok(())
378}
379
380fn validate_optional_summary(
381    reported: u64,
382    unreported: u64,
383    total: u64,
384    collected: u64,
385    field: &'static str,
386) -> Result<(), NnsNeuronDistributionValidationError> {
387    let accounted = reported
388        .checked_add(unreported)
389        .ok_or_else(|| invalid_validation(format!("{field} coverage count overflow")))?;
390    if accounted != collected {
391        return Err(invalid_validation(format!(
392            "{field} coverage accounts for {accounted} neurons, expected {collected}"
393        )));
394    }
395    if reported == 0 && total != 0 {
396        return Err(invalid_validation(format!(
397            "{field} total must be zero when no rows report the field"
398        )));
399    }
400    Ok(())
401}
402
403fn validate_state_distribution(
404    report: &NnsNeuronDistributionReport,
405) -> Result<(), NnsNeuronDistributionValidationError> {
406    let mut previous = None;
407    let mut neuron_count = 0_u64;
408    let mut stake = 0_u64;
409    for row in &report.state_distribution {
410        if previous.is_some_and(|state| state >= row.state) {
411            return Err(invalid_validation(
412                "state distribution is not strictly raw-code ordered",
413            ));
414        }
415        if row.state_text != NnsNeuronState::from_code(row.state) {
416            return Err(invalid_validation(format!(
417                "state classification for raw code {} is inconsistent",
418                row.state
419            )));
420        }
421        neuron_count = add_distribution_count(neuron_count, row.neuron_count, "state")?;
422        stake = add_validation_total(stake, row.effective_stake_e8s, "state stake")?;
423        previous = Some(row.state);
424    }
425    validate_distribution_totals(report, neuron_count, stake, "state")
426}
427
428fn validate_visibility_distribution(
429    report: &NnsNeuronDistributionReport,
430) -> Result<(), NnsNeuronDistributionValidationError> {
431    let mut previous: Option<Option<i32>> = None;
432    let mut neuron_count = 0_u64;
433    let mut stake = 0_u64;
434    for row in &report.visibility_distribution {
435        if previous.is_some_and(|visibility| visibility >= row.visibility) {
436            return Err(invalid_validation(
437                "visibility distribution is not strictly optional-raw-code ordered",
438            ));
439        }
440        if row.visibility_text != NnsNeuronVisibility::from_code(row.visibility) {
441            return Err(invalid_validation(format!(
442                "visibility classification for raw code {:?} is inconsistent",
443                row.visibility
444            )));
445        }
446        neuron_count = add_distribution_count(neuron_count, row.neuron_count, "visibility")?;
447        stake = add_validation_total(stake, row.effective_stake_e8s, "visibility stake")?;
448        previous = Some(row.visibility);
449    }
450    validate_distribution_totals(report, neuron_count, stake, "visibility")
451}
452
453fn validate_neuron_type_distribution(
454    report: &NnsNeuronDistributionReport,
455) -> Result<(), NnsNeuronDistributionValidationError> {
456    let mut previous: Option<Option<i32>> = None;
457    let mut neuron_count = 0_u64;
458    let mut stake = 0_u64;
459    for row in &report.neuron_type_distribution {
460        if previous.is_some_and(|neuron_type| neuron_type >= row.neuron_type) {
461            return Err(invalid_validation(
462                "neuron-type distribution is not strictly optional-raw-code ordered",
463            ));
464        }
465        if row.neuron_type_text != NnsNeuronType::from_code(row.neuron_type) {
466            return Err(invalid_validation(format!(
467                "neuron-type classification for raw code {:?} is inconsistent",
468                row.neuron_type
469            )));
470        }
471        neuron_count = add_distribution_count(neuron_count, row.neuron_count, "neuron-type")?;
472        stake = add_validation_total(stake, row.effective_stake_e8s, "neuron-type stake")?;
473        previous = Some(row.neuron_type);
474    }
475    validate_distribution_totals(report, neuron_count, stake, "neuron-type")
476}
477
478fn add_distribution_count(
479    total: u64,
480    count: u64,
481    dimension: &'static str,
482) -> Result<u64, NnsNeuronDistributionValidationError> {
483    if count == 0 {
484        return Err(invalid_validation(format!(
485            "{dimension} distribution row must contain at least one neuron"
486        )));
487    }
488    add_validation_total(total, count, dimension)
489}
490
491fn add_validation_total(
492    total: u64,
493    value: u64,
494    field: &'static str,
495) -> Result<u64, NnsNeuronDistributionValidationError> {
496    total
497        .checked_add(value)
498        .ok_or_else(|| invalid_validation(format!("{field} total overflow")))
499}
500
501fn validate_distribution_totals(
502    report: &NnsNeuronDistributionReport,
503    neuron_count: u64,
504    stake: u64,
505    dimension: &'static str,
506) -> Result<(), NnsNeuronDistributionValidationError> {
507    if neuron_count != report.collected_neuron_count {
508        return Err(invalid_validation(format!(
509            "{dimension} neuron counts sum to {neuron_count}, expected {}",
510            report.collected_neuron_count
511        )));
512    }
513    if stake != report.total_effective_stake_e8s {
514        return Err(invalid_validation(format!(
515            "{dimension} effective stake sums to {stake}, expected {}",
516            report.total_effective_stake_e8s
517        )));
518    }
519    Ok(())
520}
521
522fn invalid_validation(reason: impl Into<String>) -> NnsNeuronDistributionValidationError {
523    NnsNeuronDistributionValidationError {
524        reason: reason.into(),
525    }
526}
527
528#[derive(Clone, Copy, Default)]
529struct DimensionAccumulator {
530    neuron_count: u64,
531    effective_stake_e8s: u64,
532}
533
534impl DimensionAccumulator {
535    fn observe(
536        &mut self,
537        effective_stake_e8s: u64,
538        count_field: &'static str,
539        stake_field: &'static str,
540    ) -> Result<(), NnsNeuronDistributionError> {
541        increment(&mut self.neuron_count, count_field)?;
542        add(
543            &mut self.effective_stake_e8s,
544            effective_stake_e8s,
545            stake_field,
546        )
547    }
548}
549
550#[derive(Default)]
551struct OptionalValueAccumulator {
552    reported_neuron_count: u64,
553    unreported_neuron_count: u64,
554    total_reported_value: u64,
555}
556
557impl OptionalValueAccumulator {
558    fn observe(
559        &mut self,
560        value: Option<u64>,
561        reported_field: &'static str,
562        unreported_field: &'static str,
563        total_field: &'static str,
564    ) -> Result<(), NnsNeuronDistributionError> {
565        if let Some(value) = value {
566            increment(&mut self.reported_neuron_count, reported_field)?;
567            add(&mut self.total_reported_value, value, total_field)
568        } else {
569            increment(&mut self.unreported_neuron_count, unreported_field)
570        }
571    }
572}
573
574#[derive(Default)]
575struct DistributionAccumulator {
576    states: BTreeMap<i32, DimensionAccumulator>,
577    visibilities: BTreeMap<Option<i32>, DimensionAccumulator>,
578    neuron_types: BTreeMap<Option<i32>, DimensionAccumulator>,
579    total_effective_stake_e8s: u64,
580    staked_maturity: OptionalValueAccumulator,
581    deciding_voting_power: OptionalValueAccumulator,
582    potential_voting_power: OptionalValueAccumulator,
583    known_neuron_metadata_count: u64,
584    neurons_fund_join_timestamp_present_count: u64,
585    earliest_retrieved_at_timestamp_seconds: Option<u64>,
586    latest_retrieved_at_timestamp_seconds: Option<u64>,
587}
588
589impl DistributionAccumulator {
590    fn observe(&mut self, neuron: &NnsNeuronRow) -> Result<(), NnsNeuronDistributionError> {
591        add(
592            &mut self.total_effective_stake_e8s,
593            neuron.stake_e8s,
594            "total_effective_stake_e8s",
595        )?;
596        self.states.entry(neuron.state).or_default().observe(
597            neuron.stake_e8s,
598            "state_neuron_count",
599            "state_effective_stake_e8s",
600        )?;
601        self.visibilities
602            .entry(neuron.visibility)
603            .or_default()
604            .observe(
605                neuron.stake_e8s,
606                "visibility_neuron_count",
607                "visibility_effective_stake_e8s",
608            )?;
609        self.neuron_types
610            .entry(neuron.neuron_type)
611            .or_default()
612            .observe(
613                neuron.stake_e8s,
614                "neuron_type_neuron_count",
615                "neuron_type_effective_stake_e8s",
616            )?;
617        self.staked_maturity.observe(
618            neuron.staked_maturity_e8s_equivalent,
619            "reported_staked_maturity_neuron_count",
620            "unreported_staked_maturity_neuron_count",
621            "total_reported_staked_maturity_e8s_equivalent",
622        )?;
623        self.deciding_voting_power.observe(
624            neuron.deciding_voting_power,
625            "reported_deciding_voting_power_neuron_count",
626            "unreported_deciding_voting_power_neuron_count",
627            "total_reported_deciding_voting_power",
628        )?;
629        self.potential_voting_power.observe(
630            neuron.potential_voting_power,
631            "reported_potential_voting_power_neuron_count",
632            "unreported_potential_voting_power_neuron_count",
633            "total_reported_potential_voting_power",
634        )?;
635        if neuron.known_neuron_data.is_some() {
636            increment(
637                &mut self.known_neuron_metadata_count,
638                "known_neuron_metadata_count",
639            )?;
640        }
641        if neuron.joined_community_fund_timestamp_seconds.is_some() {
642            increment(
643                &mut self.neurons_fund_join_timestamp_present_count,
644                "neurons_fund_join_timestamp_present_count",
645            )?;
646        }
647        let retrieved_at = neuron.retrieved_at_timestamp_seconds;
648        self.earliest_retrieved_at_timestamp_seconds = Some(
649            self.earliest_retrieved_at_timestamp_seconds
650                .map_or(retrieved_at, |earliest| earliest.min(retrieved_at)),
651        );
652        self.latest_retrieved_at_timestamp_seconds = Some(
653            self.latest_retrieved_at_timestamp_seconds
654                .map_or(retrieved_at, |latest| latest.max(retrieved_at)),
655        );
656        Ok(())
657    }
658
659    fn into_report(
660        self,
661        collection: &NnsNeuronCollectionState,
662        collected_neuron_count: u64,
663        source: NnsGovernanceSourceProvenance,
664    ) -> NnsNeuronDistributionReport {
665        NnsNeuronDistributionReport {
666            schema_version: NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION,
667            network: collection.network().to_string(),
668            governance_canister_id: collection.governance_canister_id().to_string(),
669            source,
670            collection_started_at: collection.started_at().to_string(),
671            collection_updated_at: collection.updated_at().to_string(),
672            collection_page_count: collection.pages_fetched(),
673            collected_neuron_count,
674            point_in_time_guaranteed: false,
675            earliest_retrieved_at_timestamp_seconds: self.earliest_retrieved_at_timestamp_seconds,
676            latest_retrieved_at_timestamp_seconds: self.latest_retrieved_at_timestamp_seconds,
677            total_effective_stake_e8s: self.total_effective_stake_e8s,
678            reported_staked_maturity_neuron_count: self.staked_maturity.reported_neuron_count,
679            unreported_staked_maturity_neuron_count: self.staked_maturity.unreported_neuron_count,
680            total_reported_staked_maturity_e8s_equivalent: self
681                .staked_maturity
682                .total_reported_value,
683            reported_deciding_voting_power_neuron_count: self
684                .deciding_voting_power
685                .reported_neuron_count,
686            unreported_deciding_voting_power_neuron_count: self
687                .deciding_voting_power
688                .unreported_neuron_count,
689            total_reported_deciding_voting_power: self.deciding_voting_power.total_reported_value,
690            reported_potential_voting_power_neuron_count: self
691                .potential_voting_power
692                .reported_neuron_count,
693            unreported_potential_voting_power_neuron_count: self
694                .potential_voting_power
695                .unreported_neuron_count,
696            total_reported_potential_voting_power: self.potential_voting_power.total_reported_value,
697            known_neuron_metadata_count: self.known_neuron_metadata_count,
698            neurons_fund_join_timestamp_present_count: self
699                .neurons_fund_join_timestamp_present_count,
700            state_distribution: self
701                .states
702                .into_iter()
703                .map(|(state, distribution)| NnsNeuronStateDistribution {
704                    state,
705                    state_text: NnsNeuronState::from_code(state),
706                    neuron_count: distribution.neuron_count,
707                    effective_stake_e8s: distribution.effective_stake_e8s,
708                })
709                .collect(),
710            visibility_distribution: self
711                .visibilities
712                .into_iter()
713                .map(
714                    |(visibility, distribution)| NnsNeuronVisibilityDistribution {
715                        visibility,
716                        visibility_text: NnsNeuronVisibility::from_code(visibility),
717                        neuron_count: distribution.neuron_count,
718                        effective_stake_e8s: distribution.effective_stake_e8s,
719                    },
720                )
721                .collect(),
722            neuron_type_distribution: self
723                .neuron_types
724                .into_iter()
725                .map(|(neuron_type, distribution)| NnsNeuronTypeDistribution {
726                    neuron_type,
727                    neuron_type_text: NnsNeuronType::from_code(neuron_type),
728                    neuron_count: distribution.neuron_count,
729                    effective_stake_e8s: distribution.effective_stake_e8s,
730                })
731                .collect(),
732        }
733    }
734}
735
736fn increment(value: &mut u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
737    *value = value
738        .checked_add(1)
739        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
740    Ok(())
741}
742
743fn add(total: &mut u64, value: u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
744    *total = total
745        .checked_add(value)
746        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
747    Ok(())
748}
749
750#[cfg(test)]
751mod tests;