From 9d208639d737ee2dcc4c6fc326b534d201274eb2 Mon Sep 17 00:00:00 2001 From: Jason Zhu Date: Fri, 3 Apr 2026 16:07:18 -0700 Subject: [PATCH 1/2] feat(governance): clear voting power snapshots on Mission 70 activation When Mission 70 activates, voting power distribution changes significantly (convex dissolve delay bonus, reduced max dissolve delay). Stale snapshots from before activation would cause false spike detections, since the new voting power totals will differ substantially from the old ones. - Add VotingPowerSnapshots::clear() method that empties both stable BTreeMaps - Call it during governance initialization when the Mission 70 flag is enabled - Add a log line when a voting power spike is detected for observability - Add unit test for clear() verifying both maps are emptied --- rs/nns/governance/src/governance.rs | 4 +++ .../src/governance/voting_power_snapshots.rs | 17 ++++++++++- .../voting_power_snapshots_tests.rs | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 41e8c09fb468..fe7d09f5e58b 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -8,6 +8,7 @@ use crate::{ validate_merge_neurons_before_commit, }, split_neuron::{SplitNeuronEffect, calculate_split_neuron_effect}, + voting_power_snapshots::VotingPowerSnapshots, }, heap_governance_data::{ HeapGovernanceData, XdrConversionRate, initialize_governance, reassemble_governance_proto, @@ -1356,6 +1357,9 @@ impl Governance { .clamp_dissolve_delay_for_all_neurons_or_panic(now); } + if is_mission_70_voting_rewards_enabled() { + VOTING_POWER_SNAPSHOTS.with_borrow_mut(VotingPowerSnapshots::clear); + } governance } diff --git a/rs/nns/governance/src/governance/voting_power_snapshots.rs b/rs/nns/governance/src/governance/voting_power_snapshots.rs index fd2f0071a89d..d2951f1a351b 100644 --- a/rs/nns/governance/src/governance/voting_power_snapshots.rs +++ b/rs/nns/governance/src/governance/voting_power_snapshots.rs @@ -5,7 +5,7 @@ use crate::{ pb::v1::{Ballot, NeuronIdToVotingPowerMap, VotingPowerTotal}, }; -use ic_cdk::eprintln; +use ic_cdk::{eprintln, println}; use ic_nervous_system_common::ONE_MONTH_SECONDS; use ic_stable_structures::{ DefaultMemoryImpl, StableBTreeMap, Storable, memory_manager::VirtualMemory, storable::Bound, @@ -202,6 +202,14 @@ impl VotingPowerSnapshots { voting_power_map, totals_with_minimum_total_potential_voting_power, )); + println!( + "{}Voting power spike detected at timestamp {}, total potential voting power: {}, \ + minimum total potential voting power: {}", + LOG_PREFIX, + timestamp_with_minimum_total_potential_voting_power, + total_potential_voting_power, + totals_with_minimum_total_potential_voting_power.total_potential_voting_power + ); Some(( timestamp_with_minimum_total_potential_voting_power, previous_voting_power_snapshot, @@ -214,6 +222,13 @@ impl VotingPowerSnapshots { .last_key_value() .map(|(timestamp, _)| timestamp) } + + /// Clears the voting power snapshots. + // TODO(NNS1-4323): Remove this method after Mission 70 is fully deployed. + pub(crate) fn clear(&mut self) { + self.neuron_id_to_voting_power_maps.clear_new(); + self.voting_power_totals.clear_new(); + } } impl Storable for NeuronIdToVotingPowerMap { diff --git a/rs/nns/governance/src/governance/voting_power_snapshots_tests.rs b/rs/nns/governance/src/governance/voting_power_snapshots_tests.rs index 7f8b9a7b3f19..c1ac267c6902 100644 --- a/rs/nns/governance/src/governance/voting_power_snapshots_tests.rs +++ b/rs/nns/governance/src/governance/voting_power_snapshots_tests.rs @@ -113,3 +113,32 @@ fn test_record_voting_power_snapshot() { None, ); } + +// TODO(NNS1-4323): Remove this test after Mission 70 is fully deployed. +#[test] +fn test_clear() { + let memory_manager = MemoryManager::init(DefaultMemoryImpl::default()); + let mut snapshots = VotingPowerSnapshots::new( + memory_manager.get(MemoryId::new(0)), + memory_manager.get(MemoryId::new(1)), + ); + + // Record a few snapshots. + for i in 1..=3 { + snapshots.record_voting_power_snapshot(i, voting_power_snapshot(vec![90, 10], 100 + i)); + } + + // Verify that snapshots are present. + assert_eq!(snapshots.latest_snapshot_timestamp_seconds(), Some(3)); + + // Clear the snapshots. + snapshots.clear(); + + // Verify that everything is empty. + assert_eq!(snapshots.latest_snapshot_timestamp_seconds(), None); + assert_eq!( + snapshots.previous_ballots_if_voting_power_spike_detected(u64::MAX, 10), + None + ); + assert!(!snapshots.is_latest_snapshot_a_spike(10)); +} From b0ec459a41357eddfd55347fd6874f88d4c73113 Mon Sep 17 00:00:00 2001 From: Jason Zhu Date: Thu, 9 Apr 2026 16:07:17 -0700 Subject: [PATCH 2/2] address PR feedback from #9731: make snapshot clearing a one-time migration Move VOTING_POWER_SNAPSHOTS clearing inside the existing dissolve-delay-clamping block, which uses neuron_id_to_pre_clamp_dissolve_state.is_empty() as an idempotency guard. This ensures snapshots are only cleared once on first M70 activation, not on every subsequent upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) --- rs/nns/governance/src/governance.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index fe7d09f5e58b..93e0672cf36f 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -1355,11 +1355,10 @@ impl Governance { governance.heap_data.neuron_id_to_pre_clamp_dissolve_state = governance .neuron_store .clamp_dissolve_delay_for_all_neurons_or_panic(now); - } - if is_mission_70_voting_rewards_enabled() { VOTING_POWER_SNAPSHOTS.with_borrow_mut(VotingPowerSnapshots::clear); } + governance }