Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nns): Add pagination to list_neurons API #3358

Merged
merged 25 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use candid::{Decode, Encode, Nat, Principal};
use canister_test::Wasm;
use futures::stream;
use futures::StreamExt;
use futures::{stream, StreamExt};
use ic_base_types::{CanisterId, PrincipalId, SubnetId};
use ic_ledger_core::Tokens;
use ic_nervous_system_agent::pocketic_impl::{PocketIcAgent, PocketIcCallError};
use ic_nervous_system_agent::sns::Sns;
use ic_nervous_system_agent::CallCanisters;
use ic_nervous_system_agent::{
pocketic_impl::{PocketIcAgent, PocketIcCallError},
sns::Sns,
CallCanisters,
};
use ic_nervous_system_common::{E8, ONE_DAY_SECONDS};
use ic_nervous_system_common_test_keys::{TEST_NEURON_1_ID, TEST_NEURON_1_OWNER_PRINCIPAL};
use ic_nns_common::pb::v1::{NeuronId, ProposalId};
Expand Down Expand Up @@ -60,17 +61,15 @@ use icrc_ledger_types::icrc1::{
account::Account,
transfer::{TransferArg, TransferError},
};
use itertools::EitherOrBoth;
use itertools::Itertools;
use itertools::{EitherOrBoth, Itertools};
use maplit::btreemap;
use pocket_ic::{
management_canister::CanisterSettings, nonblocking::PocketIc, ErrorCode, PocketIcBuilder,
RejectResponse,
};
use prost::Message;
use rust_decimal::prelude::ToPrimitive;
use std::ops::Range;
use std::{collections::BTreeMap, fmt::Write, time::Duration};
use std::{collections::BTreeMap, fmt::Write, ops::Range, time::Duration};

pub const STARTING_CYCLES_PER_CANISTER: u128 = 2_000_000_000_000_000;

