diff --git a/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs b/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs index 3a4e455f734..eb1957b21cc 100644 --- a/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs +++ b/rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs @@ -98,6 +98,21 @@ pub struct NeuronInfo { /// See the Visibility enum. #[prost(enumeration = "Visibility", optional, tag = "12")] pub visibility: Option, + /// The last time that voting power was "refreshed". There are two ways to + /// refresh the voting power of a neuron: set following, or vote directly. In + /// the future, there will be a dedicated API for refreshing. Note that direct + /// voting implies that refresh also occurs when a proposal is created, because + /// direct voting is part of proposal creation. + /// + /// Effect: When this becomes > 6 months ago, the amount of voting power that + /// this neuron can exercise decreases linearly down to 0 over the course of 1 + /// month. After that, following is cleared, except for ManageNeuron proposals. + /// + /// This will always be populated. If the underlying neuron was never + /// refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds + /// after the UNIX epoch). + #[prost(uint64, optional, tag = "13")] + pub voting_power_refreshed_timestamp_seconds: ::core::option::Option, } /// A transfer performed from some account to stake a new neuron. #[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)] @@ -256,6 +271,21 @@ pub struct Neuron { /// See the Visibility enum. #[prost(enumeration = "Visibility", optional, tag = "23")] pub visibility: Option, + /// The last time that voting power was "refreshed". There are two ways to + /// refresh the voting power of a neuron: set following, or vote directly. In + /// the future, there will be a dedicated API for refreshing. Note that direct + /// voting implies that refresh also occurs when a proposal is created, because + /// direct voting is part of proposal creation. + /// + /// Effect: When this becomes > 6 months ago, the amount of voting power that + /// this neuron can exercise decreases linearly down to 0 over the course of 1 + /// month. After that, following is cleared, except for ManageNeuron proposals. + /// + /// This will always be populated. If the underlying neuron was never + /// refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds + /// after the UNIX epoch). + #[prost(uint64, optional, tag = "24")] + pub voting_power_refreshed_timestamp_seconds: ::core::option::Option, /// At any time, at most one of `when_dissolved` and /// `dissolve_delay` are specified. /// @@ -364,6 +394,8 @@ pub struct AbridgedNeuron { pub neuron_type: Option, #[prost(enumeration = "Visibility", optional, tag = "23")] pub visibility: Option, + #[prost(uint64, optional, tag = "24")] + pub voting_power_refreshed_timestamp_seconds: ::core::option::Option, #[prost(oneof = "abridged_neuron::DissolveState", tags = "9, 10")] pub dissolve_state: Option, } diff --git a/rs/nns/governance/canister/governance.did b/rs/nns/governance/canister/governance.did index 85388e41921..98438e971b4 100644 --- a/rs/nns/governance/canister/governance.did +++ b/rs/nns/governance/canister/governance.did @@ -577,6 +577,7 @@ type Neuron = record { transfer : opt NeuronStakeTransfer; known_neuron_data : opt KnownNeuronData; spawn_at_timestamp_seconds : opt nat64; + voting_power_refreshed_timestamp_seconds : opt nat64; }; type NeuronBasketConstructionParameters = record { @@ -628,6 +629,7 @@ type NeuronInfo = record { known_neuron_data : opt KnownNeuronData; voting_power : nat64; age_seconds : nat64; + voting_power_refreshed_timestamp_seconds : opt nat64; }; type NeuronStakeTransfer = record { diff --git a/rs/nns/governance/canister/governance_test.did b/rs/nns/governance/canister/governance_test.did index 866a187e843..6423caa63fe 100644 --- a/rs/nns/governance/canister/governance_test.did +++ b/rs/nns/governance/canister/governance_test.did @@ -579,6 +579,7 @@ type Neuron = record { transfer : opt NeuronStakeTransfer; known_neuron_data : opt KnownNeuronData; spawn_at_timestamp_seconds : opt nat64; + voting_power_refreshed_timestamp_seconds : opt nat64; }; type NeuronBasketConstructionParameters = record { @@ -630,6 +631,7 @@ type NeuronInfo = record { known_neuron_data : opt KnownNeuronData; voting_power : nat64; age_seconds : nat64; + voting_power_refreshed_timestamp_seconds : opt nat64; }; type NeuronStakeTransfer = record { diff --git a/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto b/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto index f8220f01c20..ec7e095bde3 100644 --- a/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto +++ b/rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto @@ -246,6 +246,20 @@ message NeuronInfo { optional NeuronType neuron_type = 11; // See the Visibility enum. optional Visibility visibility = 12; + // The last time that voting power was "refreshed". There are two ways to + // refresh the voting power of a neuron: set following, or vote directly. In + // the future, there will be a dedicated API for refreshing. Note that direct + // voting implies that refresh also occurs when a proposal is created, because + // direct voting is part of proposal creation. + // + // Effect: When this becomes > 6 months ago, the amount of voting power that + // this neuron can exercise decreases linearly down to 0 over the course of 1 + // month. After that, following is cleared, except for ManageNeuron proposals. + // + // This will always be populated. If the underlying neuron was never + // refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds + // after the UNIX epoch). + optional uint64 voting_power_refreshed_timestamp_seconds = 13; } // A transfer performed from some account to stake a new neuron. @@ -429,6 +443,21 @@ message Neuron { // See the Visibility enum. optional Visibility visibility = 23; + + // The last time that voting power was "refreshed". There are two ways to + // refresh the voting power of a neuron: set following, or vote directly. In + // the future, there will be a dedicated API for refreshing. Note that direct + // voting implies that refresh also occurs when a proposal is created, because + // direct voting is part of proposal creation. + // + // Effect: When this becomes > 6 months ago, the amount of voting power that + // this neuron can exercise decreases linearly down to 0 over the course of 1 + // month. After that, following is cleared, except for ManageNeuron proposals. + // + // This will always be populated. If the underlying neuron was never + // refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds + // after the UNIX epoch). + optional uint64 voting_power_refreshed_timestamp_seconds = 24; } // Subset of Neuron that has no collections or big fields that might not exist in most neurons, and @@ -454,6 +483,7 @@ message AbridgedNeuron { optional uint64 joined_community_fund_timestamp_seconds = 17; optional NeuronType neuron_type = 22; optional Visibility visibility = 23; + optional uint64 voting_power_refreshed_timestamp_seconds = 24; reserved 1; reserved "id"; diff --git a/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs b/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs index 9884147080a..d617a969144 100644 --- a/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs +++ b/rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs @@ -113,6 +113,21 @@ pub struct NeuronInfo { /// See the Visibility enum. #[prost(enumeration = "Visibility", optional, tag = "12")] pub visibility: ::core::option::Option, + /// The last time that voting power was "refreshed". There are two ways to + /// refresh the voting power of a neuron: set following, or vote directly. In + /// the future, there will be a dedicated API for refreshing. Note that direct + /// voting implies that refresh also occurs when a proposal is created, because + /// direct voting is part of proposal creation. + /// + /// Effect: When this becomes > 6 months ago, the amount of voting power that + /// this neuron can exercise decreases linearly down to 0 over the course of 1 + /// month. After that, following is cleared, except for ManageNeuron proposals. + /// + /// This will always be populated. If the underlying neuron was never + /// refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds + /// after the UNIX epoch). + #[prost(uint64, optional, tag = "13")] + pub voting_power_refreshed_timestamp_seconds: ::core::option::Option, } /// A transfer performed from some account to stake a new neuron. #[derive( @@ -276,6 +291,21 @@ pub struct Neuron { /// See the Visibility enum. #[prost(enumeration = "Visibility", optional, tag = "23")] pub visibility: ::core::option::Option, + /// The last time that voting power was "refreshed". There are two ways to + /// refresh the voting power of a neuron: set following, or vote directly. In + /// the future, there will be a dedicated API for refreshing. Note that direct + /// voting implies that refresh also occurs when a proposal is created, because + /// direct voting is part of proposal creation. + /// + /// Effect: When this becomes > 6 months ago, the amount of voting power that + /// this neuron can exercise decreases linearly down to 0 over the course of 1 + /// month. After that, following is cleared, except for ManageNeuron proposals. + /// + /// This will always be populated. If the underlying neuron was never + /// refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds + /// after the UNIX epoch). + #[prost(uint64, optional, tag = "24")] + pub voting_power_refreshed_timestamp_seconds: ::core::option::Option, /// At any time, at most one of `when_dissolved` and /// `dissolve_delay` are specified. /// @@ -401,6 +431,8 @@ pub struct AbridgedNeuron { pub neuron_type: ::core::option::Option, #[prost(enumeration = "Visibility", optional, tag = "23")] pub visibility: ::core::option::Option, + #[prost(uint64, optional, tag = "24")] + pub voting_power_refreshed_timestamp_seconds: ::core::option::Option, #[prost(oneof = "abridged_neuron::DissolveState", tags = "9, 10")] pub dissolve_state: ::core::option::Option, } diff --git a/rs/nns/governance/src/neuron/types.rs b/rs/nns/governance/src/neuron/types.rs index 9c60e6648cc..cae41e540f5 100644 --- a/rs/nns/governance/src/neuron/types.rs +++ b/rs/nns/governance/src/neuron/types.rs @@ -23,6 +23,18 @@ use ic_nns_common::pb::v1::{NeuronId, ProposalId}; use icp_ledger::Subaccount; use std::collections::{BTreeSet, HashMap}; +/// Value: one second after midnight, 2024-11-05 (UTC). +/// +/// How this value was chosen: This is around the earliest time when +/// "refreshing" a neuron's voting power might be released, (assuming the usual +/// NNS release cycle). Significantly different values could also work, but this +/// seems like a nice "neutral" value. +/// +/// How this value is used: when a neuron does not have a value in the +/// voting_power_refreshed_timestamp_seconds field (because it was created before +/// this feature), we pretend as though this value is in that field. +const DEFAULT_VOTING_POWER_REFRESHED_TIMESTAMP_SECONDS: u64 = 1731628801; + /// A neuron type internal to the governance crate. Currently, this type is identical to the /// prost-generated Neuron type (except for derivations for prost). Gradually, this type will evolve /// towards having all private fields while exposing methods for mutations, which allows it to hold @@ -113,8 +125,21 @@ pub struct Neuron { /// How much unprivileged principals (i.e. is neither controller, nor /// hotkey) can see about this neuron. visibility: Option, + /// The last time that voting power was "refreshed". There are two ways to + /// refresh the voting power of a neuron: set following, or vote directly. + /// When this becomes > 6 months ago, the amount of voting power that this + /// neuron can exercise decreases linearly down to 0 over the course of 1 + /// month. After that, following is cleared, except for ManageNeuron + /// proposals. + voting_power_refreshed_timestamp_seconds: u64, } +/// This is mostly the same as the version of PartialEq generated by derive. The +/// one difference: visibility: None is considered equal to visibility: +/// Some(Private). +// We can get rid of this if we ever decide to backfill the visibility field. +// More precisely, we change all stored neurons (in heap and stable memory) so +// that instead of not having a value in this field, they have Some(Private). impl PartialEq for Neuron { fn eq(&self, other: &Self) -> bool { #[derive(PartialEq)] @@ -139,6 +164,7 @@ impl PartialEq for Neuron { joined_community_fund_timestamp_seconds: &'a Option, known_neuron_data: &'a Option, neuron_type: &'a Option, + voting_power_refreshed_timestamp_seconds: &'a u64, visibility: Visibility, } @@ -166,6 +192,7 @@ impl PartialEq for Neuron { joined_community_fund_timestamp_seconds, known_neuron_data, neuron_type, + voting_power_refreshed_timestamp_seconds, visibility: _, } = src; @@ -193,6 +220,7 @@ impl PartialEq for Neuron { joined_community_fund_timestamp_seconds, known_neuron_data, neuron_type, + voting_power_refreshed_timestamp_seconds, visibility, } @@ -826,6 +854,9 @@ impl Neuron { known_neuron_data: self.known_neuron_data.clone(), neuron_type: self.neuron_type, visibility, + voting_power_refreshed_timestamp_seconds: Some( + self.voting_power_refreshed_timestamp_seconds, + ), } } @@ -993,6 +1024,10 @@ impl Neuron { .dissolved_at_timestamp_seconds() } + pub fn voting_power_refreshed_timestamp_seconds(&self) -> u64 { + self.voting_power_refreshed_timestamp_seconds + } + pub fn subtract_staked_maturity(&mut self, amount_e8s: u64) { let new_staked_maturity_e8s = self .staked_maturity_e8s_equivalent @@ -1044,6 +1079,7 @@ impl From for NeuronProto { known_neuron_data, neuron_type, visibility: _, + voting_power_refreshed_timestamp_seconds, } = neuron; let id = Some(id); @@ -1053,6 +1089,8 @@ impl From for NeuronProto { dissolve_state, aging_since_timestamp_seconds, } = StoredDissolveStateAndAge::from(dissolve_state_and_age); + let voting_power_refreshed_timestamp_seconds = + Some(voting_power_refreshed_timestamp_seconds); NeuronProto { id, @@ -1077,6 +1115,7 @@ impl From for NeuronProto { known_neuron_data, neuron_type, visibility, + voting_power_refreshed_timestamp_seconds, } } } @@ -1108,6 +1147,7 @@ impl TryFrom for Neuron { known_neuron_data, neuron_type, visibility, + voting_power_refreshed_timestamp_seconds, } = proto; let id = id.ok_or("Neuron ID is missing")?; @@ -1127,6 +1167,8 @@ impl TryFrom for Neuron { ) })?), }; + let voting_power_refreshed_timestamp_seconds = voting_power_refreshed_timestamp_seconds + .unwrap_or(DEFAULT_VOTING_POWER_REFRESHED_TIMESTAMP_SECONDS); Ok(Neuron { id, @@ -1150,6 +1192,7 @@ impl TryFrom for Neuron { known_neuron_data, neuron_type, visibility, + voting_power_refreshed_timestamp_seconds, }) } } @@ -1242,6 +1285,7 @@ impl TryFrom for DecomposedNeuron { known_neuron_data, neuron_type, visibility, + voting_power_refreshed_timestamp_seconds, } = source; let account = subaccount.to_vec(); @@ -1252,6 +1296,8 @@ impl TryFrom for DecomposedNeuron { } = StoredDissolveStateAndAge::from(dissolve_state_and_age); let dissolve_state = dissolve_state.map(AbridgedNeuronDissolveState::from); let visibility = visibility.map(|visibility| visibility as i32); + let voting_power_refreshed_timestamp_seconds = + Some(voting_power_refreshed_timestamp_seconds); let main = AbridgedNeuron { account, @@ -1270,6 +1316,7 @@ impl TryFrom for DecomposedNeuron { neuron_type, dissolve_state, visibility, + voting_power_refreshed_timestamp_seconds, }; Ok(Self { @@ -1319,6 +1366,7 @@ impl From for Neuron { neuron_type, dissolve_state, visibility, + voting_power_refreshed_timestamp_seconds, } = main; let subaccount = @@ -1331,6 +1379,9 @@ impl From for Neuron { .expect("Neuron dissolve state and age is invalid"); let visibility = visibility.and_then(|visibility| Visibility::try_from(visibility).ok()); + let voting_power_refreshed_timestamp_seconds = voting_power_refreshed_timestamp_seconds + .unwrap_or(DEFAULT_VOTING_POWER_REFRESHED_TIMESTAMP_SECONDS); + Neuron { id, subaccount, @@ -1353,6 +1404,7 @@ impl From for Neuron { known_neuron_data, neuron_type, visibility, + voting_power_refreshed_timestamp_seconds, } } } @@ -1381,6 +1433,7 @@ pub struct NeuronBuilder { joined_community_fund_timestamp_seconds: Option, neuron_type: Option, visibility: Option, + voting_power_refreshed_timestamp_seconds: u64, // Fields that don't exist when a neuron is first built. We allow them to be set in tests. #[cfg(test)] @@ -1421,6 +1474,7 @@ impl NeuronBuilder { joined_community_fund_timestamp_seconds: None, neuron_type: None, visibility: None, + voting_power_refreshed_timestamp_seconds: created_timestamp_seconds, #[cfg(test)] neuron_fees_e8s: 0, @@ -1576,6 +1630,7 @@ impl NeuronBuilder { #[cfg(test)] known_neuron_data, visibility, + voting_power_refreshed_timestamp_seconds, } = self; let auto_stake_maturity = if auto_stake_maturity { @@ -1618,6 +1673,7 @@ impl NeuronBuilder { known_neuron_data, neuron_type, visibility, + voting_power_refreshed_timestamp_seconds, } } } diff --git a/rs/nns/governance/src/neuron/types/tests.rs b/rs/nns/governance/src/neuron/types/tests.rs index 60e0fcf132e..a7944fe4704 100644 --- a/rs/nns/governance/src/neuron/types/tests.rs +++ b/rs/nns/governance/src/neuron/types/tests.rs @@ -126,12 +126,13 @@ fn test_abridged_neuron_size() { u64::MAX, )), visibility: None, + voting_power_refreshed_timestamp_seconds: Some(u64::MAX), }; assert!(abridged_neuron.encoded_len() as u32 <= AbridgedNeuron::BOUND.max_size()); - // This size can be updated. This assertion is created so that we are aware of the available - // headroom. - assert_eq!(abridged_neuron.encoded_len(), 184); + // This size can be updated. This assertion is here to make sure we are very aware of growth. + // Reminder: the amount that we allocated for AbridgedNeuron is 380 bytes. + assert_eq!(abridged_neuron.encoded_len(), 196); } fn create_neuron_with_stake_dissolve_state_and_age( diff --git a/rs/nns/governance/src/pb/conversions.rs b/rs/nns/governance/src/pb/conversions.rs index aeefb93f420..7f3d2ce4db8 100644 --- a/rs/nns/governance/src/pb/conversions.rs +++ b/rs/nns/governance/src/pb/conversions.rs @@ -65,6 +65,7 @@ impl From for pb_api::NeuronInfo { known_neuron_data: item.known_neuron_data.map(|x| x.into()), neuron_type: item.neuron_type, visibility: item.visibility, + voting_power_refreshed_timestamp_seconds: item.voting_power_refreshed_timestamp_seconds, } } } @@ -83,6 +84,7 @@ impl From for pb::NeuronInfo { known_neuron_data: item.known_neuron_data.map(|x| x.into()), neuron_type: item.neuron_type, visibility: item.visibility, + voting_power_refreshed_timestamp_seconds: item.voting_power_refreshed_timestamp_seconds, } } } @@ -143,6 +145,7 @@ impl From for pb_api::Neuron { neuron_type: item.neuron_type, dissolve_state: item.dissolve_state.map(|x| x.into()), visibility: item.visibility, + voting_power_refreshed_timestamp_seconds: item.voting_power_refreshed_timestamp_seconds, } } } @@ -175,6 +178,7 @@ impl From for pb::Neuron { neuron_type: item.neuron_type, dissolve_state: item.dissolve_state.map(|x| x.into()), visibility: item.visibility, + voting_power_refreshed_timestamp_seconds: item.voting_power_refreshed_timestamp_seconds, } } } @@ -258,6 +262,7 @@ impl From for pb_api::AbridgedNeuron { neuron_type: item.neuron_type, dissolve_state: item.dissolve_state.map(|x| x.into()), visibility: item.visibility, + voting_power_refreshed_timestamp_seconds: item.voting_power_refreshed_timestamp_seconds, } } } @@ -280,6 +285,7 @@ impl From for pb::AbridgedNeuron { neuron_type: item.neuron_type, dissolve_state: item.dissolve_state.map(|x| x.into()), visibility: item.visibility, + voting_power_refreshed_timestamp_seconds: item.voting_power_refreshed_timestamp_seconds, } } } diff --git a/rs/nns/governance/src/storage/neurons/neurons_tests.rs b/rs/nns/governance/src/storage/neurons/neurons_tests.rs index 2da3e35fe9c..df650149d78 100644 --- a/rs/nns/governance/src/storage/neurons/neurons_tests.rs +++ b/rs/nns/governance/src/storage/neurons/neurons_tests.rs @@ -505,12 +505,13 @@ fn test_abridged_neuron_size() { neuron_type: Some(i32::MAX), dissolve_state: Some(DissolveState::WhenDissolvedTimestampSeconds(u64::MAX)), visibility: None, + voting_power_refreshed_timestamp_seconds: Some(u64::MAX), }; assert!(abridged_neuron.encoded_len() as u32 <= AbridgedNeuron::BOUND.max_size()); - // This size can be updated. This assertion is created so that we are aware of the available - // headroom. - assert_eq!(abridged_neuron.encoded_len(), 184); + // This size can be updated. This assertion is here to make sure we are very aware of growth. + // Reminder: the amount we allocated for AbridgedNeuron is 380 bytes. + assert_eq!(abridged_neuron.encoded_len(), 196); } #[test] diff --git a/rs/nns/governance/tests/governance.rs b/rs/nns/governance/tests/governance.rs index 2f8bdbd2880..b00a9f5aee0 100644 --- a/rs/nns/governance/tests/governance.rs +++ b/rs/nns/governance/tests/governance.rs @@ -148,6 +148,11 @@ pub mod fixtures; // https://github.com/rust-lang/rust/issues/46379 pub mod common; +// Some time in Oct, 2024. There is nothing magical about this value. The only +// thing special about it is that it is "realistic". This was chosen by simply +// looking at the clock. +const START_TIMESTAMP_SECONDS: u64 = 1730132754; + lazy_static! { static ref RANDOM_PRINCIPAL_ID: PrincipalId = PrincipalId::new_user_test_id(0xDEAD_BEEF); } @@ -4271,7 +4276,7 @@ fn governance_with_staked_neuron( }); let driver = fake::FakeDriver::default() - .at(56) + .at(START_TIMESTAMP_SECONDS) .with_ledger_accounts(vec![fake::FakeAccount { id: AccountIdentifier::new( ic_base_types::PrincipalId::from(GOVERNANCE_CANISTER_ID), @@ -4360,6 +4365,7 @@ fn create_mature_neuron(dissolved: bool) -> (fake::FakeDriver, Governance, Neuro dissolve_state: Some(DissolveState::DissolveDelaySeconds(dissolve_delay_seconds)), kyc_verified: true, visibility, + voting_power_refreshed_timestamp_seconds: Some(START_TIMESTAMP_SECONDS), ..Default::default() } ); @@ -10051,6 +10057,8 @@ fn test_include_public_neurons_in_full_neurons() { controller: Some(controller), dissolve_state: Some(DissolveState::DissolveDelaySeconds(ONE_YEAR_SECONDS)), aging_since_timestamp_seconds: 1_721_727_936, + voting_power_refreshed_timestamp_seconds: Some(START_TIMESTAMP_SECONDS), + ..Default::default() } }; @@ -10080,7 +10088,7 @@ fn test_include_public_neurons_in_full_neurons() { let total_icp_suppply = Tokens::new(200, 0).unwrap(); let driver = fake::FakeDriver::default() - .at(60 * 60 * 24 * 30) + .at(START_TIMESTAMP_SECONDS) .with_supply(total_icp_suppply); let governance = Governance::new( governance_proto, diff --git a/rs/nns/integration_tests/test_canisters/governance_mem_test_canister.rs b/rs/nns/integration_tests/test_canisters/governance_mem_test_canister.rs index cbdf2ddf0c4..af060db57fb 100644 --- a/rs/nns/integration_tests/test_canisters/governance_mem_test_canister.rs +++ b/rs/nns/integration_tests/test_canisters/governance_mem_test_canister.rs @@ -313,6 +313,7 @@ fn allocate_neuron(id: u64) -> Neuron { spawn_at_timestamp_seconds: None, neuron_type: None, visibility: None, + voting_power_refreshed_timestamp_seconds: None, } }