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