Files
nym/nym-api/src/node_describe_cache/query_helpers.rs
T

278 lines
10 KiB
Rust

// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::node_describe_cache::NodeDescribeCacheError;
use futures::future::{maybe_done, MaybeDone};
use futures::{FutureExt, TryFutureExt};
use nym_api_requests::models::described::type_translation::LewesProtocolDetailsV1;
use nym_api_requests::models::described::v2::{
AuthenticatorDetailsV2, DeclaredRolesV2, HostInformationV2, IpPacketRouterDetailsV2,
NetworkRequesterDetailsV2, NymNodeDataV2, WebSocketsV2, WireguardDetailsV2,
};
use nym_api_requests::models::described::v3::NymNodeAuxiliaryDetailsV3;
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_config::defaults::mainnet;
use nym_mixnet_contract_common::NodeId;
use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt};
use nym_node_requests::api::Client;
use pin_project::pin_project;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use time::OffsetDateTime;
use tracing::debug;
async fn network_requester_future(
client: &Client,
) -> Result<Option<NetworkRequesterDetailsV2>, NymNodeApiClientError> {
let Ok(nr) = client.get_network_requester().await else {
return Ok(None);
};
client.get_exit_policy().await.map(|exit_policy| {
let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL;
Some(NetworkRequesterDetailsV2 {
address: nr.address,
uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy,
})
})
}
// try v2 first; nodes that haven't been upgraded yet won't expose it, so fall back to v1
// (the v1 path yields no chain address).
async fn auxiliary_details_future(client: &Client, node_id: NodeId) -> NymNodeAuxiliaryDetailsV3 {
if let Ok(v2) = client.get_auxiliary_details_v2().await {
return v2.into();
}
client
.get_auxiliary_details()
.await
.inspect_err(|err| {
debug!("could not obtain auxiliary details of node {node_id}: {err} is it running an old version?")
})
.map(Into::into)
.unwrap_or_default()
}
pub(crate) async fn query_for_described_data(
client: &Client,
node_id: NodeId,
) -> Result<UnwrappedResolvedNodeDescribedInfo, NodeDescribeCacheError> {
let map_query_err = |source| NodeDescribeCacheError::ApiFailure {
node_id,
source: Box::new(source),
};
// all of those should be happening concurrently.
NodeDescribedInfoMegaFuture::new(
client.get_build_information().map_err(map_query_err),
client.get_roles().ok_into().map_err(map_query_err),
auxiliary_details_future(client, node_id),
client.get_mixnet_websockets().ok_into().map_err(map_query_err),
network_requester_future(client).map_err(map_query_err),
// `ok_into` ultimately calls `IpPacketRouter::into` to transform it into `IpPacketRouterDetails`
client.get_ip_packet_router().ok_into().map(Result::ok),
client.get_authenticator().ok_into().map(Result::ok),
client.get_wireguard().ok_into().map(Result::ok),
client.get_lewes_protocol().inspect_err(|err| {
// old nym-nodes will not have this field, so use the default instead
debug!("could not obtain lewes protocol information of node {node_id}: {err} is it running an old version?")
}).ok_into().map(Result::ok)
)
.await
}
// just a helper to have named fields as opposed to a mega tuple
// could I have used something more sophisticated? sure.
// is this code disgusting? yes. does it work? also yes
// (note: I've just mostly copied code from `futures-util::generate` macro where
// they derive code for `join2`, `join3`, etc.)
#[pin_project]
struct NodeDescribedInfoMegaFuture<F1, F2, F3, F4, F5, F6, F7, F8, F9>
where
F1: Future,
F2: Future,
F3: Future,
F4: Future,
F5: Future,
F6: Future,
F7: Future,
F8: Future,
F9: Future,
{
#[pin]
build_info: MaybeDone<F1>,
#[pin]
roles: MaybeDone<F2>,
#[pin]
auxiliary_details: MaybeDone<F3>,
#[pin]
websockets: MaybeDone<F4>,
#[pin]
network_requester: MaybeDone<F5>,
#[pin]
ipr: MaybeDone<F6>,
#[pin]
authenticator: MaybeDone<F7>,
#[pin]
wireguard: MaybeDone<F8>,
#[pin]
lewes_protocol: MaybeDone<F9>,
}
impl<F1, F2, F3, F4, F5, F6, F7, F8, F9> Future
for NodeDescribedInfoMegaFuture<F1, F2, F3, F4, F5, F6, F7, F8, F9>
where
F1: Future<Output = Result<BinaryBuildInformationOwned, NodeDescribeCacheError>>,
F2: Future<Output = Result<DeclaredRolesV2, NodeDescribeCacheError>>,
F3: Future<Output = NymNodeAuxiliaryDetailsV3>,
F4: Future<Output = Result<WebSocketsV2, NodeDescribeCacheError>>,
F5: Future<Output = Result<Option<NetworkRequesterDetailsV2>, NodeDescribeCacheError>>,
F6: Future<Output = Option<IpPacketRouterDetailsV2>>,
F7: Future<Output = Option<AuthenticatorDetailsV2>>,
F8: Future<Output = Option<WireguardDetailsV2>>,
F9: Future<Output = Option<LewesProtocolDetailsV1>>,
{
type Output = Result<UnwrappedResolvedNodeDescribedInfo, NodeDescribeCacheError>;
// SAFETY: we've explicitly checked all futures have completed thus the unwraps are fine
#[allow(clippy::unwrap_used)]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut all_done = true;
let mut futures = self.project();
all_done &= futures.build_info.as_mut().poll(cx).is_ready();
all_done &= futures.roles.as_mut().poll(cx).is_ready();
all_done &= futures.auxiliary_details.as_mut().poll(cx).is_ready();
all_done &= futures.websockets.as_mut().poll(cx).is_ready();
all_done &= futures.network_requester.as_mut().poll(cx).is_ready();
all_done &= futures.ipr.as_mut().poll(cx).is_ready();
all_done &= futures.authenticator.as_mut().poll(cx).is_ready();
all_done &= futures.wireguard.as_mut().poll(cx).is_ready();
all_done &= futures.lewes_protocol.as_mut().poll(cx).is_ready();
if all_done {
Poll::Ready(
ResolvedNodeDescribedInfo {
build_info: futures.build_info.take_output().unwrap(),
roles: futures.roles.take_output().unwrap(),
auxiliary_details: futures.auxiliary_details.take_output().unwrap(),
websockets: futures.websockets.take_output().unwrap(),
network_requester: futures.network_requester.take_output().unwrap(),
ipr: futures.ipr.take_output().unwrap(),
authenticator: futures.authenticator.take_output().unwrap(),
wireguard: futures.wireguard.take_output().unwrap(),
lewes_protocol: futures.lewes_protocol.take_output().unwrap(),
}
.try_unwrap(),
)
} else {
Poll::Pending
}
}
}
impl<F1, F2, F3, F4, F5, F6, F7, F8, F9>
NodeDescribedInfoMegaFuture<F1, F2, F3, F4, F5, F6, F7, F8, F9>
where
F1: Future,
F2: Future,
F3: Future,
F4: Future,
F5: Future,
F6: Future,
F7: Future,
F8: Future,
F9: Future,
{
// okay. the fact I have to bypass clippy here means it wasn't a good idea to create this abomination after all
#[allow(clippy::too_many_arguments)]
fn new(
build_info: F1,
roles: F2,
auxiliary_details: F3,
websockets: F4,
network_requester: F5,
ipr: F6,
authenticator: F7,
wireguard: F8,
lewes_protocol: F9,
) -> Self {
NodeDescribedInfoMegaFuture {
build_info: maybe_done(build_info),
roles: maybe_done(roles),
auxiliary_details: maybe_done(auxiliary_details),
websockets: maybe_done(websockets),
network_requester: maybe_done(network_requester),
ipr: maybe_done(ipr),
authenticator: maybe_done(authenticator),
wireguard: maybe_done(wireguard),
lewes_protocol: maybe_done(lewes_protocol),
}
}
}
struct ResolvedNodeDescribedInfo {
build_info: Result<BinaryBuildInformationOwned, NodeDescribeCacheError>,
roles: Result<DeclaredRolesV2, NodeDescribeCacheError>,
// TODO: in the future make it return a Result as well.
auxiliary_details: NymNodeAuxiliaryDetailsV3,
websockets: Result<WebSocketsV2, NodeDescribeCacheError>,
network_requester: Result<Option<NetworkRequesterDetailsV2>, NodeDescribeCacheError>,
ipr: Option<IpPacketRouterDetailsV2>,
authenticator: Option<AuthenticatorDetailsV2>,
wireguard: Option<WireguardDetailsV2>,
lewes_protocol: Option<LewesProtocolDetailsV1>,
}
impl ResolvedNodeDescribedInfo {
fn try_unwrap(self) -> Result<UnwrappedResolvedNodeDescribedInfo, NodeDescribeCacheError> {
Ok(UnwrappedResolvedNodeDescribedInfo {
build_info: self.build_info?,
roles: self.roles?,
auxiliary_details: self.auxiliary_details,
websockets: self.websockets?,
network_requester: self.network_requester?,
ipr: self.ipr,
authenticator: self.authenticator,
wireguard: self.wireguard,
lewes_protocol: self.lewes_protocol,
})
}
}
#[derive(Debug)]
pub(crate) struct UnwrappedResolvedNodeDescribedInfo {
pub(crate) build_info: BinaryBuildInformationOwned,
pub(crate) roles: DeclaredRolesV2,
pub(crate) auxiliary_details: NymNodeAuxiliaryDetailsV3,
pub(crate) websockets: WebSocketsV2,
pub(crate) network_requester: Option<NetworkRequesterDetailsV2>,
pub(crate) ipr: Option<IpPacketRouterDetailsV2>,
pub(crate) authenticator: Option<AuthenticatorDetailsV2>,
pub(crate) wireguard: Option<WireguardDetailsV2>,
pub(crate) lewes_protocol: Option<LewesProtocolDetailsV1>,
}
impl UnwrappedResolvedNodeDescribedInfo {
pub(crate) fn into_node_description(
self,
host_info: impl Into<HostInformationV2>,
) -> NymNodeDataV2 {
NymNodeDataV2 {
host_information: host_info.into(),
last_polled: OffsetDateTime::now_utc().into(),
build_information: self.build_info,
network_requester: self.network_requester,
ip_packet_router: self.ipr,
authenticator: self.authenticator,
wireguard: self.wireguard,
lewes_protocol: self.lewes_protocol,
mixnet_websockets: self.websockets,
auxiliary_details: self.auxiliary_details.into(),
declared_role: self.roles,
}
}
}