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