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