Skip to content
Open
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
8 changes: 3 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ webrtc-direct = [
"saorsa-transport/webrtc-direct",
]

[patch.crates-io]
# ADR-0020 (V2-1358): saorsa-core's closer-peers encoding and
# ant-protocol's get-or-closer request, each rebased onto its main. Drop
# both once releases include them.
saorsa-core = { git = "https://github.com/WithAutonomi/saorsa-core", rev = "c3c4c81e63db7e3048f488f7cf0fec4cff3131d4" }
ant-protocol = { git = "https://github.com/WithAutonomi/ant-protocol", rev = "0c6ae27b6261227a32e6fe0f976163c42bde960e" }

[profile.release]
lto = true
codegen-units = 1
Expand Down
331 changes: 331 additions & 0 deletions docs/adr/ADR-0020-kademlia-chunk-get-or-closer.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ See [`TOOLING.md`](./TOOLING.md) for `adrs`, `adr-kit`, and AI harness setup.
- [ADR-0017: Bounded fresh-replication offers and copy-free message sends](./ADR-0017-bounded-fresh-offers-and-copy-free-sends.md)
- [ADR-0018: Pointer ownership transfer by final redirection](./ADR-0018-pointer-transfer-by-final-redirection.md)
- [ADR-0019: Pointer audits serve the record round 1 bound](./ADR-0019-pointer-audits-serve-the-record-round-one-bound.md)
- [ADR-0020: Kademlia-style chunk GET: return the chunk or closer peers](./ADR-0020-kademlia-chunk-get-or-closer.md)
- [ADR-0022: Remove the LMDB Chunk Store](./ADR-0022-remove-the-lmdb-chunk-store.md)
- [ADR-0023: Spread hot-record reads across the close group, and refuse full browser sessions explicitly](./ADR-0023-spread-hot-reads-and-refuse-full-browser-sessions.md)
7 changes: 4 additions & 3 deletions src/ant_protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
pub use ::ant_protocol::chunk;

