Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ path = "src/bin/saorsa-client/main.rs"

[dependencies]
# Core (provides EVERYTHING: networking, DHT, security, trust, storage)
saorsa-core = "0.10.4"
saorsa-core = "0.11.0"
saorsa-pqc = "0.4.0"

# Payment verification - autonomi network lookup + EVM payment
Expand Down
75 changes: 75 additions & 0 deletions src/client/data_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ pub fn compute_address(content: &[u8]) -> XorName {
address
}

/// Compute the XOR distance between two 32-byte addresses.
///
/// Lexicographic comparison of the result gives correct Kademlia distance ordering.
#[must_use]
pub fn xor_distance(a: &XorName, b: &XorName) -> XorName {
let mut result = [0u8; 32];
for i in 0..32 {
result[i] = a[i] ^ b[i];
}
result
}

/// Convert a hex-encoded peer ID string to an `XorName`.
///
/// Returns `None` if the string is not valid hex or is not exactly 32 bytes (64 hex chars).
#[must_use]
pub fn peer_id_to_xor_name(peer_id: &str) -> Option<XorName> {
let bytes = hex::decode(peer_id).ok()?;
if bytes.len() != 32 {
return None;
}
let mut name = [0u8; 32];
name.copy_from_slice(&bytes);
Some(name)
}

/// A content-addressed identifier (32 bytes).
///
/// The address is computed as SHA256(content) for chunks,
Expand Down Expand Up @@ -118,6 +144,55 @@ mod tests {
assert!(chunk.verify());
}

#[test]
fn test_xor_distance_identity() {
let a = [0xAB; 32];
assert_eq!(xor_distance(&a, &a), [0u8; 32]);
}

#[test]
fn test_xor_distance_symmetry() {
let a = [0x01; 32];
let b = [0xFF; 32];
assert_eq!(xor_distance(&a, &b), xor_distance(&b, &a));
}

#[test]
fn test_xor_distance_known_values() {
let a = [0x00; 32];
let b = [0xFF; 32];
assert_eq!(xor_distance(&a, &b), [0xFF; 32]);

let mut c = [0x00; 32];
c[0] = 0x80;
let mut expected = [0x00; 32];
expected[0] = 0x80;
assert_eq!(xor_distance(&a, &c), expected);
}

#[test]
fn test_peer_id_to_xor_name_valid() {
let hex_str = "ab".repeat(32);
let result = peer_id_to_xor_name(&hex_str);
assert_eq!(result, Some([0xAB; 32]));
}

#[test]
fn test_peer_id_to_xor_name_invalid_hex() {
assert_eq!(peer_id_to_xor_name("not_hex_at_all!"), None);
}

#[test]
fn test_peer_id_to_xor_name_wrong_length() {
// 16 bytes instead of 32
let short = "ab".repeat(16);
assert_eq!(peer_id_to_xor_name(&short), None);

// 33 bytes
let long = "ab".repeat(33);
assert_eq!(peer_id_to_xor_name(&long), None);
}

