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