pub use ::ant_protocol::chunk::{
settlement_compatibility, ChunkGetRequest, ChunkGetResponse, ChunkMessage, ChunkMessageBody,
advertises_get_or_closer, settlement_compatibility, ChunkGetOrCloserRequest,
ChunkGetOrCloserResponse, ChunkGetRequest, ChunkGetResponse, ChunkMessage, ChunkMessageBody,
ChunkPutRequest, ChunkPutResponse, ChunkQuoteRequest, ChunkQuoteRequestV2, ChunkQuoteResponse,
MerkleCandidateQuoteRequest, MerkleCandidateQuoteRequestV2, MerkleCandidateQuoteResponse,
ProtocolError, SettlementCompatibility, XorName, CHUNK_PROTOCOL_ID, CLOSE_GROUP_MAJORITY,
CLOSE_GROUP_SIZE, CURRENT_SETTLEMENT_VERSION, DATA_TYPE_CHUNK, MAX_CHUNK_SIZE,
MAX_WIRE_MESSAGE_SIZE, MIN_SUPPORTED_SETTLEMENT_VERSION, PROOF_TAG_MERKLE,
CLOSE_GROUP_SIZE, CURRENT_SETTLEMENT_VERSION, DATA_TYPE_CHUNK, GET_OR_CLOSER_AGENT_TOKEN,
MAX_CHUNK_SIZE, MAX_WIRE_MESSAGE_SIZE, MIN_SUPPORTED_SETTLEMENT_VERSION, PROOF_TAG_MERKLE,
PROOF_TAG_SINGLE_NODE, PROTOCOL_VERSION, XORNAME_LEN,
};
12 changes: 7 additions & 5 deletions src/devnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,10 @@ async fn handle_chunk_message(
let handled = protocol
.try_handle_request_with_context(
&data,
Some(ChunkRequestContext::new(
source.to_string(),
received_at.into(),
Duration::ZERO,
)),
Some(
ChunkRequestContext::new(source.to_string(), received_at.into(), Duration::ZERO)
.with_requester(source),
),
)
.await;
let telemetry = handled.get_telemetry;
Expand Down Expand Up @@ -1012,6 +1011,9 @@ impl Devnet {
.bootstrap_peers
.clone_from(&node.bootstrap_addrs);
core_config.diversity_config = Some(IPDiversityConfig::permissive());
// Devnet nodes serve chunks, so they announce get-or-closer
// (ADR-0020) as a production node does.
core_config.custom_user_agent = Some(crate::node::user_agent(true));

let index = node.index;
let p2p_node = P2PNode::new(core_config)
Expand Down
65 changes: 53 additions & 12 deletions src/node.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Node implementation - thin wrapper around saorsa-core's `P2PNode`.

use crate::ant_protocol::CHUNK_PROTOCOL_ID;
use crate::ant_protocol::{CHUNK_PROTOCOL_ID, GET_OR_CLOSER_AGENT_TOKEN};
use crate::config::{
default_nodes_dir, default_root_dir, NetworkMode, NodeConfig, NODE_IDENTITY_FILENAME,
};
Expand Down Expand Up @@ -53,8 +53,18 @@ const PROTOCOL_DRAIN_GRACE: std::time::Duration = std::time::Duration::from_secs
/// that lost the prefix would stop being routed to. The version is this build's rather than
/// the transport's, because it is the one a peer needs in order to tell which release it is
/// talking to.
fn user_agent() -> String {
format!("node/{}", env!("CARGO_PKG_VERSION"))
///
/// A node that serves chunks also carries the get-or-closer token (ADR-0020). Native peers
/// exchange no capabilities, and a node built before that request drops it without a reply,
/// so this token is how a client knows it may send one. A node with storage switched off
/// never answers chunk requests, so it does not announce it.
pub(crate) fn user_agent(serves_chunks: bool) -> String {
let agent = format!("node/{}", env!("CARGO_PKG_VERSION"));
if serves_chunks {
format!("{agent} {GET_OR_CLOSER_AGENT_TOKEN}")
} else {
agent
}
}

/// Builder for constructing an Ant node.
Expand Down Expand Up @@ -334,7 +344,7 @@ impl NodeBuilder {
}
}

core_config.custom_user_agent = Some(user_agent());
core_config.custom_user_agent = Some(user_agent(config.storage.enabled));

// Persist close group peers + trust scores across restarts.
// Default to root_dir (alongside node_identity.key) when not explicitly set.
Expand Down Expand Up @@ -968,6 +978,28 @@ impl RunningNode {
Ok(())
}

/// Answer one inbound chunk-protocol message from `source` as a running
/// node's router does, and send the reply. For test networks that route
/// messages themselves.
#[cfg(any(test, feature = "test-utils"))]
pub async fn answer_chunk_request(
protocol: &Arc<AntProtocol>,
p2p: &Arc<P2PNode>,
source: &saorsa_core::identity::PeerId,
data: &[u8],
) {
Self::answer_one_request(
protocol,
p2p,
source,
data,
"chunk",
CHUNK_PROTOCOL_ID,
Instant::now(),
)
.await;
}

/// Handle one inbound protocol message and send whatever it produced.
async fn answer_one_request(
protocol: &Arc<AntProtocol>,
Expand All @@ -985,11 +1017,10 @@ impl RunningNode {
let handled = protocol
.try_handle_request_with_context(
data,
Some(ChunkRequestContext::new(
source.to_string(),
received_at,
queue_wait,
)),
Some(
ChunkRequestContext::new(source.to_string(), received_at, queue_wait)
.with_requester(*source),
),
)
.await;
let telemetry = handled.get_telemetry;
Expand Down Expand Up @@ -1615,13 +1646,23 @@ mod tests {
}

/// The prefix is what keeps this node in its peers' routing tables, and the version is
/// this build's, with nothing after it.
/// this build's, with nothing after it on a node that does not serve chunks.
#[test]
fn the_user_agent_is_this_builds_version_under_the_node_prefix() {
assert_eq!(
super::user_agent(),
super::user_agent(false),
format!("node/{}", env!("CARGO_PKG_VERSION"))
);
assert!(super::user_agent().starts_with("node/"));
assert!(super::user_agent(true).starts_with("node/"));
}

#[test]
fn user_agent_advertises_get_or_closer_only_when_serving_chunks() {
assert!(crate::ant_protocol::advertises_get_or_closer(
&super::user_agent(true)
));
assert!(!crate::ant_protocol::advertises_get_or_closer(
&super::user_agent(false)
));
}
}
Loading
Loading