Skip to main content

ic_query/sns/report/model/reports/neurons/
reward.rs

1//! Module: sns::report::model::reports::neurons::reward
2//!
3//! Responsibility: SNS reward-event and API-exhausted maturity checkpoint DTOs.
4//! Does not own: live Governance calls, strict pagination, or filesystem loading.
5//! Boundary: retains raw bracketing evidence, variable neuron evidence, and recomputed summaries.
6
7use super::{
8    SnsMaturityDisbursementRow, SnsNeuronPermissionRow,
9    detail::{
10        SnsPolicyObservationStatus, neuron_policy_observations, permission_code_is_assessable,
11        permission_code_policy_observations, validate_canonical_principal,
12        validate_maturity_disbursements, validate_neuron_permissions,
13    },
14};
15use crate::{
16    hex::is_lowercase_hex_32_bytes,
17    report::ReportDataSource,
18    sns::report::{
19        MAINNET_SNS_WASM_CANISTER_ID, MainnetSnsCanisters, SNS_REWARD_CHECKPOINT_MAX_NEURONS,
20        SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION,
21        model::reports::{SnsGovernanceParameters, SnsRunningVersionResponse},
22    },
23    subnet_catalog::MAINNET_NETWORK,
24};
25use ic_platform::candid::contract::{CandidType, serde::Deserialize};
26use serde::Serialize;
27use std::collections::HashSet;
28use thiserror::Error as ThisError;
29
30///
31/// SnsRewardProposalId
32///
33/// Native proposal identifier retained in one Governance reward event.
34///
35
36#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)]
37#[candid_path("ic_platform::candid::contract")]
38#[serde(deny_unknown_fields)]
39pub struct SnsRewardProposalId {
40    /// Native SNS Governance proposal identifier.
41    pub id: u64,
42}
43
44///
45/// SnsRewardEvent
46///
47/// Complete native SNS Governance reward-event response.
48///
49
50#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)]
51#[candid_path("ic_platform::candid::contract")]
52#[serde(deny_unknown_fields)]
53pub struct SnsRewardEvent {
54    /// Number of reward rounds represented by this event when supplied.
55    pub rounds_since_last_distribution: Option<u64>,
56    /// Native timestamp at which distribution actually ran.
57    pub actual_timestamp_seconds: u64,
58    /// Canonical reward-event position when supplied.
59    pub end_timestamp_seconds: Option<u64>,
60    /// Total maturity available for distribution when supplied.
61    pub total_available_e8s_equivalent: Option<u64>,
62    /// Exact maturity amount distributed by this event.
63    pub distributed_e8s_equivalent: u64,
64    /// Deprecated native reward round retained as continuity evidence.
65    pub round: u64,
66    /// Complete settled-proposal identifiers retained from Governance.
67    pub settled_proposals: Vec<SnsRewardProposalId>,
68}
69
70///
71/// SnsRewardCollectionStatus
72///
73/// Authority level of one completed SNS reward checkpoint collection.
74///
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
77#[serde(rename_all = "snake_case")]
78pub enum SnsRewardCollectionStatus {
79    /// The native neuron API was exhausted with stable bracket responses.
80    ApiExhaustedObserved,
81}
82
83impl SnsRewardCollectionStatus {
84    /// Return the stable report label for this collection status.
85    #[must_use]
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::ApiExhaustedObserved => "api_exhausted_observed",
89        }
90    }
91}
92
93///
94/// SnsRewardCheckpointRow
95///
96/// Variable-size maturity and permission evidence for one observed SNS neuron.
97///
98
99#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
100#[serde(deny_unknown_fields)]
101pub struct SnsRewardCheckpointRow {
102    /// Exact 32-byte neuron identifier as lowercase hexadecimal text.
103    pub neuron_id: String,
104    /// Unix timestamp at which Governance reports the neuron was created.
105    pub created_timestamp_seconds: u64,
106    /// Raw unstaked maturity in e8s-equivalent units.
107    pub maturity_e8s_equivalent: u64,
108    /// Raw staked maturity in e8s-equivalent units when present.
109    pub staked_maturity_e8s_equivalent: Option<u64>,
110    /// Native automatic maturity-staking state when supplied.
111    pub auto_stake_maturity: Option<bool>,
112    /// Every current principal permission entry returned by Governance.
113    pub permissions: Vec<SnsNeuronPermissionRow>,
114    /// Every pending maturity disbursement returned by Governance.
115    pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
116}
117
118impl SnsRewardCheckpointRow {
119    /// Recompute combined maturity from the two raw maturity components.
120    #[must_use]
121    pub fn checked_combined_maturity(&self) -> Option<u64> {
122        self.maturity_e8s_equivalent
123            .checked_add(self.staked_maturity_e8s_equivalent.unwrap_or(0))
124    }
125
126    /// Recompute both row-local maturity policy observations from raw evidence.
127    #[must_use]
128    pub fn derived_policy_observations(
129        &self,
130    ) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
131        neuron_policy_observations(
132            &self.permissions,
133            !self.disburse_maturity_in_progress.is_empty(),
134        )
135    }
136}
137
138///
139/// SnsRewardCheckpointReport
140///
141/// Versioned API-exhausted observed SNS maturity checkpoint.
142///
143
144#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
145#[serde(deny_unknown_fields)]
146pub struct SnsRewardCheckpointReport {
147    /// Report schema version.
148    pub schema_version: u32,
149    /// Requested IC network identity.
150    pub network: String,
151    /// Stable mainnet SNS-W discovery canister identity.
152    pub sns_wasm_canister_id: String,
153    /// Explicit IC API endpoint used for every live call.
154    pub source_endpoint: String,
155    /// Collector identity recorded in report provenance.
156    pub fetched_by: String,
157    /// Current SNS-W list position retained only as display metadata.
158    pub id: usize,
159    /// Current SNS name retained only as display metadata.
160    pub name: String,
161    /// Complete deployed-SNS canister identity, serialized as flat fields.
162    #[serde(flatten)]
163    pub canisters: MainnetSnsCanisters,
164    /// Explicit source classification; checkpoint reports are live-only.
165    pub data_source: ReportDataSource,
166    /// Unix timestamp captured immediately before targeted discovery.
167    pub collection_started_at_unix_secs: u64,
168    /// Unix timestamp captured after the final running-version bracket.
169    pub collection_completed_at_unix_secs: u64,
170    /// Number of neuron pages fetched, including the final exhaustion page.
171    pub page_count: u32,
172    /// Explicit authority classification for the completed collection.
173    pub collection_status: SnsRewardCollectionStatus,
174    /// Always false because Governance exposes no point-in-time neuron snapshot version.
175    pub point_in_time_guaranteed: bool,
176    /// Complete nervous-system parameters read before neuron pagination.
177    pub parameters_before: SnsGovernanceParameters,
178    /// Complete nervous-system parameters read after neuron pagination.
179    pub parameters_after: SnsGovernanceParameters,
180    /// Complete reward event read before neuron pagination.
181    pub reward_event_before: SnsRewardEvent,
182    /// Complete reward event read after neuron pagination.
183    pub reward_event_after: SnsRewardEvent,
184    /// Complete running-version response read before neuron pagination.
185    pub running_version_before: SnsRunningVersionResponse,
186    /// Complete running-version response read after neuron pagination.
187    pub running_version_after: SnsRunningVersionResponse,
188    /// Strictly increasing full neuron rows retained as raw checkpoint evidence.
189    pub rows: Vec<SnsRewardCheckpointRow>,
190}
191
192///
193/// SnsRewardCheckpointValidationError
194///
195/// Pure validation failure for untrusted serialized or in-memory checkpoint evidence.
196///
197
198#[derive(Debug, Eq, PartialEq, ThisError)]
199#[error("invalid SNS reward checkpoint: {reason}")]
200pub struct SnsRewardCheckpointValidationError {
201    /// Deterministic invariant failure.
202    pub reason: String,
203}
204
205/// Recompute and validate every checkpoint invariant available without live host calls.
206pub fn validate_sns_reward_checkpoint_report(
207    report: &SnsRewardCheckpointReport,
208) -> Result<(), SnsRewardCheckpointValidationError> {
209    validate_checkpoint_evidence(report)?;
210    report.summary().map(|_| ())
211}
212
213///
214/// RewardCheckpointAssessment
215///
216/// Validation outcome retaining recomputed evidence from rejected checkpoints.
217///
218
219pub(in crate::sns::report) enum RewardCheckpointAssessment {
220    Valid(SnsRewardCheckpointSummary),
221    Invalid {
222        error: SnsRewardCheckpointValidationError,
223        summary: Option<SnsRewardCheckpointSummary>,
224    },
225}
226
227impl RewardCheckpointAssessment {
228    pub(in crate::sns::report) const fn summary(&self) -> Option<&SnsRewardCheckpointSummary> {
229        match self {
230            Self::Valid(summary) => Some(summary),
231            Self::Invalid { summary, .. } => summary.as_ref(),
232        }
233    }
234}
235
236/// Retain recomputed evidence even when checkpoint validation fails during reconciliation.
237pub(in crate::sns::report) fn assess_reward_checkpoint(
238    report: &SnsRewardCheckpointReport,
239) -> RewardCheckpointAssessment {
240    let evidence = validate_checkpoint_evidence(report);
241    match report.summary() {
242        Ok(summary) => match evidence {
243            Ok(()) => RewardCheckpointAssessment::Valid(summary),
244            Err(error) => RewardCheckpointAssessment::Invalid {
245                error,
246                summary: Some(summary),
247            },
248        },
249        Err(reason) => RewardCheckpointAssessment::Invalid {
250            error: evidence.err().unwrap_or(reason),
251            summary: None,
252        },
253    }
254}
255
256impl SnsRewardCheckpointReport {
257    /// Derive checked totals and policy observations from the retained raw evidence.
258    pub fn summary(
259        &self,
260    ) -> Result<SnsRewardCheckpointSummary, SnsRewardCheckpointValidationError> {
261        recompute_reward_checkpoint_summary(&self.parameters_before, &self.rows)
262            .map_err(invalid_validation)
263    }
264
265    /// Number of retained raw neuron rows.
266    #[must_use]
267    pub const fn row_count(&self) -> usize {
268        self.rows.len()
269    }
270
271    /// Mandatory collection ceiling admitted from the raw parameter bracket.
272    #[must_use]
273    pub fn collection_row_ceiling(&self) -> Option<u64> {
274        reward_checkpoint_neuron_ceiling(&self.parameters_before)
275    }
276
277    /// Targeted discovery, bracket and page calls implied by this collection protocol.
278    #[must_use]
279    pub const fn client_query_count(&self) -> Option<u32> {
280        self.page_count.checked_add(8)
281    }
282}
283
284fn validate_checkpoint_evidence(
285    report: &SnsRewardCheckpointReport,
286) -> Result<(), SnsRewardCheckpointValidationError> {
287    validate_checkpoint_header(report)?;
288    validate_checkpoint_brackets(report)?;
289    validate_checkpoint_rows(&report.rows, report.collection_completed_at_unix_secs)
290}
291
292fn validate_checkpoint_header(
293    report: &SnsRewardCheckpointReport,
294) -> Result<(), SnsRewardCheckpointValidationError> {
295    if report.schema_version != SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION {
296        return Err(invalid_validation(format!(
297            "schema version {} does not equal {}",
298            report.schema_version, SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION
299        )));
300    }
301    for (field, principal) in [
302        ("sns_wasm_canister_id", report.sns_wasm_canister_id.as_str()),
303        (
304            "root_canister_id",
305            report.canisters.root_canister_id.as_str(),
306        ),
307        (
308            "governance_canister_id",
309            report.canisters.governance_canister_id.as_str(),
310        ),
311        (
312            "ledger_canister_id",
313            report.canisters.ledger_canister_id.as_str(),
314        ),
315        (
316            "swap_canister_id",
317            report.canisters.swap_canister_id.as_str(),
318        ),
319        (
320            "index_canister_id",
321            report.canisters.index_canister_id.as_str(),
322        ),
323    ] {
324        validate_canonical_principal(principal, field).map_err(invalid_validation)?;
325    }
326    let canister_ids = [
327        report.sns_wasm_canister_id.as_str(),
328        report.canisters.root_canister_id.as_str(),
329        report.canisters.governance_canister_id.as_str(),
330        report.canisters.ledger_canister_id.as_str(),
331        report.canisters.swap_canister_id.as_str(),
332        report.canisters.index_canister_id.as_str(),
333    ];
334    if canister_ids.into_iter().collect::<HashSet<_>>().len() != canister_ids.len() {
335        return Err(invalid_validation(
336            "checkpoint canister roles do not contain unique principals",
337        ));
338    }
339    if report.sns_wasm_canister_id != MAINNET_SNS_WASM_CANISTER_ID {
340        return Err(invalid_validation("unexpected SNS-W canister identity"));
341    }
342    if report.network != MAINNET_NETWORK {
343        return Err(invalid_validation("checkpoint network must be ic"));
344    }
345    if report.source_endpoint.is_empty() || report.fetched_by.is_empty() {
346        return Err(invalid_validation(
347            "checkpoint source_endpoint and fetched_by must be non-empty",
348        ));
349    }
350    if report.id == 0 || report.name.is_empty() {
351        return Err(invalid_validation(
352            "checkpoint SNS list id and display name must be non-empty",
353        ));
354    }
355    if report.data_source != ReportDataSource::Live {
356        return Err(invalid_validation("checkpoint data_source must be live"));
357    }
358    if report.point_in_time_guaranteed {
359        return Err(invalid_validation(
360            "checkpoint cannot claim a point-in-time neuron snapshot",
361        ));
362    }
363    validate_checkpoint_collection_metadata(report)
364}
365
366fn validate_checkpoint_collection_metadata(
367    report: &SnsRewardCheckpointReport,
368) -> Result<(), SnsRewardCheckpointValidationError> {
369    if report.collection_completed_at_unix_secs < report.collection_started_at_unix_secs {
370        return Err(invalid_validation(
371            "collection completion precedes collection start",
372        ));
373    }
374    if report.page_count == 0 {
375        return Err(invalid_validation(
376            "checkpoint must contain at least one page",
377        ));
378    }
379    report
380        .client_query_count()
381        .ok_or_else(|| invalid_validation("page_count + 8 exceeds the native u32 contract"))?;
382    let ceiling = report
383        .collection_row_ceiling()
384        .ok_or_else(|| invalid_validation("invalid max_number_of_neurons"))?;
385    if u64::try_from(report.rows.len())
386        .ok()
387        .is_none_or(|rows| rows > ceiling)
388    {
389        return Err(invalid_validation(
390            "raw row count exceeds the mandatory collection ceiling",
391        ));
392    }
393    Ok(())
394}
395
396pub(in crate::sns::report) fn reward_checkpoint_neuron_ceiling(
397    parameters: &SnsGovernanceParameters,
398) -> Option<u64> {
399    parameters
400        .max_number_of_neurons
401        .filter(|value| (1..=SNS_REWARD_CHECKPOINT_MAX_NEURONS).contains(value))
402}
403
404/// Return the first unstable component and its persisted-report diagnostic.
405pub(in crate::sns::report) fn reward_checkpoint_bracket_mismatch(
406    parameters: (&SnsGovernanceParameters, &SnsGovernanceParameters),
407    events: (&SnsRewardEvent, &SnsRewardEvent),
408    versions: (&SnsRunningVersionResponse, &SnsRunningVersionResponse),
409) -> Option<(&'static str, &'static str)> {
410    [
411        (
412            "nervous-system parameters",
413            "nervous-system parameter brackets differ",
414            parameters.0 == parameters.1,
415        ),
416        (
417            "reward event",
418            "reward-event brackets differ",
419            events.0 == events.1,
420        ),
421        (
422            "running SNS version",
423            "running-version brackets differ",
424            versions.0 == versions.1,
425        ),
426    ]
427    .into_iter()
428    .find_map(|(component, reason, stable)| (!stable).then_some((component, reason)))
429}
430
431fn validate_checkpoint_brackets(
432    report: &SnsRewardCheckpointReport,
433) -> Result<(), SnsRewardCheckpointValidationError> {
434    if let Some((_, reason)) = reward_checkpoint_bracket_mismatch(
435        (&report.parameters_before, &report.parameters_after),
436        (&report.reward_event_before, &report.reward_event_after),
437        (
438            &report.running_version_before,
439            &report.running_version_after,
440        ),
441    ) {
442        return Err(invalid_validation(reason));
443    }
444    validate_sns_reward_event_evidence(&report.reward_event_after).map_err(invalid_validation)?;
445    validate_reward_event_position(report)?;
446    validate_sns_reward_running_version_evidence(&report.running_version_after)
447        .map_err(invalid_validation)?;
448    validate_sns_reward_checkpoint_parameter_evidence(&report.parameters_after)
449        .map_err(invalid_validation)
450}
451
452fn validate_reward_event_position(
453    report: &SnsRewardCheckpointReport,
454) -> Result<(), SnsRewardCheckpointValidationError> {
455    let event = &report.reward_event_after;
456    if event.actual_timestamp_seconds > report.collection_completed_at_unix_secs
457        || event
458            .end_timestamp_seconds
459            .is_none_or(|end| end > report.collection_completed_at_unix_secs)
460    {
461        return Err(invalid_validation(
462            "reward event timestamps exceed checkpoint collection completion",
463        ));
464    }
465    Ok(())
466}
467
468fn validate_checkpoint_rows(
469    rows: &[SnsRewardCheckpointRow],
470    collection_completed_at_unix_secs: u64,
471) -> Result<(), SnsRewardCheckpointValidationError> {
472    let mut previous_id: Option<&str> = None;
473    for row in rows {
474        if !is_lowercase_hex_32_bytes(&row.neuron_id) {
475            return Err(invalid_validation(format!(
476                "neuron id {} is not 32-byte lowercase hexadecimal text",
477                row.neuron_id
478            )));
479        }
480        if previous_id.is_some_and(|previous| previous >= row.neuron_id.as_str()) {
481            return Err(invalid_validation(
482                "checkpoint neuron ids are not strictly increasing",
483            ));
484        }
485        previous_id = Some(&row.neuron_id);
486        if row.created_timestamp_seconds > collection_completed_at_unix_secs {
487            return Err(invalid_validation(format!(
488                "neuron {} creation timestamp exceeds collection completion",
489                row.neuron_id
490            )));
491        }
492        validate_neuron_permissions(&row.permissions, false)
493            .map_err(|reason| invalid_validation(format!("neuron {}: {reason}", row.neuron_id)))?;
494        validate_maturity_disbursements(&row.disburse_maturity_in_progress)
495            .map_err(invalid_validation)?;
496    }
497    Ok(())
498}
499
500pub(in crate::sns::report) fn validate_sns_reward_running_version_evidence(
501    response: &SnsRunningVersionResponse,
502) -> Result<(), String> {
503    let deployed = response
504        .deployed_version
505        .as_ref()
506        .ok_or_else(|| "running version has no deployed_version".to_string())?;
507    deployed.validate_hashes("deployed_version", Some(32))?;
508    if let Some(target) = response
509        .pending_version
510        .as_ref()
511        .and_then(|pending| pending.target_version.as_ref())
512    {
513        target.validate_hashes("pending_version.target_version", Some(32))?;
514    }
515    Ok(())
516}
517
518pub(in crate::sns::report) fn validate_sns_reward_checkpoint_parameter_evidence(
519    parameters: &SnsGovernanceParameters,
520) -> Result<(), String> {
521    validate_default_followees(parameters)?;
522    validate_parameter_permissions(parameters)
523}
524
525fn validate_default_followees(parameters: &SnsGovernanceParameters) -> Result<(), String> {
526    let Some(defaults) = parameters.default_followees.as_ref() else {
527        return Ok(());
528    };
529    let mut function_ids = HashSet::new();
530    for row in &defaults.followees {
531        if !function_ids.insert(row.function_id) {
532            return Err("default followees contain a duplicate function id".to_string());
533        }
534        let mut neuron_ids = HashSet::new();
535        for id in &row.followee_neuron_ids {
536            if !is_lowercase_hex_32_bytes(id) {
537                return Err("default followees contain a non-canonical neuron id".to_string());
538            }
539            if !neuron_ids.insert(id) {
540                return Err("default followees contain a duplicate neuron id".to_string());
541            }
542        }
543    }
544    Ok(())
545}
546
547fn validate_parameter_permissions(parameters: &SnsGovernanceParameters) -> Result<(), String> {
548    for (field, permissions) in [
549        (
550            "neuron_claimer_permissions",
551            parameters.neuron_claimer_permissions.as_ref(),
552        ),
553        (
554            "neuron_grantable_permissions",
555            parameters.neuron_grantable_permissions.as_ref(),
556        ),
557    ] {
558        let Some(permissions) = permissions else {
559            continue;
560        };
561        let mut codes = HashSet::new();
562        if permissions
563            .permissions
564            .iter()
565            .any(|code| !codes.insert(*code))
566        {
567            return Err(format!("{field} contains a duplicate permission code"));
568        }
569    }
570    Ok(())
571}
572
573pub(in crate::sns::report) fn validate_sns_reward_event_evidence(
574    event: &SnsRewardEvent,
575) -> Result<(), String> {
576    if event.end_timestamp_seconds.is_none() {
577        return Err("reward event is missing canonical end_timestamp_seconds".to_string());
578    }
579    let mut proposal_ids = HashSet::new();
580    if event
581        .settled_proposals
582        .iter()
583        .any(|proposal| !proposal_ids.insert(proposal.id))
584    {
585        return Err("reward event contains duplicate settled proposal ids".to_string());
586    }
587    Ok(())
588}
589
590fn invalid_validation(reason: impl Into<String>) -> SnsRewardCheckpointValidationError {
591    SnsRewardCheckpointValidationError {
592        reason: reason.into(),
593    }
594}
595
596///
597/// SnsRewardCheckpointSummary
598///
599/// Pure recomputation result derived from raw checkpoint parameters and rows.
600///
601
602#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
603#[serde(deny_unknown_fields)]
604pub struct SnsRewardCheckpointSummary {
605    /// Checked aggregate unstaked maturity across every row.
606    pub aggregate_maturity_e8s_equivalent: u64,
607    /// Checked aggregate staked maturity across every row.
608    pub aggregate_staked_maturity_e8s_equivalent: u64,
609    /// Checked aggregate combined maturity across every row.
610    pub aggregate_combined_maturity_e8s_equivalent: u64,
611    /// Number of principal permission entries observed across every row.
612    pub permission_entry_count: usize,
613    /// Number of raw unknown or anomalous permission codes observed globally and per-neuron.
614    pub unassessable_permission_code_count: usize,
615    /// Number of pending maturity disbursements observed across every row.
616    pub pending_maturity_disbursement_count: usize,
617    /// Number of neurons with automatic maturity staking enabled.
618    pub auto_stake_maturity_enabled_count: usize,
619    /// Number of neurons with automatic maturity staking explicitly disabled.
620    pub auto_stake_maturity_disabled_count: usize,
621    /// Number of neurons without an automatic maturity-staking value.
622    pub auto_stake_maturity_unspecified_count: usize,
623    /// Whether permission 2 is currently grantable, or unknown when the list is missing.
624    pub manage_principals_grantable: Option<bool>,
625    /// Global observation for disabling permissions 7 and 8 and pending disbursements.
626    pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
627    /// Global observation for disabling manual maturity-staking permission 9.
628    pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
629    /// Combined observed maturity-conversion policy status.
630    pub maturity_conversion_policy_observed_status: SnsPolicyObservationStatus,
631}
632
633pub(in crate::sns::report) fn recompute_reward_checkpoint_summary(
634    parameters: &SnsGovernanceParameters,
635    rows: &[SnsRewardCheckpointRow],
636) -> Result<SnsRewardCheckpointSummary, String> {
637    let mut summary = SnsRewardCheckpointSummary {
638        aggregate_maturity_e8s_equivalent: 0,
639        aggregate_staked_maturity_e8s_equivalent: 0,
640        aggregate_combined_maturity_e8s_equivalent: 0,
641        permission_entry_count: 0,
642        unassessable_permission_code_count: 0,
643        pending_maturity_disbursement_count: 0,
644        auto_stake_maturity_enabled_count: 0,
645        auto_stake_maturity_disabled_count: 0,
646        auto_stake_maturity_unspecified_count: 0,
647        manage_principals_grantable: parameters
648            .neuron_grantable_permissions
649            .as_ref()
650            .map(|permissions| permissions.permissions.contains(&2)),
651        maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
652        manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
653        maturity_conversion_policy_observed_status: SnsPolicyObservationStatus::ObservedSatisfied,
654    };
655    let (parameter_mint, parameter_staking, parameter_unknown_count) =
656        parameter_policy_observations(parameters);
657    summary.maturity_mint_conversion_observed_disabled = parameter_mint;
658    summary.manual_maturity_staking_observed_disabled = parameter_staking;
659    summary.unassessable_permission_code_count = parameter_unknown_count;
660
661    for row in rows {
662        accumulate_reward_row(&mut summary, row)?;
663    }
664    summary.maturity_conversion_policy_observed_status = summary
665        .maturity_mint_conversion_observed_disabled
666        .combine(summary.manual_maturity_staking_observed_disabled);
667    Ok(summary)
668}
669
670fn accumulate_reward_row(
671    summary: &mut SnsRewardCheckpointSummary,
672    row: &SnsRewardCheckpointRow,
673) -> Result<(), String> {
674    let combined = row.checked_combined_maturity().ok_or_else(|| {
675        format!(
676            "neuron {} combined maturity exceeds the native u64 contract",
677            row.neuron_id
678        )
679    })?;
680    let (row_mint, row_staking) = row.derived_policy_observations();
681    summary.aggregate_maturity_e8s_equivalent = checked_sum(
682        summary.aggregate_maturity_e8s_equivalent,
683        row.maturity_e8s_equivalent,
684        "aggregate maturity",
685    )?;
686    summary.aggregate_staked_maturity_e8s_equivalent = checked_sum(
687        summary.aggregate_staked_maturity_e8s_equivalent,
688        row.staked_maturity_e8s_equivalent.unwrap_or(0),
689        "aggregate staked maturity",
690    )?;
691    summary.aggregate_combined_maturity_e8s_equivalent = checked_sum(
692        summary.aggregate_combined_maturity_e8s_equivalent,
693        combined,
694        "aggregate combined maturity",
695    )?;
696    accumulate_reward_row_counts(summary, row)?;
697    summary.maturity_mint_conversion_observed_disabled = summary
698        .maturity_mint_conversion_observed_disabled
699        .combine(row_mint);
700    summary.manual_maturity_staking_observed_disabled = summary
701        .manual_maturity_staking_observed_disabled
702        .combine(row_staking);
703    Ok(())
704}
705
706fn accumulate_reward_row_counts(
707    summary: &mut SnsRewardCheckpointSummary,
708    row: &SnsRewardCheckpointRow,
709) -> Result<(), String> {
710    summary.permission_entry_count = summary
711        .permission_entry_count
712        .checked_add(row.permissions.len())
713        .ok_or_else(|| "permission entry count overflow".to_string())?;
714    summary.pending_maturity_disbursement_count = summary
715        .pending_maturity_disbursement_count
716        .checked_add(row.disburse_maturity_in_progress.len())
717        .ok_or_else(|| "pending maturity disbursement count overflow".to_string())?;
718    summary.unassessable_permission_code_count = summary
719        .unassessable_permission_code_count
720        .checked_add(
721            row.permissions
722                .iter()
723                .flat_map(|permission| permission.permission_types.iter())
724                .filter(|value| !permission_code_is_assessable(**value))
725                .count(),
726        )
727        .ok_or_else(|| "unassessable permission code count overflow".to_string())?;
728    match row.auto_stake_maturity {
729        Some(true) => increment(&mut summary.auto_stake_maturity_enabled_count)?,
730        Some(false) => increment(&mut summary.auto_stake_maturity_disabled_count)?,
731        None => increment(&mut summary.auto_stake_maturity_unspecified_count)?,
732    }
733    Ok(())
734}
735
736fn parameter_policy_observations(
737    parameters: &SnsGovernanceParameters,
738) -> (
739    SnsPolicyObservationStatus,
740    SnsPolicyObservationStatus,
741    usize,
742) {
743    let mut mint = SnsPolicyObservationStatus::ObservedSatisfied;
744    let mut staking = SnsPolicyObservationStatus::ObservedSatisfied;
745    let mut unknown_count = 0;
746    for permissions in [
747        parameters.neuron_claimer_permissions.as_ref(),
748        parameters.neuron_grantable_permissions.as_ref(),
749    ] {
750        let Some(permissions) = permissions else {
751            mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
752            staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
753            continue;
754        };
755        if permissions.permissions.is_empty() {
756            mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
757            staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
758        }
759        for code in &permissions.permissions {
760            let (code_mint, code_staking) = permission_code_policy_observations(*code);
761            mint = mint.combine(code_mint);
762            staking = staking.combine(code_staking);
763            if !permission_code_is_assessable(*code) {
764                unknown_count += 1;
765            }
766        }
767    }
768    (mint, staking, unknown_count)
769}
770
771fn checked_sum(left: u64, right: u64, field: &str) -> Result<u64, String> {
772    left.checked_add(right)
773        .ok_or_else(|| format!("{field} exceeds the native u64 contract"))
774}
775
776fn increment(value: &mut usize) -> Result<(), String> {
777    *value = value
778        .checked_add(1)
779        .ok_or_else(|| "checkpoint count overflow".to_string())?;
780    Ok(())
781}