use super::{
NnsNeuronCollectionState, collection::validate_collection_state, model::NnsNeuronRow,
source::validate_neuron_rows,
};
use crate::nns::governance::{NnsGovernanceCollectionStatus, NnsGovernanceSourceProvenance};
use serde::Serialize;
use std::collections::BTreeMap;
use thiserror::Error as ThisError;
pub const NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct NnsNeuronStateDistribution {
pub state: i32,
pub neuron_count: u64,
pub effective_stake_e8s: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct NnsNeuronVisibilityDistribution {
pub visibility: Option<i32>,
pub neuron_count: u64,
pub effective_stake_e8s: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct NnsNeuronTypeDistribution {
pub neuron_type: Option<i32>,
pub neuron_count: u64,
pub effective_stake_e8s: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct NnsNeuronDistributionReport {
#[serde(flatten)]
pub context: crate::nns::governance::NnsGovernanceAnalyticsContext,
pub collected_neuron_count: u64,
pub earliest_retrieved_at_timestamp_seconds: Option<u64>,
pub latest_retrieved_at_timestamp_seconds: Option<u64>,
pub total_effective_stake_e8s: u64,
pub reported_staked_maturity_neuron_count: u64,
pub unreported_staked_maturity_neuron_count: u64,
pub total_reported_staked_maturity_e8s_equivalent: u64,
pub reported_deciding_voting_power_neuron_count: u64,
pub unreported_deciding_voting_power_neuron_count: u64,
pub total_reported_deciding_voting_power: u64,
pub reported_potential_voting_power_neuron_count: u64,
pub unreported_potential_voting_power_neuron_count: u64,
pub total_reported_potential_voting_power: u64,
pub known_neuron_metadata_count: u64,
pub neurons_fund_join_timestamp_present_count: u64,
pub state_distribution: Vec<NnsNeuronStateDistribution>,
pub visibility_distribution: Vec<NnsNeuronVisibilityDistribution>,
pub neuron_type_distribution: Vec<NnsNeuronTypeDistribution>,
}
#[derive(Debug, ThisError)]
pub enum NnsNeuronDistributionError {
#[error("invalid NNS neuron collection state for distribution projection: {reason}")]
InvalidCollectionState {
reason: String,
},
#[error("NNS neuron distribution requires a complete collection; state is {status}")]
CollectionNotComplete {
status: NnsGovernanceCollectionStatus,
},
#[error(
"NNS neuron distribution received {actual} rows; complete collection accounts for {expected}"
)]
NeuronCountMismatch {
expected: u64,
actual: u64,
},
#[error("invalid NNS neuron rows for distribution projection: {reason}")]
InvalidNeuronRows {
reason: String,
},
#[error("NNS neuron distribution accounting overflow while updating {field}")]
AccountingOverflow {
field: &'static str,
},
}
pub fn build_nns_neuron_distribution_report(
collection: &NnsNeuronCollectionState,
neurons: &[NnsNeuronRow],
) -> Result<NnsNeuronDistributionReport, NnsNeuronDistributionError> {
validate_collection_state(collection).map_err(|error| {
NnsNeuronDistributionError::InvalidCollectionState {
reason: error.to_string(),
}
})?;
if !collection.is_complete() {
return Err(NnsNeuronDistributionError::CollectionNotComplete {
status: collection.status(),
});
}
let source = collection.source().cloned().ok_or_else(|| {
NnsNeuronDistributionError::InvalidCollectionState {
reason: "complete collection has no concrete source provenance".to_string(),
}
})?;
let expected = collection.neurons_fetched();
let actual = u64::try_from(neurons.len()).map_err(|_| {
NnsNeuronDistributionError::AccountingOverflow {
field: "supplied_neuron_count",
}
})?;
if actual != expected {
return Err(NnsNeuronDistributionError::NeuronCountMismatch { expected, actual });
}
validate_neuron_rows(neurons).map_err(|error| {
NnsNeuronDistributionError::InvalidNeuronRows {
reason: error.to_string(),
}
})?;
aggregate_distribution(collection, neurons, source)
}
#[cfg(feature = "nns-host")]
pub(super) fn build_distribution_from_cache(
cache: &super::cache::AdmittedNeuronCache,
) -> Result<NnsNeuronDistributionReport, NnsNeuronDistributionError> {
let collection = cache.collection_state();
let source = collection.source().cloned().ok_or_else(|| {
NnsNeuronDistributionError::InvalidCollectionState {
reason: "complete collection has no concrete source provenance".to_string(),
}
})?;
aggregate_distribution(collection, cache.rows(), source)
}
fn aggregate_distribution(
collection: &NnsNeuronCollectionState,
neurons: &[NnsNeuronRow],
source: NnsGovernanceSourceProvenance,
) -> Result<NnsNeuronDistributionReport, NnsNeuronDistributionError> {
let mut distribution = DistributionAccumulator::default();
for neuron in neurons {
distribution.observe(neuron)?;
}
Ok(distribution.into_report(collection, source))
}
#[derive(Default)]
struct DistributionTotals {
count: u64,
total: u64,
}
#[derive(Default)]
struct DistributionAccumulator {
states: BTreeMap<i32, DistributionTotals>,
visibilities: BTreeMap<Option<i32>, DistributionTotals>,
neuron_types: BTreeMap<Option<i32>, DistributionTotals>,
total_effective_stake: DistributionTotals,
staked_maturity: DistributionTotals,
deciding_voting_power: DistributionTotals,
potential_voting_power: DistributionTotals,
known_neuron_metadata_count: u64,
neurons_fund_join_timestamp_present_count: u64,
retrieved_timestamp_bounds: Option<(u64, u64)>,
}
impl DistributionAccumulator {
fn observe(&mut self, neuron: &NnsNeuronRow) -> Result<(), NnsNeuronDistributionError> {
record_distribution(
&mut self.total_effective_stake,
Some(neuron.stake_e8s),
"collected_neuron_count",
"total_effective_stake_e8s",
)?;
record_distribution(
self.states.entry(neuron.state).or_default(),
Some(neuron.stake_e8s),
"state_neuron_count",
"state_effective_stake_e8s",
)?;
record_distribution(
self.visibilities.entry(neuron.visibility).or_default(),
Some(neuron.stake_e8s),
"visibility_neuron_count",
"visibility_effective_stake_e8s",
)?;
record_distribution(
self.neuron_types.entry(neuron.neuron_type).or_default(),
Some(neuron.stake_e8s),
"neuron_type_neuron_count",
"neuron_type_effective_stake_e8s",
)?;
record_distribution(
&mut self.staked_maturity,
neuron.staked_maturity_e8s_equivalent,
"reported_staked_maturity_neuron_count",
"total_reported_staked_maturity_e8s_equivalent",
)?;
record_distribution(
&mut self.deciding_voting_power,
neuron.deciding_voting_power,
"reported_deciding_voting_power_neuron_count",
"total_reported_deciding_voting_power",
)?;
record_distribution(
&mut self.potential_voting_power,
neuron.potential_voting_power,
"reported_potential_voting_power_neuron_count",
"total_reported_potential_voting_power",
)?;
if neuron.known_neuron_data.is_some() {
increment(
&mut self.known_neuron_metadata_count,
"known_neuron_metadata_count",
)?;
}
if neuron.joined_community_fund_timestamp_seconds.is_some() {
increment(
&mut self.neurons_fund_join_timestamp_present_count,
"neurons_fund_join_timestamp_present_count",
)?;
}
let retrieved_at = neuron.retrieved_at_timestamp_seconds;
self.retrieved_timestamp_bounds = Some(match self.retrieved_timestamp_bounds {
Some((earliest, latest)) => (earliest.min(retrieved_at), latest.max(retrieved_at)),
None => (retrieved_at, retrieved_at),
});
Ok(())
}
fn into_report(
self,
collection: &NnsNeuronCollectionState,
source: NnsGovernanceSourceProvenance,
) -> NnsNeuronDistributionReport {
let collected_neuron_count = collection.neurons_fetched();
NnsNeuronDistributionReport {
context: crate::nns::governance::NnsGovernanceAnalyticsContext {
schema_version: NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION,
network: collection.network().to_string(),
governance_canister_id: collection.governance_canister_id().to_string(),
source,
collection_started_at: collection.started_at().to_string(),
collection_updated_at: collection.updated_at().to_string(),
collection_page_count: collection.pages_fetched(),
point_in_time_guaranteed: false,
},
collected_neuron_count,
earliest_retrieved_at_timestamp_seconds: self
.retrieved_timestamp_bounds
.map(|bounds| bounds.0),
latest_retrieved_at_timestamp_seconds: self
.retrieved_timestamp_bounds
.map(|bounds| bounds.1),
total_effective_stake_e8s: self.total_effective_stake.total,
reported_staked_maturity_neuron_count: self.staked_maturity.count,
unreported_staked_maturity_neuron_count: collected_neuron_count
- self.staked_maturity.count,
total_reported_staked_maturity_e8s_equivalent: self.staked_maturity.total,
reported_deciding_voting_power_neuron_count: self.deciding_voting_power.count,
unreported_deciding_voting_power_neuron_count: collected_neuron_count
- self.deciding_voting_power.count,
total_reported_deciding_voting_power: self.deciding_voting_power.total,
reported_potential_voting_power_neuron_count: self.potential_voting_power.count,
unreported_potential_voting_power_neuron_count: collected_neuron_count
- self.potential_voting_power.count,
total_reported_potential_voting_power: self.potential_voting_power.total,
known_neuron_metadata_count: self.known_neuron_metadata_count,
neurons_fund_join_timestamp_present_count: self
.neurons_fund_join_timestamp_present_count,
state_distribution: self
.states
.into_iter()
.map(|(state, distribution)| NnsNeuronStateDistribution {
state,
neuron_count: distribution.count,
effective_stake_e8s: distribution.total,
})
.collect(),
visibility_distribution: self
.visibilities
.into_iter()
.map(
|(visibility, distribution)| NnsNeuronVisibilityDistribution {
visibility,
neuron_count: distribution.count,
effective_stake_e8s: distribution.total,
},
)
.collect(),
neuron_type_distribution: self
.neuron_types
.into_iter()
.map(|(neuron_type, distribution)| NnsNeuronTypeDistribution {
neuron_type,
neuron_count: distribution.count,
effective_stake_e8s: distribution.total,
})
.collect(),
}
}
}
fn increment(value: &mut u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
*value = value
.checked_add(1)
.ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
Ok(())
}
fn record_distribution(
values: &mut DistributionTotals,
value: Option<u64>,
count_field: &'static str,
total_field: &'static str,
) -> Result<(), NnsNeuronDistributionError> {
if let Some(value) = value {
let count = values
.count
.checked_add(1)
.ok_or(NnsNeuronDistributionError::AccountingOverflow { field: count_field })?;
let total = values
.total
.checked_add(value)
.ok_or(NnsNeuronDistributionError::AccountingOverflow { field: total_field })?;
*values = DistributionTotals { count, total };
}
Ok(())
}
#[cfg(test)]
mod tests;