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