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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
it (`npm update -g @withautonomi/ant`). Self-replacing an npm-managed binary left npm's
metadata describing a file that was no longer there, so the next `npm update` silently rolled
the user back.
- A browser read asks a record's holders sooner. Its first hints are its bootstrap peers, which
rarely hold the record, and it used to ask them one at a time, each over a new connection,
while discovery had already reached a holder. Now a hint stops being read once lookup replies
list a close group of peers nearer the record. Only peers a reply proves, with an owner-signed
address record, and its authenticated responder count. A read that continues no other now
fetches the record after it once it has finished, so that fetch no longer competes with the
record the read waits for. In `ant-core`, a `BrowserNetwork` adapter reports listed peers
through the new `ReadProgress::listed`; `ReadProgress::may_hold` gives the rule. Native reads
are unchanged. (#242)

## [0.3.6] - 2026-09-01

Expand Down
15 changes: 15 additions & 0 deletions ant-core/src/browser/wasm_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,21 @@ impl LookupQuery<BrowserLookupCandidate> for BrowserNetworkLookupQuery {
match result {
Ok(nodes) => {
if let Some(progress) = &read_progress {
// The responder's session authenticated it, and
// `find_node` verified every owner proof it
// returned. A peer listed without one is unproven.
progress.listed(
std::iter::once(responder)
.chain(
nodes
.iter()
.filter(|node| node.address_record.is_some())
.filter_map(|node| {
parse_lookup_key(&node.peer_id, "peer ID").ok()
}),
)
.map(PeerId::from_bytes),
);
let hints = std::iter::once(&candidate.wire)
.chain(nodes.iter())
.filter(|node| {
Expand Down
15 changes: 13 additions & 2 deletions ant-core/src/browser/wasm_transport/read_ahead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,10 @@ impl ReadAhead {
/// sequential read also moves the read-ahead window to its start, so the
/// records ahead are fetched alongside it. A read is sequential when it
/// continues a recent read or the reader streams. Another read fetches only
/// the record after it, in case it starts a new stream. A streaming read from
/// the record after it, in case it starts a new stream, and only once the
/// read has finished: that record's discovery and transfer would otherwise
/// compete with the record the read waits for, such as the head of a video
/// a player is about to start. A streaming read from
/// the start of the file also fetches the last record, where containers such
/// as MP4 and WebM often keep the index a player reads next. A read that
/// needs no records, such as one of zero length, changes nothing.
Expand All @@ -302,6 +305,7 @@ impl ReadAhead {
read: None,
indices: HashMap::new(),
addresses: Vec::new(),
next: None,
};
}
let read = self.next_id();
Expand Down Expand Up @@ -338,10 +342,11 @@ impl ReadAhead {
break;
}
}
let mut next = None;
if sequential {
self.fill();
} else if needed.end < self.layout.infos().len() {
self.prefetch(needed.end);
next = Some(needed.end);
}
if self.streaming && start == 0 {
if let Some(last) = self.layout.infos().len().checked_sub(1) {
Expand All @@ -360,6 +365,7 @@ impl ReadAhead {
read: Some(read),
indices,
addresses,
next,
}
}

Expand Down Expand Up @@ -559,6 +565,8 @@ pub(super) struct ReadLease {
/// Index of each record the read needs, by address.
indices: HashMap<[u8; 32], usize>,
addresses: Vec<[u8; 32]>,
/// The record after a read that continues no other, fetched once it ends.
next: Option<usize>,
}

impl ReadLease {
Expand Down Expand Up @@ -591,5 +599,8 @@ impl Drop for ReadLease {
self.read_ahead.pool.awaited.remove(&self.addresses);
self.read_ahead.last_read.set(Instant::now());
}
if let Some(next) = self.next {
self.read_ahead.prefetch(next);
}
}
}
38 changes: 38 additions & 0 deletions ant-core/src/browser/wasm_transport/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::data::client::adaptive::{AdaptiveConfig, AdaptiveController, ChannelS
use crate::data::network::{BrowserNetwork, RequestDispatched};
use ant_protocol::chunk::{PointerGetResponse, PointerPutResponse};
use ant_protocol::pointer::Pointer;
use ant_protocol::transport::signed_address::SignedAddressRecord;
use ant_protocol::transport::{KnownReachability, NodeIdentity, TransportAddressRecord};
use self_encryption::{ChunkInfo, DataMap, XorName};

mod node_session;
Expand All @@ -26,6 +28,10 @@ const EXCLUSIVE_GATHERS_FIRST: Duration = Duration::from_millis(10);
const EXCLUSIVE_ABANDONED_AFTER: Duration = Duration::from_millis(50);
/// Well past the abandonment, and far short of an ordinary admission timeout.
const SPECULATIVE_ADMISSION_WAIT: Duration = Duration::from_millis(500);
/// Publication sequence of a mock node's owner proof; it never republishes.
const SIGNED_PEER_SEQUENCE: u64 = 1;
/// Reliability a lookup reply reports for a mock node.
const SIGNED_PEER_RELIABILITY: f64 = 1.0;

/// Exercise bootstrap readiness after actual LRU pool eviction.
#[wasm_bindgen]
Expand Down Expand Up @@ -458,6 +464,32 @@ impl BrowserTestNode {
pub fn endpoint(&self) -> String {
self.endpoint.clone()
}
/// This node as a lookup reply lists it, with an owner proof of its WebRTC
/// Direct endpoint signed with its own key.
pub fn signed_peer(&self) -> JsValue {
let identity = NodeIdentity::import(
&serde_json::from_value(serde_json::json!({
"secret_key": self.secret.clone().into_bytes().to_vec(),
"public_key": self.public,
}))
.unwrap(),
)
.unwrap();
let address = self.endpoint.parse().unwrap();
let record = TransportAddressRecord::from_multiaddr(&address, KnownReachability::Direct)
.unwrap()
.unwrap();
let signed =
SignedAddressRecord::sign(&identity, SIGNED_PEER_SEQUENCE, vec![record]).unwrap();
let node = shared::browser_record(
signed
.verify()
.unwrap()
.peer_record(SIGNED_PEER_RELIABILITY),
)
.unwrap();
serde_wasm_bindgen::to_value(&node).unwrap()
}
pub fn last_method(&self) -> String {
self.last_method.clone()
}
Expand Down Expand Up @@ -1106,6 +1138,12 @@ pub async fn test_shared_identity_and_timers() {
);
}

/// The close group size the read's rules use, for tests that build one.
#[wasm_bindgen]
pub fn test_close_group_size() -> usize {
ant_protocol::CLOSE_GROUP_SIZE
}

/// Build a real, non-expiring owner proof for browser forwarding regressions.
#[wasm_bindgen]
pub fn test_signed_address_node() -> JsValue {
Expand Down
15 changes: 10 additions & 5 deletions ant-core/src/client_engine/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,16 @@ fn settle<T, E>(
}

/// Discovery converges on the target, so a hint with fewer than a close group of
/// attempted peers nearer the target may still hold the record.
fn may_hold(attempted: &HashSet<[u8; 32]>, candidate: &[u8; 32], target: &[u8; 32]) -> bool {
/// `known` peers nearer the target, such as peers already attempted, may still
/// hold the record.
pub(crate) fn may_hold<'a>(
known: impl IntoIterator<Item = &'a [u8; 32]>,
candidate: &[u8; 32],
target: &[u8; 32],
) -> bool {
let distance = ant_protocol::transport::xor_distance(candidate, target);
attempted
.iter()
known
.into_iter()
.filter(|peer| ant_protocol::transport::xor_distance(peer, target) < distance)
.count()
< ant_protocol::CLOSE_GROUP_SIZE
Expand Down Expand Up @@ -484,7 +489,7 @@ where
.iter()
.map(|(peer, _)| peer)
.chain(refused_now.keys())
.filter(|peer| !misses.contains(*peer) && may_hold(&misses, peer, &target))
.filter(|peer| !misses.contains(*peer) && may_hold(misses.iter(), peer, &target))
.map(|peer| refused_now.get(peer).copied().unwrap_or_default())
.min();
(absent, refusal)
Expand Down
Loading
Loading