1use super::{
8 SnsMaturityDisbursementRow, SnsNeuronPermissionRow,
9 detail::{
10 SnsPolicyObservationStatus, neuron_policy_observations,
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, 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 pub root_canister_id: String,
170 pub governance_canister_id: String,
172 pub ledger_canister_id: String,
174 pub swap_canister_id: String,
176 pub index_canister_id: String,
178 pub data_source: ReportDataSource,
180 pub collection_started_at_unix_secs: u64,
182 pub collection_started_at: String,
184 pub collection_completed_at_unix_secs: u64,
186 pub collection_completed_at: String,
188 pub page_size: u32,
190 pub page_count: u32,
192 pub row_count: usize,
194 pub unique_neuron_id_count: usize,
196 pub collection_row_ceiling: u64,
198 pub client_query_count: u32,
200 pub collection_status: SnsRewardCollectionStatus,
202 pub point_in_time_guaranteed: bool,
204 pub parameters_before: SnsGovernanceParameters,
206 pub parameters_after: SnsGovernanceParameters,
208 pub reward_event_before: SnsRewardEvent,
210 pub reward_event_after: SnsRewardEvent,
212 pub running_version_before: SnsRunningVersionResponse,
214 pub running_version_after: SnsRunningVersionResponse,
216 pub aggregate_maturity_e8s_equivalent: u64,
218 pub aggregate_staked_maturity_e8s_equivalent: u64,
220 pub aggregate_combined_maturity_e8s_equivalent: u64,
222 pub permission_entry_count: usize,
224 pub unassessable_permission_code_count: usize,
226 pub pending_maturity_disbursement_count: usize,
228 pub auto_stake_maturity_enabled_count: usize,
230 pub auto_stake_maturity_disabled_count: usize,
232 pub auto_stake_maturity_unspecified_count: usize,
234 pub manage_principals_grantable: Option<bool>,
236 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
238 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
240 pub maturity_conversion_policy_observed_status: SnsPolicyObservationStatus,
242 pub rows: Vec<SnsRewardCheckpointRow>,
244}
245
246#[derive(Debug, Eq, PartialEq, ThisError)]
253#[error("invalid SNS reward checkpoint: {reason}")]
254pub struct SnsRewardCheckpointValidationError {
255 pub reason: String,
257}
258
259pub fn validate_sns_reward_checkpoint_report(
261 report: &SnsRewardCheckpointReport,
262) -> Result<(), SnsRewardCheckpointValidationError> {
263 validated_reward_checkpoint_summary(report).map(|_| ())
264}
265
266pub(in crate::sns::report) fn validated_reward_checkpoint_summary(
268 report: &SnsRewardCheckpointReport,
269) -> Result<SnsRewardCheckpointSummary, SnsRewardCheckpointValidationError> {
270 validate_checkpoint_evidence(report)?;
271 let summary = recompute_reward_checkpoint_summary(&report.parameters_before, &report.rows)
272 .map_err(invalid_validation)?;
273 validate_checkpoint_summary(report, &summary)?;
274 Ok(summary)
275}
276
277impl SnsRewardCheckpointReport {
278 #[cfg(feature = "sns-host")]
280 pub(in crate::sns::report) fn validate_collected_summary(
281 &self,
282 summary: &SnsRewardCheckpointSummary,
283 ) -> Result<(), SnsRewardCheckpointValidationError> {
284 validate_checkpoint_evidence(self)?;
285 validate_checkpoint_summary(self, summary)
286 }
287}
288
289fn validate_checkpoint_evidence(
290 report: &SnsRewardCheckpointReport,
291) -> Result<(), SnsRewardCheckpointValidationError> {
292 validate_checkpoint_header(report)?;
293 validate_checkpoint_brackets(report)?;
294 validate_checkpoint_rows(&report.rows, report.collection_completed_at_unix_secs)
295}
296
297fn validate_checkpoint_header(
298 report: &SnsRewardCheckpointReport,
299) -> Result<(), SnsRewardCheckpointValidationError> {
300 if report.schema_version != SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION {
301 return Err(invalid_validation(format!(
302 "schema version {} does not equal {}",
303 report.schema_version, SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION
304 )));
305 }
306 for (field, principal) in [
307 ("sns_wasm_canister_id", report.sns_wasm_canister_id.as_str()),
308 ("root_canister_id", report.root_canister_id.as_str()),
309 (
310 "governance_canister_id",
311 report.governance_canister_id.as_str(),
312 ),
313 ("ledger_canister_id", report.ledger_canister_id.as_str()),
314 ("swap_canister_id", report.swap_canister_id.as_str()),
315 ("index_canister_id", report.index_canister_id.as_str()),
316 ] {
317 validate_canonical_principal(principal, field).map_err(invalid_validation)?;
318 }
319 let canister_ids = [
320 report.sns_wasm_canister_id.as_str(),
321 report.root_canister_id.as_str(),
322 report.governance_canister_id.as_str(),
323 report.ledger_canister_id.as_str(),
324 report.swap_canister_id.as_str(),
325 report.index_canister_id.as_str(),
326 ];
327 if canister_ids.into_iter().collect::<HashSet<_>>().len() != canister_ids.len() {
328 return Err(invalid_validation(
329 "checkpoint canister roles do not contain unique principals",
330 ));
331 }
332 if report.sns_wasm_canister_id != MAINNET_SNS_WASM_CANISTER_ID {
333 return Err(invalid_validation("unexpected SNS-W canister identity"));
334 }
335 if report.network != MAINNET_NETWORK {
336 return Err(invalid_validation("checkpoint network must be ic"));
337 }
338 if report.source_endpoint.is_empty() || report.fetched_by.is_empty() {
339 return Err(invalid_validation(
340 "checkpoint source_endpoint and fetched_by must be non-empty",
341 ));
342 }
343 if report.id == 0 || report.name.is_empty() {
344 return Err(invalid_validation(
345 "checkpoint SNS list id and display name must be non-empty",
346 ));
347 }
348 if report.data_source != ReportDataSource::Live {
349 return Err(invalid_validation("checkpoint data_source must be live"));
350 }
351 if report.collection_status != SnsRewardCollectionStatus::ApiExhaustedObserved {
352 return Err(invalid_validation(
353 "checkpoint collection is not API-exhausted",
354 ));
355 }
356 if report.point_in_time_guaranteed {
357 return Err(invalid_validation(
358 "checkpoint cannot claim a point-in-time neuron snapshot",
359 ));
360 }
361 validate_checkpoint_collection_metadata(report)
362}
363
364fn validate_checkpoint_collection_metadata(
365 report: &SnsRewardCheckpointReport,
366) -> Result<(), SnsRewardCheckpointValidationError> {
367 if report.collection_completed_at_unix_secs < report.collection_started_at_unix_secs {
368 return Err(invalid_validation(
369 "collection completion precedes collection start",
370 ));
371 }
372 if report.collection_started_at
373 != format_utc_timestamp_secs(report.collection_started_at_unix_secs)
374 || report.collection_completed_at
375 != format_utc_timestamp_secs(report.collection_completed_at_unix_secs)
376 {
377 return Err(invalid_validation(
378 "collection UTC timestamps do not match raw Unix timestamps",
379 ));
380 }
381 if report.page_size != SNS_REWARD_CHECKPOINT_PAGE_SIZE || report.page_count == 0 {
382 return Err(invalid_validation(
383 "checkpoint must contain at least one fixed-size-100 page",
384 ));
385 }
386 let expected_query_count = report
387 .page_count
388 .checked_add(8)
389 .ok_or_else(|| invalid_validation("page_count + 8 exceeds the native u32 contract"))?;
390 if report.client_query_count != expected_query_count {
391 return Err(invalid_validation(
392 "client_query_count does not equal page_count + 8",
393 ));
394 }
395 if report.row_count != report.rows.len() || report.unique_neuron_id_count != report.rows.len() {
396 return Err(invalid_validation(
397 "serialized row counts do not match raw checkpoint rows",
398 ));
399 }
400 let max_neurons = report.parameters_before.max_number_of_neurons;
401 if max_neurons != Some(report.collection_row_ceiling)
402 || !(1..=SNS_REWARD_CHECKPOINT_MAX_NEURONS).contains(&report.collection_row_ceiling)
403 {
404 return Err(invalid_validation(
405 "collection_row_ceiling does not match a valid max_number_of_neurons",
406 ));
407 }
408 if u64::try_from(report.rows.len())
409 .ok()
410 .is_none_or(|rows| rows > report.collection_row_ceiling)
411 {
412 return Err(invalid_validation(
413 "raw row count exceeds the mandatory collection ceiling",
414 ));
415 }
416 Ok(())
417}
418
419fn validate_checkpoint_brackets(
420 report: &SnsRewardCheckpointReport,
421) -> Result<(), SnsRewardCheckpointValidationError> {
422 if report.parameters_before != report.parameters_after {
423 return Err(invalid_validation(
424 "nervous-system parameter brackets differ",
425 ));
426 }
427 if report.reward_event_before != report.reward_event_after {
428 return Err(invalid_validation("reward-event brackets differ"));
429 }
430 if report.running_version_before != report.running_version_after {
431 return Err(invalid_validation("running-version brackets differ"));
432 }
433 validate_sns_reward_event_evidence(&report.reward_event_after).map_err(invalid_validation)?;
434 validate_reward_event_position(report)?;
435 validate_sns_reward_running_version_evidence(&report.running_version_after)
436 .map_err(invalid_validation)?;
437 validate_sns_reward_checkpoint_parameter_evidence(&report.parameters_after)
438 .map_err(invalid_validation)
439}
440
441fn validate_reward_event_position(
442 report: &SnsRewardCheckpointReport,
443) -> Result<(), SnsRewardCheckpointValidationError> {
444 let event = &report.reward_event_after;
445 if event.actual_timestamp_seconds > report.collection_completed_at_unix_secs
446 || event
447 .end_timestamp_seconds
448 .is_none_or(|end| end > report.collection_completed_at_unix_secs)
449 {
450 return Err(invalid_validation(
451 "reward event timestamps exceed checkpoint collection completion",
452 ));
453 }
454 Ok(())
455}
456
457fn validate_checkpoint_rows(
458 rows: &[SnsRewardCheckpointRow],
459 collection_completed_at_unix_secs: u64,
460) -> Result<(), SnsRewardCheckpointValidationError> {
461 let mut previous_id: Option<&str> = None;
462 for row in rows {
463 if !is_lowercase_hex_32_bytes(&row.neuron_id) {
464 return Err(invalid_validation(format!(
465 "neuron id {} is not 32-byte lowercase hexadecimal text",
466 row.neuron_id
467 )));
468 }
469 if previous_id.is_some_and(|previous| previous >= row.neuron_id.as_str()) {
470 return Err(invalid_validation(
471 "checkpoint neuron ids are not strictly increasing",
472 ));
473 }
474 previous_id = Some(&row.neuron_id);
475 if row.created_timestamp_seconds > collection_completed_at_unix_secs {
476 return Err(invalid_validation(format!(
477 "neuron {} creation timestamp exceeds collection completion",
478 row.neuron_id
479 )));
480 }
481 validate_neuron_permissions(&row.permissions, false)
482 .map_err(|reason| invalid_validation(format!("neuron {}: {reason}", row.neuron_id)))?;
483 validate_maturity_disbursements(&row.disburse_maturity_in_progress)
484 .map_err(invalid_validation)?;
485 }
486 Ok(())
487}
488
489fn validate_checkpoint_summary(
490 report: &SnsRewardCheckpointReport,
491 summary: &SnsRewardCheckpointSummary,
492) -> Result<(), SnsRewardCheckpointValidationError> {
493 let valid = report.aggregate_maturity_e8s_equivalent
494 == summary.aggregate_maturity_e8s_equivalent
495 && report.aggregate_staked_maturity_e8s_equivalent
496 == summary.aggregate_staked_maturity_e8s_equivalent
497 && report.aggregate_combined_maturity_e8s_equivalent
498 == summary.aggregate_combined_maturity_e8s_equivalent
499 && report.permission_entry_count == summary.permission_entry_count
500 && report.unassessable_permission_code_count == summary.unassessable_permission_code_count
501 && report.pending_maturity_disbursement_count
502 == summary.pending_maturity_disbursement_count
503 && report.auto_stake_maturity_enabled_count == summary.auto_stake_maturity_enabled_count
504 && report.auto_stake_maturity_disabled_count == summary.auto_stake_maturity_disabled_count
505 && report.auto_stake_maturity_unspecified_count
506 == summary.auto_stake_maturity_unspecified_count
507 && report.manage_principals_grantable == summary.manage_principals_grantable
508 && report.maturity_mint_conversion_observed_disabled
509 == summary.maturity_mint_conversion_observed_disabled
510 && report.manual_maturity_staking_observed_disabled
511 == summary.manual_maturity_staking_observed_disabled
512 && report.maturity_conversion_policy_observed_status
513 == summary.maturity_conversion_policy_observed_status;
514 if valid {
515 Ok(())
516 } else {
517 Err(invalid_validation(
518 "serialized checkpoint summary does not match raw evidence",
519 ))
520 }
521}
522
523pub(in crate::sns::report) fn validate_sns_reward_running_version_evidence(
524 response: &SnsRunningVersionResponse,
525) -> Result<(), String> {
526 let deployed = response
527 .deployed_version
528 .as_ref()
529 .ok_or_else(|| "running version has no deployed_version".to_string())?;
530 deployed.validate_hashes("deployed_version", Some(32))?;
531 if let Some(target) = response
532 .pending_version
533 .as_ref()
534 .and_then(|pending| pending.target_version.as_ref())
535 {
536 target.validate_hashes("pending_version.target_version", Some(32))?;
537 }
538 Ok(())
539}
540
541pub(in crate::sns::report) fn validate_sns_reward_checkpoint_parameter_evidence(
542 parameters: &SnsGovernanceParameters,
543) -> Result<(), String> {
544 validate_default_followees(parameters)?;
545 validate_parameter_permissions(parameters)
546}
547
548fn validate_default_followees(parameters: &SnsGovernanceParameters) -> Result<(), String> {
549 let Some(defaults) = parameters.default_followees.as_ref() else {
550 return Ok(());
551 };
552 let mut function_ids = HashSet::new();
553 for row in &defaults.followees {
554 if !function_ids.insert(row.function_id) {
555 return Err("default followees contain a duplicate function id".to_string());
556 }
557 let mut neuron_ids = HashSet::new();
558 for id in &row.followee_neuron_ids {
559 if !is_lowercase_hex_32_bytes(id) {
560 return Err("default followees contain a non-canonical neuron id".to_string());
561 }
562 if !neuron_ids.insert(id) {
563 return Err("default followees contain a duplicate neuron id".to_string());
564 }
565 }
566 }
567 Ok(())
568}
569
570fn validate_parameter_permissions(parameters: &SnsGovernanceParameters) -> Result<(), String> {
571 for (field, permissions) in [
572 (
573 "neuron_claimer_permissions",
574 parameters.neuron_claimer_permissions.as_ref(),
575 ),
576 (
577 "neuron_grantable_permissions",
578 parameters.neuron_grantable_permissions.as_ref(),
579 ),
580 ] {
581 let Some(permissions) = permissions else {
582 continue;
583 };
584 let mut codes = HashSet::new();
585 if permissions
586 .permissions
587 .iter()
588 .any(|code| !codes.insert(*code))
589 {
590 return Err(format!("{field} contains a duplicate permission code"));
591 }
592 }
593 Ok(())
594}
595
596pub(in crate::sns::report) fn validate_sns_reward_event_evidence(
597 event: &SnsRewardEvent,
598) -> Result<(), String> {
599 if event.end_timestamp_seconds.is_none() {
600 return Err("reward event is missing canonical end_timestamp_seconds".to_string());
601 }
602 let mut proposal_ids = HashSet::new();
603 if event
604 .settled_proposals
605 .iter()
606 .any(|proposal| !proposal_ids.insert(proposal.id))
607 {
608 return Err("reward event contains duplicate settled proposal ids".to_string());
609 }
610 Ok(())
611}
612
613fn invalid_validation(reason: impl Into<String>) -> SnsRewardCheckpointValidationError {
614 SnsRewardCheckpointValidationError {
615 reason: reason.into(),
616 }
617}
618
619#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
626#[serde(deny_unknown_fields)]
627pub struct SnsRewardCheckpointSummary {
628 pub aggregate_maturity_e8s_equivalent: u64,
630 pub aggregate_staked_maturity_e8s_equivalent: u64,
632 pub aggregate_combined_maturity_e8s_equivalent: u64,
634 pub permission_entry_count: usize,
636 pub unassessable_permission_code_count: usize,
638 pub pending_maturity_disbursement_count: usize,
640 pub auto_stake_maturity_enabled_count: usize,
642 pub auto_stake_maturity_disabled_count: usize,
644 pub auto_stake_maturity_unspecified_count: usize,
646 pub manage_principals_grantable: Option<bool>,
648 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
650 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
652 pub maturity_conversion_policy_observed_status: SnsPolicyObservationStatus,
654}
655
656pub(in crate::sns::report) fn recompute_reward_checkpoint_summary(
657 parameters: &SnsGovernanceParameters,
658 rows: &[SnsRewardCheckpointRow],
659) -> Result<SnsRewardCheckpointSummary, String> {
660 let mut summary = SnsRewardCheckpointSummary {
661 aggregate_maturity_e8s_equivalent: 0,
662 aggregate_staked_maturity_e8s_equivalent: 0,
663 aggregate_combined_maturity_e8s_equivalent: 0,
664 permission_entry_count: 0,
665 unassessable_permission_code_count: 0,
666 pending_maturity_disbursement_count: 0,
667 auto_stake_maturity_enabled_count: 0,
668 auto_stake_maturity_disabled_count: 0,
669 auto_stake_maturity_unspecified_count: 0,
670 manage_principals_grantable: parameters
671 .neuron_grantable_permissions
672 .as_ref()
673 .map(|permissions| permissions.permissions.contains(&2)),
674 maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
675 manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
676 maturity_conversion_policy_observed_status: SnsPolicyObservationStatus::ObservedSatisfied,
677 };
678 let (parameter_mint, parameter_staking, parameter_unknown_count) =
679 parameter_policy_observations(parameters);
680 summary.maturity_mint_conversion_observed_disabled = parameter_mint;
681 summary.manual_maturity_staking_observed_disabled = parameter_staking;
682 summary.unassessable_permission_code_count = parameter_unknown_count;
683
684 for row in rows {
685 accumulate_reward_row(&mut summary, row)?;
686 }
687 summary.maturity_conversion_policy_observed_status = summary
688 .maturity_mint_conversion_observed_disabled
689 .combine(summary.manual_maturity_staking_observed_disabled);
690 Ok(summary)
691}
692
693fn accumulate_reward_row(
694 summary: &mut SnsRewardCheckpointSummary,
695 row: &SnsRewardCheckpointRow,
696) -> Result<(), String> {
697 let combined = row.checked_combined_maturity().ok_or_else(|| {
698 format!(
699 "neuron {} combined maturity exceeds the native u64 contract",
700 row.neuron_id
701 )
702 })?;
703 if row.combined_maturity_e8s_equivalent != combined {
704 return Err(format!(
705 "neuron {} combined maturity does not match raw components",
706 row.neuron_id
707 ));
708 }
709 let (row_mint, row_staking) = row.derived_policy_observations();
710 if row.maturity_mint_conversion_observed_disabled != row_mint
711 || row.manual_maturity_staking_observed_disabled != row_staking
712 {
713 return Err(format!(
714 "neuron {} policy observations do not match raw evidence",
715 row.neuron_id
716 ));
717 }
718 summary.aggregate_maturity_e8s_equivalent = checked_sum(
719 summary.aggregate_maturity_e8s_equivalent,
720 row.maturity_e8s_equivalent,
721 "aggregate maturity",
722 )?;
723 summary.aggregate_staked_maturity_e8s_equivalent = checked_sum(
724 summary.aggregate_staked_maturity_e8s_equivalent,
725 row.staked_maturity_e8s_equivalent.unwrap_or(0),
726 "aggregate staked maturity",
727 )?;
728 summary.aggregate_combined_maturity_e8s_equivalent = checked_sum(
729 summary.aggregate_combined_maturity_e8s_equivalent,
730 combined,
731 "aggregate combined maturity",
732 )?;
733 accumulate_reward_row_counts(summary, row)?;
734 summary.maturity_mint_conversion_observed_disabled = summary
735 .maturity_mint_conversion_observed_disabled
736 .combine(row_mint);
737 summary.manual_maturity_staking_observed_disabled = summary
738 .manual_maturity_staking_observed_disabled
739 .combine(row_staking);
740 Ok(())
741}
742
743fn accumulate_reward_row_counts(
744 summary: &mut SnsRewardCheckpointSummary,
745 row: &SnsRewardCheckpointRow,
746) -> Result<(), String> {
747 summary.permission_entry_count = summary
748 .permission_entry_count
749 .checked_add(row.permissions.len())
750 .ok_or_else(|| "permission entry count overflow".to_string())?;
751 summary.pending_maturity_disbursement_count = summary
752 .pending_maturity_disbursement_count
753 .checked_add(row.disburse_maturity_in_progress.len())
754 .ok_or_else(|| "pending maturity disbursement count overflow".to_string())?;
755 summary.unassessable_permission_code_count = summary
756 .unassessable_permission_code_count
757 .checked_add(
758 row.permissions
759 .iter()
760 .flat_map(|permission| permission.permission_types.iter())
761 .filter(|value| !(1..=10).contains(&value.code))
762 .count(),
763 )
764 .ok_or_else(|| "unassessable permission code count overflow".to_string())?;
765 match row.auto_stake_maturity {
766 Some(true) => increment(&mut summary.auto_stake_maturity_enabled_count)?,
767 Some(false) => increment(&mut summary.auto_stake_maturity_disabled_count)?,
768 None => increment(&mut summary.auto_stake_maturity_unspecified_count)?,
769 }
770 Ok(())
771}
772
773fn parameter_policy_observations(
774 parameters: &SnsGovernanceParameters,
775) -> (
776 SnsPolicyObservationStatus,
777 SnsPolicyObservationStatus,
778 usize,
779) {
780 let mut mint = SnsPolicyObservationStatus::ObservedSatisfied;
781 let mut staking = SnsPolicyObservationStatus::ObservedSatisfied;
782 let mut unknown_count = 0;
783 for permissions in [
784 parameters.neuron_claimer_permissions.as_ref(),
785 parameters.neuron_grantable_permissions.as_ref(),
786 ] {
787 let Some(permissions) = permissions else {
788 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
789 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
790 continue;
791 };
792 if permissions.permissions.is_empty() {
793 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
794 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
795 }
796 for code in &permissions.permissions {
797 let (code_mint, code_staking) = permission_code_policy_observations(*code);
798 mint = mint.combine(code_mint);
799 staking = staking.combine(code_staking);
800 if !(1..=10).contains(code) {
801 unknown_count += 1;
802 }
803 }
804 }
805 (mint, staking, unknown_count)
806}
807
808fn checked_sum(left: u64, right: u64, field: &str) -> Result<u64, String> {
809 left.checked_add(right)
810 .ok_or_else(|| format!("{field} exceeds the native u64 contract"))
811}
812
813fn increment(value: &mut usize) -> Result<(), String> {
814 *value = value
815 .checked_add(1)
816 .ok_or_else(|| "checkpoint count overflow".to_string())?;
817 Ok(())
818}