Compare commits

..

36 Commits

Author SHA1 Message Date
Mark Sinclair 80b590d50d bug-fix: nym-credential-proxy webhook request is the correct shape and added reporting errors via webhook (#5077)
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2024-11-01 21:48:04 +01:00
Bogdan-Ștefan Neacşu f9b363648f Fix expiration date as today + 7 days (#5076) 2024-11-01 16:01:24 +02:00
Bogdan-Ștefan Neacşu b73561f1c9 Fix gateway decreasing bandwidth (#5075)
* Update storage peers after periodic check

* Reset storage bytes on restart

* Fix clippy
2024-11-01 15:40:22 +02:00
Dinko Zdravac 09b68a8204 Cherry pick NS API from develop (#5074)
* Revert "NS API with directory v2 (#5068)"

This reverts commit cf4fe5f875.

* Merge pull request #5050 from nymtech/dz-node-status-api

Node Status API

* Ns agent workflow (#5055)

* feat: add dockerfile

* add github workflow for node status agent

---------

Co-authored-by: Fran Arbanas <arbanasfran@gmail.com>

* NS API with directory v2 (#5058)

* Use unstable explorer client

* Clean up stale testruns & logging
- log gw identity key
- better agent testrun logging
- log responses
- change response code for agents

* Better logging on agent

* Testrun stores gw identity key instead of gw pk

* Agent 0.1.3

* Agent 0.1.4

* Sqlx offline query data + clippy

* Compatible with directory v2

* Point to internal deps + rebase + v0.1.5

* self described field not null

* Fix build.rs typo

* Fix clippy

---------

Co-authored-by: Fran Arbanas <arbanasfran@gmail.com>
2024-11-01 01:24:41 +01:00
Fouad 0374626960 Allow custom http port to be reset (#5073)
* allow custom port to be reset in wallet
2024-10-31 16:53:55 +00:00
Dinko Zdravac cf4fe5f875 NS API with directory v2 (#5068)
* Use unstable explorer client

* Clean up stale testruns & logging
- log gw identity key
- better agent testrun logging
- log responses
- change response code for agents

* Better logging on agent

* Testrun stores gw identity key instead of gw pk

* Agent 0.1.3

* Agent 0.1.4

* Sqlx offline query data + clippy

* Compatible with directory v2

* Point to internal deps + rebase + v0.1.5

* self described field not null

* Fix build.rs typo
2024-10-31 13:52:20 +01:00
Jędrzej Stuczyński 9f8bf2d080 bugfix: wallet backend fixes (#5070)
* fixed simulation arguments

* make sure 'try_convert_pubkey_to_node_id' checks for native nymnodes
2024-10-31 12:23:20 +00:00
Jędrzej Stuczyński b9d1fc40e7 deprecated old nym-api client methods and replaced them when possible (#5069) 2024-10-31 12:08:58 +00:00
Jędrzej Stuczyński be67234093 bugfix: credential-proxy obtain-async (#5067)
* removed foreign key constraint on deposit table

* fixed sql nullability

* fixed swagger arguments for '/api/v1/ticketbook/shares/device/{device_id}/credential/{credential_id}' route

* fixed missing swagger component definitions
2024-10-31 10:33:38 +00:00
Fouad 8b0b70a727 allow nym node config updates (#5066) 2024-10-31 09:59:22 +00:00
Fouad c90ebf0a6a Feature/wallet bonding fixes (#5064)
* bonding and unbonding for nym nodes
2024-10-30 17:15:38 +00:00
Jędrzej Stuczyński 07ff2639ec bugfix: use corrext axum extractors for ecash route arguments (#5065) 2024-10-30 16:05:16 +00:00
Jędrzej Stuczyński 753a21f8ca bugfix/feature: added NymApiClient method to get all skimmed nodes (#5062)
* bugfix/feature: added NymApiClient method to get all skimmed nodes

* wasm

* helper: utility method for getting ed25519 identity directly from node description
2024-10-30 12:21:27 +00:00
Jędrzej Stuczyński 76da4ab532 bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set (#5049) 2024-10-30 09:11:13 +00:00
Jędrzej Stuczyński 317f7fffa9 added hacky routes to return nymnodes alongside legacy nodes (#5051)
* added hacky routes to return nymnodes alongside legacy nodes

* fixed mixing role

* Update client (#5054)

* removed hacky mixnodes endpoint for its not used

* construct explorer-api client with timeout

---------

Co-authored-by: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com>
2024-10-29 08:35:07 +00:00
Jędrzej Stuczyński 4396def133 bugfix: adjust runtime storage migration (#5047) 2024-10-28 10:07:51 +00:00
Jędrzej Stuczyński a56a318a7f bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using query parameters (#5046) 2024-10-28 09:57:14 +00:00
Jędrzej Stuczyński 4d08047c57 bugfix: restore default http port for nym-api (#5045)
when it was run under 'rocket' server the port used was 8000. let's restore that value
2024-10-28 09:28:47 +00:00
Jędrzej Stuczyński cb13be27f8 bugfix: fix ecash handlers routes (#5043) 2024-10-28 09:12:40 +00:00
Jędrzej Stuczyński fa392169c1 bugfix: use human readable roles for annotations (#5036)
* bugfix: use human readable roles for annotations

* update the wallet code to use 'DisplayRole'
2024-10-28 09:08:17 +00:00
Jędrzej Stuczyński 3167fb34e6 bugfix: don't assign exit gateways to standby set (#5041) 2024-10-25 16:53:51 +01:00
Jędrzej Stuczyński 9ca6301e1c bugfix: make sure nym-nodes are also tested by network monitor (#5040) 2024-10-25 15:20:39 +01:00
Jędrzej Stuczyński e16a73338e bugfix: use bonded nym-nodes for determining initial network monitor nodes (#5039) 2024-10-25 12:34:25 +01:00
Bogdan-Ștefan Neacşu bfa3825d70 Pass the Poisson flag on authenticator config (#5037) 2024-10-25 14:08:52 +03:00
Jędrzej Stuczyński d626e7689f bugfix: make gateways insert themselves into [local] topology (#5038)
* added explicit SP suffix to started tasks

* added 'GatewayTopologyProvider' that always injects itself into the network

* use the new topology provider to bypass described bootstrapping problem
2024-10-25 12:06:16 +01:00
Jędrzej Stuczyński 9234474565 bugfix: use old name for 'epoch_role' in SkimmedNode (#5034)
* bugfix: use old name for 'epoch_role' in SkimmedNode

* clippy
2024-10-25 09:29:37 +01:00
Jędrzej Stuczyński 29f8386b50 bugfix: make sure to use correct highest node id when assigning role (#5032)
* bugfix: make sure to use correct highest node id when assigning role

* make sure nym-api provides sorted values for older contracts
2024-10-24 17:47:57 +01:00
Jędrzej Stuczyński 0edb9631a6 feature: use axum_client_ip for attempting to extract source ip (#5031) 2024-10-24 17:38:32 +01:00
Jędrzej Stuczyński 4b0153f5f2 bugfix: fixed backwards incompatibility for /gateways/described endpoint (#5030) 2024-10-24 15:37:41 +01:00
Jędrzej Stuczyński c09a17b66d bugfix: verifying signed information of legacy nodes (#5029)
* Added new legacy variant of HostInformation

* fixed 'option_bs58_x25519_pubkey' for empty string

* 'Debug' impl for x25519 and ed25519 to use human-readable representation

* HttpClient to use explicit 'serde_json' conversion for better errors

* additional 'Debug' derives
2024-10-24 15:00:34 +01:00
Jędrzej Stuczyński d18ddcdc11 bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' (#5028)
* bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change'

* updated schema files due to removal of '#[serde(deny_unknown_fields)]'
2024-10-24 10:54:00 +01:00
Jędrzej Stuczyński d2df542280 bugfix: missing #[serde(default)] for announce port (#5024) 2024-10-23 16:52:17 +01:00
Jędrzej Stuczyński 6fafd8c03a bugfix: directory v2.1 get_all_avg_gateway_reliability_in_interval query (#5023)
* log full storage errors on failures

* use query_as! macro
2024-10-23 16:36:21 +01:00
Jędrzej Stuczyński 38e66f6ddf added 'get_all_described_nodes' to NymApiClient and adjusted return type on api itself (#5016) 2024-10-23 09:48:25 +01:00
Bogdan-Ștefan Neacşu b9fbe0b8f3 Reapply fixes to new branch (#5014) 2024-10-22 18:33:18 +03:00
Bogdan-Ștefan Neacşu daafb5cae4 Consume only positive bandwidth (#5013) 2024-10-22 17:46:46 +03:00
206 changed files with 3644 additions and 4038 deletions
@@ -1,55 +0,0 @@
name: Build and upload Data observatory container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "nym-data-observatory"
CONTAINER_NAME: "data-observatory"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v4
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Check if tag exists
run: |
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
echo "Tag ${{ steps.get_version.outputs.value }} already exists"
fi
- name: Remove existing tag if exists
run: |
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
fi
- name: Create tag
run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
Generated
+469 -841
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -54,7 +54,6 @@ members = [
"common/exit-policy",
"common/gateway-requests",
"common/gateway-storage",
"common/gateway-stats-storage",
"common/http-api-client",
"common/http-api-common",
"common/inclusion-probability",
@@ -190,9 +189,10 @@ aes = "0.8.1"
aes-gcm = "0.10.1"
aes-gcm-siv = "0.11.1"
aead = "0.5.2"
anyhow = "1.0.90"
anyhow = "1.0.89"
argon2 = "0.5.0"
async-trait = "0.1.83"
axum-client-ip = "0.6.1"
axum = "0.7.5"
axum-extra = "0.9.4"
base64 = "0.22.1"
@@ -215,7 +215,7 @@ chacha20 = "0.9.0"
chacha20poly1305 = "0.10.1"
chrono = "0.4.31"
cipher = "0.4.3"
clap = "4.5.20"
clap = "4.5.18"
clap_complete = "4.5"
clap_complete_fig = "4.5"
colored = "2.0"
@@ -285,7 +285,7 @@ opentelemetry-jaeger = "0.18.0"
parking_lot = "0.12.3"
pem = "0.8"
petgraph = "0.6.5"
pin-project = "1.1"
pin-project = "1.0"
pin-project-lite = "0.2.14"
pretty_env_logger = "0.4.0"
publicsuffix = "2.2.3"
@@ -305,7 +305,7 @@ rocket_okapi = "0.8.0"
safer-ffi = "0.1.13"
schemars = "0.8.21"
semver = "1.0.23"
serde = "1.0.211"
serde = "1.0.210"
serde_bytes = "0.11.15"
serde_derive = "1.0"
serde_json = "1.0.132"
@@ -402,10 +402,10 @@ indexed_db_futures = { git = "https://github.com/TiemenSch/rust-indexed-db", bra
js-sys = "0.3.70"
serde-wasm-bindgen = "0.6.5"
tsify = "0.4.5"
wasm-bindgen = "0.2.95"
wasm-bindgen-futures = "0.4.45"
wasm-bindgen = "0.2.93"
wasm-bindgen-futures = "0.4.43"
wasmtimer = "0.2.0"
web-sys = "0.3.72"
web-sys = "0.3.70"
# Profile settings for individual crates
@@ -112,7 +112,7 @@ impl GeoAwareTopologyProvider {
async fn get_topology(&self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
@@ -6,7 +6,6 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
use futures::StreamExt;
use log::*;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopologyError;
use std::time::Duration;
@@ -18,7 +17,11 @@ use wasmtimer::tokio::sleep;
mod accessor;
pub mod geo_aware_provider;
pub(crate) mod nym_api_provider;
pub mod nym_api_provider;
pub use geo_aware_provider::GeoAwareTopologyProvider;
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
pub use nym_topology::provider_trait::TopologyProvider;
// TODO: move it to config later
const MAX_FAILURE_COUNT: usize = 10;
@@ -14,9 +14,10 @@ use url::Url;
pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50;
pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50;
pub(crate) struct Config {
pub(crate) min_mixnode_performance: u8,
pub(crate) min_gateway_performance: u8,
#[derive(Debug)]
pub struct Config {
pub min_mixnode_performance: u8,
pub min_gateway_performance: u8,
}
impl Default for Config {
@@ -29,7 +30,7 @@ impl Default for Config {
}
}
pub(crate) struct NymApiTopologyProvider {
pub struct NymApiTopologyProvider {
config: Config,
validator_client: nym_validator_client::client::NymApiClient,
@@ -40,7 +41,7 @@ pub(crate) struct NymApiTopologyProvider {
}
impl NymApiTopologyProvider {
pub(crate) fn new(
pub fn new(
config: Config,
mut nym_api_urls: Vec<Url>,
client_version: String,
@@ -98,7 +99,7 @@ impl NymApiTopologyProvider {
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
+3 -1
View File
@@ -121,7 +121,9 @@ pub async fn current_mixnodes<R: Rng>(
log::trace!("Fetching list of mixnodes from: {nym_api}");
let mixnodes = client.get_basic_active_mixing_assigned_nodes(None).await?;
let mixnodes = client
.get_all_basic_active_mixing_assigned_nodes(None)
.await?;
let valid_mixnodes = mixnodes
.iter()
.filter_map(|mixnode| mixnode.try_into().ok())
@@ -18,8 +18,8 @@ use nym_api_requests::ecash::{
PartialExpirationDateSignatureResponse, VerificationKeyResponse,
};
use nym_api_requests::models::{
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
RewardEstimationResponse, StakeSaturationResponse,
ApiHealthResponse, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::SkimmedNode;
@@ -30,10 +30,10 @@ use time::Date;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
use nym_mixnet_contract_common::NymNodeDetails;
pub use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId,
};
// re-export the type to not break existing imports
pub use crate::coconut::EcashApiClient;
@@ -106,7 +106,9 @@ impl Config {
pub struct Client<C, S = NoSigner> {
// ideally they would have been read-only, but unfortunately rust doesn't have such features
// #[deprecated(note = "please use `nym_api_client` instead")]
pub nym_api: nym_api::Client,
// pub nym_api_client: NymApiClient,
pub nyxd: NyxdClient<C, S>,
}
@@ -190,6 +192,8 @@ impl<C, S> Client<C, S> {
}
// validator-api wrappers
// we have to allow the use of deprecated method here as they're calling the deprecated trait methods
#[allow(deprecated)]
impl<C, S> Client<C, S> {
pub fn api_url(&self) -> &Url {
self.nym_api.current_url()
@@ -199,50 +203,102 @@ impl<C, S> Client<C, S> {
self.nym_api.change_base_url(new_endpoint)
}
#[deprecated]
pub async fn get_cached_mixnodes(&self) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
Ok(self.nym_api.get_mixnodes().await?)
}
#[deprecated]
pub async fn get_cached_mixnodes_detailed(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorClientError> {
Ok(self.nym_api.get_mixnodes_detailed().await?)
}
#[deprecated]
pub async fn get_cached_mixnodes_detailed_unfiltered(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorClientError> {
Ok(self.nym_api.get_mixnodes_detailed_unfiltered().await?)
}
#[deprecated]
pub async fn get_cached_rewarded_mixnodes(
&self,
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
Ok(self.nym_api.get_rewarded_mixnodes().await?)
}
#[deprecated]
pub async fn get_cached_rewarded_mixnodes_detailed(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorClientError> {
Ok(self.nym_api.get_rewarded_mixnodes_detailed().await?)
}
#[deprecated]
pub async fn get_cached_active_mixnodes(
&self,
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
Ok(self.nym_api.get_active_mixnodes().await?)
}
#[deprecated]
pub async fn get_cached_active_mixnodes_detailed(
&self,
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorClientError> {
Ok(self.nym_api.get_active_mixnodes_detailed().await?)
}
#[deprecated]
pub async fn get_cached_gateways(&self) -> Result<Vec<GatewayBond>, ValidatorClientError> {
Ok(self.nym_api.get_gateways().await?)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -258,6 +314,8 @@ pub struct NymApiClient {
// we could re-implement the communication with the REST API on port 1317
}
// we have to allow the use of deprecated method here as they're calling the deprecated trait methods
#[allow(deprecated)]
impl NymApiClient {
pub fn new(api_url: Url) -> Self {
let nym_api = nym_api::Client::new(api_url, None);
@@ -290,7 +348,7 @@ impl NymApiClient {
self.nym_api.change_base_url(new_endpoint);
}
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes instead")]
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
pub async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
@@ -327,7 +385,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_all_basic_entry_assigned_nodes(
.get_basic_entry_assigned_nodes(
semver_compatibility.clone(),
false,
Some(page),
@@ -348,7 +406,7 @@ impl NymApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
pub async fn get_basic_active_mixing_assigned_nodes(
pub async fn get_all_basic_active_mixing_assigned_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
@@ -378,32 +436,142 @@ impl NymApiClient {
Ok(nodes)
}
/// retrieve basic information for nodes are capable of operating as a mixnode
/// this includes legacy mixnodes and nym-nodes
pub async fn get_all_basic_mixing_capable_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
loop {
let mut res = self
.nym_api
.get_basic_mixing_capable_nodes(
semver_compatibility.clone(),
false,
Some(page),
None,
)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
/// retrieve basic information for all bonded nodes on the network
pub async fn get_all_basic_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
loop {
let mut res = self
.nym_api
.get_basic_nodes(semver_compatibility.clone(), false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
Ok(self.nym_api.health().await?)
}
#[deprecated]
pub async fn get_cached_active_mixnodes(
&self,
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
Ok(self.nym_api.get_active_mixnodes().await?)
}
#[deprecated]
pub async fn get_cached_rewarded_mixnodes(
&self,
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
Ok(self.nym_api.get_rewarded_mixnodes().await?)
}
#[deprecated]
pub async fn get_cached_mixnodes(&self) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
Ok(self.nym_api.get_mixnodes().await?)
}
#[deprecated]
pub async fn get_cached_gateways(&self) -> Result<Vec<GatewayBond>, ValidatorClientError> {
Ok(self.nym_api.get_gateways().await?)
}
#[deprecated]
pub async fn get_cached_described_gateways(
&self,
) -> Result<Vec<LegacyDescribedGateway>, ValidatorClientError> {
Ok(self.nym_api.get_gateways_described().await?)
}
pub async fn get_all_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
pub async fn get_all_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
#[deprecated]
pub async fn get_gateway_core_status_count(
&self,
identity: IdentityKeyRef<'_>,
@@ -415,6 +583,7 @@ impl NymApiClient {
.await?)
}
#[deprecated]
pub async fn get_mixnode_core_status_count(
&self,
mix_id: NodeId,
@@ -426,6 +595,7 @@ impl NymApiClient {
.await?)
}
#[deprecated]
pub async fn get_mixnode_status(
&self,
mix_id: NodeId,
@@ -433,6 +603,7 @@ impl NymApiClient {
Ok(self.nym_api.get_mixnode_status(mix_id).await?)
}
#[deprecated]
pub async fn get_mixnode_reward_estimation(
&self,
mix_id: NodeId,
@@ -440,6 +611,7 @@ impl NymApiClient {
Ok(self.nym_api.get_mixnode_reward_estimation(mix_id).await?)
}
#[deprecated]
pub async fn get_mixnode_stake_saturation(
&self,
mix_id: NodeId,
@@ -471,6 +643,7 @@ impl NymApiClient {
.await?)
}
#[deprecated]
pub async fn spent_credentials_filter(
&self,
) -> Result<SpentCredentialsResponse, ValidatorClientError> {
@@ -164,7 +164,7 @@ async fn test_nym_api_connection(
) -> ConnectionResult {
let result = match timeout(
Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC),
client.get_cached_mixnodes(),
client.health(),
)
.await
{
@@ -11,9 +11,11 @@ use nym_api_requests::ecash::models::{
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse,
AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NymNodeDescription,
};
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
ecash::{
models::{
@@ -38,7 +40,7 @@ use nym_contracts_common::IdentityKey;
pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails};
use time::format_description::BorrowedFormatItem;
use time::Date;
use tracing::instrument;
@@ -53,12 +55,26 @@ pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NymApiClientExt: ApiClient {
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::API_STATUS_ROUTES,
routes::HEALTH,
],
NO_PARAMS,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
@@ -73,6 +89,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
@@ -87,6 +104,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_detailed_unfiltered(
&self,
@@ -103,12 +121,14 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
self.get_json(
@@ -118,6 +138,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
self.get_json(
@@ -127,6 +148,47 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nodes_described(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDescription>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "described"], &params)
.await
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nym_nodes(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], &params)
.await
}
#[deprecated]
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(
&self,
@@ -151,6 +213,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_basic_gateways(
&self,
@@ -178,7 +241,7 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_all_basic_entry_assigned_nodes(
async fn get_basic_entry_assigned_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
@@ -259,6 +322,82 @@ pub trait NymApiClientExt: ApiClient {
.await
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_mixing_capable_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[
routes::API_VERSION,
"unstable",
"nym-nodes",
"skimmed",
"mixnodes",
"all",
],
&params,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[routes::API_VERSION, "unstable", "nym-nodes", "skimmed"],
&params,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
@@ -268,6 +407,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
@@ -283,6 +423,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
@@ -292,6 +433,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_report(
&self,
@@ -310,6 +452,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateway_report(
&self,
@@ -328,6 +471,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_history(
&self,
@@ -346,6 +490,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateway_history(
&self,
@@ -364,6 +509,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_rewarded_mixnodes_detailed(
&self,
@@ -381,6 +527,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateway_core_status_count(
&self,
@@ -413,6 +560,7 @@ pub trait NymApiClientExt: ApiClient {
}
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_core_status_count(
&self,
@@ -446,6 +594,7 @@ pub trait NymApiClientExt: ApiClient {
}
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_status(
&self,
@@ -464,6 +613,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_reward_estimation(
&self,
@@ -482,6 +632,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn compute_mixnode_reward_estimation(
&self,
@@ -502,6 +653,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_stake_saturation(
&self,
@@ -520,6 +672,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_inclusion_probability(
&self,
@@ -555,6 +708,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
self.get_json(
&[
@@ -569,6 +723,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
self.get_json(
@@ -578,6 +733,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
self.get_json(
@@ -638,6 +794,7 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn double_spending_filter_v1(&self) -> Result<SpentCredentialsResponse, NymAPIError> {
self.get_json(
@@ -36,6 +36,8 @@ pub mod ecash {
}
pub const STATUS_ROUTES: &str = "status";
pub const API_STATUS_ROUTES: &str = "api-status";
pub const HEALTH: &str = "health";
pub const MIXNODE: &str = "mixnode";
pub const GATEWAY: &str = "gateway";
pub const NYM_NODES: &str = "nym-nodes";
@@ -2,10 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::context::QueryClientWithNyxd;
use crate::utils::{pretty_cosmwasm_coin, show_error};
use crate::utils::show_error;
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::client::NymApiClientExt;
#[derive(Debug, Parser)]
pub struct Args {
@@ -15,12 +14,11 @@ pub struct Args {
}
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
match client.nym_api.get_gateways().await {
match client.get_all_cached_described_nodes().await {
Ok(res) => match args.identity_key {
Some(identity_key) => {
let node = res.iter().find(|node| {
node.gateway
.identity_key
node.ed25519_identity_key()
.to_string()
.eq_ignore_ascii_case(&identity_key)
});
@@ -32,14 +30,16 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) {
None => {
let mut table = Table::new();
table.set_header(vec!["Identity Key", "Owner", "Host", "Bond", "Version"]);
for node in res {
table.set_header(vec!["Node Id", "Identity Key", "Version", "Is Legacy"]);
for node in res
.into_iter()
.filter(|node| node.description.declared_role.entry)
{
table.add_row(vec![
node.gateway.identity_key.to_string(),
node.owner.to_string(),
node.gateway.host.to_string(),
pretty_cosmwasm_coin(&node.pledge_amount),
node.gateway.version.clone(),
node.node_id.to_string(),
node.ed25519_identity_key().to_base58_string(),
node.description.build_information.build_version,
(!node.contract_node_type.is_nym_node()).to_string(),
]);
}
@@ -2,10 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::context::QueryClientWithNyxd;
use crate::utils::{pretty_decimal_with_denom, show_error};
use crate::utils::show_error;
use clap::Parser;
use comfy_table::Table;
use nym_validator_client::client::NymApiClientExt;
#[derive(Debug, Parser)]
pub struct Args {
@@ -15,13 +14,11 @@ pub struct Args {
}
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
match client.nym_api.get_mixnodes().await {
match client.get_all_cached_described_nodes().await {
Ok(res) => match args.identity_key {
Some(identity_key) => {
let node = res.iter().find(|node| {
node.bond_information
.mix_node
.identity_key
node.ed25519_identity_key()
.to_string()
.eq_ignore_ascii_case(&identity_key)
});
@@ -33,25 +30,16 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) {
None => {
let mut table = Table::new();
table.set_header(vec![
"Mix id",
"Identity Key",
"Owner",
"Host",
"Bond",
"Total Delegations",
"Version",
]);
for node in res {
let denom = &node.bond_information.original_pledge().denom;
table.set_header(vec!["Node Id", "Identity Key", "Version", "Is Legacy"]);
for node in res
.into_iter()
.filter(|node| node.description.declared_role.mixnode)
{
table.add_row(vec![
node.mix_id().to_string(),
node.bond_information.mix_node.identity_key.clone(),
node.bond_information.owner.clone().into_string(),
node.bond_information.mix_node.host.clone(),
pretty_decimal_with_denom(node.rewarding_details.operator, denom),
pretty_decimal_with_denom(node.rewarding_details.delegates, denom),
node.bond_information.mix_node.version,
node.node_id.to_string(),
node.ed25519_identity_key().to_base58_string(),
node.description.build_information.build_version,
(!node.contract_node_type.is_nym_node()).to_string(),
]);
}
@@ -17,6 +17,7 @@ use crate::{
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
/// Full details associated with given mixnode.
@@ -647,14 +648,39 @@ impl From<LegacyMixLayer> for u8 {
export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts"
)
)]
#[cw_serde]
#[derive(Default, Copy)]
// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want
// with the addition of .cost_params_change field
#[derive(
::cosmwasm_schema::serde::Serialize,
::cosmwasm_schema::serde::Deserialize,
::std::clone::Clone,
::std::fmt::Debug,
::std::cmp::PartialEq,
::cosmwasm_schema::schemars::JsonSchema,
Default,
Copy,
)]
#[schemars(crate = "::cosmwasm_schema::schemars")]
pub struct PendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
#[serde(default)]
pub cost_params_change: Option<IntervalEventId>,
}
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct LegacyPendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
}
impl From<PendingMixNodeChanges> for LegacyPendingMixNodeChanges {
fn from(value: PendingMixNodeChanges) -> Self {
LegacyPendingMixNodeChanges {
pledge_change: value.pledge_change,
}
}
}
impl PendingMixNodeChanges {
pub fn new_empty() -> PendingMixNodeChanges {
PendingMixNodeChanges {
+2 -2
View File
@@ -6,7 +6,7 @@ use std::sync::Arc;
use time::{Date, OffsetDateTime};
use tracing::*;
use nym_credentials::ecash::utils::{ecash_today, EcashTime};
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
use nym_gateway_requests::models::CredentialSpendingRequest;
use nym_gateway_storage::Storage;
@@ -131,7 +131,7 @@ impl<S: Storage + Clone + 'static> CredentialVerifier<S> {
let bandwidth = Bandwidth::ticket_amount(credential_type.into());
self.bandwidth_storage_manager
.increase_bandwidth(bandwidth, spend_date)
.increase_bandwidth(bandwidth, cred_exp_date())
.await?;
Ok(self
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -112,12 +112,18 @@ impl PemStorableKeyPair for KeyPair {
}
}
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
pub struct PublicKey(x25519_dalek::PublicKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base58_string())
Display::fmt(&self.to_base58_string(), f)
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.to_base58_string(), f)
}
}
@@ -31,8 +31,16 @@ pub mod option_bs58_x25519_pubkey {
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<PublicKey>, D::Error> {
let s = Option::<String>::deserialize(deserializer)?;
s.map(|s| PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
.transpose()
match Option::<String>::deserialize(deserializer)? {
None => Ok(None),
Some(s) => {
if s.is_empty() {
Ok(None)
} else {
Some(PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
.transpose()
}
}
}
}
}
+9 -3
View File
@@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -119,12 +119,18 @@ impl PemStorableKeyPair for KeyPair {
}
/// ed25519 EdDSA Public Key
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct PublicKey(ed25519_dalek::VerifyingKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base58_string())
Display::fmt(&self.to_base58_string(), f)
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.to_base58_string(), f)
}
}
-34
View File
@@ -1,34 +0,0 @@
[package]
name = "nym-gateway-stats-storage"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
"time",
] }
time = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
nym-sphinx = { path = "../nymsphinx" }
nym-credentials-interface = { path = "../credentials-interface" }
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
-28
View File
@@ -1,28 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let database_path = format!("{}/gateway-stats-example.sqlite", out_dir);
let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path))
.await
.expect("Failed to create SQLx database connection");
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
#[cfg(target_family = "windows")]
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
@@ -1,26 +0,0 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE sessions_active
(
client_address TEXT NOT NULL PRIMARY KEY UNIQUE,
start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
typ TEXT NOT NULL
);
CREATE TABLE sessions_finished
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
day DATE NOT NULL,
duration_ms INTEGER NOT NULL,
typ TEXT NOT NULL
);
CREATE TABLE sessions_unique_users
(
day DATE NOT NULL,
client_address TEXT NOT NULL,
PRIMARY KEY (day, client_address)
);
-13
View File
@@ -1,13 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StatsStorageError {
#[error("Database experienced an internal error: {0}")]
InternalDatabaseError(#[from] sqlx::Error),
#[error("Failed to perform database migration: {0}")]
MigrationError(#[from] sqlx::migrate::MigrateError),
}
-195
View File
@@ -1,195 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use error::StatsStorageError;
use models::{ActiveSession, FinishedSession, SessionType, StoredFinishedSession};
use nym_sphinx::DestinationAddressBytes;
use sessions::SessionManager;
use sqlx::ConnectOptions;
use std::path::Path;
use time::Date;
use tracing::{debug, error};
pub mod error;
pub mod models;
mod sessions;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct PersistentStatsStorage {
session_manager: SessionManager,
}
impl PersistentStatsStorage {
/// Initialises `PersistentStatsStorage` using the provided path.
///
/// # Arguments
///
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
// TODO: we can inject here more stuff based on our gateway global config
// struct. Maybe different pool size or timeout intervals?
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(true)
.disable_statement_logging();
// TODO: do we want auto_vacuum ?
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(db) => db,
Err(err) => {
error!("Failed to connect to SQLx database: {err}");
return Err(err.into());
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
return Err(err.into());
}
// the cloning here are cheap as connection pool is stored behind an Arc
Ok(PersistentStatsStorage {
session_manager: sessions::SessionManager::new(connection_pool),
})
}
//Sessions fn
pub async fn insert_finished_session(
&self,
date: Date,
session: FinishedSession,
) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.insert_finished_session(
date,
session.duration.whole_milliseconds() as i64,
session.typ.to_string().into(),
)
.await?)
}
pub async fn get_finished_sessions(
&self,
date: Date,
) -> Result<Vec<StoredFinishedSession>, StatsStorageError> {
Ok(self.session_manager.get_finished_sessions(date).await?)
}
pub async fn delete_finished_sessions(
&self,
before_date: Date,
) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.delete_finished_sessions(before_date)
.await?)
}
pub async fn insert_unique_user(
&self,
date: Date,
client_address_bs58: String,
) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.insert_unique_user(date, client_address_bs58)
.await?)
}
pub async fn get_unique_users_count(&self, date: Date) -> Result<i32, StatsStorageError> {
Ok(self.session_manager.get_unique_users_count(date).await?)
}
pub async fn delete_unique_users(&self, before_date: Date) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.delete_unique_users(before_date)
.await?)
}
pub async fn insert_active_session(
&self,
client_address: DestinationAddressBytes,
session: ActiveSession,
) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.insert_active_session(
client_address.as_base58_string(),
session.start,
session.typ.to_string().into(),
)
.await?)
}
pub async fn update_active_session_type(
&self,
client_address: DestinationAddressBytes,
session_type: SessionType,
) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.update_active_session_type(
client_address.as_base58_string(),
session_type.to_string().into(),
)
.await?)
}
pub async fn get_active_session(
&self,
client_address: DestinationAddressBytes,
) -> Result<Option<ActiveSession>, StatsStorageError> {
Ok(self
.session_manager
.get_active_session(client_address.as_base58_string())
.await?
.map(Into::into))
}
pub async fn get_all_active_sessions(&self) -> Result<Vec<ActiveSession>, StatsStorageError> {
Ok(self
.session_manager
.get_all_active_sessions()
.await?
.into_iter()
.map(Into::into)
.collect())
}
pub async fn get_started_sessions_count(
&self,
start_date: Date,
) -> Result<i32, StatsStorageError> {
Ok(self
.session_manager
.get_started_sessions_count(start_date)
.await?)
}
pub async fn get_active_users(&self) -> Result<Vec<String>, StatsStorageError> {
Ok(self.session_manager.get_active_users().await?)
}
pub async fn delete_active_session(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), StatsStorageError> {
Ok(self
.session_manager
.delete_active_session(client_address.as_base58_string())
.await?)
}
pub async fn cleanup_active_sessions(&self) -> Result<(), StatsStorageError> {
Ok(self.session_manager.cleanup_active_sessions().await?)
}
}
-109
View File
@@ -1,109 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_credentials_interface::TicketType;
use sqlx::prelude::FromRow;
use time::{Duration, OffsetDateTime};
#[derive(FromRow)]
pub struct StoredFinishedSession {
duration_ms: i64,
typ: String,
}
impl StoredFinishedSession {
pub fn serialize(&self) -> (u64, String) {
(
self.duration_ms as u64, //we are sure that it fits in a u64, see `fn end_at`
self.typ.clone(),
)
}
}
pub struct FinishedSession {
pub duration: Duration,
pub typ: SessionType,
}
#[derive(PartialEq)]
pub enum SessionType {
Vpn,
Mixnet,
Unknown,
}
impl SessionType {
pub fn to_string(&self) -> &str {
match self {
Self::Vpn => "vpn",
Self::Mixnet => "mixnet",
Self::Unknown => "unknown",
}
}
pub fn from_string(s: &str) -> Self {
match s {
"vpn" => Self::Vpn,
"mixnet" => Self::Mixnet,
_ => Self::Unknown,
}
}
}
impl From<TicketType> for SessionType {
fn from(value: TicketType) -> Self {
match value {
TicketType::V1MixnetEntry => Self::Mixnet,
TicketType::V1MixnetExit => Self::Mixnet,
TicketType::V1WireguardEntry => Self::Vpn,
TicketType::V1WireguardExit => Self::Vpn,
}
}
}
#[derive(FromRow)]
pub(crate) struct StoredActiveSession {
start_time: OffsetDateTime,
typ: String,
}
pub struct ActiveSession {
pub start: OffsetDateTime,
pub typ: SessionType,
}
impl ActiveSession {
pub fn new(start_time: OffsetDateTime) -> Self {
ActiveSession {
start: start_time,
typ: SessionType::Unknown,
}
}
pub fn set_type(&mut self, ticket_type: TicketType) {
self.typ = ticket_type.into();
}
pub fn end_at(self, stop_time: OffsetDateTime) -> Option<FinishedSession> {
let session_duration = stop_time - self.start;
//ensure duration is positive to fit in a u64
//u64::max milliseconds is 500k millenia so no overflow issue
if session_duration > Duration::ZERO {
Some(FinishedSession {
duration: session_duration,
typ: self.typ,
})
} else {
None
}
}
}
impl From<StoredActiveSession> for ActiveSession {
fn from(value: StoredActiveSession) -> Self {
ActiveSession {
start: value.start_time,
typ: SessionType::from_string(&value.typ),
}
}
}
@@ -1,177 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use time::{Date, OffsetDateTime};
use crate::models::{StoredActiveSession, StoredFinishedSession};
pub(crate) type Result<T> = std::result::Result<T, sqlx::Error>;
#[derive(Clone)]
pub(crate) struct SessionManager {
connection_pool: sqlx::SqlitePool,
}
impl SessionManager {
/// Creates new instance of the `SessionsManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self {
SessionManager { connection_pool }
}
pub(crate) async fn insert_finished_session(
&self,
date: Date,
duration_ms: i64,
typ: String,
) -> Result<()> {
sqlx::query!(
"INSERT INTO sessions_finished (day, duration_ms, typ) VALUES (?, ?, ?)",
date,
duration_ms,
typ
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_finished_sessions(
&self,
date: Date,
) -> Result<Vec<StoredFinishedSession>> {
sqlx::query_as("SELECT duration_ms, typ FROM sessions_finished WHERE day = ?")
.bind(date)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn delete_finished_sessions(&self, before_date: Date) -> Result<()> {
sqlx::query!("DELETE FROM sessions_finished WHERE day <= ? ", before_date)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn insert_unique_user(
&self,
date: Date,
client_address_b58: String,
) -> Result<()> {
sqlx::query!(
"INSERT OR IGNORE INTO sessions_unique_users (day, client_address) VALUES (?, ?)",
date,
client_address_b58,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_unique_users_count(&self, date: Date) -> Result<i32> {
Ok(sqlx::query!(
"SELECT COUNT(*) as count FROM sessions_unique_users WHERE day = ?",
date
)
.fetch_one(&self.connection_pool)
.await?
.count)
}
pub(crate) async fn delete_unique_users(&self, before_date: Date) -> Result<()> {
sqlx::query!(
"DELETE FROM sessions_unique_users WHERE day <= ? ",
before_date
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn insert_active_session(
&self,
client_address_b58: String,
start_time: OffsetDateTime,
typ: String,
) -> Result<()> {
sqlx::query!(
"INSERT INTO sessions_active (client_address, start_time, typ) VALUES (?, ?, ?)",
client_address_b58,
start_time,
typ
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn update_active_session_type(
&self,
client_address_b58: String,
typ: String,
) -> Result<()> {
sqlx::query!(
"UPDATE sessions_active SET typ = ? WHERE client_address = ?",
typ,
client_address_b58,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_active_session(
&self,
client_address_b58: String,
) -> Result<Option<StoredActiveSession>> {
sqlx::query_as("SELECT start_time, typ FROM sessions_active WHERE client_address = ?")
.bind(client_address_b58)
.fetch_optional(&self.connection_pool)
.await
}
pub(crate) async fn get_all_active_sessions(&self) -> Result<Vec<StoredActiveSession>> {
sqlx::query_as("SELECT start_time, typ FROM sessions_active")
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result<i32> {
Ok(sqlx::query!(
"SELECT COUNT(*) as count FROM sessions_active WHERE date(start_time) = ?",
start_date
)
.fetch_one(&self.connection_pool)
.await?
.count)
}
pub(crate) async fn get_active_users(&self) -> Result<Vec<String>> {
Ok(sqlx::query!("SELECT client_address from sessions_active")
.fetch_all(&self.connection_pool)
.await?
.into_iter()
.map(|record| record.client_address)
.collect())
}
pub(crate) async fn delete_active_session(&self, client_address_b58: String) -> Result<()> {
sqlx::query!(
"DELETE FROM sessions_active WHERE client_address = ?",
client_address_b58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn cleanup_active_sessions(&self) -> Result<()> {
sqlx::query!("DELETE FROM sessions_active")
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+3
View File
@@ -35,6 +35,9 @@ pub enum HttpClientError<E: Display = String> {
source: reqwest::Error,
},
#[error("failed to deserialise received response: {source}")]
ResponseDeserialisationFailure { source: serde_json::Error },
#[error("provided url is malformed: {source}")]
MalformedUrl {
#[from]
+1
View File
@@ -11,6 +11,7 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum-client-ip.workspace = true
axum.workspace = true
bytes = { workspace = true }
colored.workspace = true
+3 -3
View File
@@ -1,18 +1,18 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::extract::{ConnectInfo, Request};
use axum::extract::Request;
use axum::http::header::{HOST, USER_AGENT};
use axum::http::HeaderValue;
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum_client_ip::InsecureClientIp;
use colored::Colorize;
use std::net::SocketAddr;
use std::time::Instant;
use tracing::info;
pub async fn logger(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
InsecureClientIp(addr): InsecureClientIp,
request: Request,
next: Next,
) -> impl IntoResponse {
+6 -15
View File
@@ -14,7 +14,6 @@ use nym_task::TaskClient;
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
@@ -313,7 +312,7 @@ impl VerlocMeasurer {
info!("Starting verloc measurements");
// TODO: should we also measure gateways?
let all_mixes = match self.validator_client.get_cached_mixnodes().await {
let all_mixes = match self.validator_client.get_all_described_nodes().await {
Ok(nodes) => nodes,
Err(err) => {
error!(
@@ -332,22 +331,14 @@ impl VerlocMeasurer {
// we only care about address and identity
let tested_nodes = all_mixes
.into_iter()
.filter(|n| n.description.declared_role.mixnode)
.filter_map(|node| {
let mix_node = node.bond_information.mix_node;
// check if the node has sufficient version to be able to understand the packets
let node_version = parse_version(&mix_node.version).ok()?;
if node_version < self.config.minimum_compatible_node_version {
return None;
}
// try to parse the identity and host
let node_identity =
identity::PublicKey::from_base58_string(mix_node.identity_key).ok()?;
let node_identity = node.ed25519_identity_key();
let verloc_host = (&*mix_node.host, mix_node.verloc_port)
.to_socket_addrs()
.ok()?
.next()?;
let ip = node.description.host_information.ip_address.first()?;
let verloc_port = node.description.verloc_port();
let verloc_host = SocketAddr::new(*ip, verloc_port);
// TODO: possible problem in the future, this does name resolution and theoretically
// if a lot of nodes maliciously mis-configured themselves, it might take a while to resolve them all
+1 -2
View File
@@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TestrunAssignment {
/// has nothing to do with GW identity key. This is PK from `gateways` table
pub testrun_id: i64,
pub gateway_pk_id: i64,
pub gateway_identity_key: String,
}
@@ -21,7 +21,7 @@ nym-sphinx-types = { path = "../types" }
nym-topology = { path = "../../topology" }
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
version = "0.2.95"
version = "0.2.93"
[dev-dependencies]
rand_chacha = { workspace = true }
+9 -34
View File
@@ -42,32 +42,8 @@ impl PendingSync {
}
}
#[derive(Debug, Clone)]
pub struct BlockProcessorConfig {
pub pruning_options: PruningOptions,
pub store_precommits: bool,
}
impl Default for BlockProcessorConfig {
fn default() -> Self {
Self {
pruning_options: PruningOptions::nothing(),
store_precommits: true,
}
}
}
impl BlockProcessorConfig {
pub fn new(pruning_options: PruningOptions, store_precommits: bool) -> Self {
Self {
pruning_options,
store_precommits,
}
}
}
pub struct BlockProcessor {
config: BlockProcessorConfig,
pruning_options: PruningOptions,
cancel: CancellationToken,
synced: Arc<Notify>,
last_processed_height: u32,
@@ -89,10 +65,9 @@ pub struct BlockProcessor {
msg_modules: Vec<Box<dyn MsgModule + Send>>,
}
#[allow(clippy::too_many_arguments)]
impl BlockProcessor {
pub async fn new(
config: BlockProcessorConfig,
pruning_options: PruningOptions,
cancel: CancellationToken,
synced: Arc<Notify>,
incoming: UnboundedReceiver<BlockToProcess>,
@@ -107,7 +82,7 @@ impl BlockProcessor {
let last_pruned_height = last_pruned.try_into().unwrap_or_default();
Ok(BlockProcessor {
config,
pruning_options,
cancel,
synced,
last_processed_height,
@@ -126,7 +101,7 @@ impl BlockProcessor {
}
pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self {
self.config.pruning_options = pruning_options;
self.pruning_options = pruning_options;
self
}
@@ -153,7 +128,7 @@ impl BlockProcessor {
// we won't end up with a corrupted storage.
let mut tx = self.storage.begin_processing_tx().await?;
persist_block(&full_info, &mut tx, self.config.store_precommits).await?;
persist_block(&full_info, &mut tx).await?;
// let the modules do whatever they want
// the ones wanting the full block:
@@ -266,7 +241,7 @@ impl BlockProcessor {
#[instrument(skip(self))]
async fn prune_storage(&mut self) -> Result<(), ScraperError> {
let keep_recent = self.config.pruning_options.strategy_keep_recent();
let keep_recent = self.pruning_options.strategy_keep_recent();
let last_to_keep = self.last_processed_height - keep_recent;
info!(
@@ -307,12 +282,12 @@ impl BlockProcessor {
async fn maybe_prune_storage(&mut self) -> Result<(), ScraperError> {
debug!("checking for storage pruning");
if self.config.pruning_options.strategy.is_nothing() {
if self.pruning_options.strategy.is_nothing() {
trace!("the current pruning strategy is 'nothing'");
return Ok(());
}
let interval = self.config.pruning_options.strategy_interval();
let interval = self.pruning_options.strategy_interval();
if self.last_pruned_height + interval <= self.last_processed_height {
self.prune_storage().await?;
}
@@ -396,7 +371,7 @@ impl BlockProcessor {
if latest_block > self.last_processed_height && self.last_processed_height != 0 {
// in case we were offline for a while,
// make sure we don't request blocks we'd have to prune anyway
let keep_recent = self.config.pruning_options.strategy_keep_recent();
let keep_recent = self.pruning_options.strategy_keep_recent();
let last_to_keep = latest_block - keep_recent;
self.last_processed_height = max(self.last_processed_height, last_to_keep);
+3 -14
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::BlockToProcess;
use crate::block_processor::{BlockProcessor, BlockProcessorConfig};
use crate::block_processor::BlockProcessor;
use crate::block_requester::{BlockRequest, BlockRequester};
use crate::error::ScraperError;
use crate::modules::{BlockModule, MsgModule, TxModule};
@@ -34,8 +34,6 @@ pub struct Config {
pub database_path: PathBuf,
pub pruning_options: PruningOptions,
pub store_precommits: bool,
}
pub struct NyxdScraperBuilder {
@@ -62,14 +60,8 @@ impl NyxdScraperBuilder {
req_rx,
processing_tx.clone(),
);
let block_processor_config = BlockProcessorConfig::new(
scraper.config.pruning_options,
scraper.config.store_precommits,
);
let mut block_processor = BlockProcessor::new(
block_processor_config,
scraper.config.pruning_options,
scraper.cancel_token.clone(),
scraper.startup_sync.clone(),
processing_rx,
@@ -283,11 +275,8 @@ impl NyxdScraper {
req_tx: Sender<BlockRequest>,
processing_rx: UnboundedReceiver<BlockToProcess>,
) -> Result<BlockProcessor, ScraperError> {
let block_processor_config =
BlockProcessorConfig::new(self.config.pruning_options, self.config.store_precommits);
BlockProcessor::new(
block_processor_config,
self.config.pruning_options,
self.cancel_token.clone(),
self.startup_sync.clone(),
processing_rx,
+5 -7
View File
@@ -212,7 +212,6 @@ impl ScraperStorage {
pub async fn persist_block(
block: &FullBlockInformation,
tx: &mut StorageTransaction,
store_precommits: bool,
) -> Result<(), ScraperError> {
let total_gas = crate::helpers::tx_gas_sum(&block.transactions);
@@ -225,12 +224,11 @@ pub async fn persist_block(
// persist block data
persist_block_data(&block.block, total_gas, tx).await?;
if store_precommits {
if let Some(commit) = &block.block.last_commit {
persist_commits(commit, &block.validators, tx).await?;
} else {
warn!("no commits for block {}", block.block.header.height)
}
// persist commits
if let Some(commit) = &block.block.last_commit {
persist_commits(commit, &block.validators, tx).await?;
} else {
warn!("no commits for block {}", block.block.header.height)
}
// persist txs
+4
View File
@@ -286,6 +286,10 @@ impl NymTopology {
self.get_gateway(gateway_identity).is_some()
}
pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) {
self.gateways.push(gateway)
}
pub fn set_gateways(&mut self, gateways: Vec<gateway::LegacyNode>) {
self.gateways = gateways
}
+1 -1
View File
@@ -116,7 +116,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode {
});
}
let layer = match value.epoch_role {
let layer = match value.role {
NodeRole::Mixnode { layer } => layer
.try_into()
.map_err(|_| MixnodeConversionError::InvalidLayer)?,
+1 -1
View File
@@ -68,7 +68,7 @@ pub async fn current_network_topology_async(
let api_client = NymApiClient::new(url);
let mixnodes = api_client
.get_basic_active_mixing_assigned_nodes(None)
.get_all_basic_active_mixing_assigned_nodes(None)
.await?;
let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?;
+14 -1
View File
@@ -99,12 +99,25 @@ pub async fn start_wireguard<St: nym_gateway_storage::Storage + Clone + 'static>
let peers = all_peers
.into_iter()
.map(Peer::try_from)
.collect::<Result<Vec<_>, _>>()?;
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|mut peer| {
// since WGApi doesn't set those values on init, let's set them to 0
peer.rx_bytes = 0;
peer.tx_bytes = 0;
peer
})
.collect::<Vec<_>>();
for peer in peers.iter() {
let bandwidth_manager =
PeerController::generate_bandwidth_manager(storage.clone(), &peer.public_key)
.await?
.map(|bw_m| Arc::new(RwLock::new(bw_m)));
// Update storage with *x_bytes set to 0, as in kernel peers we can't set those values
// so we need to restart counting. Hopefully the bandwidth was counted in available_bandwidth
storage
.insert_wireguard_peer(peer, bandwidth_manager.is_some())
.await?;
peer_bandwidth_managers.insert(peer.public_key.clone(), bandwidth_manager);
}
wg_api.create_interface()?;
+5 -2
View File
@@ -158,10 +158,13 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
.ok_or(Error::MissingClientBandwidthEntry)?
.client_id
{
storage.create_bandwidth_entry(client_id).await?;
let bandwidth = storage
.get_available_bandwidth(client_id)
.await?
.ok_or(Error::MissingClientBandwidthEntry)?;
Ok(Some(BandwidthStorageManager::new(
storage,
ClientBandwidth::new(Default::default()),
ClientBandwidth::new(bandwidth.into()),
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
+13 -9
View File
@@ -75,8 +75,8 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
async fn active_peer(
&mut self,
storage_peer: WireguardPeer,
kernel_peer: Peer,
storage_peer: &WireguardPeer,
kernel_peer: &Peer,
) -> Result<bool, Error> {
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
@@ -84,12 +84,13 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
.ok_or(Error::InconsistentConsumedBytes)?
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
if bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
if spent_bandwidth > 0
&& bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
{
let success = self.remove_peer().await?;
return Ok(!success);
@@ -135,9 +136,12 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key);
return Ok(());
};
if !self.active_peer(storage_peer, kernel_peer).await? {
if !self.active_peer(&storage_peer, &kernel_peer).await? {
log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key);
return Ok(());
} else {
// Update storage values
self.storage.insert_wireguard_peer(&kernel_peer, self.bandwidth_storage_manager.is_some()).await?;
}
}
@@ -3689,8 +3689,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -5239,8 +5238,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -5575,8 +5573,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -7595,8 +7592,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -315,8 +315,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -323,8 +323,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -317,8 +317,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -319,8 +319,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -4,6 +4,7 @@
use super::helpers::must_get_gateway_bond_by_owner;
use super::storage;
use crate::constants::default_node_costs;
use crate::interval::storage as interval_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::nodes::helpers::save_new_nymnode_with_id;
use crate::nodes::transactions::add_nym_node_inner;
@@ -115,6 +116,10 @@ pub fn try_migrate_to_nymnode(
comment: "legacy gateway did not have a pre-assigned node id".to_string(),
})?;
let current_epoch =
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
let previous_epoch = current_epoch.saturating_sub(1);
// create nym-node entry
// for gateways it's quite straightforward as there are no delegations or rewards to worry about
save_new_nymnode_with_id(
@@ -125,6 +130,7 @@ pub fn try_migrate_to_nymnode(
cost_params,
info.sender.clone(),
gateway_bond.pledge_amount,
previous_epoch,
)?;
storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone());
+6 -3
View File
@@ -22,6 +22,8 @@ pub(crate) fn save_new_nymnode(
pledge: Coin,
) -> Result<NodeId, MixnetContractError> {
let node_id = next_nymnode_id_counter(storage)?;
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
save_new_nymnode_with_id(
storage,
node_id,
@@ -30,11 +32,13 @@ pub(crate) fn save_new_nymnode(
cost_params,
owner,
pledge,
current_epoch,
)?;
Ok(node_id)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn save_new_nymnode_with_id(
storage: &mut dyn Storage,
node_id: NodeId,
@@ -43,10 +47,9 @@ pub(crate) fn save_new_nymnode_with_id(
cost_params: NodeCostParams,
owner: Addr,
pledge: Coin,
last_rewarding_epoch: u32,
) -> Result<(), MixnetContractError> {
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?;
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, last_rewarding_epoch)?;
let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height);
// save node bond data
+32 -2
View File
@@ -52,8 +52,8 @@ pub(crate) fn save_assignment(
// update metadata
let mut metadata = ROLES_METADATA.load(storage, inactive)?;
let last = assignment.nodes.last().copied().unwrap_or_default();
metadata.set_highest_id(last, assignment.role);
let highest_id = assignment.nodes.iter().max().copied().unwrap_or_default();
metadata.set_highest_id(highest_id, assignment.role);
metadata.set_role_count(assignment.role, assignment.nodes.len() as u32);
if assignment.is_final_assignment() {
metadata.fully_assigned = true
@@ -140,6 +140,7 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet
mod tests {
use super::*;
use crate::support::tests::test_helpers;
use crate::support::tests::test_helpers::TestSetup;
#[test]
fn next_id() {
@@ -149,4 +150,33 @@ mod tests {
assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap());
}
}
#[test]
fn assigning_role_uses_highest_id_even_if_not_sorted() {
let mut test = TestSetup::new();
let deps = test.deps_mut();
let sorted = RoleAssignment {
role: Role::EntryGateway,
nodes: vec![1, 2, 3],
};
let unsorted = RoleAssignment {
role: Role::Layer1,
nodes: vec![8, 5, 4],
};
save_assignment(deps.storage, sorted).unwrap();
save_assignment(deps.storage, unsorted).unwrap();
let storage = deps.as_ref().storage;
let active_bucket = ACTIVE_ROLES_BUCKET.load(storage).unwrap();
let inactive = active_bucket.other() as u8;
let metadata = ROLES_METADATA.load(storage, inactive).unwrap();
assert_eq!(metadata.entry_gateway_metadata.highest_id, 3);
assert_eq!(metadata.layer1_metadata.highest_id, 8);
assert_eq!(metadata.highest_rewarded_id(), 8)
}
}
+9
View File
@@ -13,6 +13,8 @@ pub use nym_explorer_api_requests::{
// Paths
const API_VERSION: &str = "v1";
const TMP: &str = "tmp";
const UNSTABLE: &str = "unstable";
const MIXNODES: &str = "mix-nodes";
const GATEWAYS: &str = "gateways";
@@ -96,6 +98,13 @@ impl ExplorerClient {
pub async fn get_gateways(&self) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, GATEWAYS]).await
}
pub async fn unstable_get_gateways(
&self,
) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS])
.await
}
}
fn combine_url(mut base_url: Url, paths: &[&str]) -> Result<Url, ExplorerApiError> {
@@ -4,6 +4,7 @@
use crate::state::ExplorerApiStateContext;
use log::{info, warn};
use nym_explorer_api_requests::Location;
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_task::TaskClient;
pub(crate) struct GeoLocateTask {
@@ -25,6 +26,7 @@ impl GeoLocateTask {
_ = interval_timer.tick() => {
self.locate_mix_nodes().await;
self.locate_gateways().await;
self.locate_nym_nodes().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
@@ -109,6 +111,83 @@ impl GeoLocateTask {
trace!("All mix nodes located");
}
async fn locate_nym_nodes(&mut self) {
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await;
let geo_ip = self.state.inner.geo_ip.0.clone();
for (i, cache_item) in nym_nodes.values().enumerate() {
if self
.state
.inner
.nymnodes
.is_location_valid(cache_item.node_id())
.await
{
// when the cached location is valid, don't locate and continue to next mix node
continue;
}
let bonded_host = &cache_item.bond_information.node.host;
match geo_ip.query(
bonded_host,
Some(
cache_item
.bond_information
.node
.custom_http_port
.unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT),
),
) {
Ok(opt) => match opt {
Some(location) => {
let location: Location = location.into();
trace!(
"{} mix nodes already located. host {} is located in {:#?}",
i,
bonded_host,
location.three_letter_iso_country_code,
);
if i > 0 && (i % 100) == 0 {
info!("Located {} nym-nodes...", i + 1,);
}
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), Some(location))
.await;
// one node has been located, so return out of the loop
return;
}
None => {
warn!("❌ Location for {bonded_host} not found.");
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), None)
.await;
}
},
Err(_e) => {
// warn!(
// "❌ Oh no! Location for {} failed. Error: {:#?}",
// cache_item.mix_node().host,
// e
// );
}
};
}
trace!("All nym-nodes nodes located");
}
async fn locate_gateways(&mut self) {
let gateways = self.state.inner.gateways.get_gateways().await;
+2
View File
@@ -10,6 +10,7 @@ use crate::gateways::http::gateways_make_default_routes;
use crate::http::swagger::get_docs;
use crate::mix_node::http::mix_node_make_default_routes;
use crate::mix_nodes::http::mix_nodes_make_default_routes;
use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes;
use crate::overview::http::overview_make_default_routes;
use crate::ping::http::ping_make_default_routes;
use crate::service_providers::http::service_providers_make_default_routes;
@@ -58,6 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
"/ping" => ping_make_default_routes(&openapi_settings),
"/validators" => validators_make_default_routes(&openapi_settings),
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
"/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings),
};
building_rocket
+1
View File
@@ -22,6 +22,7 @@ mod http;
mod location;
mod mix_node;
pub(crate) mod mix_nodes;
mod nym_nodes;
mod overview;
mod ping;
pub(crate) mod service_providers;
+3
View File
@@ -8,6 +8,9 @@ use nym_contracts_common::truncate_decimal;
use nym_mixnet_contract_common::NodeId;
use nym_validator_client::client::NymApiClientExt;
// use deprecated method as hopefully this whole API will be sunset soon-enough...
// and we're only getting info for legacy node so the relevant data should still exist
#[allow(deprecated)]
pub(crate) async fn retrieve_mixnode_econ_stats(
client: &ThreadsafeValidatorClient,
mix_id: NodeId,
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
use okapi::openapi3::OpenApi;
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
pub fn unstable_temp_nymnodes_make_default_routes(
settings: &OpenApiSettings,
) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_detailed_gateways().await;
gateways.append(&mut state.inner.nymnodes.pretty_gateways().await);
Json(gateways)
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::NodeId;
use crate::location::LocationCache;
pub(crate) type NymNodeLocationCache = LocationCache<NodeId>;
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
pub(crate) mod http;
pub(crate) mod location;
pub(crate) mod models;
pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200);
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::location::{LocationCache, LocationCacheItem};
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails};
use nym_validator_client::models::NymNodeDescription;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{RwLock, RwLockReadGuard};
pub(crate) struct NymNodesCache {
pub(crate) valid_until: SystemTime,
pub(crate) bonded_nym_nodes: HashMap<NodeId, NymNodeDetails>,
pub(crate) described_nodes: HashMap<NodeId, NymNodeDescription>,
}
impl NymNodesCache {
fn new() -> Self {
NymNodesCache {
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
bonded_nym_nodes: Default::default(),
described_nodes: Default::default(),
}
}
// fn is_valid(&self) -> bool {
// self.valid_until >= SystemTime::now()
// }
}
#[derive(Clone)]
pub(crate) struct ThreadSafeNymNodesCache {
nymnodes: Arc<RwLock<NymNodesCache>>,
locations: Arc<RwLock<LocationCache<NodeId>>>,
}
impl ThreadSafeNymNodesCache {
pub(crate) fn new() -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(NymNodeLocationCache::new())),
}
}
pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(locations)),
}
}
pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool {
self.locations
.read()
.await
.get(&node_id)
.map_or(false, |cache_item| {
cache_item.valid_until > SystemTime::now()
})
}
pub(crate) async fn get_bonded_nymnodes(
&self,
) -> RwLockReadGuard<HashMap<NodeId, NymNodeDetails>> {
let guard = self.nymnodes.read().await;
RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes)
}
pub(crate) async fn get_locations(&self) -> NymNodeLocationCache {
self.locations.read().await.clone()
}
pub(crate) async fn set_location(&self, node_id: NodeId, location: Option<Location>) {
// cache the location for this mix node so that it can be used when the mix node list is refreshed
self.locations
.write()
.await
.insert(node_id, LocationCacheItem::new_from_location(location));
}
pub(crate) async fn update_cache(
&self,
all_bonds: Vec<NymNodeDetails>,
descriptions: Vec<NymNodeDescription>,
) {
let mut guard = self.nymnodes.write().await;
guard.bonded_nym_nodes = all_bonds
.into_iter()
.map(|details| (details.node_id(), details))
.collect();
guard.described_nodes = descriptions
.into_iter()
.map(|description| (description.node_id, description))
.collect();
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
}
pub(crate) async fn pretty_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
let nodes_guard = self.nymnodes.read().await;
let location_guard = self.locations.read().await;
let mut pretty_gateways = vec![];
for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes {
let Some(description) = nodes_guard.described_nodes.get(node_id) else {
continue;
};
if description.description.declared_role.entry {
let location = location_guard.get(node_id);
let bond = &native_nymnode.bond_information;
pretty_gateways.push(PrettyDetailedGatewayBond {
pledge_amount: bond.original_pledge.clone(),
owner: bond.owner.clone(),
block_height: bond.bonding_height,
gateway: Gateway {
host: bond.node.host.clone(),
mix_port: description.description.mix_port(),
clients_port: description.description.mixnet_websockets.ws_port,
location: description
.description
.auxiliary_details
.location
.as_ref()
.map(|l| l.to_string())
.unwrap_or_default(),
sphinx_key: description
.description
.host_information
.keys
.x25519
.to_base58_string(),
identity_key: bond.node.identity_key.clone(),
version: description
.description
.build_information
.build_version
.clone(),
},
proxy: None,
location: location.and_then(|l| l.location.clone()),
})
}
}
pretty_gateways
}
}
+9
View File
@@ -18,6 +18,8 @@ use crate::gateways::models::ThreadsafeGatewayCache;
use crate::mix_node::models::ThreadsafeMixNodeCache;
use crate::mix_nodes::location::MixnodeLocationCache;
use crate::mix_nodes::models::ThreadsafeMixNodesCache;
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::models::ThreadSafeNymNodesCache;
use crate::ping::models::ThreadsafePingCache;
use crate::validators::models::ThreadsafeValidatorCache;
@@ -30,6 +32,7 @@ pub struct ExplorerApiState {
pub(crate) gateways: ThreadsafeGatewayCache,
pub(crate) mixnode: ThreadsafeMixNodeCache,
pub(crate) mixnodes: ThreadsafeMixNodesCache,
pub(crate) nymnodes: ThreadSafeNymNodesCache,
pub(crate) ping: ThreadsafePingCache,
pub(crate) validators: ThreadsafeValidatorCache,
pub(crate) geo_ip: ThreadsafeGeoIp,
@@ -49,6 +52,7 @@ pub struct ExplorerApiStateOnDisk {
pub(crate) country_node_distribution: CountryNodesDistribution,
pub(crate) mixnode_location_cache: MixnodeLocationCache,
pub(crate) gateway_location_cache: GatewayLocationCache,
pub(crate) nymnode_location_cache: NymNodeLocationCache,
pub(crate) as_at: DateTime<Utc>,
}
@@ -85,6 +89,9 @@ impl ExplorerApiStateContext {
mixnodes: ThreadsafeMixNodesCache::new_with_location_cache(
state.mixnode_location_cache,
),
nymnodes: ThreadSafeNymNodesCache::new_with_location_cache(
state.nymnode_location_cache,
),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -101,6 +108,7 @@ impl ExplorerApiStateContext {
gateways: ThreadsafeGatewayCache::new(),
mixnode: ThreadsafeMixNodeCache::new(),
mixnodes: ThreadsafeMixNodesCache::new(),
nymnodes: ThreadSafeNymNodesCache::new(),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -117,6 +125,7 @@ impl ExplorerApiStateContext {
country_node_distribution: self.inner.country_node_distribution.get_all().await,
mixnode_location_cache: self.inner.mixnodes.get_locations().await,
gateway_location_cache: self.inner.gateways.get_locations().await,
nymnode_location_cache: self.inner.nymnodes.get_locations().await,
as_at: Utc::now(),
};
serde_json::to_writer(file, &state).expect("error writing state to disk");
+56 -5
View File
@@ -1,22 +1,24 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::GatewayBond;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails};
use nym_task::TaskClient;
use nym_validator_client::models::MixNodeBondAnnotated;
use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription};
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse};
use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError};
use std::future::Future;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use tokio::time::MissedTickBehavior;
pub(crate) struct ExplorerApiTasks {
state: ExplorerApiStateContext,
shutdown: TaskClient,
}
// allow usage of deprecated methods here as we actually want to be explicitly querying for legacy data
#[allow(deprecated)]
impl ExplorerApiTasks {
pub(crate) fn new(state: ExplorerApiStateContext, shutdown: TaskClient) -> Self {
ExplorerApiTasks { state, shutdown }
@@ -39,6 +41,28 @@ impl ExplorerApiTasks {
bonds
}
async fn retrieve_bonded_nymnodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
info!("About to retrieve all nymnode bonds...");
self.state
.inner
.validator_client
.0
.get_all_cached_bonded_nym_nodes()
.await
}
async fn retrieve_node_descriptions(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
info!("About to retrieve node descriptions...");
self.state
.inner
.validator_client
.0
.get_all_cached_described_nodes()
.await
}
async fn retrieve_all_mixnodes(&self) -> Vec<MixNodeBondAnnotated> {
info!("About to retrieve all mixnode bonds...");
self.retrieve_mixnodes(
@@ -130,10 +154,33 @@ impl ExplorerApiTasks {
}
}
async fn update_nymnodes_cache(&self) {
let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| {
error!("failed to retrieve nym node bonds: {err}");
Vec::new()
});
let all_descriptions = self
.retrieve_node_descriptions()
.await
.unwrap_or_else(|err| {
error!("failed to retrieve node descriptions: {err}");
Vec::new()
});
self.state
.inner
.nymnodes
.update_cache(nym_node_bonds, all_descriptions)
.await
}
pub(crate) fn start(mut self) {
info!("Spawning mix nodes task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE);
interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
@@ -147,6 +194,10 @@ impl ExplorerApiTasks {
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
info!("Updating nymnode cache...");
self.update_nymnodes_cache().await;
info!("Done");
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
+2 -1
View File
@@ -69,7 +69,6 @@ nym-credentials-interface = { path = "../common/credentials-interface" }
nym-credential-verification = { path = "../common/credential-verification" }
nym-crypto = { path = "../common/crypto" }
nym-gateway-storage = { path = "../common/gateway-storage" }
nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" }
nym-gateway-requests = { path = "../common/gateway-requests" }
nym-mixnet-client = { path = "../common/client-libs/mixnet-client" }
nym-mixnode-common = { path = "../common/mixnode-common" }
@@ -77,9 +76,11 @@ nym-network-defaults = { path = "../common/network-defaults" }
nym-network-requester = { path = "../service-providers/network-requester" }
nym-node-http-api = { path = "../nym-node/nym-node-http-api" }
nym-pemstore = { path = "../common/pemstore" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-sphinx = { path = "../common/nymsphinx" }
nym-statistics-common = { path = "../common/statistics" }
nym-task = { path = "../common/task" }
nym-topology = { path = "../common/topology" }
nym-types = { path = "../common/types" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
+1 -8
View File
@@ -12,7 +12,6 @@ pub const DEFAULT_PRIVATE_SPHINX_KEY_FILENAME: &str = "private_sphinx.pem";
pub const DEFAULT_PUBLIC_SPHINX_KEY_FILENAME: &str = "public_sphinx.pem";
pub const DEFAULT_CLIENTS_STORAGE_FILENAME: &str = "db.sqlite";
pub const DEFAULT_STATS_STORAGE_FILENAME: &str = "stats.sqlite";
pub const DEFAULT_NETWORK_REQUESTER_CONFIG_FILENAME: &str = "network_requester_config.toml";
pub const DEFAULT_NETWORK_REQUESTER_DATA_DIR: &str = "network-requester-data";
@@ -40,9 +39,6 @@ pub struct GatewayPaths {
#[serde(alias = "persistent_storage")]
pub clients_storage: PathBuf,
/// Path to sqlite database containing all persistent stats data.
pub stats_storage: PathBuf,
/// Path to the configuration of the embedded network requester.
#[serde(deserialize_with = "de_maybe_stringified")]
pub network_requester_config: Option<PathBuf>,
@@ -58,9 +54,7 @@ impl GatewayPaths {
pub fn new_default<P: AsRef<Path>>(id: P) -> Self {
GatewayPaths {
keys: KeysPaths::new_default(id.as_ref()),
clients_storage: default_data_directory(id.as_ref())
.join(DEFAULT_CLIENTS_STORAGE_FILENAME),
stats_storage: default_data_directory(id).join(DEFAULT_STATS_STORAGE_FILENAME),
clients_storage: default_data_directory(id).join(DEFAULT_CLIENTS_STORAGE_FILENAME),
// node_description: default_config_filepath(id).join(DEFAULT_DESCRIPTION_FILENAME),
network_requester_config: None,
ip_packet_router_config: None,
@@ -76,7 +70,6 @@ impl GatewayPaths {
public_sphinx_key_file: Default::default(),
},
clients_storage: Default::default(),
stats_storage: Default::default(),
network_requester_config: None,
ip_packet_router_config: None,
}
-7
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-3.0-only
use nym_authenticator::error::AuthenticatorError;
use nym_gateway_stats_storage::error::StatsStorageError;
use nym_gateway_storage::error::StorageError;
use nym_ip_packet_router::error::IpPacketRouterError;
use nym_network_requester::error::{ClientCoreError, NetworkRequesterError};
@@ -116,12 +115,6 @@ pub enum GatewayError {
source: StorageError,
},
#[error("stats storage failure: {source}")]
StatsStorageError {
#[from]
source: StatsStorageError,
},
#[error("Path to network requester configuration file hasn't been specified. Perhaps try to run `setup-network-requester`?")]
UnspecifiedNetworkRequesterConfig,
+60 -11
View File
@@ -3,14 +3,18 @@
use crate::config::Config;
use crate::error::GatewayError;
use async_trait::async_trait;
use nym_crypto::asymmetric::encryption;
use nym_gateway_stats_storage::PersistentStatsStorage;
use nym_gateway_storage::PersistentStorage;
use nym_pemstore::traits::PemStorableKeyPair;
use nym_pemstore::KeyPairPath;
use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent};
use nym_topology::{gateway, NymTopology, TopologyProvider};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::debug;
use url::Url;
pub async fn load_network_requester_config<P: AsRef<Path>>(
id: &str,
@@ -75,14 +79,6 @@ pub(crate) async fn initialise_main_storage(
Ok(PersistentStorage::init(path, retrieval_limit).await?)
}
pub(crate) async fn initialise_stats_storage(
config: &Config,
) -> Result<PersistentStatsStorage, GatewayError> {
let path = &config.storage_paths.stats_storage;
Ok(PersistentStatsStorage::init(path).await?)
}
pub fn load_keypair<T: PemStorableKeyPair>(
paths: KeyPairPath,
name: impl Into<String>,
@@ -102,3 +98,56 @@ pub(crate) fn load_sphinx_keys(config: &Config) -> Result<encryption::KeyPair, G
);
load_keypair(sphinx_paths, "gateway sphinx")
}
#[derive(Clone)]
pub struct GatewayTopologyProvider {
inner: Arc<Mutex<GatewayTopologyProviderInner>>,
}
impl GatewayTopologyProvider {
pub fn new(
gateway_node: gateway::LegacyNode,
user_agent: UserAgent,
nym_api_url: Vec<Url>,
) -> GatewayTopologyProvider {
GatewayTopologyProvider {
inner: Arc::new(Mutex::new(GatewayTopologyProviderInner {
inner: NymApiTopologyProvider::new(
NymApiTopologyProviderConfig {
min_mixnode_performance: 50,
min_gateway_performance: 0,
},
nym_api_url,
env!("CARGO_PKG_VERSION").to_string(),
Some(user_agent),
),
gateway_node,
})),
}
}
}
struct GatewayTopologyProviderInner {
inner: NymApiTopologyProvider,
gateway_node: gateway::LegacyNode,
}
#[async_trait]
impl TopologyProvider for GatewayTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
let mut guard = self.inner.lock().await;
match guard.inner.get_new_topology().await {
None => None,
Some(mut base) => {
if !base.gateway_exists(&guard.gateway_node.identity_key) {
debug!(
"{} didn't exist in topology. inserting it.",
guard.gateway_node.identity_key
);
base.insert_gateway(guard.gateway_node.clone());
}
Some(base)
}
}
}
}
+67 -28
View File
@@ -13,10 +13,11 @@ use crate::node::client_handling::active_clients::ActiveClientsStore;
use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter};
use crate::node::client_handling::websocket;
use crate::node::helpers::{
initialise_main_storage, initialise_stats_storage, load_network_requester_config,
initialise_main_storage, load_network_requester_config, GatewayTopologyProvider,
};
use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler;
use futures::channel::{mpsc, oneshot};
use nym_bin_common::bin_info;
use nym_credential_verification::ecash::{
credential_sender::CredentialHandlerConfig, EcashManager,
};
@@ -27,13 +28,15 @@ use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilte
use nym_node_http_api::state::metrics::SharedSessionStats;
use nym_statistics_common::events::{self, StatsEventSender};
use nym_task::{TaskClient, TaskHandle, TaskManager};
use nym_topology::NetworkAddress;
use nym_types::gateway::GatewayNodeDetailsResponse;
use nym_validator_client::client::NodeId;
use nym_validator_client::nyxd::{Coin, CosmWasmClient};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use rand::seq::SliceRandom;
use rand::thread_rng;
use statistics::GatewayStatisticsCollector;
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::*;
@@ -43,7 +46,6 @@ pub(crate) mod helpers;
pub(crate) mod mixnet_handling;
pub(crate) mod statistics;
pub use nym_gateway_stats_storage::PersistentStatsStorage;
pub use nym_gateway_storage::{PersistentStorage, Storage};
// TODO: should this struct live here?
@@ -99,8 +101,6 @@ pub async fn create_gateway(
let storage = initialise_main_storage(&config).await?;
let stats_storage = initialise_stats_storage(&config).await?;
let nr_opts = network_requester_config.map(|config| LocalNetworkRequesterOpts {
config: config.clone(),
custom_mixnet_path: custom_mixnet.clone(),
@@ -111,7 +111,7 @@ pub async fn create_gateway(
custom_mixnet_path: custom_mixnet.clone(),
});
Gateway::new(config, nr_opts, ip_opts, storage, stats_storage)
Gateway::new(config, nr_opts, ip_opts, storage)
}
#[derive(Debug, Clone)]
@@ -152,9 +152,7 @@ pub struct Gateway<St = PersistentStorage> {
/// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation.
sphinx_keypair: Arc<encryption::KeyPair>,
client_storage: St,
stats_storage: PersistentStatsStorage,
storage: St,
wireguard_data: Option<nym_wireguard::WireguardData>,
@@ -170,12 +168,10 @@ impl<St> Gateway<St> {
config: Config,
network_requester_opts: Option<LocalNetworkRequesterOpts>,
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
client_storage: St,
stats_storage: PersistentStatsStorage,
storage: St,
) -> Result<Self, GatewayError> {
Ok(Gateway {
client_storage,
stats_storage,
storage,
identity_keypair: Arc::new(load_identity_keys(&config)?),
sphinx_keypair: Arc::new(helpers::load_sphinx_keys(&config)?),
config,
@@ -188,7 +184,7 @@ impl<St> Gateway<St> {
task_client: None,
})
}
#[allow(clippy::too_many_arguments)]
pub fn new_loaded(
config: Config,
network_requester_opts: Option<LocalNetworkRequesterOpts>,
@@ -196,8 +192,7 @@ impl<St> Gateway<St> {
authenticator_opts: Option<LocalAuthenticatorOpts>,
identity_keypair: Arc<identity::KeyPair>,
sphinx_keypair: Arc<encryption::KeyPair>,
client_storage: St,
stats_storage: PersistentStatsStorage,
storage: St,
) -> Self {
Gateway {
config,
@@ -206,8 +201,7 @@ impl<St> Gateway<St> {
authenticator_opts,
identity_keypair,
sphinx_keypair,
client_storage,
stats_storage,
storage,
wireguard_data: None,
session_stats: None,
run_http_server: true,
@@ -236,6 +230,39 @@ impl<St> Gateway<St> {
crate::helpers::node_details(&self.config).await
}
fn gateway_topology_provider(&self) -> GatewayTopologyProvider {
GatewayTopologyProvider::new(
self.as_topology_node(),
bin_info!().into(),
self.config.gateway.nym_api_urls.clone(),
)
}
fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode {
let ip = self
.config
.host
.public_ips
.first()
.copied()
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port);
nym_topology::gateway::LegacyNode {
// those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate.
// the only thing that matters is the identity key (and maybe version)
node_id: NodeId::MAX,
mix_host,
host: NetworkAddress::IpAddr(ip),
clients_ws_port: self.config.gateway.clients_port,
clients_wss_port: self.config.gateway.clients_wss_port,
sphinx_key: *self.sphinx_keypair.public_key(),
identity_key: *self.identity_keypair.public_key(),
version: env!("CARGO_PKG_VERSION").into(),
}
}
fn start_mix_socket_listener(
&self,
ack_sender: MixForwardingSender,
@@ -251,7 +278,7 @@ impl<St> Gateway<St> {
let connection_handler = ConnectionHandler::new(
packet_processor,
self.client_storage.clone(),
self.storage.clone(),
ack_sender,
active_clients_store,
);
@@ -268,6 +295,7 @@ impl<St> Gateway<St> {
async fn start_authenticator(
&mut self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
ecash_verifier: Arc<EcashManager<St>>,
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>>
@@ -286,7 +314,7 @@ impl<St> Gateway<St> {
forwarding_channel,
router_tx,
);
let all_peers = self.client_storage.get_all_wireguard_peers().await?;
let all_peers = self.storage.get_all_wireguard_peers().await?;
let used_private_network_ips = all_peers
.iter()
.cloned()
@@ -315,6 +343,7 @@ impl<St> Gateway<St> {
.with_shutdown(shutdown.fork("authenticator"))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &opts.custom_mixnet_path {
@@ -341,7 +370,7 @@ impl<St> Gateway<St> {
.start_with_shutdown(router_shutdown);
let wg_api = nym_wireguard::start_wireguard(
self.client_storage.clone(),
self.storage.clone(),
all_peers,
shutdown,
wireguard_data,
@@ -363,6 +392,7 @@ impl<St> Gateway<St> {
async fn start_authenticator(
&self,
_forwarding_channel: MixForwardingSender,
_topology_provider: GatewayTopologyProvider,
_shutdown: TaskClient,
_ecash_verifier: Arc<EcashManager<St>>,
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>> {
@@ -388,7 +418,7 @@ impl<St> Gateway<St> {
let shared_state = websocket::CommonHandlerState {
ecash_verifier,
storage: self.client_storage.clone(),
storage: self.storage.clone(),
local_identity: Arc::clone(&self.identity_keypair),
only_coconut_credentials: self.config.gateway.only_coconut_credentials,
bandwidth_cfg: (&self.config).into(),
@@ -426,7 +456,7 @@ impl<St> Gateway<St> {
info!("Starting gateway stats collector...");
let (mut stats_collector, stats_event_sender) =
GatewayStatisticsCollector::new(shared_session_stats, self.stats_storage.clone());
GatewayStatisticsCollector::new(shared_session_stats);
tokio::spawn(async move { stats_collector.run(shutdown).await });
stats_event_sender
}
@@ -435,6 +465,7 @@ impl<St> Gateway<St> {
async fn start_network_requester(
&self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
) -> Result<StartedNetworkRequester, GatewayError> {
info!("Starting network requester...");
@@ -462,6 +493,7 @@ impl<St> Gateway<St> {
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path {
@@ -499,6 +531,7 @@ impl<St> Gateway<St> {
async fn start_ip_packet_router(
&self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
) -> Result<LocalEmbeddedClientHandle, GatewayError> {
info!("Starting IP packet provider...");
@@ -527,6 +560,7 @@ impl<St> Gateway<St> {
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path {
@@ -588,7 +622,7 @@ impl<St> Gateway<St> {
// TODO: if anything, this should be getting data directly from the contract
// as opposed to the validator API
let validator_client = self.random_api_client()?;
let existing_nodes = match validator_client.get_cached_gateways().await {
let existing_nodes = match validator_client.get_all_basic_nodes(None).await {
Ok(nodes) => nodes,
Err(err) => {
error!("failed to grab initial network gateways - {err}\n Please try to startup again in few minutes");
@@ -596,9 +630,9 @@ impl<St> Gateway<St> {
}
};
Ok(existing_nodes.iter().any(|node| {
node.gateway.identity_key == self.identity_keypair.public_key().to_base58_string()
}))
Ok(existing_nodes
.iter()
.any(|node| &node.ed25519_identity_pubkey == self.identity_keypair.public_key()))
}
pub async fn run(mut self) -> Result<(), GatewayError>
@@ -643,6 +677,8 @@ impl<St> Gateway<St> {
shutdown.fork("statistics::GatewayStatisticsCollector"),
);
let topology_provider = self.gateway_topology_provider();
let handler_config = CredentialHandlerConfig {
revocation_bandwidth_penalty: self
.config
@@ -665,7 +701,7 @@ impl<St> Gateway<St> {
nyxd_client,
self.identity_keypair.public_key().to_bytes(),
shutdown.fork("EcashVerifier"),
self.client_storage.clone(),
self.storage.clone(),
)
.await?,
);
@@ -691,6 +727,7 @@ impl<St> Gateway<St> {
let embedded_nr = self
.start_network_requester(
mix_forwarding_channel.clone(),
topology_provider.clone(),
shutdown.fork("NetworkRequester"),
)
.await?;
@@ -706,6 +743,7 @@ impl<St> Gateway<St> {
let embedded_ip_sp = self
.start_ip_packet_router(
mix_forwarding_channel.clone(),
topology_provider.clone(),
shutdown.fork("ip_service_provider"),
)
.await?;
@@ -718,6 +756,7 @@ impl<St> Gateway<St> {
let embedded_auth = self
.start_authenticator(
mix_forwarding_channel,
topology_provider,
shutdown.fork("authenticator"),
ecash_verifier,
)
+4 -25
View File
@@ -2,14 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only
use futures::{channel::mpsc, StreamExt};
use nym_gateway_stats_storage::PersistentStatsStorage;
use nym_node_http_api::state::metrics::SharedSessionStats;
use nym_statistics_common::events::{StatsEvent, StatsEventReceiver, StatsEventSender};
use nym_task::TaskClient;
use sessions::SessionStatsHandler;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{error, trace, warn};
use tracing::trace;
pub mod sessions;
@@ -24,38 +23,21 @@ pub(crate) struct GatewayStatisticsCollector {
impl GatewayStatisticsCollector {
pub fn new(
shared_session_stats: SharedSessionStats,
stats_storage: PersistentStatsStorage,
) -> (GatewayStatisticsCollector, StatsEventSender) {
let (stats_event_tx, stats_event_rx) = mpsc::unbounded();
let session_stats = SessionStatsHandler::new(shared_session_stats, stats_storage);
let collector = GatewayStatisticsCollector {
stats_event_rx,
session_stats,
session_stats: SessionStatsHandler::new(shared_session_stats),
};
(collector, stats_event_tx)
}
async fn update_shared_state(&mut self, update_time: OffsetDateTime) {
if let Err(e) = self
.session_stats
.maybe_update_shared_state(update_time)
.await
{
error!("Failed to update session stats - {e}");
}
self.session_stats.update_shared_state(update_time).await;
//here goes additionnal stats handler update
}
async fn on_start(&mut self) {
if let Err(e) = self.session_stats.on_start().await {
error!("Failed to cleanup session stats handler - {e}");
}
//here goes additionnal stats handler start cleanup
}
pub async fn run(&mut self, mut shutdown: TaskClient) {
self.on_start().await;
let mut update_interval = tokio::time::interval(STATISTICS_UPDATE_TIMER_INTERVAL);
while !shutdown.is_shutdown() {
tokio::select! {
@@ -71,10 +53,7 @@ impl GatewayStatisticsCollector {
Some(stat_event) = self.stats_event_rx.next() => {
//dispatching event to proper handler
match stat_event {
StatsEvent::SessionStatsEvent(event) => {
if let Err(e) = self.session_stats.handle_event(event).await{
warn!("Session event handling error - {e}");
}},
StatsEvent::SessionStatsEvent(event) => self.session_stats.handle_event(event),
}
},
+127 -109
View File
@@ -2,158 +2,176 @@
// SPDX-License-Identifier: GPL-3.0-only
use nym_credentials_interface::TicketType;
use nym_gateway_stats_storage::models::FinishedSession;
use nym_gateway_stats_storage::PersistentStatsStorage;
use nym_gateway_stats_storage::{error::StatsStorageError, models::ActiveSession};
use nym_node_http_api::state::metrics::SharedSessionStats;
use nym_sphinx::DestinationAddressBytes;
use std::collections::{HashMap, HashSet};
use time::{Date, Duration, OffsetDateTime};
use nym_statistics_common::events::SessionEvent;
const FINISHED_SESSIONS_CAP: usize = 1_000_000; //to be on the safe side of memory blowups until persistent storage
#[derive(PartialEq)]
enum SessionType {
Vpn,
Mixnet,
Unknown,
}
impl SessionType {
fn to_string(&self) -> &str {
match self {
Self::Vpn => "vpn",
Self::Mixnet => "mixnet",
Self::Unknown => "unknown",
}
}
}
impl From<TicketType> for SessionType {
fn from(value: TicketType) -> Self {
match value {
TicketType::V1MixnetEntry => Self::Mixnet,
TicketType::V1MixnetExit => Self::Mixnet,
TicketType::V1WireguardEntry => Self::Vpn,
TicketType::V1WireguardExit => Self::Vpn,
}
}
}
struct FinishedSession {
duration: Duration,
typ: SessionType,
}
impl FinishedSession {
fn serialize(&self) -> (u64, String) {
(
self.duration.whole_milliseconds() as u64, //we are sure that it fits in a u64, see `fn end_at`
self.typ.to_string().into(),
)
}
}
struct ActiveSession {
start: OffsetDateTime,
typ: SessionType,
}
impl ActiveSession {
fn new(start_time: OffsetDateTime) -> Self {
ActiveSession {
start: start_time,
typ: SessionType::Unknown,
}
}
fn set_type(&mut self, ticket_type: TicketType) {
self.typ = ticket_type.into();
}
fn end_at(self, stop_time: OffsetDateTime) -> Option<FinishedSession> {
let session_duration = stop_time - self.start;
//ensure duration is positive to fit in a u64
//u64::max milliseconds is 500k millenia so no overflow issue
if session_duration > Duration::ZERO {
Some(FinishedSession {
duration: session_duration,
typ: self.typ,
})
} else {
None
}
}
}
pub(crate) struct SessionStatsHandler {
storage: PersistentStatsStorage,
current_day: Date,
last_update_day: Date,
shared_session_stats: SharedSessionStats,
active_sessions: HashMap<DestinationAddressBytes, ActiveSession>,
unique_users: HashSet<DestinationAddressBytes>,
sessions_started: u32,
finished_sessions: Vec<FinishedSession>,
}
impl SessionStatsHandler {
pub fn new(shared_session_stats: SharedSessionStats, storage: PersistentStatsStorage) -> Self {
pub fn new(shared_session_stats: SharedSessionStats) -> Self {
SessionStatsHandler {
storage,
current_day: OffsetDateTime::now_utc().date(),
last_update_day: OffsetDateTime::now_utc().date(),
shared_session_stats,
active_sessions: Default::default(),
unique_users: Default::default(),
sessions_started: 0,
finished_sessions: Default::default(),
}
}
pub(crate) async fn handle_event(
&mut self,
event: SessionEvent,
) -> Result<(), StatsStorageError> {
pub(crate) fn handle_event(&mut self, event: SessionEvent) {
match event {
SessionEvent::SessionStart { start_time, client } => {
self.handle_session_start(start_time, client).await
self.handle_session_start(start_time, client);
}
SessionEvent::SessionStop { stop_time, client } => {
self.handle_session_stop(stop_time, client).await
self.handle_session_stop(stop_time, client);
}
SessionEvent::EcashTicket {
ticket_type,
client,
} => self.handle_ecash_ticket(ticket_type, client).await,
} => self.handle_ecash_ticket(ticket_type, client),
}
}
async fn handle_session_start(
fn handle_session_start(
&mut self,
start_time: OffsetDateTime,
client: DestinationAddressBytes,
) -> Result<(), StatsStorageError> {
self.storage
.insert_unique_user(self.current_day, client.as_base58_string())
.await?;
self.storage
.insert_active_session(client, ActiveSession::new(start_time))
.await?;
Ok(())
) {
self.sessions_started += 1;
self.unique_users.insert(client);
self.active_sessions
.insert(client, ActiveSession::new(start_time));
}
async fn handle_session_stop(
&mut self,
stop_time: OffsetDateTime,
client: DestinationAddressBytes,
) -> Result<(), StatsStorageError> {
if let Some(session) = self.storage.get_active_session(client).await? {
fn handle_session_stop(&mut self, stop_time: OffsetDateTime, client: DestinationAddressBytes) {
if let Some(session) = self.active_sessions.remove(&client) {
if let Some(finished_session) = session.end_at(stop_time) {
self.storage
.insert_finished_session(self.current_day, finished_session)
.await?;
self.storage.delete_active_session(client).await?;
if self.finished_sessions.len() < FINISHED_SESSIONS_CAP {
self.finished_sessions.push(finished_session);
}
}
}
Ok(())
}
async fn handle_ecash_ticket(
&mut self,
ticket_type: TicketType,
client: DestinationAddressBytes,
) -> Result<(), StatsStorageError> {
self.storage
.update_active_session_type(client, ticket_type.into())
.await?;
Ok(())
}
pub(crate) async fn on_start(&mut self) -> Result<(), StatsStorageError> {
let yesterday = OffsetDateTime::now_utc().date() - Duration::DAY;
//publish yesterday's data if any
self.publish_stats(yesterday).await?;
//store "active" sessions as duration 0
for active_session in self.storage.get_all_active_sessions().await? {
self.storage
.insert_finished_session(
self.current_day,
FinishedSession {
duration: Duration::ZERO,
typ: active_session.typ,
},
)
.await?
fn handle_ecash_ticket(&mut self, ticket_type: TicketType, client: DestinationAddressBytes) {
if let Some(active_session) = self.active_sessions.get_mut(&client) {
if active_session.typ == SessionType::Unknown {
active_session.set_type(ticket_type);
}
}
//cleanup active sessions
self.storage.cleanup_active_sessions().await?;
//delete old entries
self.delete_old_stats(yesterday - Duration::DAY).await?;
Ok(())
}
//update shared state once a day has passed, with data from the previous day
async fn publish_stats(&mut self, stats_date: Date) -> Result<(), StatsStorageError> {
let finished_sessions = self.storage.get_finished_sessions(stats_date).await?;
let user_count = self.storage.get_unique_users_count(stats_date).await?;
let session_started = self.storage.get_started_sessions_count(stats_date).await? as u32;
{
let mut shared_state = self.shared_session_stats.write().await;
shared_state.update_time = stats_date;
shared_state.unique_active_users = user_count as u32;
shared_state.session_started = session_started;
shared_state.sessions = finished_sessions.iter().map(|s| s.serialize()).collect();
}
Ok(())
}
pub(crate) async fn maybe_update_shared_state(
&mut self,
update_time: OffsetDateTime,
) -> Result<(), StatsStorageError> {
pub(crate) async fn update_shared_state(&mut self, update_time: OffsetDateTime) {
let update_date = update_time.date();
if update_date != self.current_day {
self.publish_stats(self.current_day).await?;
self.delete_old_stats(self.current_day - Duration::DAY)
.await?;
self.reset_stats(update_date).await?;
self.current_day = update_date;
if update_date != self.last_update_day {
{
let mut shared_state = self.shared_session_stats.write().await;
shared_state.update_time = self.last_update_day;
shared_state.unique_active_users = self.unique_users.len() as u32;
shared_state.session_started = self.sessions_started;
shared_state.sessions = self
.finished_sessions
.iter()
.map(|s| s.serialize())
.collect();
}
self.reset_stats(update_date);
}
Ok(())
}
async fn reset_stats(&mut self, reset_day: Date) -> Result<(), StatsStorageError> {
//active users reset
let new_active_users = self.storage.get_active_users().await?;
for user in new_active_users {
self.storage.insert_unique_user(reset_day, user).await?;
}
Ok(())
}
async fn delete_old_stats(&mut self, delete_before: Date) -> Result<(), StatsStorageError> {
self.storage.delete_finished_sessions(delete_before).await?;
self.storage.delete_unique_users(delete_before).await?;
Ok(())
fn reset_stats(&mut self, reset_day: Date) {
self.last_update_day = reset_day;
self.unique_users = self.active_sessions.keys().copied().collect();
self.finished_sessions = Default::default();
self.sessions_started = 0;
}
}
+4 -5
View File
@@ -234,7 +234,7 @@ impl MixNode {
// TODO: if anything, this should be getting data directly from the contract
// as opposed to the validator API
let validator_client = self.random_api_client();
let existing_nodes = match validator_client.get_cached_mixnodes().await {
let existing_nodes = match validator_client.get_all_basic_nodes(None).await {
Ok(nodes) => nodes,
Err(err) => {
error!(
@@ -245,10 +245,9 @@ impl MixNode {
}
};
existing_nodes.iter().any(|node| {
node.bond_information.mix_node.identity_key
== self.identity_keypair.public_key().to_base58_string()
})
existing_nodes
.iter()
.any(|node| &node.ed25519_identity_pubkey == self.identity_keypair.public_key())
}
async fn wait_for_interrupt(&self, shutdown: TaskHandle) {
+3 -15
View File
@@ -2,10 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmwasm_std::Decimal;
use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges;
use nym_mixnet_contract_common::{
GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding,
};
use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges;
use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
@@ -64,7 +62,7 @@ pub struct LegacyMixNodeDetailsWithLayer {
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
#[serde(default)]
pub pending_changes: PendingMixNodeChanges,
pub pending_changes: LegacyPendingMixNodeChanges,
}
impl LegacyMixNodeDetailsWithLayer {
@@ -80,13 +78,3 @@ impl LegacyMixNodeDetailsWithLayer {
self.bond_information.is_unbonding
}
}
impl From<LegacyMixNodeDetailsWithLayer> for MixNodeDetails {
fn from(value: LegacyMixNodeDetailsWithLayer) -> Self {
MixNodeDetails {
bond_information: value.bond_information.into(),
rewarding_details: value.rewarding_details,
pending_changes: value.pending_changes,
}
}
}
+59 -7
View File
@@ -16,7 +16,7 @@ use nym_crypto::asymmetric::x25519::{
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent};
use nym_mixnet_contract_common::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent};
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::Wireguard;
@@ -138,6 +138,48 @@ pub struct NodePerformance {
pub last_24h: Performance,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
)]
pub enum DisplayRole {
EntryGateway,
Layer1,
Layer2,
Layer3,
ExitGateway,
Standby,
}
impl From<Role> for DisplayRole {
fn from(role: Role) -> Self {
match role {
Role::EntryGateway => DisplayRole::EntryGateway,
Role::Layer1 => DisplayRole::Layer1,
Role::Layer2 => DisplayRole::Layer2,
Role::Layer3 => DisplayRole::Layer3,
Role::ExitGateway => DisplayRole::ExitGateway,
Role::Standby => DisplayRole::Standby,
}
}
}
impl From<DisplayRole> for Role {
fn from(role: DisplayRole) -> Self {
match role {
DisplayRole::EntryGateway => Role::EntryGateway,
DisplayRole::Layer1 => Role::Layer1,
DisplayRole::Layer2 => Role::Layer2,
DisplayRole::Layer3 => Role::Layer3,
DisplayRole::ExitGateway => Role::ExitGateway,
DisplayRole::Standby => Role::Standby,
}
}
}
// imo for now there's no point in exposing more than that,
// nym-api shouldn't be calculating apy or stake saturation for you.
// it should just return its own metrics (performance) and then you can do with it as you wish
@@ -153,7 +195,7 @@ pub struct NodePerformance {
pub struct NodeAnnotation {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub last_24h_performance: Performance,
pub current_role: Option<Role>,
pub current_role: Option<DisplayRole>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
@@ -286,7 +328,7 @@ impl MixNodeBondAnnotated {
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
epoch_role: role,
role,
supported_roles: DeclaredRoles {
mixnode: true,
entry: false,
@@ -345,7 +387,7 @@ impl GatewayBondAnnotated {
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
epoch_role: role,
role,
supported_roles: DeclaredRoles {
mixnode: false,
entry: true,
@@ -810,6 +852,10 @@ impl NymNodeDescription {
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
@@ -827,7 +873,7 @@ impl NymNodeDescription {
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
epoch_role: role,
role,
supported_roles: self.description.declared_role,
entry,
performance,
@@ -851,6 +897,12 @@ pub enum DescribedNodeType {
NymNode,
}
impl DescribedNodeType {
pub fn is_nym_node(&self) -> bool {
matches!(self, DescribedNodeType::NymNode)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
@@ -935,14 +987,14 @@ impl NymNodeData {
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedGateway {
pub bond: LegacyGatewayBondWithId,
pub bond: GatewayBond,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway {
fn from(bond: LegacyGatewayBondWithId) -> Self {
LegacyDescribedGateway {
bond,
bond: bond.bond,
self_described: None,
}
}
+3 -3
View File
@@ -141,8 +141,8 @@ pub struct SkimmedNode {
#[schemars(with = "String")]
pub x25519_sphinx_pubkey: x25519::PublicKey,
#[serde(alias = "role")]
pub epoch_role: NodeRole,
#[serde(alias = "epoch_role")]
pub role: NodeRole,
// needed for the purposes of sending appropriate test packets
#[serde(default)]
@@ -157,7 +157,7 @@ pub struct SkimmedNode {
impl SkimmedNode {
pub fn get_mix_layer(&self) -> Option<u8> {
match self.epoch_role {
match self.role {
NodeRole::Mixnode { layer } => Some(layer),
_ => None,
}
+6 -6
View File
@@ -24,21 +24,21 @@ use utoipa::IntoParams;
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
Router::new()
.route(
"/master-verification-key:epoch_id",
"/master-verification-key",
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",
"/aggregated-expiration-date-signatures",
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",
"/aggregated-coin-indices-signatures",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
@@ -52,7 +52,7 @@ pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppStat
params(
EpochIdParam
),
path = "/v1/ecash/master-verification-key/{epoch_id}",
path = "/v1/ecash/master-verification-key",
responses(
(status = 200, body = VerificationKeyResponse)
)
@@ -83,7 +83,7 @@ struct ExpirationDateParam {
params(
ExpirationDateParam
),
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
path = "/v1/ecash/aggregated-expiration-date-signatures",
responses(
(status = 200, body = AggregatedExpirationDateSignatureResponse)
)
@@ -120,7 +120,7 @@ async fn expiration_date_signatures(
params(
EpochIdParam
),
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
path = "/v1/ecash/aggregated-coin-indices-signatures",
responses(
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
)
@@ -7,7 +7,7 @@ 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::extract::Query;
use axum::{Json, Router};
use nym_api_requests::ecash::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
@@ -32,14 +32,14 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<App
}),
)
.route(
"/partial-expiration-date-signatures:expiration_date",
"/partial-expiration-date-signatures",
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",
"/partial-coin-indices-signatures",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
@@ -127,14 +127,14 @@ struct ExpirationDateParam {
params(
ExpirationDateParam
),
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
path = "/v1/ecash/partial-expiration-date-signatures",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn partial_expiration_date_signatures(
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
state.ensure_signer().await?;
@@ -165,14 +165,14 @@ async fn partial_expiration_date_signatures(
params(
EpochIdParam
),
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
path = "/v1/ecash/partial-coin-indices-signatures",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn partial_coin_indices_signatures(
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
Query(EpochIdParam { epoch_id }): Query<EpochIdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
state.ensure_signer().await?;
+2
View File
@@ -20,6 +20,7 @@ use time::macros::time;
use time::{OffsetDateTime, Time};
use tracing::{error, warn};
#[allow(deprecated)]
pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
Router::new()
.route(
@@ -242,6 +243,7 @@ async fn batch_redeem_tickets(
(status = 500, body = ErrorResponse, description = "bloomfilters got disabled"),
)
)]
#[deprecated]
async fn double_spending_filter_v1(
_state: Arc<EcashState>,
) -> AxumResult<Json<SpentCredentialsResponse>> {
@@ -169,7 +169,7 @@ impl EpochAdvancer {
let standby_eligible = all_choices
.iter()
.filter(|node| {
exit_gateways.contains(&node.0.node_id)
!exit_gateways.contains(&node.0.node_id)
&& !entry_gateways.contains(&node.0.node_id)
&& !mixnodes.contains(&node.0.node_id)
})
@@ -228,14 +228,24 @@ impl EpochAdvancer {
)
}
Ok(RewardedSet {
let mut rewarded_set = RewardedSet {
entry_gateways: entry_gateways.into_iter().collect(),
exit_gateways: exit_gateways.into_iter().collect(),
layer1,
layer2,
layer3,
standby,
})
};
// make sure to sort the rewarded set values
rewarded_set.entry_gateways.sort();
rewarded_set.exit_gateways.sort();
rewarded_set.layer1.sort();
rewarded_set.layer2.sort();
rewarded_set.layer3.sort();
rewarded_set.standby.sort();
Ok(rewarded_set)
}
async fn attach_performance_to_eligible_nodes(
+2 -2
View File
@@ -274,8 +274,8 @@ impl<R: MessageReceiver + Send> Monitor<R> {
info!("Received {}/{} packets", total_received, total_sent);
let summary = self.summary_producer.produce_summary(
prepared_packets.tested_mixnodes,
prepared_packets.tested_gateways,
prepared_packets.mixnodes_under_test,
prepared_packets.gateways_under_test,
received,
prepared_packets.invalid_mixnodes,
prepared_packets.invalid_gateways,
+46 -31
View File
@@ -25,7 +25,7 @@ use std::collections::{HashMap, HashSet};
use std::fmt::{self, Display, Formatter};
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, trace};
use tracing::{debug, error, info, trace};
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200);
@@ -59,10 +59,10 @@ pub(crate) struct PreparedPackets {
pub(super) packets: Vec<GatewayPackets>,
/// Vector containing list of public keys and owners of all nodes mixnodes being tested.
pub(super) tested_mixnodes: Vec<TestableNode>,
pub(super) mixnodes_under_test: Vec<TestableNode>,
/// Vector containing list of public keys and owners of all gateways being tested.
pub(super) tested_gateways: Vec<TestableNode>,
pub(super) gateways_under_test: Vec<TestableNode>,
/// All mixnodes that failed to get parsed correctly or were not version compatible.
/// They will be marked to the validator as being down for the test.
@@ -151,34 +151,38 @@ impl PacketPreparer {
self.contract_cache.wait_for_initial_values().await;
self.described_cache.naive_wait_for_initial_values().await;
let described_nodes = self
.described_cache
.get()
.await
.expect("the self-describe cache should have been initialised!");
// now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online
info!("Waiting for minimal topology to be online");
let initialisation_backoff = Duration::from_secs(30);
loop {
let gateways = self.contract_cache.legacy_gateways_all().await;
let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await;
let nym_nodes = self.contract_cache.nym_nodes().await;
if gateways.len() < minimum_full_routes {
self.topology_wait_backoff(initialisation_backoff).await;
continue;
}
let mut gateways_count = gateways.len();
let mut mixnodes_count = mixnodes.len();
let mut layer1_count = 0;
let mut layer2_count = 0;
let mut layer3_count = 0;
for mix in mixnodes {
match mix.layer {
LegacyMixLayer::One => layer1_count += 1,
LegacyMixLayer::Two => layer2_count += 1,
LegacyMixLayer::Three => layer3_count += 1,
for nym_node in nym_nodes {
if let Some(described) = described_nodes.get_description(&nym_node.node_id()) {
if described.declared_role.mixnode {
mixnodes_count += 1;
} else if described.declared_role.entry {
gateways_count += 1;
}
}
}
if layer1_count >= minimum_full_routes
&& layer2_count >= minimum_full_routes
&& layer3_count >= minimum_full_routes
{
debug!(
"we have {mixnodes_count} possible mixnodes and {gateways_count} possible gateways"
);
if gateways_count >= minimum_full_routes && mixnodes_count * 3 >= minimum_full_routes {
break;
}
@@ -529,30 +533,41 @@ impl PacketPreparer {
let mixing_nym_nodes = descriptions.mixing_nym_nodes();
let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes();
let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes);
let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways);
let (mut mixnodes_to_test_details, invalid_mixnodes) =
self.filter_outdated_and_malformed_mixnodes(mixnodes);
let (mut gateways_to_test_details, invalid_gateways) =
self.filter_outdated_and_malformed_gateways(gateways);
let mut tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::<Vec<_>>();
let mut tested_gateways = gateways.iter().map(|node| node.into()).collect::<Vec<_>>();
// summary of nodes that got tested
let mut mixnodes_under_test = mixnodes_to_test_details
.iter()
.map(|node| node.into())
.collect::<Vec<_>>();
let mut gateways_under_test = gateways_to_test_details
.iter()
.map(|node| node.into())
.collect::<Vec<_>>();
// try to add nym-nodes into the fold
if let Some(rewarded_set) = rewarded_set {
let mut rng = thread_rng();
for mix in mixing_nym_nodes {
if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, mix) {
tested_mixnodes.push(TestableNode::from(&parsed));
mixnodes_under_test.push(TestableNode::from(&parsed));
mixnodes_to_test_details.push(parsed);
}
}
}
for gateway in gateway_capable_nym_nodes {
if let Some(parsed) = self.nym_node_to_legacy_gateway(gateway) {
tested_gateways.push((&parsed, gateway.node_id).into())
gateways_under_test.push((&parsed, gateway.node_id).into());
gateways_to_test_details.push((parsed, gateway.node_id));
}
}
let packets_to_create = (test_routes.len() * self.per_node_test_packets)
* (tested_mixnodes.len() + tested_gateways.len());
* (mixnodes_under_test.len() + gateways_under_test.len());
info!("Need to create {} mix packets", packets_to_create);
let mut all_gateway_packets = HashMap::new();
@@ -574,7 +589,7 @@ impl PacketPreparer {
#[allow(clippy::unwrap_used)]
let mixnode_test_packets = mix_tester
.mixnodes_test_packets(
&mixnodes,
&mixnodes_to_test_details,
route_ext,
self.per_node_test_packets as u32,
None,
@@ -588,7 +603,7 @@ impl PacketPreparer {
gateway_packets.push_packets(mix_packets);
// and generate test packets for gateways (note the variable recipient)
for (gateway, node_id) in &gateways {
for (gateway, node_id) in &gateways_to_test_details {
let recipient = self.create_packet_sender(gateway);
let gateway_identity = gateway.identity_key;
let gateway_address = gateway.clients_address();
@@ -624,8 +639,8 @@ impl PacketPreparer {
PreparedPackets {
packets,
tested_mixnodes,
tested_gateways,
mixnodes_under_test,
gateways_under_test,
invalid_mixnodes,
invalid_gateways,
}
+2
View File
@@ -145,6 +145,7 @@ impl NodeDescriptionTopologyExt for NymNodeDescription {
}
}
#[derive(Debug, Clone)]
pub struct DescribedNodes {
nodes: HashMap<NodeId, NymNodeDescription>,
}
@@ -290,6 +291,7 @@ async fn try_get_description(
})
}
#[derive(Debug)]
struct RefreshData {
host: String,
node_id: NodeId,
@@ -210,6 +210,7 @@ impl ResolvedNodeDescribedInfo {
}
}
#[derive(Debug)]
pub(crate) struct UnwrappedResolvedNodeDescribedInfo {
pub(crate) build_info: BinaryBuildInformationOwned,
pub(crate) roles: DeclaredRoles,
+3 -3
View File
@@ -209,7 +209,7 @@ pub(crate) async fn produce_node_annotations(
legacy_mix.mix_id(),
NodeAnnotation {
last_24h_performance: perf,
current_role: rewarded_set.role(legacy_mix.mix_id()),
current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()),
},
);
}
@@ -229,7 +229,7 @@ pub(crate) async fn produce_node_annotations(
legacy_gateway.node_id,
NodeAnnotation {
last_24h_performance: perf,
current_role: rewarded_set.role(legacy_gateway.node_id),
current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()),
},
);
}
@@ -249,7 +249,7 @@ pub(crate) async fn produce_node_annotations(
nym_node.node_id(),
NodeAnnotation {
last_24h_performance: perf,
current_role: rewarded_set.role(nym_node.node_id()),
current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()),
},
);
}
+10 -1
View File
@@ -16,6 +16,7 @@ use nym_serde_helpers::date::DATE_FORMAT;
use reqwest::StatusCode;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sqlx::Error;
use std::fmt::Display;
use thiserror::Error;
use time::{Date, OffsetDateTime};
@@ -438,7 +439,7 @@ pub enum NymApiStorageError {
// I don't think we want to expose errors to the user about what really happened
#[error("experienced internal database error")]
InternalDatabaseError(#[from] sqlx::Error),
InternalDatabaseError(sqlx::Error),
// the same is true here (also note that the message is subtly different so we would be able to distinguish them)
#[error("experienced internal storage error")]
@@ -449,6 +450,14 @@ pub enum NymApiStorageError {
StartupMigrationFailure(#[from] sqlx::migrate::MigrateError),
}
impl From<sqlx::Error> for NymApiStorageError {
fn from(err: Error) -> Self {
// those should realistically never be happening so an `error!` is warranted
error!("storage failure: {err}");
NymApiStorageError::InternalDatabaseError(err)
}
}
impl NymApiStorageError {
pub fn database_inconsistency<S: Into<String>>(reason: S) -> NymApiStorageError {
NymApiStorageError::DatabaseInconsistency {
+1 -1
View File
@@ -173,7 +173,7 @@ impl NymContractCacheRefresher {
layer,
},
rewarding_details: detail.rewarding_details,
pending_changes: detail.pending_changes,
pending_changes: detail.pending_changes.into(),
})
}
+1 -1
View File
@@ -49,7 +49,7 @@ async fn get_gateways_described(
.into_iter()
.map(|bond| LegacyDescribedGateway {
self_described: self_descriptions.get_description(&bond.node_id).cloned(),
bond,
bond: bond.bond,
})
.collect(),
)
+7 -12
View File
@@ -9,7 +9,7 @@ use axum::routing::get;
use axum::{Json, Router};
use nym_api_requests::models::{
AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NoiseDetails,
NymNodeData, PerformanceHistoryResponse, UptimeHistoryResponse,
NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse,
};
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_contracts_common::NaiveFloat;
@@ -125,32 +125,27 @@ async fn get_bonded_nodes(
path = "/described",
context_path = "/v1/nym-nodes",
responses(
(status = 200, body = PaginatedResponse<NymNodeData>)
(status = 200, body = PaginatedResponse<NymNodeDescription>)
),
params(PaginationRequest)
)]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<Json<PaginatedResponse<NymNodeData>>> {
) -> AxumResult<Json<PaginatedResponse<NymNodeDescription>>> {
// TODO: implement it
let _ = pagination;
let cache = state.described_nodes_cache.get().await?;
let descriptions = cache.all_nodes();
let data = descriptions
.map(|n| &n.description)
.cloned()
.collect::<Vec<_>>();
let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
Ok(Json(PaginatedResponse {
pagination: Pagination {
total: data.len(),
total: descriptions.len(),
page: 0,
size: data.len(),
size: descriptions.len(),
},
data,
data: descriptions,
}))
}
+2 -2
View File
@@ -234,9 +234,9 @@ impl Config {
fn default_http_socket_addr() -> SocketAddr {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8000)
} else {
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080)
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8000)
}
}
}
+4 -5
View File
@@ -119,9 +119,8 @@ impl StorageManager {
start_ts_secs: i64,
end_ts_secs: i64,
) -> Result<Vec<AvgGatewayReliability>, sqlx::Error> {
// we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations.
// some (like v3 directory) happens at runtime
let result = sqlx::query_as(
let result = sqlx::query_as!(
AvgGatewayReliability,
r#"
SELECT
d.node_id as "node_id: NodeId",
@@ -135,9 +134,9 @@ impl StorageManager {
timestamp <= ?
GROUP BY 1
"#,
start_ts_secs,
end_ts_secs
)
.bind(start_ts_secs)
.bind(end_ts_secs)
.fetch_all(&self.connection_pool)
.await?;
Ok(result)
-4
View File
@@ -30,10 +30,6 @@ pub async fn migrate_v3_database(
let contract_gateways = nyxd_client.get_gateways().await?;
let nym_nodes = nyxd_client.get_nymnodes().await?;
if preassigned_ids.len() != contract_gateways.len() {
bail!("CONTRACT DATA CORRUPTION: THE NUMBER OF PREASSIGNED GATEWAY IDS IS DIFFERENT THAN THE NUMBER OF GATEWAYS")
}
// assign node_id to every gateway
let all_known = storage.get_all_known_gateways().await?;
for gateway in all_known {
+34 -5
View File
@@ -214,6 +214,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-client-ip"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac"
dependencies = [
"axum",
"forwarded-header-value",
"serde",
]
[[package]]
name = "axum-core"
version = "0.4.5"
@@ -1311,6 +1322,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e"
[[package]]
name = "forwarded-header-value"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [
"nonempty",
"thiserror",
]
[[package]]
name = "futures"
version = "0.3.31"
@@ -2147,6 +2168,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nonempty"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -2315,7 +2342,7 @@ dependencies = [
"digest 0.9.0",
"ff",
"group",
"itertools 0.12.1",
"itertools 0.13.0",
"nym-network-defaults",
"nym-pemstore",
"rand",
@@ -2355,7 +2382,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.1.0"
version = "0.1.3"
dependencies = [
"anyhow",
"async-trait",
@@ -2540,6 +2567,7 @@ name = "nym-http-api-common"
version = "0.1.0"
dependencies = [
"axum",
"axum-client-ip",
"bytes",
"colored",
"mime",
@@ -2663,7 +2691,6 @@ dependencies = [
"flate2",
"futures",
"itertools 0.13.0",
"log",
"nym-api-requests",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
@@ -2687,6 +2714,7 @@ dependencies = [
"thiserror",
"time",
"tokio",
"tracing",
"url",
"wasmtimer",
"zeroize",
@@ -3763,9 +3791,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -4889,6 +4917,7 @@ dependencies = [
"quote",
"regex",
"syn 2.0.79",
"uuid",
]
[[package]]
@@ -22,12 +22,12 @@ reqwest = { workspace = true, features = ["json"] }
wasm-bindgen = { workspace = true, optional = true }
## openapi:
utoipa = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true, features = ["uuid"] }
nym-credentials = { path = "../../common/credentials" }
nym-credentials-interface = { path = "../../common/credentials-interface" }
nym-http-api-common = { path = "../../common/http-api-common", optional = true }
nym-http-api-client = { path = "../../common/http-api-client" }
nym-http-api-client = { path = "../../common/http-api-client" }
nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] }
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
@@ -10,7 +10,7 @@ use schemars::schema::Schema;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
use time::Date;
use time::{Date, OffsetDateTime};
#[cfg(feature = "query-types")]
use nym_http_api_common::Output;
@@ -40,6 +40,7 @@ pub struct TicketbookRequest {
/// you **MUST** provide a valid value otherwise blacklisting won't work
#[schemars(with = "String")]
#[serde(with = "bs58_ecash")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub ecash_pubkey: PublicKeyUser,
// needs to be explicit in case user creates request at 23:59:59.999, but it reaches vpn-api at 00:00:00.001
@@ -48,6 +49,7 @@ pub struct TicketbookRequest {
pub expiration_date: Date,
#[schemars(with = "String")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub ticketbook_type: TicketType,
pub is_freepass_request: bool,
@@ -234,22 +236,28 @@ pub struct TicketbookWalletSharesAsyncResponse {
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct BlindedWalletSharesResponse {
pub struct WebhookTicketbookWalletShares {
pub id: i64,
pub status: String,
pub device_id: String,
pub credential_id: String,
pub data: Option<TicketbookWalletSharesResponse>,
pub error_message: Option<String>,
pub created: String,
pub updated: String,
#[schemars(with = "String")]
#[serde(with = "time::serde::rfc3339")]
pub created: OffsetDateTime,
#[schemars(with = "String")]
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct WebhookBlindedSharesResponse {
pub blinded_shares: BlindedWalletSharesResponse,
pub struct WebhookTicketbookWalletSharesRequest {
pub ticketbook_wallet_shares: WebhookTicketbookWalletShares,
pub secret: String,
}
@@ -1,6 +1,6 @@
[package]
name = "nym-credential-proxy"
version = "0.1.1"
version = "0.1.3"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -0,0 +1,20 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
DROP TABLE blinded_shares;
CREATE TABLE blinded_shares
(
id INTEGER NOT NULL PRIMARY KEY,
-- removed reference to `ticketbook_deposit` as the deposit wouldn't actually have been made before the pending share is inserted
request_uuid TEXT NOT NULL,
status TEXT NOT NULL,
device_id TEXT NOT NULL,
credential_id TEXT NOT NULL,
available_shares INTEGER NOT NULL DEFAULT 0,
error_message TEXT DEFAULT NULL,
created TIMESTAMP WITHOUT TIME ZONE NOT NULL,
updated TIMESTAMP WITHOUT TIME ZONE NOT NULL
);
@@ -3,10 +3,12 @@
use crate::error::VpnApiError;
use crate::http::state::ApiState;
use crate::storage::models::BlindedShares;
use futures::{stream, StreamExt};
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest,
TicketbookWalletSharesResponse, WalletShare,
TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares,
WebhookTicketbookWalletSharesRequest,
};
use nym_credentials::IssuanceTicketBook;
use nym_credentials_interface::Base58;
@@ -217,11 +219,13 @@ async fn try_obtain_blinded_ticketbook_async_inner(
requested_on: OffsetDateTime,
request_data: TicketbookAsyncRequest,
params: TicketbookObtainQueryParams,
pending: &BlindedShares,
) -> Result<(), VpnApiError> {
let epoch_id = state.current_epoch_id().await?;
let device_id = &request_data.device_id;
let credential_id = &request_data.credential_id;
let secret = request_data.secret.clone();
// 1. try to obtain global data
let (
@@ -259,19 +263,70 @@ async fn try_obtain_blinded_ticketbook_async_inner(
error!(uuid = %request, "failed to update db with issued information: {err}")
}
// 4. build the response
let response = TicketbookWalletSharesResponse {
// 4. build the webhook request body
let data = Some(TicketbookWalletSharesResponse {
epoch_id,
shares,
master_verification_key,
aggregated_coin_index_signatures,
aggregated_expiration_date_signatures,
});
let ticketbook_wallet_shares = WebhookTicketbookWalletShares {
id: pending.id,
status: pending.status.to_string(),
device_id: device_id.clone(),
credential_id: credential_id.clone(),
data,
error_message: None,
created: pending.created,
updated: pending.updated,
};
let webhook_request = WebhookTicketbookWalletSharesRequest {
ticketbook_wallet_shares,
secret,
};
// 5. call the webhook
state
.zk_nym_web_hook()
.try_trigger(request, &response)
.try_trigger(request, &webhook_request)
.await;
Ok(())
}
async fn try_trigger_webhook_request_for_error(
state: &ApiState,
request: Uuid,
request_data: TicketbookAsyncRequest,
pending: &BlindedShares,
error_message: String,
) -> Result<(), VpnApiError> {
let device_id = &request_data.device_id;
let credential_id = &request_data.credential_id;
let secret = request_data.secret.clone();
let ticketbook_wallet_shares = WebhookTicketbookWalletShares {
id: pending.id,
status: "error".to_string(),
device_id: device_id.clone(),
credential_id: credential_id.clone(),
data: None,
error_message: Some(error_message),
created: pending.created,
updated: pending.updated,
};
let webhook_request = WebhookTicketbookWalletSharesRequest {
ticketbook_wallet_shares,
secret,
};
state
.zk_nym_web_hook()
.try_trigger(request, &webhook_request)
.await;
Ok(())
@@ -285,16 +340,30 @@ pub(crate) async fn try_obtain_blinded_ticketbook_async(
requested_on: OffsetDateTime,
request_data: TicketbookAsyncRequest,
params: TicketbookObtainQueryParams,
pending: BlindedShares,
) {
if let Err(err) = try_obtain_blinded_ticketbook_async_inner(
&state,
request,
requested_on,
request_data,
request_data.clone(),
params,
&pending,
)
.await
{
// post to the webhook to notify of errors on this side
if let Err(webhook_err) = try_trigger_webhook_request_for_error(
&state,
request,
request_data,
&pending,
format!("Failed to get ticketbook: {err}"),
)
.await
{
error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}")
}
error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}")
} else {
info!(uuid = %request, "managed to resolve the blinded ticketbook issuance")
@@ -84,8 +84,8 @@ pub(crate) struct ApiDoc;
api_requests::v1::ticketbook::models::WalletShare,
api_requests::v1::ticketbook::models::TicketbookWalletSharesResponse,
api_requests::v1::ticketbook::models::TicketbookWalletSharesAsyncResponse,
api_requests::v1::ticketbook::models::BlindedWalletSharesResponse,
api_requests::v1::ticketbook::models::WebhookBlindedSharesResponse,
api_requests::v1::ticketbook::models::WebhookTicketbookWalletShares,
api_requests::v1::ticketbook::models::WebhookTicketbookWalletSharesRequest,
api_requests::v1::ticketbook::models::TicketbookObtainQueryParams,
api_requests::v1::ticketbook::models::SharesQueryParams,
api_requests::v1::ticketbook::models::PlaceholderJsonSchemaImpl,
@@ -192,6 +192,7 @@ pub(crate) async fn obtain_ticketbook_shares_async(
}
Ok(pending) => pending,
};
let id = pending.id;
// 3. try to spawn a new task attempting to resolve the request
if state
@@ -201,6 +202,7 @@ pub(crate) async fn obtain_ticketbook_shares_async(
requested_on,
payload,
params,
pending,
))
.is_none()
{
@@ -213,10 +215,7 @@ pub(crate) async fn obtain_ticketbook_shares_async(
}
// 4. in the meantime, return the id to the user
Ok(output.to_response(TicketbookWalletSharesAsyncResponse {
id: pending.id,
uuid,
}))
Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid }))
}
/// Obtain the current value of the bandwidth voucher deposit
@@ -149,7 +149,7 @@ pub(crate) async fn query_for_shares_by_id(
(status = 401, description = "authentication token is missing or is invalid"),
(status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"),
),
params(OutputParams),
params(SharesQueryParams),
security(
("auth_token" = [])
)
@@ -82,7 +82,11 @@ impl SqliteStorageManager {
sqlx::query_as!(
MinimalWalletShare,
r#"
SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date
SELECT
t1.node_id as "node_id!",
t1.blinded_signature as "blinded_signature!",
t1.epoch_id as "epoch_id!",
t1.expiration_date as "expiration_date!"
FROM partial_blinded_wallet as t1
JOIN ticketbook_deposit as t2
on t1.corresponding_deposit = t2.deposit_id
-12
View File
@@ -5,18 +5,6 @@ WORKDIR /usr/src/nym/nym-data-observatory
RUN cargo build --release
#-------------------------------------------------------------------
# The following environment variables are required at runtime:
#
# NYM_DATA_OBSERVATORY_CONNECTION_URL
#
# And optionally:
#
# NYM_DATA_OBSERVATORY_HTTP_PORT
#
# see https://github.com/nymtech/nym/blob/develop/nym-data-observatory/src/main.rs for details
#-------------------------------------------------------------------
FROM ubuntu:24.04
RUN apt update && apt install -yy curl ca-certificates
+1 -7
View File
@@ -18,14 +18,8 @@ services:
dockerfile: nym-data-observatory/Dockerfile
container_name: nym-data-observatory
environment:
NYM_DATA_OBSERVATORY_CONNECTION_USERNAME: "postgres"
NYM_DATA_OBSERVATORY_CONNECTION_PASSWORD: "password"
NYM_DATA_OBSERVATORY_CONNECTION_HOST: "postgres"
NYM_DATA_OBSERVATORY_CONNECTION_PORT: "5432"
NYM_DATA_OBSERVATORY_CONNECTION_DB: ""
NYM_DATA_OBSERVATORY_CONNECTION_URL: "postgres://postgres:password@postgres:5432"
NYM_DATA_OBSERVATORY_HTTP_PORT: 8000
env_file:
- ../envs/qa.env
volumes:
pgdata:
+3 -1
View File
@@ -14,7 +14,9 @@ pub(crate) struct Storage {
}
impl Storage {
pub async fn init(connection_url: String) -> Result<Self> {
pub async fn init(connection_url: Option<String>) -> Result<Self> {
let connection_url =
connection_url.ok_or_else(|| anyhow!("Missing the connection url for database!"))?;
let connect_options =
PgConnectOptions::from_str(&connection_url)?.disable_statement_logging();
+4 -29
View File
@@ -18,25 +18,9 @@ struct Args {
#[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_ENV_FILE")]
env_file: Option<String>,
/// DB connection username
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_USERNAME")]
connection_username: String,
/// DB connection password
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_PASSWORD")]
connection_password: String,
/// DB connection host
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_HOST")]
connection_host: String,
/// DB connection port
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_PORT")]
connection_port: String,
/// DB connection database name
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_DB")]
connection_db: String,
/// DB connection url
#[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_URL")]
connection_url: Option<String>,
}
#[tokio::main]
@@ -47,16 +31,7 @@ async fn main() -> anyhow::Result<()> {
setup_env(args.env_file); // Defaults to mainnet if empty
let connection_url = format!(
"postgres://{}:{}@{}:{}/{}",
args.connection_username,
args.connection_password,
args.connection_host,
args.connection_port,
args.connection_db
);
let storage = db::Storage::init(connection_url).await?;
let storage = db::Storage::init(args.connection_url).await?;
let db_pool = storage.pool_owned().await;
tokio::spawn(async move {
background_task::spawn_in_background(db_pool).await;

Some files were not shown because too many files have changed in this diff Show More