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