use super::{
NNS_NEURON_CACHE_COMPONENT, NNS_NEURON_CACHE_SCHEMA_VERSION,
model::NnsNeuronCache,
paths::{
NNS_NEURON_CACHE_COLLECTION, NNS_NEURON_CACHE_DOMAIN, NNS_NEURON_CACHE_ENTITY,
nns_neuron_cache_path, nns_neuron_cache_paths,
},
};
use crate::nns::neuron::report::source::{
NnsNeuronSource, build_nns_neuron_info_report_with_source,
build_nns_neuron_list_report_with_source,
};
use crate::{
HostCacheError,
cache::{CacheCollectionCompleteness, validate_cache_collection_completeness},
cache_file::{LoadJsonCacheRequest, open_managed_file},
nns::{
NnsGovernanceCacheRequest,
governance::{
NnsGovernanceCacheStatusReport, NnsGovernanceCacheSummary, NnsGovernanceReportContext,
read_governance_refresh_attempt_status, validate_governance_cache_metadata,
},
neuron::report::{
NnsNeuronHostError,
collection::validate_collection_state,
distribution::{NnsNeuronDistributionReport, build_nns_neuron_distribution_report},
enforce_mainnet_network,
model::{
NnsNeuronInfoReport, NnsNeuronInfoRequest, NnsNeuronListReport,
NnsNeuronListRequest,
},
source::{
NnsNeuronReportProvenance, info_report_from_row, list_report_from_rows,
validate_neuron_rows, validate_page_size,
},
},
},
snapshot_cache::{SnapshotIdentityMismatch, SnapshotKey, load_complete_snapshot_for_key},
};
use std::path::{Path, PathBuf};
pub async fn build_nns_neuron_list_report_with_cache_and_source(
request: &NnsNeuronListRequest,
cache_root: &Path,
source: &dyn NnsNeuronSource,
) -> Result<NnsNeuronListReport, NnsNeuronHostError> {
if let Some(report) = build_nns_neuron_list_report_from_cache(request, cache_root)? {
return Ok(report);
}
Ok(build_nns_neuron_list_report_with_source(request, source).await?)
}
pub async fn build_nns_neuron_info_report_with_cache_and_source(
request: &NnsNeuronInfoRequest,
cache_root: &Path,
source: &dyn NnsNeuronSource,
) -> Result<NnsNeuronInfoReport, NnsNeuronHostError> {
if let Some(report) = build_nns_neuron_info_report_from_cache(request, cache_root)? {
return Ok(report);
}
Ok(build_nns_neuron_info_report_with_source(request, source).await?)
}
pub fn build_nns_neuron_distribution_report_from_cache(
request: &NnsGovernanceCacheRequest,
) -> Result<NnsNeuronDistributionReport, NnsNeuronHostError> {
enforce_mainnet_network(&request.network)?;
let path = nns_neuron_cache_path(&request.cache_root, &request.network);
let cache = load_cache_at(&request.cache_root, &path, &request.network, None)?;
Ok(build_nns_neuron_distribution_report(
&cache.data.collection_state,
&cache.data.neurons,
)?)
}
pub fn build_nns_neuron_list_report_from_cache(
request: &NnsNeuronListRequest,
cache_root: &Path,
) -> Result<Option<NnsNeuronListReport>, NnsNeuronHostError> {
validate_page_size(request.limit)?;
enforce_mainnet_network(&request.governance.network)?;
let Some((path, cache)) = load_optional_cache(cache_root, &request.governance.network)? else {
return Ok(None);
};
let total_neuron_count = cache.data.neurons.len();
let start_index = request.exclusive_start_neuron_id.map_or(0, |start| {
cache
.data
.neurons
.partition_point(|neuron| neuron.neuron_id <= start)
});
let limit = usize::try_from(request.limit).unwrap_or(usize::MAX);
let end_index = start_index.saturating_add(limit).min(total_neuron_count);
let provenance = cache_provenance(&path, &cache);
let neurons = cache
.data
.neurons
.into_iter()
.skip(start_index)
.take(end_index - start_index)
.collect::<Vec<_>>();
let next_start_neuron_id = (end_index < total_neuron_count)
.then(|| neurons.last().map(|row| row.neuron_id))
.flatten();
Ok(Some(list_report_from_rows(
request,
provenance,
neurons,
next_start_neuron_id,
Some(total_neuron_count),
)))
}
pub fn build_nns_neuron_info_report_from_cache(
request: &NnsNeuronInfoRequest,
cache_root: &Path,
) -> Result<Option<NnsNeuronInfoReport>, NnsNeuronHostError> {
enforce_mainnet_network(&request.governance.network)?;
let Some((path, mut cache)) = load_optional_cache(cache_root, &request.governance.network)?
else {
return Ok(None);
};
let Ok(index) = cache
.data
.neurons
.binary_search_by_key(&request.neuron_id, |row| row.neuron_id)
else {
return Ok(None);
};
let provenance = cache_provenance(&path, &cache);
let neuron = cache.data.neurons.swap_remove(index);
Ok(Some(info_report_from_row(request, provenance, neuron)))
}
pub fn build_nns_neuron_cache_status_report(
request: &NnsGovernanceCacheRequest,
) -> Result<NnsGovernanceCacheStatusReport, NnsNeuronHostError> {
enforce_mainnet_network(&request.network)?;
let paths = nns_neuron_cache_paths(&request.cache_root, &request.network);
let file = open_managed_file(&request.cache_root, &paths.snapshot_path)
.map_err(|source| HostCacheError::operation(NNS_NEURON_CACHE_COMPONENT, source))?;
let cache = if let Some(file) = file {
Some(
match load_cache_at(
&request.cache_root,
&paths.snapshot_path,
&request.network,
Some(file),
) {
Ok(cache) => NnsGovernanceCacheSummary::from_snapshot(&paths.snapshot_path, &cache),
Err(error) => {
NnsGovernanceCacheSummary::invalid(&paths.snapshot_path, error.to_string())
}
},
)
} else {
None
};
let latest_attempt = read_governance_refresh_attempt_status(
&request.cache_root,
&paths.refresh_attempt_path,
&request.network,
NNS_NEURON_CACHE_COMPONENT,
)?;
Ok(NnsGovernanceCacheStatusReport::from_cache(
request,
"neurons",
&paths,
cache,
latest_attempt,
))
}
fn load_cache_at(
cache_root: &Path,
path: &Path,
network: &str,
opened_file: Option<std::fs::File>,
) -> Result<NnsNeuronCache, NnsNeuronHostError> {
let key = SnapshotKey::full(
NNS_NEURON_CACHE_DOMAIN,
network,
NNS_NEURON_CACHE_ENTITY,
NNS_NEURON_CACHE_COLLECTION,
);
let cache = load_complete_snapshot_for_key(
LoadJsonCacheRequest {
component: NNS_NEURON_CACHE_COMPONENT,
cache_root,
path: path.to_path_buf(),
network,
expected_schema_version: NNS_NEURON_CACHE_SCHEMA_VERSION,
maximum_bytes: crate::cache_file::MAX_COLLECTION_SNAPSHOT_BYTES,
},
None,
&key,
missing_neuron_cache_error,
incomplete_snapshot_error,
|mismatch| nns_neuron_identity_mismatch_error(path.to_path_buf(), mismatch),
opened_file,
)?;
validate_cache(path, &cache)?;
Ok(cache)
}
fn validate_cache(path: &Path, cache: &NnsNeuronCache) -> Result<(), NnsNeuronHostError> {
let invalid = |reason| NnsNeuronHostError::InvalidCache {
path: path.to_path_buf(),
reason,
};
validate_governance_cache_metadata(&cache.metadata).map_err(invalid)?;
validate_cache_collection_completeness(&cache.completeness, cache.data.neurons.len())
.map_err(invalid)?;
if cache.completeness.point_in_time_guaranteed {
return Err(invalid(
"point_in_time_guaranteed must be false for the Governance neuron index".to_string(),
));
}
let state = &cache.data.collection_state;
validate_collection_state(state).map_err(|error| invalid(error.to_string()))?;
if !crate::nns::governance::collection::governance_snapshot_matches!(
cache,
state,
neurons_fetched
) {
return Err(invalid(
"collection_state disagrees with snapshot identity, provenance, or completeness"
.to_string(),
));
}
validate_neuron_rows(&cache.data.neurons).map_err(|error| invalid(error.to_string()))
}
fn cache_provenance(path: &Path, cache: &NnsNeuronCache) -> NnsNeuronReportProvenance {
NnsNeuronReportProvenance {
context: NnsGovernanceReportContext::from_snapshot(cache),
cache_path: Some(path.display().to_string()),
}
}
fn incomplete_snapshot_error(completeness: &CacheCollectionCompleteness) -> NnsNeuronHostError {
NnsNeuronHostError::IncompleteRefresh {
pages_fetched: completeness.page_count,
rows_fetched: completeness.row_count,
reason: "cached NNS neuron snapshot is not complete".to_string(),
}
}
const fn missing_neuron_cache_error(path: PathBuf) -> NnsNeuronHostError {
NnsNeuronHostError::MissingNeuronCache { path }
}
fn nns_neuron_identity_mismatch_error(
path: PathBuf,
mismatch: SnapshotIdentityMismatch,
) -> NnsNeuronHostError {
NnsNeuronHostError::CacheIdentityMismatch {
path,
field: mismatch.field,
expected: mismatch.expected,
actual: mismatch.actual,
}
}
const fn is_missing_cache(error: &NnsNeuronHostError) -> bool {
matches!(error, NnsNeuronHostError::MissingNeuronCache { .. })
}
fn load_optional_cache(
cache_root: &Path,
network: &str,
) -> Result<Option<(PathBuf, NnsNeuronCache)>, NnsNeuronHostError> {
let path = nns_neuron_cache_path(cache_root, network);
match load_cache_at(cache_root, &path, network, None) {
Ok(cache) => Ok(Some((path, cache))),
Err(error) if is_missing_cache(&error) => Ok(None),
Err(error) => Err(error),
}
}