Implement topology fetching for nym-lp-speedtest

- Add topology.rs with NymTopology integration
- Fetch mix nodes and gateways from nym-api
- Build GatewayInfo with LP addresses (port 41264)
- Provide random_route_to_gateway() for Sphinx routing
- Add required Cargo.toml dependencies
This commit is contained in:
durch
2025-12-22 18:08:48 +01:00
committed by Jędrzej Stuczyński
parent 76a5d5048f
commit 7ab5e31b3c
3 changed files with 210 additions and 9 deletions
Generated
+4
View File
@@ -6711,7 +6711,9 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"nym-api-requests",
"nym-crypto",
"nym-http-api-client",
"nym-kcp",
"nym-lp",
"nym-sphinx",
@@ -6719,12 +6721,14 @@ dependencies = [
"nym-sphinx-anonymous-replies",
"nym-sphinx-framing",
"nym-sphinx-params",
"nym-sphinx-types",
"nym-topology",
"nym-validator-client",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"serde_json",
"time",
"tokio",
"tokio-util",
"tracing",
+4
View File
@@ -16,6 +16,7 @@ rand = { workspace = true }
rand_chacha = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true, features = ["codec"] }
tracing = { workspace = true }
@@ -23,7 +24,9 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] }
url = { workspace = true }
# Nym crates
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-crypto = { path = "../../common/crypto" }
nym-http-api-client = { path = "../../common/http-api-client" }
nym-kcp = { path = "../../common/nym-kcp" }
nym-lp = { path = "../../common/nym-lp" }
nym-sphinx = { path = "../../common/nymsphinx" }
@@ -31,5 +34,6 @@ nym-sphinx-framing = { path = "../../common/nymsphinx/framing", features = ["no-
nym-sphinx-anonymous-replies = { path = "../../common/nymsphinx/anonymous-replies" }
nym-sphinx-addressing = { path = "../../common/nymsphinx/addressing" }
nym-sphinx-params = { path = "../../common/nymsphinx/params" }
nym-sphinx-types = { path = "../../common/nymsphinx/types" }
nym-topology = { path = "../../common/topology" }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
+202 -9
View File
@@ -3,15 +3,208 @@
//! Queries nym-api for active mix nodes and gateways,
//! builds routes for Sphinx packet construction.
use anyhow::Result;
use anyhow::{Context, Result, anyhow, bail};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_crypto::asymmetric::ed25519;
use nym_http_api_client::UserAgent;
use nym_sphinx_types::Node as SphinxNode;
use nym_topology::{NymTopology, NymTopologyMetadata};
use nym_validator_client::nym_api::NymApiClientExt;
use rand::prelude::IteratorRandom;
use rand::{CryptoRng, Rng};
use std::net::SocketAddr;
use tracing::{debug, info};
use url::Url;
/// Fetch network topology from nym-api
pub async fn fetch_topology(_nym_api: &Url) -> Result<()> {
// TODO: Implement
// 1. Query /v1/mixnodes/active for mix nodes
// 2. Query /v1/gateways/described for gateways
// 3. Build TopologyNode structures
// 4. Select random 4-hop route
Ok(())
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Gateway information for LP connection
#[derive(Debug, Clone)]
pub struct GatewayInfo {
pub identity: ed25519::PublicKey,
pub sphinx_key: nym_crypto::asymmetric::x25519::PublicKey,
/// Mix host (IP:port for Sphinx mixing)
pub mix_host: SocketAddr,
/// LP control address (IP:41264)
pub lp_address: SocketAddr,
}
/// Topology for routing Sphinx packets
pub struct SpeedtestTopology {
topology: NymTopology,
/// Entry gateways available for LP connection
gateways: Vec<GatewayInfo>,
}
impl SpeedtestTopology {
/// Fetch network topology from nym-api
pub async fn fetch(nym_api: &Url) -> Result<Self> {
info!("Fetching topology from {}", nym_api);
let user_agent = UserAgent {
application: "nym-lp-speedtest".to_string(),
version: VERSION.to_string(),
platform: std::env::consts::OS.to_string(),
git_commit: "unknown".to_string(),
};
let api_client = nym_http_api_client::Client::builder(nym_api.clone())
.context("malformed nym api url")?
.with_user_agent(user_agent)
.build()
.context("failed to build nym api client")?;
// Fetch mixing nodes in active set
debug!("Fetching active mixing nodes...");
let mixing_nodes = api_client
.get_all_basic_active_mixing_assigned_nodes_with_metadata()
.await
.context("failed to fetch mixing nodes")?;
info!(
"Fetched {} mixing nodes",
mixing_nodes.nodes.len()
);
// Fetch entry gateways
debug!("Fetching entry gateways...");
let entry_gateways = api_client
.get_all_basic_entry_assigned_nodes_with_metadata()
.await
.context("failed to fetch entry gateways")?;
info!(
"Fetched {} entry gateways",
entry_gateways.nodes.len()
);
// Get rewarded set info
debug!("Fetching rewarded set...");
let rewarded_set = api_client
.get_rewarded_set()
.await
.context("failed to fetch rewarded set")?;
// Build NymTopology
let metadata = NymTopologyMetadata::new(
mixing_nodes.metadata.rotation_id,
rewarded_set.epoch_id,
time::OffsetDateTime::now_utc(),
);
// Convert RewardedSetResponse -> EpochRewardedSet (impl Into<CachedEpochRewardedSet>)
let epoch_rewarded_set: nym_topology::EpochRewardedSet = rewarded_set.into();
let mut topology = NymTopology::new(metadata, epoch_rewarded_set, vec![]);
// Add mixing nodes
topology.add_skimmed_nodes(&mixing_nodes.nodes);
// Add entry gateways
topology.add_skimmed_nodes(&entry_gateways.nodes);
// Extract gateway info for LP connections
let gateways = entry_gateways
.nodes
.iter()
.filter_map(|node| gateway_info_from_skimmed(node).ok())
.collect::<Vec<_>>();
if gateways.is_empty() {
bail!("No entry gateways available for LP connection");
}
info!("Built topology with {} usable gateways", gateways.len());
Ok(SpeedtestTopology { topology, gateways })
}
/// Get a specific gateway by identity string
pub fn gateway_by_identity(&self, identity: &str) -> Result<&GatewayInfo> {
let identity_key: ed25519::PublicKey = identity
.parse()
.context("invalid gateway identity")?;
self.gateways
.iter()
.find(|g| g.identity == identity_key)
.ok_or_else(|| anyhow!("gateway {} not found in topology", identity))
}
/// Select a random entry gateway
pub fn random_gateway<R: Rng + ?Sized>(&self, rng: &mut R) -> Result<&GatewayInfo> {
self.gateways
.iter()
.choose(rng)
.ok_or_else(|| anyhow!("no gateways available"))
}
/// Build a random 3-hop route through the mixnet to the given destination gateway.
/// Returns (route, destination_sphinx_node) where route has 3 mix nodes.
pub fn random_route_to_gateway<R: Rng + CryptoRng + ?Sized>(
&self,
rng: &mut R,
gateway: &GatewayInfo,
) -> Result<Vec<SphinxNode>> {
// Build route to the gateway's identity
let route = self
.topology
.random_route_to_egress(rng, gateway.identity.into(), true)
.context("failed to build route to gateway")?;
if route.is_empty() {
bail!("empty route returned from topology");
}
Ok(route)
}
/// Get number of available gateways
pub fn gateway_count(&self) -> usize {
self.gateways.len()
}
/// Get all gateways
pub fn gateways(&self) -> &[GatewayInfo] {
&self.gateways
}
}
/// Extract gateway info for LP connections from a SkimmedNode
fn gateway_info_from_skimmed(node: &SkimmedNode) -> Result<GatewayInfo> {
let first_ip = node
.ip_addresses
.first()
.ok_or_else(|| anyhow!("node has no IP addresses"))?;
// LP default control port
const LP_CONTROL_PORT: u16 = 41264;
Ok(GatewayInfo {
identity: node.ed25519_identity_pubkey,
sphinx_key: node.x25519_sphinx_pubkey,
mix_host: SocketAddr::new(*first_ip, node.mix_port),
lp_address: SocketAddr::new(*first_ip, LP_CONTROL_PORT),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "requires network access"]
async fn test_fetch_topology() {
let nym_api = Url::parse("https://validator.nymtech.net/api").unwrap();
let topology = SpeedtestTopology::fetch(&nym_api).await.unwrap();
assert!(topology.gateway_count() > 0);
println!("Found {} gateways", topology.gateway_count());
let mut rng = rand::rng();
let gateway = topology.random_gateway(&mut rng).unwrap();
println!("Selected gateway: {:?}", gateway.identity);
let route = topology.random_route_to_gateway(&mut rng, gateway).unwrap();
println!("Route has {} hops", route.len());
}
}