nym_sdk/mixnet/
socks5_discovery.rs1use std::collections::HashMap;
5
6use celes::Country;
7use rand::seq::SliceRandom;
8use tracing::{debug, info, warn};
9
10use nym_crypto::asymmetric::ed25519;
11use nym_sphinx::addressing::clients::Recipient;
12use nym_validator_client::nym_api::NymApiClientExt;
13
14use crate::ip_packet_client::discovery::create_nym_api_client;
15use crate::{Error, NymNetworkDetails};
16
17#[derive(Debug, Clone, Default)]
20pub enum NetworkRequesterSelector {
21 #[default]
23 Any,
24 InCountries(Vec<Country>),
27 Exact(Box<Recipient>),
29}
30
31impl NetworkRequesterSelector {
32 pub fn any() -> Self {
34 Self::Any
35 }
36
37 #[allow(clippy::result_large_err)]
42 pub fn in_countries<I, S>(codes: I) -> Result<Self, Error>
43 where
44 I: IntoIterator<Item = S>,
45 S: AsRef<str>,
46 {
47 let countries = codes
48 .into_iter()
49 .map(|c| {
50 Country::from_alpha2(c.as_ref())
51 .map_err(|_| Error::InvalidCountryCode(c.as_ref().to_string()))
52 })
53 .collect::<Result<Vec<_>, _>>()?;
54
55 if countries.is_empty() {
58 return Err(Error::NoCountriesSpecified);
59 }
60
61 Ok(Self::InCountries(countries))
62 }
63
64 #[allow(clippy::result_large_err)]
67 pub fn exact(address: impl AsRef<str>) -> Result<Self, Error> {
68 let recipient = address
69 .as_ref()
70 .parse()
71 .map_err(|_| Error::InvalidRecipientAddress(address.as_ref().to_string()))?;
72 Ok(Self::Exact(Box::new(recipient)))
73 }
74
75 pub async fn resolve(&self) -> Result<Recipient, Error> {
79 match self {
80 Self::Exact(addr) => Ok(**addr),
81 Self::Any => discover(&[]).await,
82 Self::InCountries(countries) if countries.is_empty() => {
85 Err(Error::NoCountriesSpecified)
86 }
87 Self::InCountries(countries) => discover(countries).await,
88 }
89 }
90}
91
92async fn discover(countries: &[Country]) -> Result<Recipient, Error> {
99 let nym_api_urls = NymNetworkDetails::new_mainnet()
100 .nym_api_urls
101 .ok_or(Error::NoNymAPIUrl)?;
102 let client = create_nym_api_client(nym_api_urls)?;
103 get_best_network_requester_in(client, countries).await
104}
105
106struct NetworkRequesterWithPerformance {
108 address: Recipient,
109 identity: ed25519::PublicKey,
110 performance: u8,
111 country: Option<Country>,
114}
115
116async fn retrieve_network_requesters_with_performance(
119 client: nym_http_api_client::Client,
120) -> Result<Vec<NetworkRequesterWithPerformance>, Error> {
121 let all_nodes = client
122 .get_all_described_nodes_v2()
123 .await?
124 .into_iter()
125 .map(|described| (described.ed25519_identity_key(), described))
126 .collect::<HashMap<_, _>>();
127
128 let basic_nodes = client.get_all_basic_nodes_with_metadata().await?.nodes;
129
130 let mut requesters = Vec::new();
131
132 for node_meta in basic_nodes {
133 let Some(node) = all_nodes.get(&node_meta.ed25519_identity_pubkey) else {
134 debug!(
138 "{} has no described-node record; skipping",
139 node_meta.ed25519_identity_pubkey
140 );
141 continue;
142 };
143
144 let Some(nr_info) = node.description.network_requester.clone() else {
145 continue;
146 };
147
148 match nr_info.address.parse() {
149 Ok(parsed_address) => requesters.push(NetworkRequesterWithPerformance {
150 address: parsed_address,
151 identity: node_meta.ed25519_identity_pubkey,
152 performance: node_meta.performance.round_to_integer(),
153 country: node.description.auxiliary_details.location,
154 }),
155 Err(err) => warn!(
159 "{} advertises an unparseable network requester address {:?}: {err}; skipping",
160 node_meta.ed25519_identity_pubkey, nr_info.address
161 ),
162 }
163 }
164
165 Ok(requesters)
166}
167
168async fn get_best_network_requester_in(
173 client: nym_http_api_client::Client,
174 countries: &[Country],
175) -> Result<Recipient, Error> {
176 let requesters = retrieve_network_requesters_with_performance(client).await?;
177 let total = requesters.len();
178
179 let pool: Vec<NetworkRequesterWithPerformance> = if countries.is_empty() {
180 requesters
181 } else {
182 requesters
183 .into_iter()
184 .filter(|nr| match nr.country {
185 Some(c) => countries
186 .iter()
187 .any(|want| want.alpha2.eq_ignore_ascii_case(c.alpha2)),
188 None => false,
189 })
190 .collect()
191 };
192
193 info!(
194 "Found {} network requesters ({} after country filter)",
195 total,
196 pool.len()
197 );
198
199 if pool.is_empty() {
200 return Err(if countries.is_empty() {
201 Error::NoGatewayAvailable
202 } else {
203 Error::NoGatewayInCountries
204 });
205 }
206
207 let mut rng = rand::thread_rng();
210 let selected = pool
211 .choose_weighted(&mut rng, |nr| nr.performance as f64)
212 .or_else(|_| pool.choose(&mut rng).ok_or(Error::NoGatewayAvailable))?;
213
214 info!(
215 "Using network requester: {} (Gateway: {}, Country: {:?}, Performance: {:?})",
216 selected.address,
217 selected.identity,
218 selected.country.map(|c| c.alpha2),
219 selected.performance
220 );
221
222 Ok(selected.address)
223}