chore: remove unused rocket code
This commit is contained in:
@@ -7,16 +7,6 @@ use nym_task::TaskManager;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod handlers;
|
||||
// pub(crate) mod routes;
|
||||
|
||||
// /// Merges the routes with http information and returns it to Rocket for serving
|
||||
// pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
// openapi_get_routes_spec![
|
||||
// settings: routes::get_full_circulating_supply,
|
||||
// routes::get_total_supply,
|
||||
// routes::get_circulating_supply
|
||||
// ]
|
||||
// }
|
||||
|
||||
/// Spawn the circulating supply cache refresher.
|
||||
pub(crate) fn start_cache_refresh(
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
|
||||
use crate::node_status_api::models::RocketErrorResponse;
|
||||
use nym_api_requests::models::CirculatingSupplyResponse;
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
|
||||
// TODO: this is not the best place to put it, it should be more centralised,
|
||||
// but for a quick fix, that's good enough for now...
|
||||
// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent
|
||||
// value from the mix denom here.
|
||||
const UNYM_RATIO: f64 = 1000000.;
|
||||
|
||||
fn unym_coin_to_float_unym(coin: Coin) -> f64 {
|
||||
// our total supply can't exceed 1B so an overflow here is impossible
|
||||
// (if it happened, then we SHOULD crash)
|
||||
coin.amount as f64 / UNYM_RATIO
|
||||
}
|
||||
|
||||
#[openapi(tag = "circulating-supply")]
|
||||
#[get("/circulating-supply")]
|
||||
pub(crate) async fn get_full_circulating_supply(
|
||||
cache: &State<CirculatingSupplyCache>,
|
||||
) -> Result<Json<CirculatingSupplyResponse>, RocketErrorResponse> {
|
||||
match cache.get_circulating_supply().await {
|
||||
Some(value) => Ok(Json(value)),
|
||||
None => Err(RocketErrorResponse::new(
|
||||
"unavailable",
|
||||
Status::InternalServerError,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "circulating-supply")]
|
||||
#[get("/circulating-supply/total-supply-value")]
|
||||
pub(crate) async fn get_total_supply(
|
||||
cache: &State<CirculatingSupplyCache>,
|
||||
) -> Result<Json<f64>, RocketErrorResponse> {
|
||||
let full_circulating_supply = match cache.get_circulating_supply().await {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return Err(RocketErrorResponse::new(
|
||||
"unavailable",
|
||||
Status::InternalServerError,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(unym_coin_to_float_unym(
|
||||
full_circulating_supply.total_supply.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[openapi(tag = "circulating-supply")]
|
||||
#[get("/circulating-supply/circulating-supply-value")]
|
||||
pub(crate) async fn get_circulating_supply(
|
||||
cache: &State<CirculatingSupplyCache>,
|
||||
) -> Result<Json<f64>, RocketErrorResponse> {
|
||||
let full_circulating_supply = match cache.get_circulating_supply().await {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return Err(RocketErrorResponse::new(
|
||||
"unavailable",
|
||||
Status::InternalServerError,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(unym_coin_to_float_unym(
|
||||
full_circulating_supply.circulating_supply.into(),
|
||||
)))
|
||||
}
|
||||
@@ -1,28 +1,66 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::error::{EcashError, Result};
|
||||
use crate::ecash::api_routes::helpers::EpochIdParam;
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::Path;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::ecash::models::{
|
||||
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_ecash_time::{cred_exp_date, EcashTime};
|
||||
use nym_validator_client::nym_api::rfc_3339_date;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
use rocket_okapi::openapi;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use time::Date;
|
||||
use tracing::trace;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
// routes with globally aggregated keys, signatures, etc.
|
||||
/// routes with globally aggregated keys, signatures, etc.
|
||||
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/master-verification-key:epoch_id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| master_verification_key(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/aggregated-expiration-date-signatures:expiration_date",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|expiration_date| expiration_date_signatures(expiration_date, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/aggregated-coin-indices-signatures:epoch_id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash Global Data")]
|
||||
#[get("/master-verification-key?<epoch_id>")]
|
||||
pub async fn master_verification_key(
|
||||
epoch_id: Option<u64>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> Result<Json<VerificationKeyResponse>> {
|
||||
#[utoipa::path(
|
||||
tag = "Ecash Global Data",
|
||||
get,
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/master-verification-key/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = VerificationKeyResponse)
|
||||
)
|
||||
)]
|
||||
async fn master_verification_key(
|
||||
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<VerificationKeyResponse>> {
|
||||
trace!("aggregated_verification_key request");
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
@@ -33,12 +71,27 @@ pub async fn master_verification_key(
|
||||
Ok(Json(VerificationKeyResponse::new(key.clone())))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash Global Data")]
|
||||
#[get("/aggregated-expiration-date-signatures?<expiration_date>")]
|
||||
pub async fn expiration_date_signatures(
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct ExpirationDateParam {
|
||||
expiration_date: Option<String>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> Result<Json<AggregatedExpirationDateSignatureResponse>> {
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash Global Data",
|
||||
get,
|
||||
params(
|
||||
ExpirationDateParam
|
||||
),
|
||||
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = AggregatedExpirationDateSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn expiration_date_signatures(
|
||||
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<AggregatedExpirationDateSignatureResponse>> {
|
||||
trace!("aggregated_expiration_date_signatures request");
|
||||
|
||||
let expiration_date = match expiration_date {
|
||||
@@ -61,12 +114,21 @@ pub async fn expiration_date_signatures(
|
||||
}))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash Global Data")]
|
||||
#[get("/aggregated-coin-indices-signatures?<epoch_id>")]
|
||||
pub async fn coin_indices_signatures(
|
||||
epoch_id: Option<u64>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> Result<Json<AggregatedCoinIndicesSignatureResponse>> {
|
||||
#[utoipa::path(
|
||||
tag = "Ecash Global Data",
|
||||
get,
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn coin_indices_signatures(
|
||||
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<AggregatedCoinIndicesSignatureResponse>> {
|
||||
trace!("aggregated_coin_indices_signatures request");
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::api_routes::helpers::EpochIdParam;
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::Path;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::ecash::models::{
|
||||
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_ecash_time::{cred_exp_date, EcashTime};
|
||||
use nym_validator_client::nym_api::rfc_3339_date;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use time::Date;
|
||||
use tracing::trace;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
/// routes with globally aggregated keys, signatures, etc.
|
||||
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/master-verification-key:epoch_id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| master_verification_key(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/aggregated-expiration-date-signatures:expiration_date",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|expiration_date| expiration_date_signatures(expiration_date, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/aggregated-coin-indices-signatures:epoch_id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash Global Data",
|
||||
get,
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/master-verification-key/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = VerificationKeyResponse)
|
||||
)
|
||||
)]
|
||||
async fn master_verification_key(
|
||||
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<VerificationKeyResponse>> {
|
||||
trace!("aggregated_verification_key request");
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
let key = state.master_verification_key(epoch_id).await?;
|
||||
|
||||
Ok(Json(VerificationKeyResponse::new(key.clone())))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct ExpirationDateParam {
|
||||
expiration_date: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash Global Data",
|
||||
get,
|
||||
params(
|
||||
ExpirationDateParam
|
||||
),
|
||||
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = AggregatedExpirationDateSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn expiration_date_signatures(
|
||||
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<AggregatedExpirationDateSignatureResponse>> {
|
||||
trace!("aggregated_expiration_date_signatures request");
|
||||
|
||||
let expiration_date = match expiration_date {
|
||||
None => cred_exp_date().ecash_date(),
|
||||
Some(raw) => Date::parse(&raw, &rfc_3339_date())
|
||||
.map_err(|_| EcashError::MalformedExpirationDate { raw })?,
|
||||
};
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
let expiration_date_signatures = state
|
||||
.master_expiration_date_signatures(expiration_date)
|
||||
.await?;
|
||||
|
||||
Ok(Json(AggregatedExpirationDateSignatureResponse {
|
||||
epoch_id: expiration_date_signatures.epoch_id,
|
||||
expiration_date,
|
||||
signatures: expiration_date_signatures.signatures.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash Global Data",
|
||||
get,
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn coin_indices_signatures(
|
||||
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<AggregatedCoinIndicesSignatureResponse>> {
|
||||
trace!("aggregated_coin_indices_signatures request");
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?;
|
||||
|
||||
Ok(Json(AggregatedCoinIndicesSignatureResponse {
|
||||
epoch_id: coin_indices_signatures.epoch_id,
|
||||
signatures: coin_indices_signatures.signatures.clone(),
|
||||
}))
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::api_routes::aggregation_axum::aggregation_routes;
|
||||
use crate::ecash::api_routes::issued_axum::issued_routes;
|
||||
use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes;
|
||||
use crate::ecash::api_routes::spending_axum::spending_routes;
|
||||
use crate::ecash::api_routes::aggregation::aggregation_routes;
|
||||
use crate::ecash::api_routes::issued::issued_routes;
|
||||
use crate::ecash::api_routes::partial_signing::partial_signing_routes;
|
||||
use crate::ecash::api_routes::spending::spending_routes;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
@@ -5,21 +5,64 @@ use crate::ecash::api_routes::helpers::build_credentials_response;
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::ecash::storage::EcashStorageExt;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::Path;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::ecash::models::{
|
||||
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse,
|
||||
};
|
||||
use nym_api_requests::ecash::CredentialsRequestBody;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
use rocket_okapi::openapi;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[get("/epoch-credentials/<epoch>")]
|
||||
pub async fn epoch_credentials(
|
||||
epoch: EpochId,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<EpochCredentialsResponse>> {
|
||||
pub(crate) fn issued_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/epoch-credentials/:epoch",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch| epoch_credentials(epoch, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/issued-credential/:id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|id| issued_credential(id, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/issued-credentials",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| issued_credentials(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct EpochParam {
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
EpochParam
|
||||
),
|
||||
path = "/v1/ecash/epoch-credentials/{epoch}",
|
||||
responses(
|
||||
(status = 200, body = EpochCredentialsResponse)
|
||||
)
|
||||
)]
|
||||
async fn epoch_credentials(
|
||||
Path(EpochParam { epoch }): Path<EpochParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<EpochCredentialsResponse>> {
|
||||
let issued = state.aux.storage.get_epoch_credentials(epoch).await?;
|
||||
|
||||
let response = if let Some(issued) = issued {
|
||||
@@ -35,12 +78,27 @@ pub async fn epoch_credentials(
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[get("/issued-credential/<id>")]
|
||||
pub async fn issued_credential(
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct IdParam {
|
||||
id: i64,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<IssuedCredentialResponse>> {
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
IdParam
|
||||
),
|
||||
path = "/v1/ecash/issued-credential/{id}",
|
||||
responses(
|
||||
(status = 200, body = IssuedCredentialResponse)
|
||||
)
|
||||
)]
|
||||
async fn issued_credential(
|
||||
Path(IdParam { id }): Path<IdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<IssuedCredentialResponse>> {
|
||||
let issued = state.aux.storage.get_issued_credential(id).await?;
|
||||
|
||||
let credential = if let Some(issued) = issued {
|
||||
@@ -52,16 +110,21 @@ pub async fn issued_credential(
|
||||
Ok(Json(IssuedCredentialResponse { credential }))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[post("/issued-credentials", data = "<params>")]
|
||||
pub async fn issued_credentials(
|
||||
params: Json<CredentialsRequestBody>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<IssuedCredentialsResponse>> {
|
||||
let params = params.into_inner();
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = CredentialsRequestBody,
|
||||
path = "/v1/ecash/issued-credentials",
|
||||
responses(
|
||||
(status = 200, body = IssuedCredentialsResponse)
|
||||
)
|
||||
)]
|
||||
async fn issued_credentials(
|
||||
Json(params): Json<CredentialsRequestBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<IssuedCredentialsResponse>> {
|
||||
if params.pagination.is_some() && !params.credential_ids.is_empty() {
|
||||
return Err(EcashError::InvalidQueryArguments);
|
||||
return Err(EcashError::InvalidQueryArguments.into());
|
||||
}
|
||||
|
||||
let credentials = if let Some(pagination) = params.pagination {
|
||||
@@ -78,5 +141,7 @@ pub async fn issued_credentials(
|
||||
.await?
|
||||
};
|
||||
|
||||
build_credentials_response(credentials).map(Json)
|
||||
build_credentials_response(credentials)
|
||||
.map(Json)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::api_routes::helpers::build_credentials_response;
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::ecash::storage::EcashStorageExt;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::Path;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::ecash::models::{
|
||||
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse,
|
||||
};
|
||||
use nym_api_requests::ecash::CredentialsRequestBody;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
pub(crate) fn issued_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/epoch-credentials/:epoch",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch| epoch_credentials(epoch, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/issued-credential/:id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|id| issued_credential(id, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/issued-credentials",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| issued_credentials(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct EpochParam {
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
EpochParam
|
||||
),
|
||||
path = "/v1/ecash/epoch-credentials/{epoch}",
|
||||
responses(
|
||||
(status = 200, body = EpochCredentialsResponse)
|
||||
)
|
||||
)]
|
||||
async fn epoch_credentials(
|
||||
Path(EpochParam { epoch }): Path<EpochParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<EpochCredentialsResponse>> {
|
||||
let issued = state.aux.storage.get_epoch_credentials(epoch).await?;
|
||||
|
||||
let response = if let Some(issued) = issued {
|
||||
issued.into()
|
||||
} else {
|
||||
EpochCredentialsResponse {
|
||||
epoch_id: epoch,
|
||||
first_epoch_credential_id: None,
|
||||
total_issued: 0,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct IdParam {
|
||||
id: i64,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
IdParam
|
||||
),
|
||||
path = "/v1/ecash/issued-credential/{id}",
|
||||
responses(
|
||||
(status = 200, body = IssuedCredentialResponse)
|
||||
)
|
||||
)]
|
||||
async fn issued_credential(
|
||||
Path(IdParam { id }): Path<IdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<IssuedCredentialResponse>> {
|
||||
let issued = state.aux.storage.get_issued_credential(id).await?;
|
||||
|
||||
let credential = if let Some(issued) = issued {
|
||||
Some(issued.try_into()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Json(IssuedCredentialResponse { credential }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = CredentialsRequestBody,
|
||||
path = "/v1/ecash/issued-credentials",
|
||||
responses(
|
||||
(status = 200, body = IssuedCredentialsResponse)
|
||||
)
|
||||
)]
|
||||
async fn issued_credentials(
|
||||
Json(params): Json<CredentialsRequestBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<IssuedCredentialsResponse>> {
|
||||
if params.pagination.is_some() && !params.credential_ids.is_empty() {
|
||||
return Err(EcashError::InvalidQueryArguments.into());
|
||||
}
|
||||
|
||||
let credentials = if let Some(pagination) = params.pagination {
|
||||
state
|
||||
.aux
|
||||
.storage
|
||||
.get_issued_credentials_paged(pagination)
|
||||
.await?
|
||||
} else {
|
||||
state
|
||||
.aux
|
||||
.storage
|
||||
.get_issued_credentials(params.credential_ids)
|
||||
.await?
|
||||
};
|
||||
|
||||
build_credentials_response(credentials)
|
||||
.map(Json)
|
||||
.map_err(From::from)
|
||||
}
|
||||
@@ -7,8 +7,8 @@ mod helpers;
|
||||
// pub(crate) mod partial_signing;
|
||||
// pub(crate) mod spending;
|
||||
|
||||
pub(crate) mod aggregation_axum;
|
||||
pub(crate) mod aggregation;
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod issued_axum;
|
||||
pub(crate) mod partial_signing_axum;
|
||||
pub(crate) mod spending_axum;
|
||||
pub(crate) mod issued;
|
||||
pub(crate) mod partial_signing;
|
||||
pub(crate) mod spending;
|
||||
|
||||
@@ -1,27 +1,65 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::api_routes::helpers::EpochIdParam;
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::helpers::blind_sign;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::Path;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::ecash::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
|
||||
PartialExpirationDateSignatureResponse,
|
||||
};
|
||||
use nym_ecash_time::{cred_exp_date, EcashTime};
|
||||
use nym_validator_client::nym_api::rfc_3339_date;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
use rocket_okapi::openapi;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use time::Date;
|
||||
use tracing::{debug, trace};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[post("/blind-sign", data = "<blind_sign_request_body>")]
|
||||
pub async fn post_blind_sign(
|
||||
blind_sign_request_body: Json<BlindSignRequestBody>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<BlindedSignatureResponse>> {
|
||||
pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/blind-sign",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| post_blind_sign(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/partial-expiration-date-signatures:expiration_date",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/partial-coin-indices-signatures:epoch_id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = BlindSignRequestBody,
|
||||
path = "/v1/ecash/blind-sign",
|
||||
responses(
|
||||
(status = 200, body = BlindedSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn post_blind_sign(
|
||||
Json(blind_sign_request_body): Json<BlindSignRequestBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<BlindedSignatureResponse>> {
|
||||
debug!("Received blind sign request");
|
||||
trace!("body: {:?}", blind_sign_request_body);
|
||||
|
||||
@@ -31,7 +69,7 @@ pub async fn post_blind_sign(
|
||||
|
||||
// basic check of expiration date validity
|
||||
if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() {
|
||||
return Err(EcashError::ExpirationDateTooLate);
|
||||
return Err(EcashError::ExpirationDateTooLate.into());
|
||||
}
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
@@ -62,24 +100,38 @@ pub async fn post_blind_sign(
|
||||
|
||||
// produce the partial signature
|
||||
debug!("producing the partial credential");
|
||||
let blinded_signature = blind_sign(blind_sign_request_body.deref(), signing_key.deref())?;
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?;
|
||||
|
||||
// store the information locally
|
||||
debug!("storing the issued credential in the database");
|
||||
state
|
||||
.store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature)
|
||||
.store_issued_credential(blind_sign_request_body, &blinded_signature)
|
||||
.await?;
|
||||
|
||||
// finally return the credential to the client
|
||||
Ok(Json(BlindedSignatureResponse { blinded_signature }))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[get("/partial-expiration-date-signatures?<expiration_date>")]
|
||||
pub async fn partial_expiration_date_signatures(
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
struct ExpirationDateParam {
|
||||
expiration_date: Option<String>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<PartialExpirationDateSignatureResponse>> {
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
ExpirationDateParam
|
||||
),
|
||||
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
|
||||
responses(
|
||||
(status = 200, body = PartialExpirationDateSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn partial_expiration_date_signatures(
|
||||
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
|
||||
let expiration_date = match expiration_date {
|
||||
None => cred_exp_date().ecash_date(),
|
||||
Some(raw) => Date::parse(&raw, &rfc_3339_date())
|
||||
@@ -100,12 +152,21 @@ pub async fn partial_expiration_date_signatures(
|
||||
}))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[get("/partial-coin-indices-signatures?<epoch_id>")]
|
||||
pub async fn partial_coin_indices_signatures(
|
||||
epoch_id: Option<u64>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<PartialCoinIndicesSignatureResponse>> {
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = PartialExpirationDateSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn partial_coin_indices_signatures(
|
||||
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::api_routes::helpers::EpochIdParam;
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::helpers::blind_sign;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::Path;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::ecash::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
|
||||
PartialExpirationDateSignatureResponse,
|
||||
};
|
||||
use nym_ecash_time::{cred_exp_date, EcashTime};
|
||||
use nym_validator_client::nym_api::rfc_3339_date;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use time::Date;
|
||||
use tracing::{debug, trace};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/blind-sign",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| post_blind_sign(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/partial-expiration-date-signatures:expiration_date",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/partial-coin-indices-signatures:epoch_id",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = BlindSignRequestBody,
|
||||
path = "/v1/ecash/blind-sign",
|
||||
responses(
|
||||
(status = 200, body = BlindedSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn post_blind_sign(
|
||||
Json(blind_sign_request_body): Json<BlindSignRequestBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<BlindedSignatureResponse>> {
|
||||
debug!("Received blind sign request");
|
||||
trace!("body: {:?}", blind_sign_request_body);
|
||||
|
||||
// check if we have the signing key available
|
||||
debug!("checking if we actually have ecash keys derived...");
|
||||
let signing_key = state.ecash_signing_key().await?;
|
||||
|
||||
// basic check of expiration date validity
|
||||
if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() {
|
||||
return Err(EcashError::ExpirationDateTooLate.into());
|
||||
}
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
// check if we already issued a credential for this deposit
|
||||
let deposit_id = blind_sign_request_body.deposit_id;
|
||||
debug!(
|
||||
"checking if we have already issued credential for this deposit (deposit_id: {deposit_id})",
|
||||
);
|
||||
if let Some(blinded_signature) = state.already_issued(deposit_id).await? {
|
||||
return Ok(Json(BlindedSignatureResponse { blinded_signature }));
|
||||
}
|
||||
|
||||
//check if account was blacklisted
|
||||
let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string();
|
||||
state.aux.ensure_not_blacklisted(&pub_key_bs58).await?;
|
||||
|
||||
// get the deposit details of the claimed id
|
||||
debug!("getting deposit details from the chain");
|
||||
let deposit = state.get_deposit(deposit_id).await?;
|
||||
|
||||
// check validity of the request
|
||||
debug!("fully validating received request");
|
||||
state
|
||||
.validate_request(&blind_sign_request_body, deposit)
|
||||
.await?;
|
||||
|
||||
// produce the partial signature
|
||||
debug!("producing the partial credential");
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?;
|
||||
|
||||
// store the information locally
|
||||
debug!("storing the issued credential in the database");
|
||||
state
|
||||
.store_issued_credential(blind_sign_request_body, &blinded_signature)
|
||||
.await?;
|
||||
|
||||
// finally return the credential to the client
|
||||
Ok(Json(BlindedSignatureResponse { blinded_signature }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
struct ExpirationDateParam {
|
||||
expiration_date: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
ExpirationDateParam
|
||||
),
|
||||
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
|
||||
responses(
|
||||
(status = 200, body = PartialExpirationDateSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn partial_expiration_date_signatures(
|
||||
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
|
||||
let expiration_date = match expiration_date {
|
||||
None => cred_exp_date().ecash_date(),
|
||||
Some(raw) => Date::parse(&raw, &rfc_3339_date())
|
||||
.map_err(|_| EcashError::MalformedExpirationDate { raw })?,
|
||||
};
|
||||
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
let expiration_date_signatures = state
|
||||
.partial_expiration_date_signatures(expiration_date)
|
||||
.await?;
|
||||
|
||||
Ok(Json(PartialExpirationDateSignatureResponse {
|
||||
epoch_id: expiration_date_signatures.epoch_id,
|
||||
expiration_date,
|
||||
signatures: expiration_date_signatures.signatures.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
|
||||
responses(
|
||||
(status = 200, body = PartialExpirationDateSignatureResponse)
|
||||
)
|
||||
)]
|
||||
async fn partial_coin_indices_signatures(
|
||||
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
|
||||
// see if we're not in the middle of new dkg
|
||||
state.ensure_dkg_not_in_progress().await?;
|
||||
|
||||
let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?;
|
||||
|
||||
Ok(Json(PartialCoinIndicesSignatureResponse {
|
||||
epoch_id: coin_indices_signatures.epoch_id,
|
||||
signatures: coin_indices_signatures.signatures.clone(),
|
||||
}))
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
|
||||
use nym_api_requests::ecash::models::{
|
||||
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection,
|
||||
@@ -10,31 +13,62 @@ use nym_api_requests::ecash::models::{
|
||||
};
|
||||
use nym_compact_ecash::identify::IdentifyResult;
|
||||
use nym_ecash_time::EcashTime;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
use rocket_okapi::openapi;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use time::macros::time;
|
||||
use time::{OffsetDateTime, Time};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/verify-ecash-ticket",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| verify_ticket(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/batch-redeem-ecash-tickets",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| batch_redeem_tickets(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/double-spending-filter-v1",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|| double_spending_filter_v1(ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const ONE_AM: Time = time!(1:00);
|
||||
|
||||
fn reject_ticket(
|
||||
reason: EcashTicketVerificationRejection,
|
||||
) -> crate::ecash::error::Result<Json<EcashTicketVerificationResponse>> {
|
||||
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
|
||||
Ok(Json(EcashTicketVerificationResponse::reject(reason)))
|
||||
}
|
||||
|
||||
// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential`
|
||||
// introduce bloomfilter checks without touching storage first, etc.
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[post("/verify-ecash-ticket", data = "<verify_ticket_body>")]
|
||||
pub async fn verify_ticket(
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = VerifyEcashTicketBody,
|
||||
path = "/v1/ecash/verify-ecash-ticket",
|
||||
responses(
|
||||
(status = 200, body = EcashTicketVerificationResponse)
|
||||
)
|
||||
)]
|
||||
async fn verify_ticket(
|
||||
// TODO in the future: make it send binary data rather than json
|
||||
verify_ticket_body: Json<VerifyEcashTicketBody>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<EcashTicketVerificationResponse>> {
|
||||
Json(verify_ticket_body): Json<VerifyEcashTicketBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
|
||||
let credential_data = &verify_ticket_body.credential;
|
||||
let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr;
|
||||
|
||||
@@ -117,26 +151,30 @@ pub async fn verify_ticket(
|
||||
}
|
||||
|
||||
// // for particular SN returns what gateway has submitted it and whether it has been verified correctly
|
||||
// pub async fn credential_status() -> ! {
|
||||
// async fn credential_status() -> ! {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[post(
|
||||
"/batch-redeem-ecash-tickets",
|
||||
data = "<batch_redeem_credentials_body>"
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = BatchRedeemTicketsBody,
|
||||
path = "/v1/ecash/batch-redeem-ecash-tickets",
|
||||
responses(
|
||||
(status = 200, body = EcashBatchTicketRedemptionResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn batch_redeem_tickets(
|
||||
async fn batch_redeem_tickets(
|
||||
// TODO in the future: make it send binary data rather than json
|
||||
batch_redeem_credentials_body: Json<BatchRedeemTicketsBody>,
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<EcashBatchTicketRedemptionResponse>> {
|
||||
Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> {
|
||||
// 1. see if that gateway has even submitted any tickets
|
||||
let Some(provider_info) = state
|
||||
.get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref())
|
||||
.await?
|
||||
else {
|
||||
return Err(EcashError::NotTicketsProvided);
|
||||
return Err(EcashError::NotTicketsProvided.into());
|
||||
};
|
||||
|
||||
// 2. check if the gateway is not trying to spam the redemption requests
|
||||
@@ -149,13 +187,14 @@ pub async fn batch_redeem_tickets(
|
||||
return Err(EcashError::TooFrequentRedemption {
|
||||
last_redemption,
|
||||
next_allowed,
|
||||
});
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. verify the request digest
|
||||
if !batch_redeem_credentials_body.verify_digest() {
|
||||
return Err(EcashError::MismatchedRequestDigest);
|
||||
return Err(EcashError::MismatchedRequestDigest.into());
|
||||
}
|
||||
|
||||
// 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.)
|
||||
@@ -164,9 +203,7 @@ pub async fn batch_redeem_tickets(
|
||||
.await?;
|
||||
|
||||
let proposal_id = batch_redeem_credentials_body.proposal_id;
|
||||
let received = batch_redeem_credentials_body
|
||||
.into_inner()
|
||||
.included_serial_numbers;
|
||||
let received = batch_redeem_credentials_body.included_serial_numbers;
|
||||
|
||||
// 5. check if **every** serial number included in the request has been verified by us
|
||||
// if we have more than requested, tough luck, they're going to lose them
|
||||
@@ -177,7 +214,8 @@ pub async fn batch_redeem_tickets(
|
||||
if !verified_tickets.contains(sn.deref()) {
|
||||
return Err(EcashError::TicketNotVerified {
|
||||
serial_number_bs58: bs58::encode(sn).into_string(),
|
||||
});
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +228,17 @@ pub async fn batch_redeem_tickets(
|
||||
|
||||
// explicitly mark it as v1 in the URL because the response type WILL change;
|
||||
// it will probably be compressed bincode or something
|
||||
#[openapi(tag = "Ecash")]
|
||||
#[get("/double-spending-filter-v1")]
|
||||
pub async fn double_spending_filter_v1(
|
||||
state: &RocketState<EcashState>,
|
||||
) -> crate::ecash::error::Result<Json<SpentCredentialsResponse>> {
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
path = "/v1/ecash/double-spending-filter-v1",
|
||||
responses(
|
||||
(status = 200, body = SpentCredentialsResponse)
|
||||
)
|
||||
)]
|
||||
async fn double_spending_filter_v1(
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<SpentCredentialsResponse>> {
|
||||
let spent_credentials_export = state.get_bloomfilter_bytes().await;
|
||||
Ok(Json(SpentCredentialsResponse::new(
|
||||
spent_credentials_export,
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::error::EcashError;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
|
||||
use nym_api_requests::ecash::models::{
|
||||
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection,
|
||||
EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_compact_ecash::identify::IdentifyResult;
|
||||
use nym_ecash_time::EcashTime;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use time::macros::time;
|
||||
use time::{OffsetDateTime, Time};
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/verify-ecash-ticket",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| verify_ticket(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/batch-redeem-ecash-tickets",
|
||||
axum::routing::post({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|body| batch_redeem_tickets(body, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/double-spending-filter-v1",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|| double_spending_filter_v1(ecash_state)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const ONE_AM: Time = time!(1:00);
|
||||
|
||||
fn reject_ticket(
|
||||
reason: EcashTicketVerificationRejection,
|
||||
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
|
||||
Ok(Json(EcashTicketVerificationResponse::reject(reason)))
|
||||
}
|
||||
|
||||
// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential`
|
||||
// introduce bloomfilter checks without touching storage first, etc.
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = VerifyEcashTicketBody,
|
||||
path = "/v1/ecash/verify-ecash-ticket",
|
||||
responses(
|
||||
(status = 200, body = EcashTicketVerificationResponse)
|
||||
)
|
||||
)]
|
||||
async fn verify_ticket(
|
||||
// TODO in the future: make it send binary data rather than json
|
||||
Json(verify_ticket_body): Json<VerifyEcashTicketBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
|
||||
let credential_data = &verify_ticket_body.credential;
|
||||
let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr;
|
||||
|
||||
// easy check: is there only a single payment attached?
|
||||
if credential_data.payment.spend_value != 1 {
|
||||
return reject_ticket(EcashTicketVerificationRejection::MultipleTickets);
|
||||
}
|
||||
|
||||
let sn = &credential_data.encoded_serial_number();
|
||||
let spend_date = credential_data.spend_date;
|
||||
let epoch_id = credential_data.epoch_id;
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let today_ecash = now.ecash_date();
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let yesterday_ecash = today_ecash.previous_day().unwrap();
|
||||
|
||||
// only accept yesterday date if we're near the day transition, i.e. before 1AM UTC
|
||||
if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash {
|
||||
return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate {
|
||||
today: today_ecash,
|
||||
yesterday: yesterday_ecash,
|
||||
received: spend_date,
|
||||
});
|
||||
}
|
||||
|
||||
// check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification
|
||||
// TODO: when blacklisting is implemented, this should get removed
|
||||
if state.check_bloomfilter(sn).await {
|
||||
return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket);
|
||||
}
|
||||
|
||||
// actual double spend detection with storage
|
||||
if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? {
|
||||
match nym_compact_ecash::identify::identify(
|
||||
&credential_data.payment,
|
||||
&previous_payment.payment,
|
||||
credential_data.pay_info,
|
||||
previous_payment.pay_info,
|
||||
) {
|
||||
IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message?
|
||||
IdentifyResult::DuplicatePayInfo(_) => {
|
||||
warn!("Identical payInfo");
|
||||
return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket);
|
||||
}
|
||||
IdentifyResult::DoubleSpendingPublicKeys(pub_key) => {
|
||||
//Actual double spending
|
||||
warn!(
|
||||
"Double spending attempt for key {}",
|
||||
pub_key.to_base58_string()
|
||||
);
|
||||
error!("UNIMPLEMENTED: blacklisting the double spend key");
|
||||
return reject_ticket(EcashTicketVerificationRejection::DoubleSpend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let verification_key = state.master_verification_key(Some(epoch_id)).await?;
|
||||
|
||||
// perform actual crypto verification
|
||||
if credential_data.verify(&verification_key).is_err() {
|
||||
return reject_ticket(EcashTicketVerificationRejection::InvalidTicket);
|
||||
}
|
||||
|
||||
// finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN
|
||||
let was_present = state
|
||||
.update_bloomfilter(sn, spend_date, today_ecash)
|
||||
.await?;
|
||||
if was_present {
|
||||
return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket);
|
||||
}
|
||||
|
||||
//store credential
|
||||
state
|
||||
.store_verified_ticket(credential_data, gateway_cosmos_addr)
|
||||
.await?;
|
||||
|
||||
Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) }))
|
||||
}
|
||||
|
||||
// // for particular SN returns what gateway has submitted it and whether it has been verified correctly
|
||||
// async fn credential_status() -> ! {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
post,
|
||||
request_body = BatchRedeemTicketsBody,
|
||||
path = "/v1/ecash/batch-redeem-ecash-tickets",
|
||||
responses(
|
||||
(status = 200, body = EcashBatchTicketRedemptionResponse)
|
||||
)
|
||||
)]
|
||||
async fn batch_redeem_tickets(
|
||||
// TODO in the future: make it send binary data rather than json
|
||||
Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>,
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> {
|
||||
// 1. see if that gateway has even submitted any tickets
|
||||
let Some(provider_info) = state
|
||||
.get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref())
|
||||
.await?
|
||||
else {
|
||||
return Err(EcashError::NotTicketsProvided.into());
|
||||
};
|
||||
|
||||
// 2. check if the gateway is not trying to spam the redemption requests
|
||||
// (we have to protect our poor chain)
|
||||
if let Some(last_redemption) = provider_info.last_batch_verification {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY;
|
||||
|
||||
if next_allowed > now {
|
||||
return Err(EcashError::TooFrequentRedemption {
|
||||
last_redemption,
|
||||
next_allowed,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. verify the request digest
|
||||
if !batch_redeem_credentials_body.verify_digest() {
|
||||
return Err(EcashError::MismatchedRequestDigest.into());
|
||||
}
|
||||
|
||||
// 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.)
|
||||
state
|
||||
.validate_redemption_proposal(&batch_redeem_credentials_body)
|
||||
.await?;
|
||||
|
||||
let proposal_id = batch_redeem_credentials_body.proposal_id;
|
||||
let received = batch_redeem_credentials_body.included_serial_numbers;
|
||||
|
||||
// 5. check if **every** serial number included in the request has been verified by us
|
||||
// if we have more than requested, tough luck, they're going to lose them
|
||||
let verified = state.get_redeemable_tickets(provider_info).await?;
|
||||
let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect();
|
||||
|
||||
for sn in &received {
|
||||
if !verified_tickets.contains(sn.deref()) {
|
||||
return Err(EcashError::TicketNotVerified {
|
||||
serial_number_bs58: bs58::encode(sn).into_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx
|
||||
state.accept_proposal(proposal_id).await?;
|
||||
Ok(Json(EcashBatchTicketRedemptionResponse {
|
||||
proposal_accepted: true,
|
||||
}))
|
||||
}
|
||||
|
||||
// explicitly mark it as v1 in the URL because the response type WILL change;
|
||||
// it will probably be compressed bincode or something
|
||||
#[utoipa::path(
|
||||
tag = "Ecash",
|
||||
get,
|
||||
path = "/v1/ecash/double-spending-filter-v1",
|
||||
responses(
|
||||
(status = 200, body = SpentCredentialsResponse)
|
||||
)
|
||||
)]
|
||||
async fn double_spending_filter_v1(
|
||||
state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<SpentCredentialsResponse>> {
|
||||
let spent_credentials_export = state.get_bloomfilter_bytes().await;
|
||||
Ok(Json(SpentCredentialsResponse::new(
|
||||
spent_credentials_export,
|
||||
)))
|
||||
}
|
||||
@@ -16,27 +16,3 @@ pub(crate) mod tests;
|
||||
|
||||
// equivalent of 100nym
|
||||
pub(crate) const MINIMUM_BALANCE: u128 = 100_000000;
|
||||
|
||||
// pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec<Route>, OpenApi) {
|
||||
// if enabled {
|
||||
// openapi_get_routes_spec![
|
||||
// settings:
|
||||
// api_routes::partial_signing::post_blind_sign,
|
||||
// api_routes::partial_signing::partial_expiration_date_signatures,
|
||||
// api_routes::partial_signing::partial_coin_indices_signatures,
|
||||
// api_routes::spending::verify_ticket,
|
||||
// api_routes::spending::batch_redeem_tickets,
|
||||
// api_routes::spending::double_spending_filter_v1,
|
||||
// api_routes::issued::epoch_credentials,
|
||||
// api_routes::issued::issued_credential,
|
||||
// api_routes::issued::issued_credentials,
|
||||
// api_routes::aggregation::master_verification_key,
|
||||
// api_routes::aggregation::coin_indices_signatures,
|
||||
// api_routes::aggregation::expiration_date_signatures
|
||||
// ]
|
||||
// } else {
|
||||
// openapi_get_routes_spec![
|
||||
// settings:
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -3,12 +3,3 @@
|
||||
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod models;
|
||||
// mod routes;
|
||||
//
|
||||
// pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
// openapi_get_routes_spec![
|
||||
// settings: routes::network_details,
|
||||
// routes::nym_contracts,
|
||||
// routes::nym_contracts_detailed
|
||||
// ]
|
||||
// }
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::network::models::{ContractInformation, NetworkDetails};
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[openapi(tag = "network")]
|
||||
#[get("/details")]
|
||||
pub(crate) fn network_details(details: &State<NetworkDetails>) -> Json<NetworkDetails> {
|
||||
Json(details.deref().clone())
|
||||
}
|
||||
|
||||
// I agree, it feels weird to be pulling contract cache here, but I feel like it makes
|
||||
// more sense to return this information here rather than in the generic cache route
|
||||
#[openapi(tag = "network")]
|
||||
#[get("/nym-contracts")]
|
||||
pub(crate) async fn nym_contracts(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<HashMap<String, ContractInformation<cw2::ContractVersion>>> {
|
||||
let info = cache.contract_details().await;
|
||||
Json(
|
||||
info.iter()
|
||||
.map(|(contract, info)| {
|
||||
(
|
||||
contract.to_owned(),
|
||||
ContractInformation {
|
||||
address: info.address.as_ref().map(|a| a.to_string()),
|
||||
details: info.base.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[openapi(tag = "network")]
|
||||
#[get("/nym-contracts-detailed")]
|
||||
pub(crate) async fn nym_contracts_detailed(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<HashMap<String, ContractInformation<ContractBuildInformation>>> {
|
||||
let info = cache.contract_details().await;
|
||||
Json(
|
||||
info.iter()
|
||||
.map(|(contract, info)| {
|
||||
(
|
||||
contract.to_owned(),
|
||||
ContractInformation {
|
||||
address: info.address.as_ref().map(|a| a.to_string()),
|
||||
details: info.detailed.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,6 @@ pub(crate) mod handlers;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod reward_estimate;
|
||||
// pub(crate) mod routes_deprecated;
|
||||
pub(crate) mod uptime_updater;
|
||||
pub(crate) mod utils;
|
||||
|
||||
@@ -24,52 +23,6 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900);
|
||||
pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600);
|
||||
pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400);
|
||||
|
||||
// pub(crate) fn node_status_routes(
|
||||
// settings: &OpenApiSettings,
|
||||
// enabled: bool,
|
||||
// ) -> (Vec<Route>, OpenApi) {
|
||||
// if enabled {
|
||||
// openapi_get_routes_spec![
|
||||
// settings: routes_deprecated::gateway_report,
|
||||
// routes_deprecated::gateway_uptime_history,
|
||||
// routes_deprecated::gateway_core_status_count,
|
||||
// routes_deprecated::mixnode_report,
|
||||
// routes_deprecated::mixnode_uptime_history,
|
||||
// routes_deprecated::mixnode_core_status_count,
|
||||
// routes_deprecated::get_mixnode_status,
|
||||
// routes_deprecated::get_mixnode_reward_estimation,
|
||||
// routes_deprecated::compute_mixnode_reward_estimation,
|
||||
// routes_deprecated::get_mixnode_stake_saturation,
|
||||
// routes_deprecated::get_mixnode_inclusion_probability,
|
||||
// routes_deprecated::get_mixnode_avg_uptime,
|
||||
// routes_deprecated::get_gateway_avg_uptime,
|
||||
// routes_deprecated::get_mixnode_inclusion_probabilities,
|
||||
// routes_deprecated::get_mixnodes_detailed,
|
||||
// routes_deprecated::get_mixnodes_detailed_unfiltered,
|
||||
// routes_deprecated::get_rewarded_set_detailed,
|
||||
// routes_deprecated::get_active_set_detailed,
|
||||
// routes_deprecated::get_gateways_detailed,
|
||||
// routes_deprecated::get_gateways_detailed_unfiltered,
|
||||
// routes_deprecated::unstable::mixnode_test_results,
|
||||
// routes_deprecated::unstable::gateway_test_results,
|
||||
// routes_deprecated::submit_gateway_monitoring_results,
|
||||
// routes_deprecated::submit_node_monitoring_results,
|
||||
// ]
|
||||
// } else {
|
||||
// // in the minimal variant we would not have access to endpoints relying on existence
|
||||
// // of the network monitor and the associated storage
|
||||
// openapi_get_routes_spec![
|
||||
// settings: routes_deprecated::get_mixnode_status,
|
||||
// routes_deprecated::get_mixnode_stake_saturation,
|
||||
// routes_deprecated::get_mixnode_inclusion_probability,
|
||||
// routes_deprecated::get_mixnode_inclusion_probabilities,
|
||||
// routes_deprecated::get_mixnodes_detailed,
|
||||
// routes_deprecated::get_rewarded_set_detailed,
|
||||
// routes_deprecated::get_active_set_detailed,
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Spawn the node status cache refresher.
|
||||
///
|
||||
/// It is primarily refreshed in-sync with the nym contract cache, however provide a fallback
|
||||
|
||||
@@ -1,615 +0,0 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::NodeStatusCache;
|
||||
use crate::node_status_api::helpers_deprecated::{
|
||||
_compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report,
|
||||
_gateway_uptime_history, _get_gateway_avg_uptime, _get_mixnode_avg_uptime,
|
||||
_get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability,
|
||||
_get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status,
|
||||
_get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report,
|
||||
_mixnode_uptime_history,
|
||||
};
|
||||
use crate::node_status_api::models::RocketErrorResponse;
|
||||
use crate::storage::NymApiStorage;
|
||||
use crate::NymContractCache;
|
||||
use nym_api_requests::models::{
|
||||
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
|
||||
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
|
||||
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
|
||||
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
|
||||
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
|
||||
UptimeResponse,
|
||||
};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_types::monitoring::MonitorMessage;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[post("/submit-gateway-monitoring-results", data = "<message>")]
|
||||
pub(crate) async fn submit_gateway_monitoring_results(
|
||||
message: Json<MonitorMessage>,
|
||||
storage: &State<NymApiStorage>,
|
||||
) -> Result<(), RocketErrorResponse> {
|
||||
todo!("rebasing");
|
||||
// if !message.from_allowed() {
|
||||
// return Err(RocketErrorResponse::new(
|
||||
// "Monitor not registered to submit results".to_string(),
|
||||
// rocket::http::Status::Forbidden,
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// if !message.timely() {
|
||||
// return Err(RocketErrorResponse::new(
|
||||
// "Message is too old".to_string(),
|
||||
// rocket::http::Status::BadRequest,
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// if !message.verify() {
|
||||
// return Err(RocketErrorResponse::new(
|
||||
// "Invalid signature".to_string(),
|
||||
// rocket::http::Status::BadRequest,
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// match storage
|
||||
// .manager
|
||||
// .submit_gateway_statuses_v2(message.results())
|
||||
// .await
|
||||
// {
|
||||
// Ok(_) => Ok(()),
|
||||
// Err(err) => {
|
||||
// error!("failed to submit gateway monitoring results: {}", err);
|
||||
// Err(RocketErrorResponse::new(
|
||||
// "failed to submit gateway monitoring results".to_string(),
|
||||
// rocket::http::Status::InternalServerError,
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
#[post("/submit-node-monitoring-results", data = "<message>")]
|
||||
pub(crate) async fn submit_node_monitoring_results(
|
||||
message: Json<MonitorMessage>,
|
||||
storage: &State<NymApiStorage>,
|
||||
) -> Result<(), RocketErrorResponse> {
|
||||
todo!("rebasing");
|
||||
|
||||
// if !message.from_allowed() {
|
||||
// return Err(RocketErrorResponse::new(
|
||||
// "Monitor not registered to submit results".to_string(),
|
||||
// rocket::http::Status::Forbidden,
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// if !message.timely() {
|
||||
// return Err(RocketErrorResponse::new(
|
||||
// "Message is too old".to_string(),
|
||||
// rocket::http::Status::BadRequest,
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// if !message.verify() {
|
||||
// return Err(RocketErrorResponse::new(
|
||||
// "Invalid signature".to_string(),
|
||||
// rocket::http::Status::BadRequest,
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// match storage
|
||||
// .manager
|
||||
// .submit_mixnode_statuses_v2(message.results())
|
||||
// .await
|
||||
// {
|
||||
// Ok(_) => Ok(()),
|
||||
// Err(err) => {
|
||||
// error!("failed to submit node monitoring results: {}", err);
|
||||
// Err(RocketErrorResponse::new(
|
||||
// "failed to submit node monitoring results".to_string(),
|
||||
// rocket::http::Status::InternalServerError,
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/gateway/<identity>/report")]
|
||||
pub(crate) async fn gateway_report(
|
||||
cache: &State<NodeStatusCache>,
|
||||
identity: &str,
|
||||
) -> Result<Json<GatewayStatusReportResponse>, RocketErrorResponse> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Ok(Json(_gateway_report(cache, identity).await?))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/gateway/<identity>/history")]
|
||||
pub(crate) async fn gateway_uptime_history(
|
||||
storage: &State<NymApiStorage>,
|
||||
nym_contract_cache: &State<NymContractCache>,
|
||||
identity: &str,
|
||||
) -> Result<Json<GatewayUptimeHistoryResponse>, RocketErrorResponse> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Ok(Json(
|
||||
// _gateway_uptime_history(storage, nym_contract_cache, identity).await?,
|
||||
// ))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/gateway/<identity>/core-status-count?<since>")]
|
||||
pub(crate) async fn gateway_core_status_count(
|
||||
storage: &State<NymApiStorage>,
|
||||
identity: &str,
|
||||
since: Option<i64>,
|
||||
) -> Result<Json<GatewayCoreStatusResponse>, RocketErrorResponse> {
|
||||
Ok(Json(
|
||||
_gateway_core_status_count(storage, identity, since).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/report")]
|
||||
pub(crate) async fn mixnode_report(
|
||||
cache: &State<NodeStatusCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<MixnodeStatusReportResponse>, RocketErrorResponse> {
|
||||
Ok(Json(_mixnode_report(cache, mix_id).await?))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/history")]
|
||||
pub(crate) async fn mixnode_uptime_history(
|
||||
storage: &State<NymApiStorage>,
|
||||
nym_contract_cache: &State<NymContractCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<MixnodeUptimeHistoryResponse>, RocketErrorResponse> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Ok(Json(
|
||||
// _mixnode_uptime_history(storage, nym_contract_cache, mix_id).await?,
|
||||
// ))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/core-status-count?<since>")]
|
||||
pub(crate) async fn mixnode_core_status_count(
|
||||
storage: &State<NymApiStorage>,
|
||||
mix_id: NodeId,
|
||||
since: Option<i64>,
|
||||
) -> Result<Json<MixnodeCoreStatusResponse>, RocketErrorResponse> {
|
||||
Ok(Json(
|
||||
_mixnode_core_status_count(storage, mix_id, since).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/status")]
|
||||
pub(crate) async fn get_mixnode_status(
|
||||
cache: &State<NymContractCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Json<MixnodeStatusResponse> {
|
||||
Json(_get_mixnode_status(cache, mix_id).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/reward-estimation")]
|
||||
pub(crate) async fn get_mixnode_reward_estimation(
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<NymContractCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<RewardEstimationResponse>, RocketErrorResponse> {
|
||||
Ok(Json(
|
||||
_get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[post(
|
||||
"/mixnode/<mix_id>/compute-reward-estimation",
|
||||
data = "<user_reward_param>"
|
||||
)]
|
||||
pub(crate) async fn compute_mixnode_reward_estimation(
|
||||
user_reward_param: Json<ComputeRewardEstParam>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<NymContractCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<RewardEstimationResponse>, RocketErrorResponse> {
|
||||
Ok(Json(
|
||||
_compute_mixnode_reward_estimation(
|
||||
user_reward_param.into_inner(),
|
||||
cache,
|
||||
validator_cache,
|
||||
mix_id,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/stake-saturation")]
|
||||
pub(crate) async fn get_mixnode_stake_saturation(
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<NymContractCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<StakeSaturationResponse>, RocketErrorResponse> {
|
||||
Ok(Json(
|
||||
_get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/inclusion-probability")]
|
||||
pub(crate) async fn get_mixnode_inclusion_probability(
|
||||
cache: &State<NodeStatusCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<InclusionProbabilityResponse>, RocketErrorResponse> {
|
||||
Ok(Json(
|
||||
_get_mixnode_inclusion_probability(cache, mix_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnode/<mix_id>/avg_uptime")]
|
||||
pub(crate) async fn get_mixnode_avg_uptime(
|
||||
cache: &State<NodeStatusCache>,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Json<UptimeResponse>, RocketErrorResponse> {
|
||||
Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/gateway/<identity>/avg_uptime")]
|
||||
pub(crate) async fn get_gateway_avg_uptime(
|
||||
cache: &State<NodeStatusCache>,
|
||||
identity: &str,
|
||||
) -> Result<Json<GatewayUptimeResponse>, RocketErrorResponse> {
|
||||
Ok(Json(_get_gateway_avg_uptime(cache, identity).await?))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnodes/inclusion_probability")]
|
||||
pub(crate) async fn get_mixnode_inclusion_probabilities(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Result<Json<AllInclusionProbabilitiesResponse>, RocketErrorResponse> {
|
||||
Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnodes/detailed")]
|
||||
pub async fn get_mixnodes_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Json(_get_legacy_mixnodes_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnodes/detailed-unfiltered")]
|
||||
pub async fn get_mixnodes_detailed_unfiltered(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(_get_mixnodes_detailed_unfiltered(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnodes/rewarded/detailed")]
|
||||
pub async fn get_rewarded_set_detailed(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/mixnodes/active/detailed")]
|
||||
pub async fn get_active_set_detailed(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/gateways/detailed")]
|
||||
pub async fn get_gateways_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<GatewayBondAnnotated>> {
|
||||
todo!("rebasing");
|
||||
|
||||
// Json(_get_legacy_gateways_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status", deprecated = true)]
|
||||
#[get("/gateways/detailed-unfiltered")]
|
||||
pub async fn get_gateways_detailed_unfiltered(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<GatewayBondAnnotated>> {
|
||||
todo!("rebasing");
|
||||
// Json(_get_legacy_gateways_detailed_unfiltered(cache).await)
|
||||
}
|
||||
|
||||
pub mod unstable {
|
||||
use crate::node_status_api::models::RocketErrorResponse;
|
||||
use crate::support::http::helpers::PaginationRequest;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use nym_api_requests::models::{
|
||||
GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode,
|
||||
TestRoute,
|
||||
};
|
||||
use nym_api_requests::pagination::Pagination;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub type DbId = i64;
|
||||
|
||||
// a simply in-memory cache of node details
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NodeInfoCache {
|
||||
inner: Arc<RwLock<NodeInfoCacheInner>>,
|
||||
}
|
||||
|
||||
impl NodeInfoCache {
|
||||
async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
|
||||
{
|
||||
let read_guard = self.inner.read().await;
|
||||
if let Some(cached) = read_guard.mixnodes.get(&db_id) {
|
||||
trace!("cache hit for mixnode {db_id}");
|
||||
return cached.clone();
|
||||
}
|
||||
}
|
||||
trace!("cache miss for mixnode {db_id}");
|
||||
|
||||
let mut write_guard = self.inner.write().await;
|
||||
// double-check the cache in case somebody already updated it while we were waiting for the lock
|
||||
if let Some(cached) = write_guard.mixnodes.get(&db_id) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
let details = match storage.get_mixnode_details_by_db_id(db_id).await {
|
||||
Ok(Some(details)) => details.into(),
|
||||
Ok(None) => {
|
||||
error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!");
|
||||
TestNode::default()
|
||||
}
|
||||
Err(err) => {
|
||||
// don't insert into the cache in case another request is successful
|
||||
error!("failed to retrieve details for mixnode {db_id}: {err}");
|
||||
return TestNode::default();
|
||||
}
|
||||
};
|
||||
|
||||
write_guard.mixnodes.insert(db_id, details.clone());
|
||||
details
|
||||
}
|
||||
|
||||
async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
|
||||
{
|
||||
let read_guard = self.inner.read().await;
|
||||
if let Some(cached) = read_guard.gateways.get(&db_id) {
|
||||
trace!("cache hit for gateway {db_id}");
|
||||
return cached.clone();
|
||||
}
|
||||
}
|
||||
trace!("cache miss for gateway {db_id}");
|
||||
|
||||
let mut write_guard = self.inner.write().await;
|
||||
// double-check the cache in case somebody already updated it while we were waiting for the lock
|
||||
if let Some(cached) = write_guard.gateways.get(&db_id) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
let details = match storage.get_gateway_details_by_db_id(db_id).await {
|
||||
Ok(Some(details)) => details.into(),
|
||||
Ok(None) => {
|
||||
error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!");
|
||||
TestNode::default()
|
||||
}
|
||||
Err(err) => {
|
||||
// don't insert into the cache in case another request is successful
|
||||
error!("failed to retrieve details for gateway {db_id}: {err}");
|
||||
return TestNode::default();
|
||||
}
|
||||
};
|
||||
|
||||
write_guard.gateways.insert(db_id, details.clone());
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct NodeInfoCacheInner {
|
||||
mixnodes: HashMap<DbId, TestNode>,
|
||||
gateways: HashMap<DbId, TestNode>,
|
||||
}
|
||||
|
||||
const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100;
|
||||
const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50;
|
||||
|
||||
async fn _mixnode_test_results(
|
||||
mix_id: NodeId,
|
||||
page: u32,
|
||||
per_page: u32,
|
||||
info_cache: &State<NodeInfoCache>,
|
||||
storage: &State<NymApiStorage>,
|
||||
) -> anyhow::Result<MixnodeTestResultResponse> {
|
||||
// convert to db offset
|
||||
// we're paging from page 0 like civilised people,
|
||||
// so we have to skip (page * per_page) results
|
||||
let offset = page * per_page;
|
||||
let limit = per_page;
|
||||
|
||||
let raw_results = storage
|
||||
.get_mixnode_detailed_statuses(mix_id, limit, offset)
|
||||
.await?;
|
||||
let total = match raw_results.first() {
|
||||
None => 0,
|
||||
Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?,
|
||||
};
|
||||
|
||||
let mut partial_results = Vec::new();
|
||||
for result in raw_results {
|
||||
let gateway = info_cache
|
||||
.get_gateway_details(result.gateway_id, storage)
|
||||
.await;
|
||||
let layer1 = info_cache
|
||||
.get_mix_node_details(result.layer1_mix_id, storage)
|
||||
.await;
|
||||
let layer2 = info_cache
|
||||
.get_mix_node_details(result.layer2_mix_id, storage)
|
||||
.await;
|
||||
let layer3 = info_cache
|
||||
.get_mix_node_details(result.layer3_mix_id, storage)
|
||||
.await;
|
||||
|
||||
partial_results.push(PartialTestResult {
|
||||
monitor_run_id: result.monitor_run_id,
|
||||
timestamp: result.timestamp,
|
||||
overall_reliability_for_all_routes_in_monitor_run: result.reliability,
|
||||
test_routes: TestRoute {
|
||||
gateway,
|
||||
layer1,
|
||||
layer2,
|
||||
layer3,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Ok(MixnodeTestResultResponse {
|
||||
pagination: Pagination {
|
||||
total,
|
||||
page,
|
||||
size: partial_results.len(),
|
||||
},
|
||||
data: partial_results,
|
||||
})
|
||||
}
|
||||
|
||||
#[openapi(tag = "UNSTABLE - DO **NOT** USE")]
|
||||
#[get("/mixnodes/unstable/<mix_id>/test-results?<pagination..>")]
|
||||
pub async fn mixnode_test_results(
|
||||
mix_id: NodeId,
|
||||
pagination: PaginationRequest,
|
||||
info_cache: &State<NodeInfoCache>,
|
||||
storage: &State<NymApiStorage>,
|
||||
) -> Result<Json<MixnodeTestResultResponse>, RocketErrorResponse> {
|
||||
let page = pagination.page.unwrap_or_default();
|
||||
let per_page = min(
|
||||
pagination
|
||||
.per_page
|
||||
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
|
||||
MAX_TEST_RESULTS_PAGE_SIZE,
|
||||
);
|
||||
|
||||
match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await {
|
||||
Ok(res) => Ok(Json(res)),
|
||||
Err(err) => Err(RocketErrorResponse::new(
|
||||
format!("failed to retrieve mixnode test results for node {mix_id}: {err}"),
|
||||
Status::InternalServerError,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn _gateway_test_results(
|
||||
gateway_identity: &str,
|
||||
page: u32,
|
||||
per_page: u32,
|
||||
info_cache: &State<NodeInfoCache>,
|
||||
storage: &State<NymApiStorage>,
|
||||
) -> anyhow::Result<GatewayTestResultResponse> {
|
||||
// convert to db offset
|
||||
// we're paging from page 0 like civilised people,
|
||||
// so we have to skip (page * per_page) results
|
||||
let offset = page * per_page;
|
||||
let limit = per_page;
|
||||
|
||||
let raw_results = storage
|
||||
.get_gateway_detailed_statuses(gateway_identity, limit, offset)
|
||||
.await?;
|
||||
let total = match raw_results.first() {
|
||||
None => 0,
|
||||
Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?,
|
||||
};
|
||||
|
||||
let mut partial_results = Vec::new();
|
||||
for result in raw_results {
|
||||
let gateway = info_cache
|
||||
.get_gateway_details(result.gateway_id, storage)
|
||||
.await;
|
||||
let layer1 = info_cache
|
||||
.get_mix_node_details(result.layer1_mix_id, storage)
|
||||
.await;
|
||||
let layer2 = info_cache
|
||||
.get_mix_node_details(result.layer2_mix_id, storage)
|
||||
.await;
|
||||
let layer3 = info_cache
|
||||
.get_mix_node_details(result.layer3_mix_id, storage)
|
||||
.await;
|
||||
|
||||
partial_results.push(PartialTestResult {
|
||||
monitor_run_id: result.monitor_run_id,
|
||||
timestamp: result.timestamp,
|
||||
overall_reliability_for_all_routes_in_monitor_run: result.reliability,
|
||||
test_routes: TestRoute {
|
||||
gateway,
|
||||
layer1,
|
||||
layer2,
|
||||
layer3,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Ok(GatewayTestResultResponse {
|
||||
pagination: Pagination {
|
||||
total,
|
||||
page,
|
||||
size: partial_results.len(),
|
||||
},
|
||||
data: partial_results,
|
||||
})
|
||||
}
|
||||
|
||||
#[openapi(tag = "UNSTABLE - DO **NOT** USE")]
|
||||
#[get("/gateways/unstable/<gateway_identity>/test-results?<pagination..>")]
|
||||
pub async fn gateway_test_results(
|
||||
gateway_identity: &str,
|
||||
pagination: PaginationRequest,
|
||||
info_cache: &State<NodeInfoCache>,
|
||||
storage: &State<NymApiStorage>,
|
||||
) -> Result<Json<GatewayTestResultResponse>, RocketErrorResponse> {
|
||||
let page = pagination.page.unwrap_or_default();
|
||||
let per_page = min(
|
||||
pagination
|
||||
.per_page
|
||||
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
|
||||
MAX_TEST_RESULTS_PAGE_SIZE,
|
||||
);
|
||||
|
||||
match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await {
|
||||
Ok(res) => Ok(Json(res)),
|
||||
Err(err) => Err(RocketErrorResponse::new(
|
||||
format!(
|
||||
"failed to retrieve mixnode test results for gateway {gateway_identity}: {err}"
|
||||
),
|
||||
Status::InternalServerError,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
@@ -9,25 +9,6 @@ use self::cache::refresher::NymContractCacheRefresher;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod legacy_helpers;
|
||||
// pub(crate) mod routes;
|
||||
//
|
||||
// pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
// openapi_get_routes_spec![
|
||||
// settings: routes::get_mixnodes,
|
||||
// routes::get_mixnodes_detailed,
|
||||
// routes::get_gateways,
|
||||
// routes::get_active_set,
|
||||
// routes::get_active_set_detailed,
|
||||
// routes::get_rewarded_set,
|
||||
// routes::get_rewarded_set_detailed,
|
||||
// routes::get_blacklisted_mixnodes,
|
||||
// routes::get_blacklisted_gateways,
|
||||
// routes::get_blacklisted_gateways_v2,
|
||||
// routes::get_interval_reward_params,
|
||||
// routes::get_current_epoch,
|
||||
// ]
|
||||
// }
|
||||
|
||||
pub(crate) fn start_refresher(
|
||||
config: &config::NodeStatusAPI,
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{node_status_api::NodeStatusCache, nym_contract_cache::cache::NymContractCache};
|
||||
use nym_api_requests::models::MixNodeBondAnnotated;
|
||||
use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId};
|
||||
|
||||
use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer;
|
||||
use rocket::{serde::json::Json, State};
|
||||
use rocket_okapi::openapi;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes")]
|
||||
pub async fn get_mixnodes(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> {
|
||||
Json(cache.legacy_mixnodes_filtered().await)
|
||||
}
|
||||
|
||||
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
|
||||
// replace this with
|
||||
// ```
|
||||
// pub fn get_mixnodes_detailed() -> Redirect {
|
||||
// Redirect::to(uri!("/v1/status/mixnodes/detailed"))
|
||||
// }
|
||||
// ```
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes/detailed")]
|
||||
pub async fn get_mixnodes_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
todo!("rebasing")
|
||||
// Json(_get_legacy_mixnodes_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/gateways")]
|
||||
pub async fn get_gateways(cache: &State<NymContractCache>) -> Json<Vec<GatewayBond>> {
|
||||
Json(
|
||||
cache
|
||||
.legacy_gateways_filtered()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes/rewarded")]
|
||||
pub async fn get_rewarded_set(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> {
|
||||
Json(cache.legacy_v1_rewarded_set_mixnodes().await.clone())
|
||||
}
|
||||
|
||||
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
|
||||
// replace this with
|
||||
// ```
|
||||
// pub fn get_mixnodes_set_detailed() -> Redirect {
|
||||
// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed"))
|
||||
// }
|
||||
// ```
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes/rewarded/detailed")]
|
||||
pub async fn get_rewarded_set_detailed(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
todo!("rebasing")
|
||||
// Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes/active")]
|
||||
pub async fn get_active_set(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> {
|
||||
Json(cache.legacy_v1_active_set_mixnodes().await.clone())
|
||||
}
|
||||
|
||||
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
|
||||
// replace this with
|
||||
// ```
|
||||
// pub fn get_active_set_detailed() -> Redirect {
|
||||
// Redirect::to(uri!("/status/mixnodes/active/detailed"))
|
||||
// }
|
||||
// ```
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes/active/detailed")]
|
||||
pub async fn get_active_set_detailed(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
todo!("rebasing")
|
||||
// Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/mixnodes/blacklisted")]
|
||||
pub async fn get_blacklisted_mixnodes(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Option<HashSet<NodeId>>> {
|
||||
let blacklist = cache.mixnodes_blacklist().await.clone();
|
||||
if blacklist.is_empty() {
|
||||
Json(None)
|
||||
} else {
|
||||
Json(Some(blacklist))
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/gateways/blacklisted")]
|
||||
pub async fn get_blacklisted_gateways(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Option<HashSet<String>>> {
|
||||
let blacklist = cache.gateways_blacklist().await.clone();
|
||||
if blacklist.is_empty() {
|
||||
Json(None)
|
||||
} else {
|
||||
let gateways = cache.legacy_gateways_all().await;
|
||||
Json(Some(
|
||||
gateways
|
||||
.into_iter()
|
||||
.filter(|g| blacklist.contains(&g.node_id))
|
||||
.map(|g| g.gateway.identity_key.clone())
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache", deprecated = true)]
|
||||
#[get("/gateways/blacklisted_v2")]
|
||||
pub async fn get_blacklisted_gateways_v2(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Option<HashSet<NodeId>>> {
|
||||
let blacklist = cache.gateways_blacklist().await.clone();
|
||||
if blacklist.is_empty() {
|
||||
Json(None)
|
||||
} else {
|
||||
Json(Some(blacklist))
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/epoch/reward_params")]
|
||||
pub async fn get_interval_reward_params(
|
||||
cache: &State<NymContractCache>,
|
||||
) -> Json<Option<RewardingParams>> {
|
||||
Json(*cache.interval_reward_params().await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/epoch/current")]
|
||||
pub async fn get_current_epoch(cache: &State<NymContractCache>) -> Json<Option<Interval>> {
|
||||
Json(*cache.current_interval().await)
|
||||
}
|
||||
@@ -2,34 +2,3 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(crate) mod handlers;
|
||||
|
||||
// pub(crate) mod routes;
|
||||
// mod unstable_routes;
|
||||
|
||||
// /// Merges the routes with http information and returns it to Rocket for serving
|
||||
// pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
// openapi_get_routes_spec![
|
||||
// settings:
|
||||
// routes::get_gateways_described,
|
||||
// routes::get_mixnodes_described,
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
// openapi_get_routes_spec![
|
||||
// settings:
|
||||
// unstable_routes::nodes_basic,
|
||||
// unstable_routes::nodes_expanded,
|
||||
// unstable_routes::nodes_detailed,
|
||||
// unstable_routes::gateways_basic,
|
||||
// unstable_routes::gateways_expanded,
|
||||
// unstable_routes::gateways_detailed,
|
||||
// unstable_routes::mixnodes_basic,
|
||||
// unstable_routes::mixnodes_expanded,
|
||||
// unstable_routes::mixnodes_detailed,
|
||||
// routes::all_described_nodes,
|
||||
// routes::node_description,
|
||||
// routes::node_annotation_by_identity,
|
||||
// routes::node_annotation
|
||||
// ]
|
||||
// }
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, LegacyDescribedGateway, LegacyDescribedMixNode, NymNodeDescription,
|
||||
};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[openapi(tag = "Nym Nodes")]
|
||||
#[get("/all/described")]
|
||||
pub async fn all_described_nodes(
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
) -> Json<Vec<NymNodeDescription>> {
|
||||
let Ok(self_descriptions) = describe_cache.get().await else {
|
||||
return Json(Vec::new());
|
||||
};
|
||||
|
||||
Json(self_descriptions.all_nodes().cloned().collect())
|
||||
}
|
||||
|
||||
#[openapi(tag = "Nym Nodes")]
|
||||
#[get("/all/<node_id>/described")]
|
||||
pub async fn node_description(
|
||||
node_id: NodeId,
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
) -> Json<Option<NymNodeDescription>> {
|
||||
let Ok(self_descriptions) = describe_cache.get().await else {
|
||||
return Json(None);
|
||||
};
|
||||
|
||||
Json(self_descriptions.get_node(&node_id).cloned())
|
||||
}
|
||||
|
||||
#[openapi(tag = "Nym Nodes")]
|
||||
#[get("/annotation-by-identity/<identity>")]
|
||||
pub async fn node_annotation_by_identity(
|
||||
identity: String,
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
) -> Json<AnnotationResponse> {
|
||||
let Some(node_id) = status_cache.map_identity_to_node_id(&identity).await else {
|
||||
return Json(Default::default());
|
||||
};
|
||||
node_annotation(node_id, status_cache).await
|
||||
}
|
||||
|
||||
#[openapi(tag = "Nym Nodes")]
|
||||
#[get("/annotation/<node_id>")]
|
||||
pub async fn node_annotation(
|
||||
node_id: NodeId,
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
) -> Json<AnnotationResponse> {
|
||||
let Some(annotation) = status_cache.node_annotations().await else {
|
||||
return Json(Default::default());
|
||||
};
|
||||
|
||||
Json(AnnotationResponse {
|
||||
node_id: Some(node_id),
|
||||
annotation: annotation.get(&node_id).cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// This only returns descriptions of **legacy** gateways
|
||||
#[openapi(tag = "Nym Nodes", deprecated = true)]
|
||||
#[get("/gateways/described")]
|
||||
pub async fn get_gateways_described(
|
||||
contract_cache: &State<NymContractCache>,
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
) -> Json<Vec<LegacyDescribedGateway>> {
|
||||
let gateways = contract_cache.legacy_gateways_filtered().await;
|
||||
if gateways.is_empty() {
|
||||
return Json(Vec::new());
|
||||
}
|
||||
|
||||
// if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways
|
||||
let Ok(self_descriptions) = describe_cache.get().await else {
|
||||
return Json(gateways.into_iter().map(Into::into).collect());
|
||||
};
|
||||
|
||||
Json(
|
||||
gateways
|
||||
.into_iter()
|
||||
.map(|bond| LegacyDescribedGateway {
|
||||
self_described: self_descriptions
|
||||
.deref()
|
||||
.get_description(&bond.node_id)
|
||||
.cloned(),
|
||||
bond,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// This only returns descriptions of **legacy** mixnodes
|
||||
#[openapi(tag = "Nym Nodes", deprecated = true)]
|
||||
#[get("/mixnodes/described")]
|
||||
pub async fn get_mixnodes_described(
|
||||
contract_cache: &State<NymContractCache>,
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
) -> Json<Vec<LegacyDescribedMixNode>> {
|
||||
let mixnodes = contract_cache
|
||||
.legacy_mixnodes_filtered()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|m| m.bond_information)
|
||||
.collect::<Vec<_>>();
|
||||
if mixnodes.is_empty() {
|
||||
return Json(Vec::new());
|
||||
}
|
||||
|
||||
// if the self describe cache is unavailable, well, don't attach describe data
|
||||
let Ok(self_descriptions) = describe_cache.get().await else {
|
||||
return Json(mixnodes.into_iter().map(Into::into).collect());
|
||||
};
|
||||
|
||||
Json(
|
||||
mixnodes
|
||||
.into_iter()
|
||||
.map(|bond| LegacyDescribedMixNode {
|
||||
self_described: self_descriptions
|
||||
.deref()
|
||||
.get_description(&bond.mix_id)
|
||||
.cloned(),
|
||||
bond,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_status_api::models::RocketErrorResponse;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use nym_api_requests::nym_nodes::{
|
||||
CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode,
|
||||
};
|
||||
use nym_bin_common::version_checker;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/*
|
||||
routes:
|
||||
|
||||
// all routes/nodes are split into three tiers:
|
||||
// /skimmed => [used by clients] returns the very basic information for routing purposes
|
||||
// /semi-skimmed => [used by other nodes/VPN] returns more additional information such noise keys
|
||||
// /full-fat => [used by explorers, et al.] returns almost everything there is about the nodes
|
||||
|
||||
// there's also additional split based on the role:
|
||||
?role => filters based on the specific role (mixnode/gateway/(in the future: entry/exit))
|
||||
/mixnodes/<tier> => only returns mixnode role data
|
||||
/gateway/<tier> => only returns (entry) gateway role data
|
||||
|
||||
|
||||
*/
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/skimmed?<role>&<semver_compatibility>")]
|
||||
pub async fn nodes_basic(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
role: Option<NodeRoleQueryParam>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, RocketErrorResponse> {
|
||||
if let Some(role) = role {
|
||||
match role {
|
||||
NodeRoleQueryParam::ActiveMixnode => {
|
||||
return mixnodes_basic(
|
||||
status_cache,
|
||||
contract_cache,
|
||||
describe_cache,
|
||||
semver_compatibility,
|
||||
)
|
||||
.await
|
||||
}
|
||||
NodeRoleQueryParam::EntryGateway => {
|
||||
return gateways_basic(
|
||||
status_cache,
|
||||
contract_cache,
|
||||
describe_cache,
|
||||
semver_compatibility,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/semi-skimmed?<role>&<semver_compatibility>")]
|
||||
pub async fn nodes_expanded(
|
||||
cache: &State<NodeStatusCache>,
|
||||
role: Option<NodeRoleQueryParam>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, RocketErrorResponse> {
|
||||
if let Some(role) = role {
|
||||
match role {
|
||||
NodeRoleQueryParam::ActiveMixnode => {
|
||||
return mixnodes_expanded(cache, semver_compatibility).await
|
||||
}
|
||||
NodeRoleQueryParam::EntryGateway => {
|
||||
return gateways_expanded(cache, semver_compatibility).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/full-fat?<role>&<semver_compatibility>")]
|
||||
pub async fn nodes_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
role: Option<NodeRoleQueryParam>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<FullFatNode>>, RocketErrorResponse> {
|
||||
if let Some(role) = role {
|
||||
match role {
|
||||
NodeRoleQueryParam::ActiveMixnode => {
|
||||
return mixnodes_detailed(cache, semver_compatibility).await
|
||||
}
|
||||
NodeRoleQueryParam::EntryGateway => {
|
||||
return gateways_detailed(cache, semver_compatibility).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/gateways/skimmed?<semver_compatibility>")]
|
||||
pub async fn gateways_basic(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, RocketErrorResponse> {
|
||||
// 1. get the rewarded set
|
||||
let rewarded_set = contract_cache
|
||||
.rewarded_set()
|
||||
.await
|
||||
.ok_or_else(RocketErrorResponse::internal_server_error)?;
|
||||
|
||||
// determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic
|
||||
let active_gateways = rewarded_set.gateways().into_iter().collect::<HashSet<_>>();
|
||||
|
||||
// 2. grab all annotations so that we could attach scores to the [nym] nodes
|
||||
let annotations = status_cache
|
||||
.node_annotations()
|
||||
.await
|
||||
.ok_or_else(RocketErrorResponse::internal_server_error)?;
|
||||
|
||||
// 3. grab all legacy gateways
|
||||
// due to legacy endpoints we already have fully annotated data on them
|
||||
let annotated_legacy_gateways = status_cache
|
||||
.annotated_legacy_gateways()
|
||||
.await
|
||||
.ok_or_else(RocketErrorResponse::internal_server_error)?;
|
||||
|
||||
// 4. grab all relevant described nym-nodes
|
||||
let describe_cache = describe_cache.get().await?;
|
||||
let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes();
|
||||
|
||||
// 5. only return nodes that are present in the active set
|
||||
let mut active_skimmed_gateways = Vec::new();
|
||||
|
||||
for (node_id, legacy) in annotated_legacy_gateways.iter() {
|
||||
if !active_gateways.contains(node_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(semver_compat) = semver_compatibility.as_ref() {
|
||||
let version = legacy.version();
|
||||
if !version_checker::is_minor_version_compatible(version, semver_compat) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have self-described info, prefer it over contract data
|
||||
if let Some(described) = describe_cache.get_node(node_id) {
|
||||
active_skimmed_gateways.push(
|
||||
described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h),
|
||||
)
|
||||
} else {
|
||||
active_skimmed_gateways.push(legacy.into());
|
||||
}
|
||||
}
|
||||
|
||||
for nym_node in gateway_capable_nym_nodes {
|
||||
// if this node is not an active gateway, ignore it
|
||||
if !active_gateways.contains(&nym_node.node_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we have wrong version, ignore
|
||||
if let Some(semver_compat) = semver_compatibility.as_ref() {
|
||||
let version = nym_node.version();
|
||||
if !version_checker::is_minor_version_compatible(version, semver_compat) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit
|
||||
let role = if rewarded_set.is_exit(&nym_node.node_id) {
|
||||
NodeRole::ExitGateway
|
||||
} else {
|
||||
NodeRole::EntryGateway
|
||||
};
|
||||
|
||||
// honestly, not sure under what exact circumstances this value could be missing,
|
||||
// but in that case just use 0 performance
|
||||
let annotation = annotations
|
||||
.get(&nym_node.node_id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
active_skimmed_gateways
|
||||
.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance));
|
||||
}
|
||||
|
||||
// min of all caches
|
||||
let refreshed_at = [
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
annotated_legacy_gateways.timestamp(),
|
||||
]
|
||||
.into_iter()
|
||||
.min()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
Ok(Json(CachedNodesResponse {
|
||||
refreshed_at,
|
||||
nodes: active_skimmed_gateways,
|
||||
}))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/gateways/semi-skimmed?<semver_compatibility>")]
|
||||
pub async fn gateways_expanded(
|
||||
cache: &State<NodeStatusCache>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, RocketErrorResponse> {
|
||||
let _ = cache;
|
||||
let _ = semver_compatibility;
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/gateways/full-fat?<semver_compatibility>")]
|
||||
pub async fn gateways_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<FullFatNode>>, RocketErrorResponse> {
|
||||
let _ = cache;
|
||||
let _ = semver_compatibility;
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/mixnodes/skimmed?<semver_compatibility>")]
|
||||
pub async fn mixnodes_basic(
|
||||
status_cache: &State<NodeStatusCache>,
|
||||
contract_cache: &State<NymContractCache>,
|
||||
describe_cache: &State<SharedCache<DescribedNodes>>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, RocketErrorResponse> {
|
||||
// 1. get the rewarded set
|
||||
let rewarded_set = contract_cache
|
||||
.rewarded_set()
|
||||
.await
|
||||
.ok_or_else(RocketErrorResponse::internal_server_error)?;
|
||||
|
||||
// determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic
|
||||
let active_mixnodes = rewarded_set
|
||||
.active_mixnodes()
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// 2. grab all annotations so that we could attach scores to the [nym] nodes
|
||||
let annotations = status_cache
|
||||
.node_annotations()
|
||||
.await
|
||||
.ok_or_else(RocketErrorResponse::internal_server_error)?;
|
||||
|
||||
// 3. grab all legacy mixnodes
|
||||
// due to legacy endpoints we already have fully annotated data on them
|
||||
let annotated_legacy_mixnodes = status_cache
|
||||
.annotated_legacy_mixnodes()
|
||||
.await
|
||||
.ok_or_else(RocketErrorResponse::internal_server_error)?;
|
||||
|
||||
// 4. grab all relevant described nym-nodes
|
||||
let describe_cache = describe_cache.get().await?;
|
||||
let mixing_nym_nodes = describe_cache.mixing_nym_nodes();
|
||||
|
||||
// TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it
|
||||
|
||||
// 5. only return nodes that are present in the active set
|
||||
let mut active_skimmed_mixnodes = Vec::new();
|
||||
|
||||
for (mix_id, legacy) in annotated_legacy_mixnodes.iter() {
|
||||
if !active_mixnodes.contains(mix_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(semver_compat) = semver_compatibility.as_ref() {
|
||||
let version = legacy.version();
|
||||
if !version_checker::is_minor_version_compatible(version, semver_compat) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have self-described info, prefer it over contract data
|
||||
if let Some(described) = describe_cache.get_node(mix_id) {
|
||||
active_skimmed_mixnodes.push(described.to_skimmed_node(
|
||||
NodeRole::Mixnode {
|
||||
layer: legacy.mixnode_details.bond_information.layer.into(),
|
||||
},
|
||||
legacy.node_performance.last_24h,
|
||||
))
|
||||
} else {
|
||||
active_skimmed_mixnodes.push(legacy.into());
|
||||
}
|
||||
}
|
||||
|
||||
for nym_node in mixing_nym_nodes {
|
||||
// if this node is not an active mixnode, ignore it
|
||||
if !active_mixnodes.contains(&nym_node.node_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we have wrong version, ignore
|
||||
if let Some(semver_compat) = semver_compatibility.as_ref() {
|
||||
let version = nym_node.version();
|
||||
if !version_checker::is_minor_version_compatible(version, semver_compat) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: if we determined our node IS active, it MUST have a layer
|
||||
// no other thread could have updated the rewarded set as we're still holding the [read] lock on the data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap();
|
||||
|
||||
// honestly, not sure under what exact circumstances this value could be missing,
|
||||
// but in that case just use 0 performance
|
||||
let annotation = annotations
|
||||
.get(&nym_node.node_id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
active_skimmed_mixnodes.push(
|
||||
nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance),
|
||||
);
|
||||
}
|
||||
|
||||
// min of all caches
|
||||
let refreshed_at = [
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
annotated_legacy_mixnodes.timestamp(),
|
||||
]
|
||||
.into_iter()
|
||||
.min()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
Ok(Json(CachedNodesResponse {
|
||||
refreshed_at,
|
||||
nodes: active_skimmed_mixnodes,
|
||||
}))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/mixnodes/semi-skimmed?<semver_compatibility>")]
|
||||
pub async fn mixnodes_expanded(
|
||||
cache: &State<NodeStatusCache>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, RocketErrorResponse> {
|
||||
let _ = cache;
|
||||
let _ = semver_compatibility;
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "Unstable Nym Nodes")]
|
||||
#[get("/mixnodes/full-fat?<semver_compatibility>")]
|
||||
pub async fn mixnodes_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Json<CachedNodesResponse<FullFatNode>>, RocketErrorResponse> {
|
||||
let _ = cache;
|
||||
let _ = semver_compatibility;
|
||||
Err(RocketErrorResponse::new(
|
||||
"unimplemented",
|
||||
Status::NotImplemented,
|
||||
))
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use nym_bin_common::build_information::BinaryBuildInformation;
|
||||
use tokio::time::Instant;
|
||||
|
||||
pub(crate) mod handlers;
|
||||
// pub(crate) mod routes;
|
||||
|
||||
pub(crate) struct ApiStatusState {
|
||||
startup_time: Instant,
|
||||
@@ -40,12 +39,3 @@ impl ApiStatusState {
|
||||
self.signer_information = Some(signer_information)
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
// openapi_get_routes_spec![
|
||||
// settings:
|
||||
// routes::health,
|
||||
// routes::build_information,
|
||||
// routes::signer_information
|
||||
// ]
|
||||
// }
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::RocketErrorResponse;
|
||||
use crate::status::ApiStatusState;
|
||||
use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse};
|
||||
use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
use nym_compact_ecash::Base58;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
|
||||
#[openapi(tag = "Api Status")]
|
||||
#[get("/health")]
|
||||
pub(crate) async fn health(state: &State<ApiStatusState>) -> Json<ApiHealthResponse> {
|
||||
let uptime = state.startup_time.elapsed();
|
||||
let health = ApiHealthResponse::new_healthy(uptime);
|
||||
Json(health)
|
||||
}
|
||||
|
||||
#[openapi(tag = "Api Status")]
|
||||
#[get("/build-information")]
|
||||
pub(crate) async fn build_information(
|
||||
state: &State<ApiStatusState>,
|
||||
) -> Json<BinaryBuildInformationOwned> {
|
||||
Json(state.build_information.to_owned())
|
||||
}
|
||||
|
||||
#[openapi(tag = "Api Status")]
|
||||
#[get("/signer-information")]
|
||||
pub(crate) async fn signer_information(
|
||||
state: &State<ApiStatusState>,
|
||||
) -> Result<Json<SignerInformationResponse>, RocketErrorResponse> {
|
||||
let signer_state = state.signer_information.as_ref().ok_or_else(|| {
|
||||
RocketErrorResponse::new(
|
||||
"this api does not expose zk-nym signing functionalities",
|
||||
Status::InternalServerError,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Json(SignerInformationResponse {
|
||||
cosmos_address: signer_state.cosmos_address.clone(),
|
||||
identity: signer_state.identity.clone(),
|
||||
announce_address: signer_state.announce_address.clone(),
|
||||
verification_key: signer_state
|
||||
.ecash_keypair
|
||||
.verification_key()
|
||||
.await
|
||||
.map(|maybe_vk| maybe_vk.to_bs58()),
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user