From 5eb8737b0dbaada16f05155263bfa71f98cd3ca3 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Wed, 25 Mar 2026 15:28:37 +0000 Subject: [PATCH 1/2] fix: select store quotes by XOR distance instead of response speed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous quote collection used FuturesUnordered and took the first 5 responses. This systematically excluded nodes behind NAT because cloud nodes always respond faster, causing the NATed node's quote to arrive 6th and be dropped — even when it was the 2nd closest peer by XOR distance. Now collects all quote responses, sorts by XOR distance to the chunk address, and takes the closest REQUIRED_QUOTES (5) peers. This ensures deterministic, distance-based placement where every reachable node gets a fair chance regardless of network latency. Co-Authored-By: Claude Opus 4.6 (1M context) --- ant-core/src/data/client/quote.rs | 37 ++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/ant-core/src/data/client/quote.rs b/ant-core/src/data/client/quote.rs index aefe292..e3b49e6 100644 --- a/ant-core/src/data/client/quote.rs +++ b/ant-core/src/data/client/quote.rs @@ -126,18 +126,22 @@ impl Client { quote_futures.push(quote_future); } - // Collect quotes as they complete - let mut quotes_with_peers = Vec::with_capacity(REQUIRED_QUOTES); + // Collect all quote responses (don't short-circuit on the first 5). + // + // The previous first-5-wins approach caused nodes behind NAT to be + // systematically excluded: cloud nodes always respond faster, so the + // NATed node's quote would arrive 6th and be dropped. By collecting + // all responses and then selecting the closest by XOR distance, every + // reachable node has a fair chance of being included. + let mut all_quotes = Vec::with_capacity(remote_peers.len()); let mut already_stored_count = 0usize; let mut failures: Vec = Vec::new(); + let total_peers = remote_peers.len(); while let Some((peer_id, addrs, quote_result)) = quote_futures.next().await { match quote_result { Ok((quote, price)) => { - quotes_with_peers.push((peer_id, addrs, quote, price)); - if quotes_with_peers.len() >= REQUIRED_QUOTES { - break; - } + all_quotes.push((peer_id, addrs, quote, price)); } Err(Error::AlreadyStored) => { already_stored_count += 1; @@ -148,10 +152,27 @@ impl Client { failures.push(format!("{peer_id}: {e}")); } } + + // Once every peer has responded (or failed), stop waiting. + let responded = all_quotes.len() + already_stored_count + failures.len(); + if responded >= total_peers { + break; + } } - // If we collected enough quotes, proceed with payment regardless - // of how many peers reported already_stored. + // Sort by XOR distance to the chunk address (closest first), then + // take the REQUIRED_QUOTES closest. This ensures deterministic, + // distance-based selection rather than speed-based racing. + all_quotes.sort_by_key(|(peer_id, _, _, _)| { + let peer_bytes = peer_id.as_bytes(); + let mut distance = [0u8; 32]; + for i in 0..32 { + distance[i] = peer_bytes[i] ^ address[i]; + } + distance + }); + let quotes_with_peers: Vec<_> = all_quotes.into_iter().take(REQUIRED_QUOTES).collect(); + if quotes_with_peers.len() >= REQUIRED_QUOTES { info!( "Collected {} quotes for address {}", From 18b1394a8b121ee34ad21a34d8ae7712cc246c29 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Wed, 25 Mar 2026 15:30:23 +0000 Subject: [PATCH 2/2] fix: force process exit to prevent tokio runtime shutdown hang The tokio runtime's graceful shutdown blocks indefinitely on open QUIC connections and pending background tasks (DHT, keep-alive). Restructure main() to call std::process::exit() after all work completes, for both success (exit 0) and error (exit 1) paths. All data is persisted/printed before the exit point, so nothing is lost. This is essential for continuous upload workflows where the uploader script needs the ant process to exit promptly so the next upload can begin. Co-Authored-By: Claude Opus 4.6 (1M context) --- ant-cli/src/main.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ant-cli/src/main.rs b/ant-cli/src/main.rs index 2c3bd0e..0b02164 100644 --- a/ant-cli/src/main.rs +++ b/ant-cli/src/main.rs @@ -16,7 +16,23 @@ use ant_core::data::{ use cli::{Cli, Commands}; #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() { + let code = match run().await { + Ok(()) => 0, + Err(e) => { + eprintln!("Error: {e:?}"); + 1 + } + }; + + // Force-exit to avoid hanging on tokio runtime shutdown. + // Open QUIC connections and pending background tasks (DHT, keep-alive) + // block the runtime's graceful shutdown indefinitely. All data has been + // persisted / printed by this point, so there is nothing left to clean up. + std::process::exit(code); +} + +async fn run() -> anyhow::Result<()> { let cli = Cli::parse(); // Initialize tracing for data commands (node commands handle their own output)