Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 867acfef26 | |||
| e4996dc0ce | |||
| a6e23a210b | |||
| 88c4e0ce6c | |||
| 5a817e1df1 | |||
| a07a24db00 | |||
| a0cb812eff | |||
| 923c1fa184 | |||
| 35ea7e4926 | |||
| d1cb9afaf0 | |||
| 79d4b4b2e3 | |||
| 8460b33946 | |||
| ae6539e07c | |||
| 18cebdfedc | |||
| c448ec823a | |||
| a266137278 | |||
| 6f4dfd1dab | |||
| 57719787db | |||
| 29a57bf172 | |||
| db813b6e3e | |||
| 1be5ba310a | |||
| 41ff3f7824 | |||
| c9d4d62446 | |||
| e839a0d80e | |||
| cd61f930bf | |||
| edcf2b1204 |
@@ -114,13 +114,12 @@ where
|
|||||||
})?;
|
})?;
|
||||||
hardcoded_topology.entry_capable_nodes().cloned().collect()
|
hardcoded_topology.entry_capable_nodes().cloned().collect()
|
||||||
} else {
|
} else {
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
crate::init::helpers::gateways_for_init(
|
crate::init::helpers::gateways_for_init(
|
||||||
&mut rng,
|
|
||||||
&core.client.nym_api_urls,
|
&core.client.nym_api_urls,
|
||||||
user_agent,
|
user_agent,
|
||||||
core.debug.topology.minimum_gateway_performance,
|
core.debug.topology.minimum_gateway_performance,
|
||||||
core.debug.topology.ignore_ingress_epoch_role,
|
core.debug.topology.ignore_ingress_epoch_role,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -173,13 +173,12 @@ where
|
|||||||
})?;
|
})?;
|
||||||
hardcoded_topology.entry_capable_nodes().cloned().collect()
|
hardcoded_topology.entry_capable_nodes().cloned().collect()
|
||||||
} else {
|
} else {
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
crate::init::helpers::gateways_for_init(
|
crate::init::helpers::gateways_for_init(
|
||||||
&mut rng,
|
|
||||||
&core.client.nym_api_urls,
|
&core.client.nym_api_urls,
|
||||||
user_agent,
|
user_agent,
|
||||||
core.debug.topology.minimum_gateway_performance,
|
core.debug.topology.minimum_gateway_performance,
|
||||||
core.debug.topology.ignore_ingress_epoch_role,
|
core.debug.topology.ignore_ingress_epoch_role,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type WsConn = JSWebsocket;
|
|||||||
|
|
||||||
const CONCURRENT_GATEWAYS_MEASURED: usize = 20;
|
const CONCURRENT_GATEWAYS_MEASURED: usize = 20;
|
||||||
const MEASUREMENTS: usize = 3;
|
const MEASUREMENTS: usize = 3;
|
||||||
|
const DEFAULT_NYM_API_RETRIES: usize = 3;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
|
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||||
@@ -132,25 +133,27 @@ impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn gateways_for_init<R: Rng>(
|
pub async fn gateways_for_init(
|
||||||
rng: &mut R,
|
|
||||||
nym_apis: &[Url],
|
nym_apis: &[Url],
|
||||||
user_agent: Option<UserAgent>,
|
user_agent: Option<UserAgent>,
|
||||||
minimum_performance: u8,
|
minimum_performance: u8,
|
||||||
ignore_epoch_roles: bool,
|
ignore_epoch_roles: bool,
|
||||||
|
retry_count: Option<usize>,
|
||||||
) -> Result<Vec<RoutingNode>, ClientCoreError> {
|
) -> Result<Vec<RoutingNode>, ClientCoreError> {
|
||||||
let nym_api = nym_apis
|
// Build client with ALL URLs for fallback support
|
||||||
.choose(rng)
|
let nym_api_urls: Vec<nym_http_api_client::Url> = nym_apis
|
||||||
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
|
.iter()
|
||||||
|
.map(|url| nym_http_api_client::Url::from(url.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Use the unified HTTP client directly with optional user agent
|
if nym_api_urls.is_empty() {
|
||||||
let mut builder = nym_http_api_client::Client::builder(nym_api.clone())
|
return Err(ClientCoreError::ListOfNymApisIsEmpty);
|
||||||
.map_err(|e| {
|
}
|
||||||
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(
|
|
||||||
e,
|
let retry_count = retry_count.unwrap_or(DEFAULT_NYM_API_RETRIES);
|
||||||
))
|
let mut builder = nym_http_api_client::ClientBuilder::new_with_urls(nym_api_urls.clone())
|
||||||
})?
|
.with_retries(retry_count)
|
||||||
.with_bincode(); // Use bincode for better performance
|
.with_bincode();
|
||||||
|
|
||||||
if let Some(user_agent) = user_agent {
|
if let Some(user_agent) = user_agent {
|
||||||
builder = builder.with_user_agent(user_agent);
|
builder = builder.with_user_agent(user_agent);
|
||||||
@@ -160,7 +163,7 @@ pub async fn gateways_for_init<R: Rng>(
|
|||||||
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(e))
|
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
tracing::debug!("Fetching list of gateways from: {nym_api}");
|
tracing::debug!("Fetching list of gateways from: {:?}", nym_api_urls);
|
||||||
|
|
||||||
// Use our helper to handle pagination
|
// Use our helper to handle pagination
|
||||||
let gateways = get_all_basic_entry_nodes_with_metadata(&client, true)
|
let gateways = get_all_basic_entry_nodes_with_metadata(&client, true)
|
||||||
@@ -172,17 +175,15 @@ pub async fn gateways_for_init<R: Rng>(
|
|||||||
|
|
||||||
// filter out gateways below minimum performance and ones that could operate as a mixnode
|
// filter out gateways below minimum performance and ones that could operate as a mixnode
|
||||||
// (we don't want instability)
|
// (we don't want instability)
|
||||||
let valid_gateways = gateways
|
let valid_gateways: Vec<RoutingNode> = gateways
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|g| ignore_epoch_roles || !g.supported_roles.mixnode)
|
.filter(|g| ignore_epoch_roles || !g.supported_roles.mixnode)
|
||||||
.filter(|g| g.performance.round_to_integer() >= minimum_performance)
|
.filter(|g| g.performance.round_to_integer() >= minimum_performance)
|
||||||
.filter_map(|gateway| gateway.try_into().ok())
|
.filter_map(|gateway| gateway.try_into().ok())
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
tracing::debug!("After checking validity: {}", valid_gateways.len());
|
|
||||||
tracing::trace!("Valid gateways: {valid_gateways:#?}");
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"and {} after validity and performance filtering",
|
"Found {} valid gateways after filtering",
|
||||||
valid_gateways.len()
|
valid_gateways.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -345,13 +346,20 @@ pub(super) fn get_specified_gateway(
|
|||||||
must_use_tls: bool,
|
must_use_tls: bool,
|
||||||
) -> Result<RoutingNode, ClientCoreError> {
|
) -> Result<RoutingNode, ClientCoreError> {
|
||||||
tracing::debug!("Requesting specified gateway: {gateway_identity}");
|
tracing::debug!("Requesting specified gateway: {gateway_identity}");
|
||||||
|
|
||||||
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
|
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
|
||||||
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
|
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
|
||||||
|
|
||||||
let gateway = gateways
|
let gateway = gateways
|
||||||
.iter()
|
.iter()
|
||||||
.find(|gateway| gateway.identity_key == user_gateway)
|
.find(|gateway| gateway.identity_key == user_gateway)
|
||||||
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string()))?;
|
.ok_or_else(|| {
|
||||||
|
tracing::debug!(
|
||||||
|
"Gateway {gateway_identity} not found in {} available gateways",
|
||||||
|
gateways.len()
|
||||||
|
);
|
||||||
|
ClientCoreError::NoGatewayWithId(gateway_identity.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
let Some(entry_details) = gateway.entry.as_ref() else {
|
let Some(entry_details) = gateway.entry.as_ref() else {
|
||||||
return Err(ClientCoreError::UnsupportedEntry {
|
return Err(ClientCoreError::UnsupportedEntry {
|
||||||
@@ -414,3 +422,52 @@ pub(super) async fn register_with_gateway(
|
|||||||
authenticated_ephemeral_client: gateway_client,
|
authenticated_ephemeral_client: gateway_client,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_url_builds_without_retries() {
|
||||||
|
let urls = [Url::parse("https://api.nym.com").unwrap()];
|
||||||
|
|
||||||
|
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
|
||||||
|
.iter()
|
||||||
|
.map(|url| nym_http_api_client::Url::from(url.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(nym_api_urls.len(), 1, "Should have exactly one URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_urls_prepared_for_retries() {
|
||||||
|
let urls = vec![
|
||||||
|
Url::parse("https://api1.nym.com").unwrap(),
|
||||||
|
Url::parse("https://api2.nym.com").unwrap(),
|
||||||
|
Url::parse("https://api3.nym.com").unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
|
||||||
|
.iter()
|
||||||
|
.map(|url| nym_http_api_client::Url::from(url.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(nym_api_urls.len(), 3, "Should have all three URLs");
|
||||||
|
assert!(
|
||||||
|
nym_api_urls.len() > 1,
|
||||||
|
"Multiple URLs trigger retry behavior"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_url_list_is_detected() {
|
||||||
|
let urls: Vec<Url> = vec![];
|
||||||
|
|
||||||
|
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
|
||||||
|
.iter()
|
||||||
|
.map(|url| nym_http_api_client::Url::from(url.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert!(nym_api_urls.is_empty(), "Empty list should remain empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -183,6 +183,11 @@ impl Url {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the underlying URL
|
||||||
|
pub fn inner_url(&self) -> &url::Url {
|
||||||
|
&self.url
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if the URL has a front domain set
|
/// Returns true if the URL has a front domain set
|
||||||
pub fn has_front(&self) -> bool {
|
pub fn has_front(&self) -> bool {
|
||||||
if let Some(fronts) = &self.fronts {
|
if let Some(fronts) = &self.fronts {
|
||||||
@@ -201,6 +206,11 @@ impl Url {
|
|||||||
.and_then(|url| url.host_str())
|
.and_then(|url| url.host_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the fronts
|
||||||
|
pub fn fronts(&self) -> Option<&[url::Url]> {
|
||||||
|
self.fronts.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the string representation of the host (domain or IP address) for this URL, if any.
|
/// Return the string representation of the host (domain or IP address) for this URL, if any.
|
||||||
pub fn host_str(&self) -> Option<&str> {
|
pub fn host_str(&self) -> Option<&str> {
|
||||||
self.url.host_str()
|
self.url.host_str()
|
||||||
|
|||||||
@@ -160,13 +160,12 @@ pub async fn setup_gateway_from_api(
|
|||||||
minimum_performance: u8,
|
minimum_performance: u8,
|
||||||
ignore_epoch_roles: bool,
|
ignore_epoch_roles: bool,
|
||||||
) -> Result<InitialisationResult, WasmCoreError> {
|
) -> Result<InitialisationResult, WasmCoreError> {
|
||||||
let mut rng = thread_rng();
|
|
||||||
let gateways = gateways_for_init(
|
let gateways = gateways_for_init(
|
||||||
&mut rng,
|
|
||||||
nym_apis,
|
nym_apis,
|
||||||
None,
|
None,
|
||||||
minimum_performance,
|
minimum_performance,
|
||||||
ignore_epoch_roles,
|
ignore_epoch_roles,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
setup_gateway_wasm(client_store, force_tls, chosen_gateway, gateways).await
|
setup_gateway_wasm(client_store, force_tls, chosen_gateway, gateways).await
|
||||||
@@ -178,13 +177,12 @@ pub async fn current_gateways_wasm(
|
|||||||
minimum_performance: u8,
|
minimum_performance: u8,
|
||||||
ignore_epoch_roles: bool,
|
ignore_epoch_roles: bool,
|
||||||
) -> Result<Vec<RoutingNode>, ClientCoreError> {
|
) -> Result<Vec<RoutingNode>, ClientCoreError> {
|
||||||
let mut rng = thread_rng();
|
|
||||||
gateways_for_init(
|
gateways_for_init(
|
||||||
&mut rng,
|
|
||||||
nym_apis,
|
nym_apis,
|
||||||
user_agent,
|
user_agent,
|
||||||
minimum_performance,
|
minimum_performance,
|
||||||
ignore_epoch_roles,
|
ignore_epoch_roles,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ pub fn try_transfer_ownership(
|
|||||||
DEALERS_INDICES.save(deps.storage, &transfer_to, ¤t_index)?;
|
DEALERS_INDICES.save(deps.storage, &transfer_to, ¤t_index)?;
|
||||||
DEALERS_INDICES.remove(deps.storage, &info.sender);
|
DEALERS_INDICES.remove(deps.storage, &info.sender);
|
||||||
|
|
||||||
// update registration detail for every epoch the current dealer has participated in the protocol
|
// update registration detail and share information for every epoch the current dealer has participated in the protocol
|
||||||
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
|
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
|
||||||
// forbids that otherwise we'd have introduced inconsistency
|
// forbids that otherwise we'd have introduced inconsistency
|
||||||
for epoch_id in 0..=epoch.epoch_id {
|
for epoch_id in 0..=epoch.epoch_id {
|
||||||
@@ -118,6 +118,11 @@ pub fn try_transfer_ownership(
|
|||||||
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
|
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
|
||||||
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
|
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
|
||||||
}
|
}
|
||||||
|
if let Some(mut vk_share) = vk_shares().may_load(deps.storage, (&info.sender, epoch_id))? {
|
||||||
|
vk_shares().remove(deps.storage, (&info.sender, epoch_id))?;
|
||||||
|
vk_share.owner = transfer_to.clone();
|
||||||
|
vk_shares().save(deps.storage, (&transfer_to, epoch_id), &vk_share)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(transaction_info) = env.transaction else {
|
let Some(transaction_info) = env.transaction else {
|
||||||
@@ -262,6 +267,7 @@ mod tests_with_mock {
|
|||||||
contract.run_initial_dummy_dkg();
|
contract.run_initial_dummy_dkg();
|
||||||
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
|
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
|
||||||
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||||
|
let old_share = vk_shares().load(&contract, (&group_member, 0))?;
|
||||||
|
|
||||||
let not_group_member = contract.addr_make("not_group_member");
|
let not_group_member = contract.addr_make("not_group_member");
|
||||||
let (deps, env) = contract.deps_mut_env();
|
let (deps, env) = contract.deps_mut_env();
|
||||||
@@ -291,13 +297,20 @@ mod tests_with_mock {
|
|||||||
assert!(EPOCH_DEALERS_MAP
|
assert!(EPOCH_DEALERS_MAP
|
||||||
.may_load(&contract, (0, &group_member))?
|
.may_load(&contract, (0, &group_member))?
|
||||||
.is_none());
|
.is_none());
|
||||||
|
assert!(vk_shares()
|
||||||
|
.may_load(&contract, (&group_member, 0))?
|
||||||
|
.is_none());
|
||||||
|
|
||||||
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
|
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
|
||||||
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
|
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
|
||||||
|
let new_share = vk_shares().load(&contract, (&new_group_member, 0))?;
|
||||||
|
|
||||||
// the underlying info hasn't changed
|
// the underlying info hasn't changed
|
||||||
assert_eq!(old_index, new_index);
|
assert_eq!(old_index, new_index);
|
||||||
assert_eq!(old_details, new_details);
|
assert_eq!(old_details, new_details);
|
||||||
|
assert_ne!(old_share, new_share);
|
||||||
|
assert_eq!(old_share.owner, group_member);
|
||||||
|
assert_eq!(new_share.owner, new_group_member);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
OWNERSHIP_TRANSFER_LOG.load(
|
OWNERSHIP_TRANSFER_LOG.load(
|
||||||
|
|||||||
@@ -56,6 +56,53 @@ pub struct MixnetClientConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BuilderConfig {
|
impl BuilderConfig {
|
||||||
|
/// Creates a new BuilderConfig with all required parameters.
|
||||||
|
///
|
||||||
|
/// However, consider using `BuilderConfig::builder()` instead.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
entry_node: NymNodeWithKeys,
|
||||||
|
exit_node: NymNodeWithKeys,
|
||||||
|
data_path: Option<PathBuf>,
|
||||||
|
mixnet_client_config: MixnetClientConfig,
|
||||||
|
two_hops: bool,
|
||||||
|
user_agent: UserAgent,
|
||||||
|
custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
|
||||||
|
network_env: NymNetworkDetails,
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
#[cfg(unix)] connection_fd_callback: Arc<dyn Fn(RawFd) + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
entry_node,
|
||||||
|
exit_node,
|
||||||
|
data_path,
|
||||||
|
mixnet_client_config,
|
||||||
|
two_hops,
|
||||||
|
user_agent,
|
||||||
|
custom_topology_provider,
|
||||||
|
network_env,
|
||||||
|
cancel_token,
|
||||||
|
#[cfg(unix)]
|
||||||
|
connection_fd_callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a builder for BuilderConfig
|
||||||
|
///
|
||||||
|
/// This is the preferred way to construct a BuilderConfig.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```ignore
|
||||||
|
/// let config = BuilderConfig::builder()
|
||||||
|
/// .entry_node(entry)
|
||||||
|
/// .exit_node(exit)
|
||||||
|
/// .user_agent(agent)
|
||||||
|
/// .build()?;
|
||||||
|
/// ```
|
||||||
|
pub fn builder() -> BuilderConfigBuilder {
|
||||||
|
BuilderConfigBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mixnet_client_debug_config(&self) -> DebugConfig {
|
pub fn mixnet_client_debug_config(&self) -> DebugConfig {
|
||||||
if self.two_hops {
|
if self.two_hops {
|
||||||
two_hop_debug_config(&self.mixnet_client_config)
|
two_hop_debug_config(&self.mixnet_client_config)
|
||||||
@@ -116,6 +163,7 @@ impl BuilderConfig {
|
|||||||
.credentials_mode(true)
|
.credentials_mode(true)
|
||||||
.with_remember_me(remember_me)
|
.with_remember_me(remember_me)
|
||||||
.custom_topology_provider(self.custom_topology_provider);
|
.custom_topology_provider(self.custom_topology_provider);
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let builder = builder.with_connection_fd_callback(self.connection_fd_callback);
|
let builder = builder.with_connection_fd_callback(self.connection_fd_callback);
|
||||||
|
|
||||||
@@ -205,3 +253,205 @@ fn log_mixnet_client_config(debug_config: &DebugConfig) {
|
|||||||
fn true_to_disabled(val: bool) -> &'static str {
|
fn true_to_disabled(val: bool) -> &'static str {
|
||||||
if val { "disabled" } else { "enabled" }
|
if val { "disabled" } else { "enabled" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error type for BuilderConfig validation
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
pub enum BuilderConfigError {
|
||||||
|
#[error("entry_node is required")]
|
||||||
|
MissingEntryNode,
|
||||||
|
#[error("exit_node is required")]
|
||||||
|
MissingExitNode,
|
||||||
|
#[error("mixnet_client_config is required")]
|
||||||
|
MissingMixnetClientConfig,
|
||||||
|
#[error("user_agent is required")]
|
||||||
|
MissingUserAgent,
|
||||||
|
#[error("custom_topology_provider is required")]
|
||||||
|
MissingTopologyProvider,
|
||||||
|
#[error("network_env is required")]
|
||||||
|
MissingNetworkEnv,
|
||||||
|
#[error("cancel_token is required")]
|
||||||
|
MissingCancelToken,
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[error("connection_fd_callback is required")]
|
||||||
|
MissingConnectionFdCallback,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for `BuilderConfig`
|
||||||
|
///
|
||||||
|
/// This provides a more convenient way to construct a `BuilderConfig` compared to the
|
||||||
|
/// `new()` constructor with many arguments.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BuilderConfigBuilder {
|
||||||
|
entry_node: Option<NymNodeWithKeys>,
|
||||||
|
exit_node: Option<NymNodeWithKeys>,
|
||||||
|
data_path: Option<PathBuf>,
|
||||||
|
mixnet_client_config: Option<MixnetClientConfig>,
|
||||||
|
two_hops: bool,
|
||||||
|
user_agent: Option<UserAgent>,
|
||||||
|
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||||
|
network_env: Option<NymNetworkDetails>,
|
||||||
|
cancel_token: Option<CancellationToken>,
|
||||||
|
#[cfg(unix)]
|
||||||
|
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuilderConfigBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entry_node(mut self, entry_node: NymNodeWithKeys) -> Self {
|
||||||
|
self.entry_node = Some(entry_node);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_node(mut self, exit_node: NymNodeWithKeys) -> Self {
|
||||||
|
self.exit_node = Some(exit_node);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_path(mut self, data_path: Option<PathBuf>) -> Self {
|
||||||
|
self.data_path = data_path;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mixnet_client_config(mut self, mixnet_client_config: MixnetClientConfig) -> Self {
|
||||||
|
self.mixnet_client_config = Some(mixnet_client_config);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn two_hops(mut self, two_hops: bool) -> Self {
|
||||||
|
self.two_hops = two_hops;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_agent(mut self, user_agent: UserAgent) -> Self {
|
||||||
|
self.user_agent = Some(user_agent);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn custom_topology_provider(
|
||||||
|
mut self,
|
||||||
|
custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
self.custom_topology_provider = Some(custom_topology_provider);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn network_env(mut self, network_env: NymNetworkDetails) -> Self {
|
||||||
|
self.network_env = Some(network_env);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_token(mut self, cancel_token: CancellationToken) -> Self {
|
||||||
|
self.cancel_token = Some(cancel_token);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn connection_fd_callback(
|
||||||
|
mut self,
|
||||||
|
connection_fd_callback: Arc<dyn Fn(RawFd) + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
self.connection_fd_callback = Some(connection_fd_callback);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the `BuilderConfig`.
|
||||||
|
///
|
||||||
|
/// Returns an error if any required field is missing.
|
||||||
|
pub fn build(self) -> Result<BuilderConfig, BuilderConfigError> {
|
||||||
|
Ok(BuilderConfig {
|
||||||
|
entry_node: self
|
||||||
|
.entry_node
|
||||||
|
.ok_or(BuilderConfigError::MissingEntryNode)?,
|
||||||
|
exit_node: self.exit_node.ok_or(BuilderConfigError::MissingExitNode)?,
|
||||||
|
data_path: self.data_path,
|
||||||
|
mixnet_client_config: self
|
||||||
|
.mixnet_client_config
|
||||||
|
.ok_or(BuilderConfigError::MissingMixnetClientConfig)?,
|
||||||
|
two_hops: self.two_hops,
|
||||||
|
user_agent: self
|
||||||
|
.user_agent
|
||||||
|
.ok_or(BuilderConfigError::MissingUserAgent)?,
|
||||||
|
custom_topology_provider: self
|
||||||
|
.custom_topology_provider
|
||||||
|
.ok_or(BuilderConfigError::MissingTopologyProvider)?,
|
||||||
|
network_env: self
|
||||||
|
.network_env
|
||||||
|
.ok_or(BuilderConfigError::MissingNetworkEnv)?,
|
||||||
|
cancel_token: self
|
||||||
|
.cancel_token
|
||||||
|
.ok_or(BuilderConfigError::MissingCancelToken)?,
|
||||||
|
#[cfg(unix)]
|
||||||
|
connection_fd_callback: self
|
||||||
|
.connection_fd_callback
|
||||||
|
.ok_or(BuilderConfigError::MissingConnectionFdCallback)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixnet_client_config_default_values() {
|
||||||
|
let config = MixnetClientConfig::default();
|
||||||
|
assert!(!config.disable_poisson_rate);
|
||||||
|
assert!(!config.disable_background_cover_traffic);
|
||||||
|
assert_eq!(config.min_mixnode_performance, None);
|
||||||
|
assert_eq!(config.min_gateway_performance, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_builder_config_builder_fails_without_required_fields() {
|
||||||
|
// Building without any fields should fail with specific error
|
||||||
|
let result = BuilderConfig::builder().build();
|
||||||
|
assert!(result.is_err());
|
||||||
|
match result {
|
||||||
|
Err(BuilderConfigError::MissingEntryNode) => (), // Expected
|
||||||
|
Err(e) => panic!("Expected MissingEntryNode, got: {}", e),
|
||||||
|
Ok(_) => panic!("Expected error, got Ok"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_builder_config_builder_validates_all_required_fields() {
|
||||||
|
// Test that each required field is validated
|
||||||
|
let result = BuilderConfig::builder().build();
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Short-circuits at first missing field, so we just verify it's one of the expected errors
|
||||||
|
#[allow(unreachable_patterns)] // All variants are covered, but keeping catch-all for safety
|
||||||
|
match result {
|
||||||
|
Err(BuilderConfigError::MissingEntryNode)
|
||||||
|
| Err(BuilderConfigError::MissingExitNode)
|
||||||
|
| Err(BuilderConfigError::MissingMixnetClientConfig)
|
||||||
|
| Err(BuilderConfigError::MissingUserAgent)
|
||||||
|
| Err(BuilderConfigError::MissingTopologyProvider)
|
||||||
|
| Err(BuilderConfigError::MissingNetworkEnv)
|
||||||
|
| Err(BuilderConfigError::MissingCancelToken) => (),
|
||||||
|
#[cfg(unix)]
|
||||||
|
Err(BuilderConfigError::MissingConnectionFdCallback) => (),
|
||||||
|
Err(e) => panic!("Unexpected error: {}", e),
|
||||||
|
Ok(_) => panic!("Expected validation error, got Ok"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_builder_config_builder_method_chaining() {
|
||||||
|
// Test that builder methods chain properly and return Self
|
||||||
|
let builder = BuilderConfig::builder();
|
||||||
|
|
||||||
|
// Verify the builder returns itself for chaining
|
||||||
|
let builder = builder.two_hops(true);
|
||||||
|
let builder = builder.two_hops(false);
|
||||||
|
let builder = builder.data_path(None);
|
||||||
|
|
||||||
|
// Builder should still fail because required fields are missing
|
||||||
|
let result = builder.build();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -555,21 +555,22 @@ where
|
|||||||
async fn available_gateways(&mut self) -> Result<Vec<RoutingNode>, ClientCoreError> {
|
async fn available_gateways(&mut self) -> Result<Vec<RoutingNode>, ClientCoreError> {
|
||||||
if let Some(ref mut custom_provider) = self.custom_topology_provider {
|
if let Some(ref mut custom_provider) = self.custom_topology_provider {
|
||||||
if let Some(topology) = custom_provider.get_new_topology().await {
|
if let Some(topology) = custom_provider.get_new_topology().await {
|
||||||
return Ok(topology.entry_gateways().cloned().collect());
|
// Use entry_capable_nodes() instead of entry_gateways() to include
|
||||||
|
// all entry-capable nodes, not just actively assigned ones
|
||||||
|
return Ok(topology.entry_capable_nodes().cloned().collect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let nym_api_endpoints = self.get_api_endpoints();
|
let nym_api_endpoints = self.get_api_endpoints();
|
||||||
let topology_cfg = &self.config.debug_config.topology;
|
let topology_cfg = &self.config.debug_config.topology;
|
||||||
let user_agent = self.user_agent.clone();
|
let user_agent = self.user_agent.clone();
|
||||||
let mut rng = OsRng;
|
|
||||||
|
|
||||||
gateways_for_init(
|
gateways_for_init(
|
||||||
&mut rng,
|
|
||||||
&nym_api_endpoints,
|
&nym_api_endpoints,
|
||||||
user_agent,
|
user_agent,
|
||||||
topology_cfg.minimum_gateway_performance,
|
topology_cfg.minimum_gateway_performance,
|
||||||
topology_cfg.ignore_ingress_epoch_role,
|
topology_cfg.ignore_ingress_epoch_role,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -880,3 +881,17 @@ impl IncludedSurbs {
|
|||||||
Self::ExposeSelfAddress
|
Self::ExposeSelfAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixnet_builder_default_no_custom_client() {
|
||||||
|
let builder = MixnetClientBuilder::new_ephemeral();
|
||||||
|
assert!(
|
||||||
|
builder.build().is_ok(),
|
||||||
|
"Builder should succeed without custom client"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user