1use 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_PAGE_SIZE, SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION,
21 model::reports::{SnsGovernanceParameters, SnsRunningVersionResponse},
22 },
23 subnet_catalog::MAINNET_NETWORK,
24};
25use ic_host_text::timestamp::format_utc_timestamp_secs;
26use ic_platform::candid::contract::{CandidType, serde::Deserialize};
27use serde::Serialize;
28use std::collections::HashSet;
29use thiserror::Error as ThisError;
30
31#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)]
38#[candid_path("ic_platform::candid::contract")]
39#[serde(deny_unknown_fields)]
40pub struct SnsRewardProposalId {
41 pub id: u64,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)]
52#[candid_path("ic_platform::candid::contract")]
53#[serde(deny_unknown_fields)]
54pub struct SnsRewardEvent {
55 pub rounds_since_last_distribution: Option<u64>,
57 pub actual_timestamp_seconds: u64,
59 pub end_timestamp_seconds: Option<u64>,
61 pub total_available_e8s_equivalent: Option<u64>,
63 pub distributed_e8s_equivalent: u64,
65 pub round: u64,
67 pub settled_proposals: Vec<SnsRewardProposalId>,
69}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
78#[serde(rename_all = "snake_case")]
79pub enum SnsRewardCollectionStatus {
80 ApiExhaustedObserved,
82}
83
84impl SnsRewardCollectionStatus {
85 #[must_use]
87 pub const fn as_str(self) -> &'static str {
88 match self {
89 Self::ApiExhaustedObserved => "api_exhausted_observed",
90 }
91 }
92}
93
94#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
101#[serde(deny_unknown_fields)]
102pub struct SnsRewardCheckpointRow {
103 pub neuron_id: String,
105 pub created_timestamp_seconds: u64,
107 pub maturity_e8s_equivalent: u64,
109 pub staked_maturity_e8s_equivalent: Option<u64>,
111 pub combined_maturity_e8s_equivalent: u64,
113 pub auto_stake_maturity: Option<bool>,
115 pub permissions: Vec<SnsNeuronPermissionRow>,
117 pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
119 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
121 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
123}
124
125impl SnsRewardCheckpointRow {
126 #[must_use]
128 pub fn checked_combined_maturity(&self) -> Option<u64> {
129 self.maturity_e8s_equivalent
130 .checked_add(self.staked_maturity_e8s_equivalent.unwrap_or(0))
131 }
132
133 #[must_use]
135 pub fn derived_policy_observations(
136 &self,
137 ) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
138 neuron_policy_observations(
139 &self.permissions,
140 !self.disburse_maturity_in_progress.is_empty(),
141 )
142 }
143}
144
145#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
152#[serde(deny_unknown_fields)]
153pub struct SnsRewardCheckpointReport {
154 pub schema_version: u32,
156 pub network: String,
158 pub sns_wasm_canister_id: String,
160 pub source_endpoint: String,
162 pub fetched_by: String,
164 pub id: usize,
166 pub name: String,
168 #[serde(flatten)]
170 pub canisters: MainnetSnsCanisters,
171 pub data_source: ReportDataSource,
173 pub collection_started_at_unix_secs: u64,
175 pub collection_started_at: String,
177 pub collection_completed_at_unix_secs: u64,
179 pub collection_completed_at: String,
181 pub page_size: u32,
183 pub page_count: u32,
185 pub row_count: usize,
187 pub unique_neuron_id_count: usize,
189 pub collection_row_ceiling: u64,
191 pub client_query_count: u32,
193 pub collection_status: SnsRewardCollectionStatus,
195 pub point_in_time_guaranteed: bool,
197 pub parameters_before: SnsGovernanceParameters,
199 pub parameters_after: SnsGovernanceParameters,
201 pub reward_event_before: SnsRewardEvent,
203 pub reward_event_after: SnsRewardEvent,
205 pub running_version_before: SnsRunningVersionResponse,
207 pub running_version_after: SnsRunningVersionResponse,
209 #[serde(flatten)]
211 pub summary: SnsRewardCheckpointSummary,
212 pub rows: Vec<SnsRewardCheckpointRow>,
214}
215
216#[derive(Debug, Eq, PartialEq, ThisError)]
223#[error("invalid SNS reward checkpoint: {reason}")]
224pub struct SnsRewardCheckpointValidationError {
225 pub reason: String,
227}
228
229pub fn validate_sns_reward_checkpoint_report(
231 report: &SnsRewardCheckpointReport,
232) -> Result<(), SnsRewardCheckpointValidationError> {
233 validate_checkpoint_evidence(report)?;
234 let summary = recompute_reward_checkpoint_summary(&report.parameters_before, &report.rows)
235 .map_err(invalid_validation)?;
236 validate_checkpoint_summary(report, &summary)
237}
238
239pub(in crate::sns::report) enum RewardCheckpointAssessment {
246 Valid(SnsRewardCheckpointSummary),
247 Invalid {
248 error: SnsRewardCheckpointValidationError,
249 summary: Option<SnsRewardCheckpointSummary>,
250 },
251}
252
253impl RewardCheckpointAssessment {
254 pub(in crate::sns::report) const fn summary(&self) -> Option<&SnsRewardCheckpointSummary> {
255 match self {
256 Self::Valid(summary) => Some(summary),
257 Self::Invalid { summary, .. } => summary.as_ref(),
258 }
259 }
260}
261
262pub(in crate::sns::report) fn assess_reward_checkpoint(
264 report: &SnsRewardCheckpointReport,
265) -> RewardCheckpointAssessment {
266 let evidence = validate_checkpoint_evidence(report);
267 match recompute_reward_checkpoint_summary(&report.parameters_before, &report.rows) {
268 Ok(summary) => {
269 match evidence.and_then(|()| validate_checkpoint_summary(report, &summary)) {
270 Ok(()) => RewardCheckpointAssessment::Valid(summary),
271 Err(error) => RewardCheckpointAssessment::Invalid {
272 error,
273 summary: Some(summary),
274 },
275 }
276 }
277 Err(reason) => RewardCheckpointAssessment::Invalid {
278 error: evidence.err().unwrap_or_else(|| invalid_validation(reason)),
279 summary: None,
280 },
281 }
282}
283
284impl SnsRewardCheckpointReport {
285 #[cfg(feature = "sns-host")]
287 pub(in crate::sns::report) fn validate_collected_evidence(
288 &self,
289 ) -> Result<(), SnsRewardCheckpointValidationError> {
290 validate_checkpoint_evidence(self)
291 }
292}
293
294fn validate_checkpoint_evidence(
295 report: &SnsRewardCheckpointReport,
296) -> Result<(), SnsRewardCheckpointValidationError> {
297 validate_checkpoint_header(report)?;
298 validate_checkpoint_brackets(report)?;
299 validate_checkpoint_rows(&report.rows, report.collection_completed_at_unix_secs)
300}
301
302fn validate_checkpoint_header(
303 report: &SnsRewardCheckpointReport,
304) -> Result<(), SnsRewardCheckpointValidationError> {
305 if report.schema_version != SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION {
306 return Err(invalid_validation(format!(
307 "schema version {} does not equal {}",
308 report.schema_version, SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION
309 )));
310 }
311 for (field, principal) in [
312 ("sns_wasm_canister_id", report.sns_wasm_canister_id.as_str()),
313 (
314 "root_canister_id",
315 report.canisters.root_canister_id.as_str(),
316 ),
317 (
318 "governance_canister_id",
319 report.canisters.governance_canister_id.as_str(),
320 ),
321 (
322 "ledger_canister_id",
323 report.canisters.ledger_canister_id.as_str(),
324 ),
325 (
326 "swap_canister_id",
327 report.canisters.swap_canister_id.as_str(),
328 ),
329 (
330 "index_canister_id",
331 report.canisters.index_canister_id.as_str(),
332 ),
333 ] {
334 validate_canonical_principal(principal, field).map_err(invalid_validation)?;
335 }
336 let canister_ids = [
337 report.sns_wasm_canister_id.as_str(),
338 report.canisters.root_canister_id.as_str(),
339 report.canisters.governance_canister_id.as_str(),
340 report.canisters.ledger_canister_id.as_str(),
341 report.canisters.swap_canister_id.as_str(),
342 report.canisters.index_canister_id.as_str(),
343 ];
344 if canister_ids.into_iter().collect::<HashSet<_>>().len() != canister_ids.len() {
345 return Err(invalid_validation(
346 "checkpoint canister roles do not contain unique principals",
347 ));
348 }
349 if report.sns_wasm_canister_id != MAINNET_SNS_WASM_CANISTER_ID {
350 return Err(invalid_validation("unexpected SNS-W canister identity"));
351 }
352 if report.network != MAINNET_NETWORK {
353 return Err(invalid_validation("checkpoint network must be ic"));
354 }
355 if report.source_endpoint.is_empty() || report.fetched_by.is_empty() {
356 return Err(invalid_validation(
357 "checkpoint source_endpoint and fetched_by must be non-empty",
358 ));
359 }
360 if report.id == 0 || report.name.is_empty() {
361 return Err(invalid_validation(
362 "checkpoint SNS list id and display name must be non-empty",
363 ));
364 }
365 if report.data_source != ReportDataSource::Live {
366 return Err(invalid_validation("checkpoint data_source must be live"));
367 }
368 if report.collection_status != SnsRewardCollectionStatus::ApiExhaustedObserved {
369 return Err(invalid_validation(
370 "checkpoint collection is not API-exhausted",
371 ));
372 }
373 if report.point_in_time_guaranteed {
374 return Err(invalid_validation(
375 "checkpoint cannot claim a point-in-time neuron snapshot",
376 ));
377 }
378 validate_checkpoint_collection_metadata(report)
379}
380
381fn validate_checkpoint_collection_metadata(
382 report: &SnsRewardCheckpointReport,
383) -> Result<(), SnsRewardCheckpointValidationError> {
384 if report.collection_completed_at_unix_secs < report.collection_started_at_unix_secs {
385 return Err(invalid_validation(
386 "collection completion precedes collection start",
387 ));
388 }
389 if report.collection_started_at
390 != format_utc_timestamp_secs(report.collection_started_at_unix_secs)
391 || report.collection_completed_at
392 != format_utc_timestamp_secs(report.collection_completed_at_unix_secs)
393 {
394 return Err(invalid_validation(
395 "collection UTC timestamps do not match raw Unix timestamps",
396 ));
397 }
398 if report.page_size != SNS_REWARD_CHECKPOINT_PAGE_SIZE || report.page_count == 0 {
399 return Err(invalid_validation(
400 "checkpoint must contain at least one fixed-size-100 page",
401 ));
402 }
403 let expected_query_count = report
404 .page_count
405 .checked_add(8)
406 .ok_or_else(|| invalid_validation("page_count + 8 exceeds the native u32 contract"))?;
407 if report.client_query_count != expected_query_count {
408 return Err(invalid_validation(
409 "client_query_count does not equal page_count + 8",
410 ));
411 }
412 if report.row_count != report.rows.len() || report.unique_neuron_id_count != report.rows.len() {
413 return Err(invalid_validation(
414 "serialized row counts do not match raw checkpoint rows",
415 ));
416 }
417 if reward_checkpoint_neuron_ceiling(&report.parameters_before)
418 != Some(report.collection_row_ceiling)
419 {
420 return Err(invalid_validation(
421 "collection_row_ceiling does not match a valid max_number_of_neurons",
422 ));
423 }
424 if u64::try_from(report.rows.len())
425 .ok()
426 .is_none_or(|rows| rows > report.collection_row_ceiling)
427 {
428 return Err(invalid_validation(
429 "raw row count exceeds the mandatory collection ceiling",
430 ));
431 }
432 Ok(())
433}
434
435pub(in crate::sns::report) fn reward_checkpoint_neuron_ceiling(
436 parameters: &SnsGovernanceParameters,
437) -> Option<u64> {
438 parameters
439 .max_number_of_neurons
440 .filter(|value| (1..=SNS_REWARD_CHECKPOINT_MAX_NEURONS).contains(value))
441}
442
443pub(in crate::sns::report) fn reward_checkpoint_bracket_mismatch(
445 parameters: (&SnsGovernanceParameters, &SnsGovernanceParameters),
446 events: (&SnsRewardEvent, &SnsRewardEvent),
447 versions: (&SnsRunningVersionResponse, &SnsRunningVersionResponse),
448) -> Option<(&'static str, &'static str)> {
449 [
450 (
451 "nervous-system parameters",
452 "nervous-system parameter brackets differ",
453 parameters.0 == parameters.1,
454 ),
455 (
456 "reward event",
457 "reward-event brackets differ",
458 events.0 == events.1,
459 ),
460 (
461 "running SNS version",
462 "running-version brackets differ",
463 versions.0 == versions.1,
464 ),
465 ]
466 .into_iter()
467 .find_map(|(component, reason, stable)| (!stable).then_some((component, reason)))
468}
469
470fn validate_checkpoint_brackets(
471 report: &SnsRewardCheckpointReport,
472) -> Result<(), SnsRewardCheckpointValidationError> {
473 if let Some((_, reason)) = reward_checkpoint_bracket_mismatch(
474 (&report.parameters_before, &report.parameters_after),
475 (&report.reward_event_before, &report.reward_event_after),
476 (
477 &report.running_version_before,
478 &report.running_version_after,
479 ),
480 ) {
481 return Err(invalid_validation(reason));
482 }
483 validate_sns_reward_event_evidence(&report.reward_event_after).map_err(invalid_validation)?;
484 validate_reward_event_position(report)?;
485 validate_sns_reward_running_version_evidence(&report.running_version_after)
486 .map_err(invalid_validation)?;
487 validate_sns_reward_checkpoint_parameter_evidence(&report.parameters_after)
488 .map_err(invalid_validation)
489}
490
491fn validate_reward_event_position(
492 report: &SnsRewardCheckpointReport,
493) -> Result<(), SnsRewardCheckpointValidationError> {
494 let event = &report.reward_event_after;
495 if event.actual_timestamp_seconds > report.collection_completed_at_unix_secs
496 || event
497 .end_timestamp_seconds
498 .is_none_or(|end| end > report.collection_completed_at_unix_secs)
499 {
500 return Err(invalid_validation(
501 "reward event timestamps exceed checkpoint collection completion",
502 ));
503 }
504 Ok(())
505}
506
507fn validate_checkpoint_rows(
508 rows: &[SnsRewardCheckpointRow],
509 collection_completed_at_unix_secs: u64,
510) -> Result<(), SnsRewardCheckpointValidationError> {
511 let mut previous_id: Option<&str> = None;
512 for row in rows {
513 if !is_lowercase_hex_32_bytes(&row.neuron_id) {
514 return Err(invalid_validation(format!(
515 "neuron id {} is not 32-byte lowercase hexadecimal text",
516 row.neuron_id
517 )));
518 }
519 if previous_id.is_some_and(|previous| previous >= row.neuron_id.as_str()) {
520 return Err(invalid_validation(
521 "checkpoint neuron ids are not strictly increasing",
522 ));
523 }
524 previous_id = Some(&row.neuron_id);
525 if row.created_timestamp_seconds > collection_completed_at_unix_secs {
526 return Err(invalid_validation(format!(
527 "neuron {} creation timestamp exceeds collection completion",
528 row.neuron_id
529 )));
530 }
531 validate_neuron_permissions(&row.permissions, false)
532 .map_err(|reason| invalid_validation(format!("neuron {}: {reason}", row.neuron_id)))?;
533 validate_maturity_disbursements(&row.disburse_maturity_in_progress)
534 .map_err(invalid_validation)?;
535 }
536 Ok(())
537}
538
539fn validate_checkpoint_summary(
540 report: &SnsRewardCheckpointReport,
541 summary: &SnsRewardCheckpointSummary,
542) -> Result<(), SnsRewardCheckpointValidationError> {
543 if report.summary == *summary {
544 Ok(())
545 } else {
546 Err(invalid_validation(
547 "serialized checkpoint summary does not match raw evidence",
548 ))
549 }
550}
551
552pub(in crate::sns::report) fn validate_sns_reward_running_version_evidence(
553 response: &SnsRunningVersionResponse,
554) -> Result<(), String> {
555 let deployed = response
556 .deployed_version
557 .as_ref()
558 .ok_or_else(|| "running version has no deployed_version".to_string())?;
559 deployed.validate_hashes("deployed_version", Some(32))?;
560 if let Some(target) = response
561 .pending_version
562 .as_ref()
563 .and_then(|pending| pending.target_version.as_ref())
564 {
565 target.validate_hashes("pending_version.target_version", Some(32))?;
566 }
567 Ok(())
568}
569
570pub(in crate::sns::report) fn validate_sns_reward_checkpoint_parameter_evidence(
571 parameters: &SnsGovernanceParameters,
572) -> Result<(), String> {
573 validate_default_followees(parameters)?;
574 validate_parameter_permissions(parameters)
575}
576
577fn validate_default_followees(parameters: &SnsGovernanceParameters) -> Result<(), String> {
578 let Some(defaults) = parameters.default_followees.as_ref() else {
579 return Ok(());
580 };
581 let mut function_ids = HashSet::new();
582 for row in &defaults.followees {
583 if !function_ids.insert(row.function_id) {
584 return Err("default followees contain a duplicate function id".to_string());
585 }
586 let mut neuron_ids = HashSet::new();
587 for id in &row.followee_neuron_ids {
588 if !is_lowercase_hex_32_bytes(id) {
589 return Err("default followees contain a non-canonical neuron id".to_string());
590 }
591 if !neuron_ids.insert(id) {
592 return Err("default followees contain a duplicate neuron id".to_string());
593 }
594 }
595 }
596 Ok(())
597}
598
599fn validate_parameter_permissions(parameters: &SnsGovernanceParameters) -> Result<(), String> {
600 for (field, permissions) in [
601 (
602 "neuron_claimer_permissions",
603 parameters.neuron_claimer_permissions.as_ref(),
604 ),
605 (
606 "neuron_grantable_permissions",
607 parameters.neuron_grantable_permissions.as_ref(),
608 ),
609 ] {
610 let Some(permissions) = permissions else {
611 continue;
612 };
613 let mut codes = HashSet::new();
614 if permissions
615 .permissions
616 .iter()
617 .any(|code| !codes.insert(*code))
618 {
619 return Err(format!("{field} contains a duplicate permission code"));
620 }
621 }
622 Ok(())
623}
624
625pub(in crate::sns::report) fn validate_sns_reward_event_evidence(
626 event: &SnsRewardEvent,
627) -> Result<(), String> {
628 if event.end_timestamp_seconds.is_none() {
629 return Err("reward event is missing canonical end_timestamp_seconds".to_string());
630 }
631 let mut proposal_ids = HashSet::new();
632 if event
633 .settled_proposals
634 .iter()
635 .any(|proposal| !proposal_ids.insert(proposal.id))
636 {
637 return Err("reward event contains duplicate settled proposal ids".to_string());
638 }
639 Ok(())
640}
641
642fn invalid_validation(reason: impl Into<String>) -> SnsRewardCheckpointValidationError {
643 SnsRewardCheckpointValidationError {
644 reason: reason.into(),
645 }
646}
647
648#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
655#[serde(deny_unknown_fields)]
656pub struct SnsRewardCheckpointSummary {
657 pub aggregate_maturity_e8s_equivalent: u64,
659 pub aggregate_staked_maturity_e8s_equivalent: u64,
661 pub aggregate_combined_maturity_e8s_equivalent: u64,
663 pub permission_entry_count: usize,
665 pub unassessable_permission_code_count: usize,
667 pub pending_maturity_disbursement_count: usize,
669 pub auto_stake_maturity_enabled_count: usize,
671 pub auto_stake_maturity_disabled_count: usize,
673 pub auto_stake_maturity_unspecified_count: usize,
675 pub manage_principals_grantable: Option<bool>,
677 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
679 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
681 pub maturity_conversion_policy_observed_status: SnsPolicyObservationStatus,
683}
684
685pub(in crate::sns::report) fn recompute_reward_checkpoint_summary(
686 parameters: &SnsGovernanceParameters,
687 rows: &[SnsRewardCheckpointRow],
688) -> Result<SnsRewardCheckpointSummary, String> {
689 let mut summary = SnsRewardCheckpointSummary {
690 aggregate_maturity_e8s_equivalent: 0,
691 aggregate_staked_maturity_e8s_equivalent: 0,
692 aggregate_combined_maturity_e8s_equivalent: 0,
693 permission_entry_count: 0,
694 unassessable_permission_code_count: 0,
695 pending_maturity_disbursement_count: 0,
696 auto_stake_maturity_enabled_count: 0,
697 auto_stake_maturity_disabled_count: 0,
698 auto_stake_maturity_unspecified_count: 0,
699 manage_principals_grantable: parameters
700 .neuron_grantable_permissions
701 .as_ref()
702 .map(|permissions| permissions.permissions.contains(&2)),
703 maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
704 manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
705 maturity_conversion_policy_observed_status: SnsPolicyObservationStatus::ObservedSatisfied,
706 };
707 let (parameter_mint, parameter_staking, parameter_unknown_count) =
708 parameter_policy_observations(parameters);
709 summary.maturity_mint_conversion_observed_disabled = parameter_mint;
710 summary.manual_maturity_staking_observed_disabled = parameter_staking;
711 summary.unassessable_permission_code_count = parameter_unknown_count;
712
713 for row in rows {
714 accumulate_reward_row(&mut summary, row)?;
715 }
716 summary.maturity_conversion_policy_observed_status = summary
717 .maturity_mint_conversion_observed_disabled
718 .combine(summary.manual_maturity_staking_observed_disabled);
719 Ok(summary)
720}
721
722fn accumulate_reward_row(
723 summary: &mut SnsRewardCheckpointSummary,
724 row: &SnsRewardCheckpointRow,
725) -> Result<(), String> {
726 let combined = row.checked_combined_maturity().ok_or_else(|| {
727 format!(
728 "neuron {} combined maturity exceeds the native u64 contract",
729 row.neuron_id
730 )
731 })?;
732 if row.combined_maturity_e8s_equivalent != combined {
733 return Err(format!(
734 "neuron {} combined maturity does not match raw components",
735 row.neuron_id
736 ));
737 }
738 let (row_mint, row_staking) = row.derived_policy_observations();
739 if row.maturity_mint_conversion_observed_disabled != row_mint
740 || row.manual_maturity_staking_observed_disabled != row_staking
741 {
742 return Err(format!(
743 "neuron {} policy observations do not match raw evidence",
744 row.neuron_id
745 ));
746 }
747 summary.aggregate_maturity_e8s_equivalent = checked_sum(
748 summary.aggregate_maturity_e8s_equivalent,
749 row.maturity_e8s_equivalent,
750 "aggregate maturity",
751 )?;
752 summary.aggregate_staked_maturity_e8s_equivalent = checked_sum(
753 summary.aggregate_staked_maturity_e8s_equivalent,
754 row.staked_maturity_e8s_equivalent.unwrap_or(0),
755 "aggregate staked maturity",
756 )?;
757 summary.aggregate_combined_maturity_e8s_equivalent = checked_sum(
758 summary.aggregate_combined_maturity_e8s_equivalent,
759 combined,
760 "aggregate combined maturity",
761 )?;
762 accumulate_reward_row_counts(summary, row)?;
763 summary.maturity_mint_conversion_observed_disabled = summary
764 .maturity_mint_conversion_observed_disabled
765 .combine(row_mint);
766 summary.manual_maturity_staking_observed_disabled = summary
767 .manual_maturity_staking_observed_disabled
768 .combine(row_staking);
769 Ok(())
770}
771
772fn accumulate_reward_row_counts(
773 summary: &mut SnsRewardCheckpointSummary,
774 row: &SnsRewardCheckpointRow,
775) -> Result<(), String> {
776 summary.permission_entry_count = summary
777 .permission_entry_count
778 .checked_add(row.permissions.len())
779 .ok_or_else(|| "permission entry count overflow".to_string())?;
780 summary.pending_maturity_disbursement_count = summary
781 .pending_maturity_disbursement_count
782 .checked_add(row.disburse_maturity_in_progress.len())
783 .ok_or_else(|| "pending maturity disbursement count overflow".to_string())?;
784 summary.unassessable_permission_code_count = summary
785 .unassessable_permission_code_count
786 .checked_add(
787 row.permissions
788 .iter()
789 .flat_map(|permission| permission.permission_types.iter())
790 .filter(|value| !permission_code_is_assessable(value.code))
791 .count(),
792 )
793 .ok_or_else(|| "unassessable permission code count overflow".to_string())?;
794 match row.auto_stake_maturity {
795 Some(true) => increment(&mut summary.auto_stake_maturity_enabled_count)?,
796 Some(false) => increment(&mut summary.auto_stake_maturity_disabled_count)?,
797 None => increment(&mut summary.auto_stake_maturity_unspecified_count)?,
798 }
799 Ok(())
800}
801
802fn parameter_policy_observations(
803 parameters: &SnsGovernanceParameters,
804) -> (
805 SnsPolicyObservationStatus,
806 SnsPolicyObservationStatus,
807 usize,
808) {
809 let mut mint = SnsPolicyObservationStatus::ObservedSatisfied;
810 let mut staking = SnsPolicyObservationStatus::ObservedSatisfied;
811 let mut unknown_count = 0;
812 for permissions in [
813 parameters.neuron_claimer_permissions.as_ref(),
814 parameters.neuron_grantable_permissions.as_ref(),
815 ] {
816 let Some(permissions) = permissions else {
817 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
818 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
819 continue;
820 };
821 if permissions.permissions.is_empty() {
822 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
823 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
824 }
825 for code in &permissions.permissions {
826 let (code_mint, code_staking) = permission_code_policy_observations(*code);
827 mint = mint.combine(code_mint);
828 staking = staking.combine(code_staking);
829 if !permission_code_is_assessable(*code) {
830 unknown_count += 1;
831 }
832 }
833 }
834 (mint, staking, unknown_count)
835}
836
837fn checked_sum(left: u64, right: u64, field: &str) -> Result<u64, String> {
838 left.checked_add(right)
839 .ok_or_else(|| format!("{field} exceeds the native u64 contract"))
840}
841
842fn increment(value: &mut usize) -> Result<(), String> {
843 *value = value
844 .checked_add(1)
845 .ok_or_else(|| "checkpoint count overflow".to_string())?;
846 Ok(())
847}