#[test]
fn test_chunk_verify() {
// Valid chunk
Expand Down
4 changes: 3 additions & 1 deletion src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,7 @@ mod data_types;
mod quantum;

pub use chunk_protocol::send_and_await_chunk_response;
pub use data_types::{compute_address, ChunkStats, DataChunk, XorName};
pub use data_types::{
compute_address, peer_id_to_xor_name, xor_distance, ChunkStats, DataChunk, XorName,
};
pub use quantum::{QuantumClient, QuantumConfig};
47 changes: 37 additions & 10 deletions src/client/quantum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ use tracing::{debug, info, warn};
/// Default timeout for network operations in seconds.
const DEFAULT_TIMEOUT_SECS: u64 = 30;

/// Number of closest peers to consider for chunk routing.
const CLOSE_GROUP_SIZE: usize = 8;
Comment on lines +36 to +37
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLOSE_GROUP_SIZE is a routing-critical constant but is hard-coded. Consider making it configurable via QuantumConfig (or a node/DHT config) so deployments can tune lookup breadth without code changes.

Copilot uses AI. Check for mistakes.

/// Default number of replicas for data redundancy.
const DEFAULT_REPLICA_COUNT: u8 = 4;

Expand Down Expand Up @@ -126,7 +129,7 @@ impl QuantumClient {
return Err(Error::Network("P2P node not configured".into()));
};

let target_peer = Self::pick_target_peer(node).await?;
let target_peer = Self::pick_target_peer(node, address).await?;

// Create and send GET request
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
Expand Down Expand Up @@ -231,11 +234,11 @@ impl QuantumClient {
return Err(Error::Network("P2P node not configured".into()));
};

let target_peer = Self::pick_target_peer(node).await?;

// Compute content address using SHA-256
// Compute content address using SHA-256 (before peer selection so we can route by it)
let address = crate::client::compute_address(&content);

let target_peer = Self::pick_target_peer(node, &address).await?;

// Create PUT request with empty payment proof
let empty_payment = rmp_serde::to_vec(&ant_evm::ProofOfPayment {
peer_quotes: vec![],
Expand Down Expand Up @@ -320,13 +323,37 @@ impl QuantumClient {
self.get_chunk(address).await.map(|opt| opt.is_some())
}

/// Pick a target peer from the connected peers list.
async fn pick_target_peer(node: &P2PNode) -> Result<String> {
let peers = node.connected_peers().await;
peers
/// Pick the closest peer to `target` using an iterative Kademlia network lookup.
///
/// Queries the DHT for the `CLOSE_GROUP_SIZE` closest nodes to the target
/// address and returns the single closest remote peer (excluding ourselves).
async fn pick_target_peer(node: &P2PNode, target: &XorName) -> Result<String> {
Comment on lines +326 to +330
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new routing behavior (closest-peer selection + local exclusion) is core logic but doesn’t appear to be covered by tests here. Adding a unit test with a mocked/stubbed DHT result set (including the local node and multiple remotes with known distances) would lock in the intended selection semantics and prevent regressions.

Copilot uses AI. Check for mistakes.
let local_peer_id = node.peer_id();
let local_transport_id = node.transport_peer_id();

Comment on lines +331 to +333
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The self-exclusion logic relies on n.peer_id being directly comparable to both node.peer_id() and node.transport_peer_id(). If find_closest_nodes returns records keyed by a different identifier (e.g., DHT node ID vs transport peer ID string formatting), this filter may fail to exclude the local node. Prefer comparing the same canonical identifier type/bytes on both sides (or use a dedicated ‘is local’/node-id equality if available from the DHT/node APIs).

Copilot uses AI. Check for mistakes.
let closest_nodes = node
.dht()
.find_closest_nodes(target, CLOSE_GROUP_SIZE)
.await
.map_err(|e| Error::Network(format!("Kademlia closest-nodes lookup failed: {e}")))?;

let closest = closest_nodes
.into_iter()
.next()
.ok_or_else(|| Error::Network("No connected peers available".into()))
.find(|n| {
n.peer_id != *local_peer_id
&& local_transport_id
.as_ref()
.map_or(true, |tid| n.peer_id != *tid)
})
Comment on lines +342 to +347
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The self-exclusion logic relies on n.peer_id being directly comparable to both node.peer_id() and node.transport_peer_id(). If find_closest_nodes returns records keyed by a different identifier (e.g., DHT node ID vs transport peer ID string formatting), this filter may fail to exclude the local node. Prefer comparing the same canonical identifier type/bytes on both sides (or use a dedicated ‘is local’/node-id equality if available from the DHT/node APIs).

Copilot uses AI. Check for mistakes.
.ok_or_else(|| Error::Network("No remote peers found near target address".into()))?;
Comment on lines +340 to +348
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find(...) selects the first non-local entry from closest_nodes, but this assumes the returned list is already sorted by XOR distance (and in the same ordering you want). If find_closest_nodes returns an unsorted set (or a differently-sorted set), this can select a non-closest peer, contradicting the function docstring. Consider explicitly selecting the minimum-distance node from the filtered set (compute XOR distance to target and choose the smallest), or document/assert that the DHT API guarantees stable closest-first ordering.

Copilot uses AI. Check for mistakes.
Comment on lines +340 to +348
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.find() returns the first non-self peer, not the closest by XOR distance

The find_closest_nodes DHT call returns nodes sorted by XOR distance, but using .find() just picks the first non-self peer instead of the actual closest. The defined xor_distance helper (line 25 in data_types.rs) is never used.

To select the true closest peer, you need to:

  1. Parse peer IDs to XorNames using peer_id_to_xor_name
  2. Compute XOR distance to target for each peer
  3. Sort by distance and pick the minimum
Suggested change
let closest = closest_nodes
.into_iter()
.next()
.ok_or_else(|| Error::Network("No connected peers available".into()))
.find(|n| {
n.peer_id != *local_peer_id
&& local_transport_id
.as_ref()
.map_or(true, |tid| n.peer_id != *tid)
})
.ok_or_else(|| Error::Network("No remote peers found near target address".into()))?;
let closest = closest_nodes
.into_iter()
.filter(|n| {
n.peer_id != *local_peer_id
&& local_transport_id
.as_ref()
.map_or(true, |tid| n.peer_id != *tid)
})
.filter_map(|n| {
crate::client::peer_id_to_xor_name(&n.peer_id)
.map(|xor| (n, crate::client::xor_distance(&xor, target)))
})
.min_by(|(_, dist_a), (_, dist_b)| dist_a.cmp(dist_b))
.map(|(n, _)| n)
.ok_or_else(|| Error::Network("No remote peers found near target address".into()))?;


debug!(
"Selected closest peer {} for target {}",
closest.peer_id,
hex::encode(target)
);

Ok(closest.peer_id)
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ pub use ant_protocol::{
ChunkGetRequest, ChunkGetResponse, ChunkMessage, ChunkMessageBody, ChunkPutRequest,
ChunkPutResponse, ChunkQuoteRequest, ChunkQuoteResponse, CHUNK_PROTOCOL_ID, MAX_CHUNK_SIZE,
};
pub use client::{compute_address, DataChunk, QuantumClient, QuantumConfig, XorName};
pub use client::{
compute_address, peer_id_to_xor_name, xor_distance, DataChunk, QuantumClient, QuantumConfig,
XorName,
};
pub use config::{BootstrapCacheConfig, NodeConfig, StorageConfig};
pub use devnet::{Devnet, DevnetConfig, DevnetManifest};
pub use error::{Error, Result};
Expand Down
Loading