Compare commits

...

1 Commits

Author SHA1 Message Date
jmwample f7bbd0c93e unfinished changes for nym api topology caching and endpoints 2025-02-25 15:47:38 -07:00
12 changed files with 319 additions and 25 deletions
@@ -497,6 +497,11 @@ impl NymApiClient {
Ok(nodes)
}
/// retrieve basic information for all bonded nodes on the network
pub async fn retrieve_basic_nodes_batch(&self, node_ids: Vec<u32>) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
Ok(self.nym_api.retrieve_basic_nodes_batch(&node_ids).await?.nodes)
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
Ok(self.nym_api.health().await?)
}
@@ -253,10 +253,10 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
"mixnodes",
"skimmed",
routes::SKIMMED,
],
NO_PARAMS,
)
@@ -269,10 +269,10 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
"gateways",
"skimmed",
routes::SKIMMED,
],
NO_PARAMS,
)
@@ -318,9 +318,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
"skimmed",
routes::SKIMMED,
"entry-gateways",
"all",
],
@@ -355,9 +355,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
"skimmed",
routes::SKIMMED,
"mixnodes",
"active",
],
@@ -392,9 +392,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
"skimmed",
routes::SKIMMED,
"mixnodes",
"all",
],
@@ -403,6 +403,31 @@ pub trait NymApiClientExt: ApiClient {
.await
}
/// Send a Post request with a set of node ids. A successful response will contain descriptors
/// for all nodes associated with those node IDs available in the current full topology.
///
/// If a provided node ID is not present there will be no descriptor for that node in the response.
///
/// If no node IDs are provided the response will contain no descriptors.
#[instrument(level = "debug", skip(self))]
async fn retrieve_basic_nodes_batch(
&self,
node_ids: &[NodeId],
) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
routes::SKIMMED,
routes::BATCH,
],
NO_PARAMS,
node_ids,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
&self,
@@ -427,9 +452,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
"skimmed",
routes::SKIMMED,
],
&params,
)
@@ -70,3 +70,7 @@ pub const SERVICE_PROVIDERS: &str = "services";
pub const DETAILS: &str = "details";
pub const NETWORK: &str = "network";
pub const UNSTABLE: &str = "unstable";
pub const SKIMMED: &str = "skimmed";
pub const BATCH: &str = "batch";
+1
View File
@@ -26,6 +26,7 @@ use utoipa::{IntoParams, ToSchema};
pub(crate) mod legacy;
pub(crate) mod unstable;
pub(crate) mod topology;
pub(crate) fn nym_node_routes() -> Router<AppState> {
Router::new()
+153
View File
@@ -0,0 +1,153 @@
#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
use crate::{
node_status_api::models::{AxumErrorResponse, AxumResult},
support::http::{helpers::NodeIdParam, topology_cache::{TopologyCache, PayloadFormat}},
};
use axum::{
extract::{Path, Query, State},
routing::{get, post},
Json, Router,
};
use nym_mixnet_contract_common::{Interval, NodeId};
use nym_api_requests::models::RewardedSetResponse;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
type SkimmedNodes = AxumResult<Json<TopologyResponse>>;
#[derive(Debug, Deserialize, utoipa::IntoParams)]
#[into_params(parameter_in = Query)]
struct TopologyParams {
#[allow(dead_code)]
semver_compatibility: Option<String>,
// Identifier for the current epoch of the topology state. When sent by a client we can check if
// the client already knows about the latest topology state, allowing a `no-updates` response
// instead of wasting bandwidth serving an unchanged topology.
epoch_id: Option<u32>,
}
#[allow(deprecated)]
pub(crate) fn topology_routes() -> Router<Arc<TopologyCache>> {
Router::new()
.route("layer-assignments", get(layer_assignments))
.nest(
"/skimmed",
Router::new()
.route("/", get(nodes_basic_all))
.route("batch", post(nodes_basic_batch))
.route("/:node_id", get(node_basic)),
)
// // NOT IMPLEMENTED
// .nest(
// "/semi-skimmed",
// Router::new().route("/", get(nodes_expanded_all)),
// )
// .nest(
// "/full-fat",
// Router::new().route("/", get(nodes_detailed_all)),
// )
}
async fn nodes_basic_all(
State(state): State<Arc<TopologyCache>>,
Query(query_params): Query<TopologyParams>,
) -> AxumResult<Json<TopologyResponse>> {
Err(AxumErrorResponse::not_implemented())
}
async fn nodes_basic_batch(
State(state): State<Arc<TopologyCache>>,
Query(query_params): Query<TopologyParams>,
Json(node_ids): Json<Vec<NodeId>>,
) -> AxumResult<Json<TopologyResponse>> {
Err(AxumErrorResponse::not_implemented())
}
async fn node_basic(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
State(state): State<Arc<TopologyCache>>,
Query(query_params): Query<TopologyParams>,
) -> AxumResult<Json<TopologyResponse>> {
Err(AxumErrorResponse::not_implemented())
}
async fn layer_assignments(
State(state): State<Arc<TopologyCache>>,
Query(query_params): Query<TopologyParams>,
) -> AxumResult<Json<LayerAssignmentsResponse>> {
Err(AxumErrorResponse::not_implemented())
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
struct LayerAssignmentsResponse {
pub status: Option<TopologyRequestStatus>,
pub assignments: RewardedSetResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
struct TopologyResponse {
pub status: Option<TopologyRequestStatus>,
payload: Vec<u8>,
payload_format: Option<PayloadFormat>,
payload_signature: Option<Vec<u8>>,
current_topology_hash: Option<Vec<u8>>,
topology_signature: Option<Vec<u8>>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TopologyRequestStatus {
NoUpdates,
Fresh(Interval),
}
#[cfg(test)]
mod test {
use std::time::Duration;
use axum_test::TestServer;
use nym_topology::NymTopology;
use time::OffsetDateTime;
use crate::support::http::topology_cache::Epoch;
use super::*;
fn build_test_topology_cache() -> TopologyCache {
let current_epoch = Epoch {
id: 123,
current_epoch_start: OffsetDateTime::now_utc(),
epoch_length: Duration::from_secs(120),
};
let topology = NymTopology::default();
TopologyCache::new(current_epoch, topology);
todo!();
}
#[tokio::test]
async fn test_topology_basic() -> Result<(), Box<dyn ::std::error::Error>> {
let state = Arc::new(build_test_topology_cache());
let app = topology_routes().with_state(state);
let server = TestServer::new(app)?;
let response = server
.get(&"/layer_assignments")
.await;
response.assert_text("hello!");
Ok(())
}
}
@@ -25,11 +25,11 @@ use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded;
use crate::nym_nodes::handlers::unstable::skimmed::{
entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active,
exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active,
nodes_basic_all,
nodes_basic_all, nodes_basic_batch,
};
use crate::support::http::helpers::PaginationRequest;
use crate::support::http::state::AppState;
use axum::routing::get;
use axum::routing::{get, post};
use axum::Router;
use nym_api_requests::nym_nodes::NodeRoleQueryParam;
use serde::Deserialize;
@@ -47,6 +47,7 @@ pub(crate) fn nym_node_routes_unstable() -> Router<AppState> {
"/skimmed",
Router::new()
.route("/", get(nodes_basic_all))
.route("batch", post(nodes_basic_batch))
.route("/active", get(nodes_basic_active))
.nest(
"/mixnodes",
@@ -25,6 +25,7 @@ use tracing::trace;
use utoipa::ToSchema;
pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<SkimmedNode>>>;
type SkimmedNodes = AxumResult<Json<CachedNodesResponse<SkimmedNode>>>;
/// Given all relevant caches, build part of response for JUST Nym Nodes
fn build_nym_nodes_response<'a, NI>(
@@ -196,7 +197,7 @@ where
pub(super) async fn deprecated_gateways_basic(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
) -> SkimmedNodes {
// 1. call '/v1/unstable/skimmed/entry-gateways/all'
let all_gateways = entry_gateways_basic_all(state, query_params).await?;
@@ -223,7 +224,7 @@ pub(super) async fn deprecated_gateways_basic(
pub(super) async fn deprecated_mixnodes_basic(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
) -> SkimmedNodes {
// 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active'
let active_mixnodes = mixnodes_basic_active(state, query_params).await?;
@@ -239,7 +240,7 @@ async fn nodes_basic(
state: State<AppState>,
Query(_query_params): Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
) -> SkimmedNodes {
// unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes
// 1. grab all relevant described nym-nodes
@@ -281,10 +282,10 @@ async fn nodes_basic(
legacy_gateways.timestamp(),
]);
Ok(Json(PaginatedCachedNodesResponse::new_full(
Ok(Json(CachedNodesResponse {
refreshed_at,
nodes,
)))
}))
}
#[allow(dead_code)] // not dead, used in OpenAPI docs
@@ -329,6 +330,31 @@ pub(super) async fn nodes_basic_all(
nodes_basic(state, Query(query_params.into()), false).await
}
/// Post request handler taking a json array of NodeId (u32) values and returning descriptors for
/// the provided NodeId values. A successful response will contain descriptors for all nodes
/// associated with those node IDs available in the current full topology.
///
/// If a provided node ID is not present in the current topology there will be no descriptor for
/// that node in the response.
///
/// If no node IDs are provided the response will contain no descriptors.
#[utoipa::path(
tag = "Unstable Nym Nodes batch by Node ID",
get,
params(NodesParamsWithRole),
path = "batch",
context_path = "/v1/unstable/nym-nodes/skimmed",
responses(
(status = 200, body = PaginatedCachedNodesResponseSchema)
)
)]
pub(super) async fn nodes_basic_batch(
state: State<AppState>,
Query(query_params): Query<NodesParamsWithRole>,
) -> SkimmedNodes {
nodes_basic(state, Query(query_params.into()), false).await
}
/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
/// that are currently bonded and are in the **active set**
#[utoipa::path(
+17 -4
View File
@@ -23,6 +23,7 @@ use crate::support::config::Config;
use crate::support::http::state::{
AppState, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S,
};
use crate::support::http::topology_cache::{Epoch, TopologyCache};
use crate::support::http::RouterBuilder;
use crate::support::nyxd;
use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::migrate_to_directory_services_v2_1;
@@ -35,6 +36,7 @@ use anyhow::{bail, Context};
use nym_config::defaults::NymNetworkDetails;
use nym_sphinx::receiver::SphinxMessageReceiver;
use nym_task::TaskManager;
use nym_topology::NymTopology;
use nym_validator_client::nyxd::Coin;
use std::net::SocketAddr;
use std::sync::Arc;
@@ -133,8 +135,6 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
let identity_keypair = config.base.storage_paths.load_identity()?;
let identity_public_key = *identity_keypair.public_key();
let router = RouterBuilder::with_default_routes(config.network_monitor.enabled);
let nym_contract_cache_state = NymContractCache::new();
let node_status_cache_state = NodeStatusCache::new();
let mix_denom = network_details.network.chain_details.mix_denom.base.clone();
@@ -190,7 +190,16 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
};
ecash_state.spawn_background_cleaner();
let router = router.with_state(AppState {
let epoch = Epoch::from(nym_contract_cache_state.current_interval().await.take());
let topology = {
todo!("cannot proceed without implementing this.");
NymTopology::default()
};
let topology_cache = TopologyCache::new(epoch, topology);
let state = AppState {
forced_refresh: ForcedRefresh::new(
config.topology_cacher.debug.node_describe_allow_illegal_ips,
),
@@ -203,7 +212,11 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
node_info_cache,
api_status: ApiStatusState::new(signer_information),
ecash_state: Arc::new(ecash_state),
});
topology_cache: Arc::new(topology_cache)
};
let router = RouterBuilder::with_default_routes(&state, config.network_monitor.enabled);
let router = router.with_state(state);
// start note describe cache refresher
// we should be doing the below, but can't due to our current startup structure
+1
View File
@@ -6,6 +6,7 @@ pub(crate) mod openapi;
pub(crate) mod router;
pub(crate) mod state;
mod unstable_routes;
pub(crate) mod topology_cache;
pub(crate) use router::RouterBuilder;
+5 -2
View File
@@ -8,6 +8,7 @@ use crate::node_status_api::handlers::status_routes;
use crate::nym_contract_cache::handlers::nym_contract_cache_routes;
use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes;
use crate::nym_nodes::handlers::nym_node_routes;
use crate::nym_nodes::handlers::topology::topology_routes;
use crate::status;
use crate::support::http::openapi::ApiDoc;
use crate::support::http::state::AppState;
@@ -38,7 +39,7 @@ pub(crate) struct RouterBuilder {
impl RouterBuilder {
/// All routes should be, if possible, added here. Exceptions are e.g.
/// routes which are added conditionally in other places based on some `if`.
pub(crate) fn with_default_routes(network_monitor: bool) -> Self {
pub(crate) fn with_default_routes(state: &AppState, network_monitor: bool) -> Self {
// https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html
// TODO rocket use tracing instead of env_logger
// https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs
@@ -64,7 +65,9 @@ impl RouterBuilder {
.nest("/api-status", status::handlers::api_status_routes())
.nest("/nym-nodes", nym_node_routes())
.nest("/ecash", ecash_routes())
.nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes
.nest("/unstable", unstable_routes())
.nest("/topology", topology_routes().with_state(state.topology_cache.clone())),
// CORS layer needs to be "outside" of routes
);
Self {
+2
View File
@@ -12,6 +12,7 @@ use crate::nym_contract_cache::cache::NymContractCache;
use crate::status::ApiStatusState;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::Cache;
use crate::support::http::topology_cache::TopologyCache;
use crate::support::storage;
use axum::extract::FromRef;
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation};
@@ -87,6 +88,7 @@ pub(crate) struct AppState {
pub(crate) api_status: ApiStatusState,
// todo: refactor it into inner: Arc<EcashStateInner>
pub(crate) ecash_state: Arc<EcashState>,
pub(crate) topology_cache: Arc<TopologyCache>
}
impl FromRef<AppState> for ApiStatusState {
@@ -0,0 +1,60 @@
#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
use std::collections::HashMap;
use nym_topology::NymTopology;
use serde::{Deserialize, Serialize};
use nym_mixnet_contract_common::{EpochId, Interval};
use std::time::Duration;
use time::OffsetDateTime;
pub struct Epoch {
pub id: EpochId,
pub current_epoch_start: OffsetDateTime,
pub epoch_length: Duration,
}
impl From<Interval> for Epoch {
fn from(value: Interval) -> Self {
Self {
id: value.current_epoch_id(),
current_epoch_start: value.current_epoch_start(),
epoch_length: value.epoch_length(),
}
}
}
/// Format for
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum PayloadFormat {
Json,
BitCode,
}
pub(crate) struct TopologyCache {
current_epoch: Epoch,
formats: HashMap<PayloadFormat, SerializedTopology>,
cached: NymTopology,
hash: Option<Vec<u8>>,
signature: Option<Vec<u8>>
}
pub(crate) struct SerializedTopology{
bytes: Vec<u8>,
signature: Vec<u8>,
}
impl TopologyCache {
pub fn new(current_epoch: Epoch, initial: NymTopology) -> Self {
Self {
current_epoch,
formats: HashMap::new(),
cached: initial,
hash: None,
signature: None
}
}
}