Expand Down Expand Up @@ -833,6 +832,8 @@ pub mod nns {
include_neurons_readable_by_caller: true,
include_empty_neurons_readable_by_caller: None,
include_public_neurons_in_full_neurons: None,
page_number: None,
page_size: None
})
.unwrap(),
)
Expand Down Expand Up @@ -1366,8 +1367,9 @@ pub mod sns {
use assert_matches::assert_matches;
use ic_crypto_sha2::Sha256;
use ic_nervous_system_agent::sns::governance::{GovernanceCanister, SubmitProposalError};
use ic_sns_governance::governance::UPGRADE_STEPS_INTERVAL_REFRESH_BACKOFF_SECONDS;
use ic_sns_governance::pb::v1::get_neuron_response;
use ic_sns_governance::{
governance::UPGRADE_STEPS_INTERVAL_REFRESH_BACKOFF_SECONDS, pb::v1::get_neuron_response,
};
use pocket_ic::ErrorCode;
use sns_pb::UpgradeSnsControlledCanister;

Expand Down
27 changes: 27 additions & 0 deletions rs/nns/governance/api/src/ic_nns_governance.pb.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,20 @@ pub struct ListProposalInfoResponse {
/// of neurons listed in `neuron_ids` and, if `caller_neurons` is true,
/// the list of neuron IDs of neurons for which the caller is the
/// controller or one of the hot keys.
///
/// Paging is available if the result set is larger than `MAX_LIST_NEURONS_RESULTS`,
/// which is currently 500 neurons. If you are unsure of the number of results in a set,
/// you can use the `total_pages_available` field in the response to determine how many
/// additional pages need to be queried. It will be based on your `page_size` parameter.
/// When paging through results, it is good to keep in mind that newly inserted neurons
/// could be missed if they are inserted between calls to pages, and this could result in missing
/// a neuron in the combined responses.
///
/// If a user provides neuron_ids that do not exist in the request, there is no guarantee that
/// each page will contain the exactly the page size, even if it is not the final request. This is
/// because neurons are retrieved by their neuron_id, and no additional checks are made on the
/// validity of the neuron_ids provided by the user before deciding which sets of neuron_ids
/// will be returned in the current page.
#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
Expand Down Expand Up @@ -3510,6 +3524,15 @@ pub struct ListNeurons {
/// existing (unmigrated) callers.
#[prost(bool, optional, tag = "4")]
pub include_public_neurons_in_full_neurons: Option<bool>,
/// If this is set, we return the batch of neurons at a given page, using the `page_size` to
/// determine how many neurons are returned in each page.
#[prost(uint64, optional, tag = "5")]
pub page_number: Option<u64>,
/// If this is set, we use the page limit provided to determine how large pages will be.
/// This cannot be greater than MAX_LIST_NEURONS_RESULTS, which is set to 500.
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
/// If not set, this defaults to MAX_LIST_NEURONS_RESULTS.
#[prost(uint64, optional, tag = "6")]
pub page_size: Option<u64>,
}
/// A response to a `ListNeurons` request.
///
Expand All @@ -3528,6 +3551,10 @@ pub struct ListNeuronsResponse {
/// `ManageNeuron` topic).
#[prost(message, repeated, tag = "2")]
pub full_neurons: Vec<Neuron>,
/// This is returned to tell the caller how many pages of results are available to query.
/// If there are fewer than the page_size neurons, this will equal 1.
#[prost(uint64, optional, tag = "3")]
pub total_pages_available: Option<u64>,
}
/// A response to "ListKnownNeurons"
#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)]
Expand Down
4 changes: 2 additions & 2 deletions rs/nns/governance/canbench/canbench_results.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ benches:
scopes: {}
list_neurons_stable:
total:
instructions: 113490014
heap_increase: 4
instructions: 113659672
heap_increase: 5
stable_memory_increase: 0
scopes: {}
list_proposals:
Expand Down
5 changes: 4 additions & 1 deletion rs/nns/governance/canister/canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,10 @@ fn get_latest_reward_event() -> RewardEvent {
#[query]
fn get_neuron_ids() -> Vec<NeuronId> {
debug_log("get_neuron_ids");
let votable = governance().get_neuron_ids_by_principal(&caller());
let votable = governance()
.get_neuron_ids_by_principal(&caller())
.into_iter()
.collect();

governance()
.get_managed_neuron_ids_for(votable)
Expand Down
3 changes: 3 additions & 0 deletions rs/nns/governance/canister/governance.did
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,14 @@ type ListNeurons = record {
neuron_ids : vec nat64;
include_empty_neurons_readable_by_caller : opt bool;
include_neurons_readable_by_caller : bool;
page_number: opt nat64;
page_size: opt nat64;
};

type ListNeuronsResponse = record {
neuron_infos : vec record { nat64; NeuronInfo };
full_neurons : vec Neuron;
total_pages_available: opt nat64;
};

type ListNodeProviderRewardsRequest = record {
Expand Down
3 changes: 3 additions & 0 deletions rs/nns/governance/canister/governance_test.did
Original file line number Diff line number Diff line change
Expand Up @@ -438,11 +438,14 @@ type ListNeurons = record {
neuron_ids : vec nat64;
include_empty_neurons_readable_by_caller : opt bool;
include_neurons_readable_by_caller : bool;
page_number: opt nat64;
page_size: opt nat64;
};

type ListNeuronsResponse = record {
neuron_infos : vec record { nat64; NeuronInfo };
full_neurons : vec Neuron;
total_pages_available: opt nat64;
};

type ListNodeProviderRewardsRequest = record {
Expand Down
51 changes: 35 additions & 16 deletions rs/nns/governance/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ use rust_decimal_macros::dec;
use std::{
borrow::Cow,
cmp::{max, Ordering},
collections::{BTreeMap, HashMap, HashSet},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
convert::{TryFrom, TryInto},
fmt,
future::Future,
Expand All @@ -147,8 +147,6 @@ pub mod tla;

use crate::storage::with_voting_state_machines_mut;
#[cfg(feature = "tla")]
use std::collections::BTreeSet;
#[cfg(feature = "tla")]
pub use tla::{
tla_update_method, InstrumentationState, ToTla, CLAIM_NEURON_DESC, DISBURSE_NEURON_DESC,
DISBURSE_TO_NEURON_DESC, MERGE_NEURONS_DESC, SPAWN_NEURONS_DESC, SPAWN_NEURON_DESC,
Expand Down Expand Up @@ -216,6 +214,9 @@ pub const MAX_NEURON_CREATION_SPIKE: u64 = MAX_SUSTAINED_NEURONS_PER_HOUR * 8;
/// The maximum number results returned by the method `list_proposals`.
pub const MAX_LIST_PROPOSAL_RESULTS: u32 = 100;

/// The maximum number of neurons returned by `list_neurons`
pub const MAX_LIST_NEURONS_RESULTS: usize = 500;

const MAX_LIST_NODE_PROVIDER_REWARDS_RESULTS: usize = 24;

/// The number of e8s per ICP;
Expand Down Expand Up @@ -2241,11 +2242,9 @@ impl Governance {
/// TODO(NNS1-2499): inline this.
/// Return the Neuron IDs of all Neurons that have `principal` as their
/// controller or as one of their hot keys.
pub fn get_neuron_ids_by_principal(&self, principal_id: &PrincipalId) -> Vec<NeuronId> {
pub fn get_neuron_ids_by_principal(&self, principal_id: &PrincipalId) -> BTreeSet<NeuronId> {
self.neuron_store
.get_neuron_ids_readable_by_caller(*principal_id)
.into_iter()
.collect()
}

/// Return the union of `followees` with the set of Neuron IDs of all
Expand Down Expand Up @@ -2277,8 +2276,15 @@ impl Governance {
include_neurons_readable_by_caller,
include_empty_neurons_readable_by_caller,
include_public_neurons_in_full_neurons,
page_number,
page_size,
} = list_neurons;

let page_number = page_number.unwrap_or(0);
let page_size = page_size
.unwrap_or(MAX_LIST_NEURONS_RESULTS as u64)
.min(MAX_LIST_NEURONS_RESULTS as u64);

let include_empty_neurons_readable_by_caller = include_empty_neurons_readable_by_caller
// This default is to maintain the previous behavior. (Unlike
// protobuf, we do not have a convention that says "the default
Expand All @@ -2305,24 +2311,36 @@ impl Governance {
.get_non_empty_neuron_ids_readable_by_caller(caller)
}
} else {
Vec::new()
BTreeSet::new()
};

// Concatenate (explicit and implicit)-ly included neurons.
let mut requested_neuron_ids: Vec<NeuronId> =
let mut requested_neuron_ids: BTreeSet<NeuronId> =
neuron_ids.iter().map(|id| NeuronId { id: *id }).collect();
requested_neuron_ids.append(&mut implicitly_requested_neuron_ids);

// These will be assembled into the final result.
let mut neuron_infos = hashmap![];
let mut full_neurons = vec![];

let chunks: Vec<Vec<NeuronId>> = requested_neuron_ids
.into_iter()
.chunks(page_size as usize)
.into_iter()
.map(|chunk| chunk.collect())
.collect();

let total_pages_available = Some(chunks.len() as u64);

let empty = Vec::new();
let current_page = chunks.get(page_number as usize).unwrap_or(&empty);

// Populate the above two neuron collections.
for neuron_id in requested_neuron_ids {
for neuron_id in current_page {
// Ignore when a neuron is not found. It is not guaranteed that a
// neuron will be found, because some of the elements in
// requested_neuron_ids are supplied by the caller.
let _ignore_when_neuron_not_found = self.with_neuron(&neuron_id, |neuron| {
let _ignore_when_neuron_not_found = self.with_neuron(neuron_id, |neuron| {
// Populate neuron_infos.
let neuron_info = neuron.get_neuron_info(self.voting_power_economics(), now, caller);

Expand Down Expand Up @@ -2359,6 +2377,7 @@ impl Governance {
ListNeuronsResponse {
neuron_infos,
full_neurons,
total_pages_available,
}
}

Expand Down Expand Up @@ -3759,7 +3778,7 @@ impl Governance {
match proposal_data {
None => None,
Some(pd) => {
let caller_neurons: HashSet<NeuronId> =
let caller_neurons: BTreeSet<NeuronId> =
self.neuron_store.get_neuron_ids_readable_by_caller(*caller);
let now = self.env.now();
Some(self.proposal_data_to_info(pd, &caller_neurons, now, false))
Expand Down Expand Up @@ -3845,7 +3864,7 @@ impl Governance {
/// retrieve dropped payloads by calling `get_proposal_info` for
/// each proposal of interest.
pub fn get_pending_proposals(&self, caller: &PrincipalId) -> Vec<ProposalInfo> {
let caller_neurons: HashSet<NeuronId> =
let caller_neurons: BTreeSet<NeuronId> =
self.neuron_store.get_neuron_ids_readable_by_caller(*caller);
let now = self.env.now();
self.get_pending_proposals_data()
Expand Down Expand Up @@ -3883,7 +3902,7 @@ impl Governance {
fn proposal_data_to_info(
&self,
data: &ProposalData,
caller_neurons: &HashSet<NeuronId>,
caller_neurons: &BTreeSet<NeuronId>,
now_seconds: u64,
multi_query: bool,
) -> ProposalInfo {
Expand Down Expand Up @@ -3915,7 +3934,7 @@ impl Governance {
/// in `except_from`.
fn remove_ballots_not_cast_by(
all_ballots: &HashMap<u64, Ballot>,
except_from: &HashSet<NeuronId>,
except_from: &BTreeSet<NeuronId>,
) -> HashMap<u64, Ballot> {
let mut ballots = HashMap::new();
for neuron_id in except_from.iter() {
Expand Down Expand Up @@ -3955,7 +3974,7 @@ impl Governance {
fn proposal_is_visible_to_neurons(
&self,
info: &ProposalData,
caller_neurons: &HashSet<NeuronId>,
caller_neurons: &BTreeSet<NeuronId>,
) -> bool {
// Is 'info' a manage neuron proposal?
if let Some(ref managed_id) = info.proposal.as_ref().and_then(|x| x.managed_neuron()) {
Expand Down Expand Up @@ -4025,7 +4044,7 @@ impl Governance {
caller: &PrincipalId,
req: &ListProposalInfo,
) -> ListProposalInfoResponse {
let caller_neurons: HashSet<NeuronId> =
let caller_neurons: BTreeSet<NeuronId> =
self.neuron_store.get_neuron_ids_readable_by_caller(*caller);
let exclude_topic: HashSet<i32> = req.exclude_topic.iter().cloned().collect();
let include_reward_status: HashSet<i32> =
Expand Down
2 changes: 2 additions & 0 deletions rs/nns/governance/src/governance/benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ fn list_neurons_benchmark() -> BenchResult {
include_neurons_readable_by_caller: true,
include_empty_neurons_readable_by_caller: Some(false),
include_public_neurons_in_full_neurons: None,
page_number: None,
page_size: None,
};

bench_fn(|| {
Expand Down
Loading
Loading