use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::{Address, B256, Signature};
use alloy::signers::local::PrivateKeySigner;
use bao_tree::io::outboard::PreOrderMemOutboard;
use decdn_cache::range_pull::{IROH_BLOCK_SIZE, align_range, encode_verified_range};
use decdn_cache::{CHUNK_GROUP_BYTES, CacheEngine, FilesystemOrigin, Hash};
use decdn_common::config::ResolvedSecurity;
use decdn_incentive::ProbeSlashData;
use decdn_node::dht::routing::NodeId;
use decdn_node::dht::staker_set::{ConfigStakerSet, StakerSet};
use decdn_node::dispatch::{ConnectionLimiter, RejectReason};
use decdn_node::handlers::probe::{ProbeHandler, StakeLanePolicy};
use decdn_node::handlers::probe_rate_limit::{ProbeRateLimiter, ProbeRejectLayer};
use decdn_node::metrics::Metrics;
use decdn_node::rate_limit::RateLimitConfig;
use decdn_protocol::{
ALPN_PROBE, APP_ERR_RATE_LIMITED, Coverage, DISCOVERY_BLOCK_BYTES, MAX_MESSAGE_SIZE,
ProbeMessage, ProbeResponseExt, SLASH_SIG_LEN, decode_message, encode_message,
message::{ProbeRequest, ProbeResponse, ProbeResponseBody},
num_blocks, parse_probe_response_ext, read_frame, write_frame,
};
use iroh::endpoint::{
ApplicationClose, Connection, ConnectionError, IdleTimeout, QuicTransportConfig, ReadError,
ReadToEndError, VarInt, presets,
};
use iroh::protocol::ProtocolHandler;
use iroh::{Endpoint, EndpointAddr, RelayMode, SecretKey};
use tokio::task::JoinHandle;
mod support;
use support::shutdown;
fn test_slash_domain() -> Eip712Domain {
decdn_incentive::slash_judge_domain(421_614, Address::repeat_byte(0x11))
}
async fn empty_cache() -> anyhow::Result<(CacheEngine, tempfile::TempDir)> {
let tmp = tempfile::tempdir()?;
let cache = CacheEngine::open(tmp.path(), vec![], 16).await?;
Ok((cache, tmp))
}
async fn cache_with_blob(payload: &[u8]) -> anyhow::Result<(CacheEngine, Hash, tempfile::TempDir)> {
let hash = Hash::new(payload);
let origin_dir = tempfile::tempdir()?;
let hex = hash.to_hex();
let shard = hex
.get(..2)
.ok_or_else(|| anyhow::anyhow!("hex too short"))?;
let dir = origin_dir.path().join(shard);
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join(hex.as_str()), payload)?;
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(FilesystemOrigin::new(origin_dir.path()).await?);
let cache = CacheEngine::open(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let _ = cache.get(hash).await?; drop(origin_dir); Ok((cache, hash, cache_dir))
}
async fn cache_with_two_blobs(
a: &[u8],
b: &[u8],
) -> anyhow::Result<(CacheEngine, Hash, Hash, tempfile::TempDir)> {
let origin_dir = tempfile::tempdir()?;
for payload in [a, b] {
let hex = Hash::new(payload).to_hex();
let shard = hex
.get(..2)
.ok_or_else(|| anyhow::anyhow!("hex too short"))?;
let dir = origin_dir.path().join(shard);
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join(hex.as_str()), payload)?;
}
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(FilesystemOrigin::new(origin_dir.path()).await?);
let cache = CacheEngine::open(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let (ha, hb) = (Hash::new(a), Hash::new(b));
let _ = cache.get(ha).await?; let _ = cache.get(hb).await?;
drop(origin_dir);
Ok((cache, ha, hb, cache_dir))
}
async fn cache_with_own_and_foreign(
own_payload: &[u8],
foreign_payload: &[u8],
) -> anyhow::Result<(
CacheEngine,
Hash,
Hash,
tempfile::TempDir,
tempfile::TempDir,
)> {
let origin_dir = tempfile::tempdir()?;
for payload in [own_payload, foreign_payload] {
let hex = Hash::new(payload).to_hex();
let shard = hex
.get(..2)
.ok_or_else(|| anyhow::anyhow!("hex too short"))?;
let dir = origin_dir.path().join(shard);
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join(hex.as_str()), payload)?;
}
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(FilesystemOrigin::new(origin_dir.path()).await?);
let cache = CacheEngine::open(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let (own_hash, foreign_hash) = (Hash::new(own_payload), Hash::new(foreign_payload));
let _ = cache.get(own_hash).await?; let _ = cache.get(foreign_hash).await?;
let hex = foreign_hash.to_hex();
let shard = hex
.get(..2)
.ok_or_else(|| anyhow::anyhow!("hex too short"))?;
std::fs::remove_file(origin_dir.path().join(shard).join(hex.as_str()))?;
cache.rescan_origins().await;
Ok((cache, own_hash, foreign_hash, origin_dir, cache_dir))
}
fn synth_blob(len: usize) -> ([u8; 32], Vec<u8>, bytes::Bytes) {
let mut plaintext = vec![0u8; len];
let mut x: u32 = 0x9e37_79b9;
for b in &mut plaintext {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
*b = x.to_le_bytes()[0];
}
let ob = PreOrderMemOutboard::create(&plaintext, IROH_BLOCK_SIZE);
(*ob.root.as_bytes(), plaintext, bytes::Bytes::from(ob.data))
}
fn bao_for(
root: [u8; 32],
plaintext: &[u8],
outboard: bytes::Bytes,
off: u64,
len: u64,
total: u64,
) -> anyhow::Result<(Hash, bao_tree::ChunkRanges, bytes::Bytes)> {
let aligned = align_range(off, len, total)?;
let s = usize::try_from(aligned.fetch_start())?;
let e = usize::try_from(aligned.fetch_end())?;
let slice = plaintext
.get(s..e)
.ok_or_else(|| anyhow::anyhow!("aligned range out of bounds"))?;
let encoded = encode_verified_range(root, &aligned, slice, outboard)?;
Ok((Hash::from(root), aligned.chunk_ranges().clone(), encoded))
}
async fn cache_with_partial_two_block_blob()
-> anyhow::Result<(CacheEngine, Hash, tempfile::TempDir)> {
let cache_dir = tempfile::tempdir()?;
let cache = CacheEngine::open(cache_dir.path(), vec![], 16).await?;
let group = CHUNK_GROUP_BYTES;
let total = DISCOVERY_BLOCK_BYTES + 3 * group;
let (root, plaintext, outboard) = synth_blob(usize::try_from(total)?);
let (hash, block0_ranges, block0_bao) = bao_for(
root,
&plaintext,
outboard.clone(),
0,
DISCOVERY_BLOCK_BYTES,
total,
)?;
cache.admit_bao(hash, block0_ranges, block0_bao).await?;
let (_, tail_ranges, tail_bao) =
bao_for(root, &plaintext, outboard, total - group, group, total)?;
cache.admit_bao(hash, tail_ranges, tail_bao).await?;
Ok((cache, hash, cache_dir))
}
fn metric_value(text: &str, name: &str) -> Option<u64> {
text.lines().find_map(|line| {
let rest = line.strip_prefix(name)?.strip_prefix(' ')?;
rest.trim().parse().ok()
})
}
#[allow(clippy::too_many_arguments)]
fn build_handler_bounds(
server_id: iroh::PublicKey,
rate: u64,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
) -> (Arc<ProbeHandler>, Arc<PrivateKeySigner>, Eip712Domain) {
build_handler_with_probe_limiter(
server_id,
rate,
metrics,
limiter,
permissive_probe_rate_limiter(metrics),
cache,
)
}
#[allow(clippy::too_many_arguments)]
fn build_handler_with_probe_limiter(
server_id: iroh::PublicKey,
rate: u64,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
probe_limiter: Arc<ProbeRateLimiter>,
cache: CacheEngine,
) -> (Arc<ProbeHandler>, Arc<PrivateKeySigner>, Eip712Domain) {
let signer = Arc::new(PrivateKeySigner::random());
let domain = test_slash_domain();
let handler = Arc::new(ProbeHandler::new(
server_id,
rate,
Arc::clone(metrics),
limiter,
probe_limiter,
cache,
Arc::clone(&signer),
domain.clone(),
None,
true,
None,
));
(handler, signer, domain)
}
#[allow(clippy::too_many_arguments)]
fn build_handler_with_lane(
server_id: iroh::PublicKey,
rate: u64,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
stakers: std::collections::HashSet<NodeId>,
reserved_holds: NonZeroUsize,
max_holds: usize,
) -> (Arc<ProbeHandler>, Arc<PrivateKeySigner>, Eip712Domain) {
let signer = Arc::new(PrivateKeySigner::random());
let domain = test_slash_domain();
let staker_set: Arc<dyn StakerSet> = Arc::new(ConfigStakerSet::new(stakers));
let policy = StakeLanePolicy::new(staker_set, reserved_holds, max_holds);
let handler = Arc::new(ProbeHandler::new(
server_id,
rate,
Arc::clone(metrics),
limiter,
permissive_probe_rate_limiter(metrics),
cache,
Arc::clone(&signer),
domain.clone(),
Some(policy),
true,
None,
));
(handler, signer, domain)
}
#[allow(clippy::too_many_arguments)]
fn build_handler_origin_only(
server_id: iroh::PublicKey,
rate: u64,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
) -> (Arc<ProbeHandler>, Arc<PrivateKeySigner>, Eip712Domain) {
let signer = Arc::new(PrivateKeySigner::random());
let domain = test_slash_domain();
let handler = Arc::new(ProbeHandler::new(
server_id,
rate,
Arc::clone(metrics),
limiter,
permissive_probe_rate_limiter(metrics),
cache,
Arc::clone(&signer),
domain.clone(),
None,
false,
None,
));
(handler, signer, domain)
}
fn build_handler(
server_id: iroh::PublicKey,
rate: u64,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
) -> (Arc<ProbeHandler>, Arc<PrivateKeySigner>, Eip712Domain) {
build_handler_bounds(server_id, rate, metrics, limiter, cache)
}
fn permissive_limiter(metrics: &Arc<Metrics>) -> Arc<ConnectionLimiter> {
let cfg = ResolvedSecurity {
max_concurrent_handlers: u32::MAX,
per_source_rate_per_sec: 1_000_000.0,
per_source_burst: u32::MAX,
max_tracked_sources: 4096,
};
Arc::new(ConnectionLimiter::new(&cfg, Arc::clone(metrics)))
}
fn permissive_probe_rate_limiter(metrics: &Arc<Metrics>) -> Arc<ProbeRateLimiter> {
let cfg = RateLimitConfig {
per_peer_rate_per_sec: 1e9,
per_peer_burst: u32::MAX,
per_ip_rate_per_sec: 1e9,
per_ip_burst: u32::MAX,
global_rate_per_sec: 1e9,
global_burst: u32::MAX,
max_tracked_per_ip: 4096,
max_tracked_per_peer: 4096,
};
Arc::new(ProbeRateLimiter::new(&cfg, Arc::clone(metrics)))
}
fn fresh_key() -> SecretKey {
SecretKey::generate()
}
async fn local_endpoint(
secret_key: SecretKey,
alpns: Vec<Vec<u8>>,
) -> anyhow::Result<(Endpoint, SocketAddr)> {
let bind = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
let ep = Endpoint::builder(presets::Minimal)
.secret_key(secret_key)
.alpns(alpns)
.relay_mode(RelayMode::Disabled)
.bind_addr(bind)
.map_err(|e| anyhow::anyhow!("bind_addr: {e}"))?
.bind()
.await
.map_err(|e| anyhow::anyhow!("bind: {e}"))?;
let addr = ep
.bound_sockets()
.into_iter()
.find(SocketAddr::is_ipv4)
.ok_or_else(|| anyhow::anyhow!("no IPv4 bound socket"))?;
let addr = match addr {
SocketAddr::V4(v4) if v4.ip().is_unspecified() => {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, v4.port()))
}
other => other,
};
Ok((ep, addr))
}
fn assert_slash_sig_valid(
resp: &ProbeResponse,
signer: &PrivateKeySigner,
domain: &Eip712Domain,
) -> anyhow::Result<()> {
anyhow::ensure!(
resp.slash_sig.len() == SLASH_SIG_LEN,
"slash_sig must be exactly {SLASH_SIG_LEN} bytes, got {}",
resp.slash_sig.len()
);
let sig = Signature::try_from(resp.slash_sig.as_slice())
.map_err(|e| anyhow::anyhow!("slash_sig parse: {e}"))?;
ProbeSlashData {
hash: B256::from(resp.body.hash),
has_blob: resp.body.has_blob,
rate_per_mb: resp.body.rate_per_mb,
timestamp_us: resp.body.timestamp_us,
}
.verify_signer(&sig, signer.address(), domain)
.map_err(|e| anyhow::anyhow!("slash_sig verify: {e}"))?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_roundtrip() -> anyhow::Result<()> {
let rate_per_mb: u64 = 42;
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (cache, _cache_tmp) = empty_cache().await?;
let (handler, signer, domain) = build_handler(server_id, rate_per_mb, &metrics, limiter, cache);
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_PROBE.to_vec()]).await?;
let server_ep_bg = server_ep.clone();
let accept_task = tokio::spawn(async move {
if let Some(incoming) = server_ep_bg.accept().await {
let connecting = incoming
.accept()
.map_err(|e| anyhow::anyhow!("accept: {e}"))?;
let conn = connecting
.await
.map_err(|e| anyhow::anyhow!("handshake: {e}"))?;
handler
.accept(conn)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
}
Ok::<_, anyhow::Error>(())
});
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let conn = client_ep
.connect(target, ALPN_PROBE)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let req = ProbeRequest {
hash: [0x5au8; 32],
timestamp_us: 0x00c0_ffee,
};
let payload = encode_message(&ProbeMessage::Request(req))?;
write_frame(&mut send, &payload)
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
let frame = read_frame(&mut recv)
.await
.map_err(|e| anyhow::anyhow!("read: {e}"))?;
let (msg, tail) = decode_message::<ProbeMessage>(&frame)?;
let (resp, resp_ext): (ProbeResponse, ProbeResponseExt) = match msg {
ProbeMessage::Response(r) => (r, parse_probe_response_ext(tail)?),
ProbeMessage::Request(_) => anyhow::bail!("unexpected request variant on client"),
};
assert_eq!(resp.body.timestamp_us, req.timestamp_us, "timestamp echoed");
assert_eq!(resp.body.hash, req.hash, "hash echoed");
assert_eq!(resp.body.rate_per_mb, rate_per_mb, "rate (unclamped)");
assert!(
!resp.body.has_blob,
"empty cache must report has_blob=false"
);
assert_eq!(resp_ext.total_bytes, None, "no size when blob absent");
assert_slash_sig_valid(&resp, &signer, &domain)?;
conn.close(0u32.into(), b"bye");
shutdown([], [&client_ep]).await?;
support::reap("accept", accept_task).await??;
shutdown([], [&server_ep]).await?;
Ok(())
}
struct Harness {
client_conn: Connection,
accept_task: JoinHandle<anyhow::Result<()>>,
metrics: Arc<Metrics>,
client_ep: Endpoint,
server_ep: Endpoint,
_cache_tmp: tempfile::TempDir,
}
async fn spin_up_probe_harness() -> anyhow::Result<Harness> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (cache, cache_tmp) = empty_cache().await?;
let (handler, _signer, _domain) = build_handler(server_id, 1, &metrics, limiter, cache);
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_PROBE.to_vec()]).await?;
let server_ep_bg = server_ep.clone();
let accept_task = tokio::spawn(async move {
let incoming = server_ep_bg
.accept()
.await
.ok_or_else(|| anyhow::anyhow!("no incoming connection"))?;
let connecting = incoming
.accept()
.map_err(|e| anyhow::anyhow!("accept: {e}"))?;
let conn = connecting
.await
.map_err(|e| anyhow::anyhow!("handshake: {e}"))?;
handler
.accept(conn)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
});
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let client_conn = client_ep
.connect(target, ALPN_PROBE)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
Ok(Harness {
client_conn,
accept_task,
metrics,
client_ep,
server_ep,
_cache_tmp: cache_tmp,
})
}
async fn assert_reset_with_code(
recv: &mut iroh::endpoint::RecvStream,
expected_code: u32,
) -> anyhow::Result<()> {
let expected = VarInt::from_u32(expected_code);
match recv.read_to_end(4096).await {
Err(ReadToEndError::Read(ReadError::Reset(code))) if code == expected => Ok(()),
Err(ReadToEndError::Read(ReadError::ConnectionLost(
ConnectionError::ApplicationClosed(ApplicationClose { error_code, .. }),
))) if error_code == expected => Ok(()),
other => {
anyhow::bail!("expected error carrying app code {expected_code:#x}, got {other:?}")
}
}
}
async fn tear_down(h: Harness) -> anyhow::Result<()> {
h.client_conn.close(0u32.into(), b"bye");
shutdown([], [&h.client_ep]).await?;
let _ = support::reap("accept", h.accept_task).await;
shutdown([], [&h.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_oversized_frame_returns_too_large_code() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
let (mut send, mut recv) = h
.client_conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let mut bogus = Vec::new();
let mut v = MAX_MESSAGE_SIZE + 1;
loop {
let byte = (v & 0x7F) as u8;
v >>= 7;
if v == 0 {
bogus.push(byte);
break;
}
bogus.push(byte | 0x80);
}
send.write_all(&bogus)
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
assert_reset_with_code(&mut recv, 0x02).await?;
tear_down(h).await
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_garbage_postcard_returns_malformed_code() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
let (mut send, mut recv) = h
.client_conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
write_frame(&mut send, &[0x00u8, 0x01])
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
assert_reset_with_code(&mut recv, 0x03).await?;
tear_down(h).await
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_unknown_discriminant_returns_unsupported_code() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
let (mut send, mut recv) = h
.client_conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
write_frame(&mut send, &[99u8, 0])
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
assert_reset_with_code(&mut recv, 0x01).await?;
tear_down(h).await
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_io_truncated_frame_returns_zero_code() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
let (mut send, mut recv) = h
.client_conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let mut bogus = Vec::new();
let mut v: u32 = 100;
loop {
let byte = (v & 0x7F) as u8;
v >>= 7;
if v == 0 {
bogus.push(byte);
break;
}
bogus.push(byte | 0x80);
}
bogus.extend_from_slice(b"abc");
send.write_all(&bogus)
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
assert_reset_with_code(&mut recv, 0x00).await?;
tear_down(h).await
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_response_on_server_stream_returns_unsupported_code() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
let (mut send, mut recv) = h
.client_conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let payload = encode_message(&ProbeMessage::Response(ProbeResponse {
body: ProbeResponseBody {
hash: [0u8; 32],
has_blob: false,
rate_per_mb: 0,
timestamp_us: 0,
},
slash_sig: vec![0u8; SLASH_SIG_LEN],
}))?;
write_frame(&mut send, &payload)
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
assert_reset_with_code(&mut recv, 0x01).await?;
tear_down(h).await
}
#[tokio::test]
async fn probe_read_timeout_closes_with_zero_code() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
let (mut send, _recv) = h
.client_conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
send.write_all(&[0x80])
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
tokio::time::sleep(Duration::from_millis(250)).await;
tokio::time::pause();
tokio::time::advance(Duration::from_secs(6)).await;
tokio::time::resume();
let closed = tokio::time::timeout(Duration::from_secs(10), h.client_conn.closed())
.await
.map_err(|_| anyhow::anyhow!("the server never closed the connection"))?;
match closed {
ConnectionError::ApplicationClosed(ApplicationClose { error_code, reason })
if error_code == VarInt::from_u32(0) && reason.as_ref() == b"probe-error" => {}
other => anyhow::bail!("expected an app close (0, \"probe-error\"), got {other:?}"),
}
let handled = support::reap("accept", h.accept_task).await?;
anyhow::ensure!(
handled.is_ok(),
"the read-timeout arm returns Ok(()), got {handled:?}"
);
let text = h.metrics.encode()?;
assert_eq!(
metric_value(&text, "decdn_probe_read_faults_total"),
Some(1),
"the read timeout is counted once as a probe read fault"
);
h.client_conn.close(0u32.into(), b"bye");
shutdown([], [&h.client_ep, &h.server_ep]).await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn probe_accept_bi_timeout_errors_handler() -> anyhow::Result<()> {
let h = spin_up_probe_harness().await?;
tokio::time::advance(Duration::from_secs(6)).await;
let joined = h
.accept_task
.await
.map_err(|e| anyhow::anyhow!("join: {e}"))?;
let Err(err) = joined else {
anyhow::bail!("handler should have returned Err on ACCEPT_BI_TIMEOUT, got Ok");
};
let msg = err.to_string();
anyhow::ensure!(
msg.contains("accept_bi timed out"),
"expected accept_bi timeout error, got: {msg}"
);
tokio::time::resume();
h.client_conn.close(0u32.into(), b"bye");
shutdown([], [&h.client_ep, &h.server_ep]).await?;
Ok(())
}
async fn observe_rate_limit_close<F, Fut>(
expected_reason: &[u8],
mut connect: F,
) -> anyhow::Result<()>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = anyhow::Result<(Endpoint, Connection)>>,
{
const MAX_ATTEMPTS: usize = 8;
let expected_code = VarInt::from_u32(APP_ERR_RATE_LIMITED);
let mut last_transient = None;
for _ in 0..MAX_ATTEMPTS {
let (ep, conn) = connect().await?;
let close_err = conn.closed().await;
match close_err {
ConnectionError::ApplicationClosed(ApplicationClose { error_code, reason })
if error_code == expected_code =>
{
if reason.as_ref() == expected_reason {
shutdown([], [&ep]).await?;
return Ok(());
}
anyhow::bail!(
"rate-limit close carried the wrong layer label: expected {:?}, got {:?}",
String::from_utf8_lossy(expected_reason),
String::from_utf8_lossy(reason.as_ref()),
);
}
other => last_transient = Some(other),
}
shutdown([], [&ep]).await?;
}
anyhow::bail!(
"never observed an APP_ERR_RATE_LIMITED close carrying {:?} in {MAX_ATTEMPTS} attempts; \
last transient close: {last_transient:?}",
String::from_utf8_lossy(expected_reason),
)
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_rate_limit_returns_rate_limited_close_code() -> anyhow::Result<()> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let strict = ResolvedSecurity {
max_concurrent_handlers: 64,
per_source_rate_per_sec: 0.001, per_source_burst: 1,
max_tracked_sources: 32,
};
let limiter = Arc::new(ConnectionLimiter::new(&strict, Arc::clone(&metrics)));
drop(
limiter
.acquire_for_test(Some(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST)))
.map_err(|r| anyhow::anyhow!("pre-drain unexpectedly rejected: {r:?}"))?,
);
let limiter_probe = Arc::clone(&limiter);
let (cache, _cache_tmp) = empty_cache().await?;
let (handler, _signer, _domain) = build_handler(server_id, 1, &metrics, limiter, cache);
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_PROBE.to_vec()]).await?;
let server_ep_bg = server_ep.clone();
let handler_bg = Arc::clone(&handler);
let accept_task = tokio::spawn(async move {
while let Some(incoming) = server_ep_bg.accept().await {
let Ok(connecting) = incoming.accept() else {
continue;
};
let Ok(conn) = connecting.await else {
continue;
};
let _ = handler_bg.accept(conn).await;
}
});
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
observe_rate_limit_close(RejectReason::PerSource.as_str().as_bytes(), move || {
let target = target.clone();
async move {
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
match client_ep.connect(target, ALPN_PROBE).await {
Ok(conn) => Ok((client_ep, conn)),
Err(e) => {
shutdown([], [&client_ep]).await?;
Err(anyhow::anyhow!("connect: {e}"))
}
}
}
})
.await?;
assert_eq!(
limiter_probe.per_source_tracked(),
1,
"live connection must hit the pre-drained 127.0.0.1 bucket"
);
shutdown([accept_task], [&server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_once_types_a_rate_limit_close_as_upstream_rate_limited() -> anyhow::Result<()> {
use decdn_client::UpstreamRateLimited;
use decdn_client::probe::probe_once;
const MAX_ATTEMPTS: usize = 8;
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let strict = ResolvedSecurity {
max_concurrent_handlers: 64,
per_source_rate_per_sec: 0.001,
per_source_burst: 1,
max_tracked_sources: 32,
};
let limiter = Arc::new(ConnectionLimiter::new(&strict, Arc::clone(&metrics)));
drop(
limiter
.acquire_for_test(Some(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST)))
.map_err(|r| anyhow::anyhow!("pre-drain unexpectedly rejected: {r:?}"))?,
);
let (cache, _cache_tmp) = empty_cache().await?;
let (handler, _signer, _domain) = build_handler(server_id, 1, &metrics, limiter, cache);
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_PROBE.to_vec()]).await?;
let server_ep_bg = server_ep.clone();
let handler_bg = Arc::clone(&handler);
let accept_task = tokio::spawn(async move {
while let Some(incoming) = server_ep_bg.accept().await {
let Ok(connecting) = incoming.accept() else {
continue;
};
let Ok(conn) = connecting.await else {
continue;
};
let _ = handler_bg.accept(conn).await;
}
});
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let mut last_transient = None;
let mut observed = None;
for _ in 0..MAX_ATTEMPTS {
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let res = probe_once(
&client_ep,
target.clone(),
[0x11u8; 32],
1_700_000_000_000_000,
Duration::from_secs(5),
)
.await;
shutdown([], [&client_ep]).await?;
let Err(err) = res else {
anyhow::bail!("a pre-drained per-source bucket must reject every probe");
};
match err.downcast_ref::<UpstreamRateLimited>() {
Some(shed) => {
observed = Some(shed.clone());
break;
}
None => last_transient = Some(err),
}
}
let shed = observed.ok_or_else(|| {
anyhow::anyhow!(
"never observed an UpstreamRateLimited probe error in {MAX_ATTEMPTS} attempts; \
last transient error: {last_transient:?}"
)
})?;
assert_eq!(
shed.label.as_deref(),
Some(RejectReason::PerSource.as_str()),
"the sentinel must carry the per-source layer label off the close reason bytes"
);
shutdown([accept_task], [&server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_three_layer_limiter_rejects_per_peer() -> anyhow::Result<()> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let conn_limiter = permissive_limiter(&metrics);
let strict_probe = RateLimitConfig {
per_peer_rate_per_sec: 0.001, per_peer_burst: 1,
per_ip_rate_per_sec: 1e9,
per_ip_burst: u32::MAX,
global_rate_per_sec: 1e9,
global_burst: u32::MAX,
max_tracked_per_ip: 4096,
max_tracked_per_peer: 4096,
};
let probe_limiter = Arc::new(ProbeRateLimiter::new(&strict_probe, Arc::clone(&metrics)));
let client_sk = fresh_key();
let client_id = client_sk.public();
let client_node = NodeId::from_bytes(*client_id.as_bytes());
probe_limiter
.check(&client_node, None)
.map_err(|l| anyhow::anyhow!("pre-drain unexpectedly rejected: {l:?}"))?;
let (cache, _cache_tmp) = empty_cache().await?;
let (handler, _signer, _domain) = build_handler_with_probe_limiter(
server_id,
1,
&metrics,
conn_limiter,
Arc::clone(&probe_limiter),
cache,
);
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_PROBE.to_vec()]).await?;
let server_ep_bg = server_ep.clone();
let handler_bg = Arc::clone(&handler);
let accept_task = tokio::spawn(async move {
while let Some(incoming) = server_ep_bg.accept().await {
let Ok(connecting) = incoming.accept() else {
continue;
};
let Ok(conn) = connecting.await else {
continue;
};
let _ = handler_bg.accept(conn).await;
}
});
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
observe_rate_limit_close(ProbeRejectLayer::PerPeer.as_str().as_bytes(), move || {
let target = target.clone();
let client_sk = client_sk.clone();
async move {
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
match client_ep.connect(target, ALPN_PROBE).await {
Ok(conn) => Ok((client_ep, conn)),
Err(e) => {
shutdown([], [&client_ep]).await?;
Err(anyhow::anyhow!("connect: {e}"))
}
}
}
})
.await?;
let scrape = metrics.encode().map_err(|e| anyhow::anyhow!("{e}"))?;
let rejected = metric_value(&scrape, "decdn_probe_rate_limit_rejected_per_peer_total");
anyhow::ensure!(
rejected.is_some_and(|v| v >= 1),
"probe per-peer rejection must appear in /metrics scrape:\n{scrape}"
);
shutdown([accept_task], [&server_ep]).await?;
Ok(())
}
async fn run_one_probe(
server_sk: SecretKey,
handler: Arc<ProbeHandler>,
req: ProbeRequest,
) -> anyhow::Result<(ProbeResponse, ProbeResponseExt)> {
run_one_probe_as(fresh_key(), server_sk, handler, req).await
}
async fn run_one_probe_as(
client_sk: SecretKey,
server_sk: SecretKey,
handler: Arc<ProbeHandler>,
req: ProbeRequest,
) -> anyhow::Result<(ProbeResponse, ProbeResponseExt)> {
let server_id = server_sk.public();
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_PROBE.to_vec()]).await?;
let server_ep_bg = server_ep.clone();
let accept_task = tokio::spawn(async move {
if let Some(incoming) = server_ep_bg.accept().await {
let connecting = incoming
.accept()
.map_err(|e| anyhow::anyhow!("accept: {e}"))?;
let conn = connecting
.await
.map_err(|e| anyhow::anyhow!("handshake: {e}"))?;
handler
.accept(conn)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
}
Ok::<_, anyhow::Error>(())
});
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let conn = client_ep
.connect(target, ALPN_PROBE)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
write_frame(&mut send, &encode_message(&ProbeMessage::Request(req))?)
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("finish: {e}"))?;
let frame = read_frame(&mut recv)
.await
.map_err(|e| anyhow::anyhow!("read: {e}"))?;
let (msg, tail) = decode_message::<ProbeMessage>(&frame)?;
let resp = match msg {
ProbeMessage::Response(r) => (r, parse_probe_response_ext(tail)?),
ProbeMessage::Request(_) => anyhow::bail!("unexpected request variant on client"),
};
conn.close(0u32.into(), b"bye");
shutdown([], [&client_ep]).await?;
support::reap("accept", accept_task).await??;
shutdown([], [&server_ep]).await?;
Ok(resp)
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_has_blob_true_for_cached_blob() -> anyhow::Result<()> {
let payload = b"probe-served content-addressed bytes";
let (cache, hash, _cache_tmp) = cache_with_blob(payload).await?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (handler, signer, domain) = build_handler(server_id, 7, &metrics, limiter, cache);
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0xabc_def,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(resp.body.has_blob, "cached blob must report has_blob=true");
anyhow::ensure!(
resp_ext.total_bytes == Some(payload.len() as u64),
"total_bytes should report the blob size, got {:?}",
resp_ext.total_bytes
);
anyhow::ensure!(resp.body.hash == *hash.as_bytes(), "hash echoed");
assert_slash_sig_valid(&resp, &signer, &domain)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_has_blob_false_when_chain_stale() -> anyhow::Result<()> {
let payload = b"probe content while the chain is unreachable";
let (cache, hash, _cache_tmp) = cache_with_blob(payload).await?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let signer = Arc::new(PrivateKeySigner::random());
let domain = test_slash_domain();
let stale = decdn_node::chain_freshness::ChainFreshness::new(Duration::from_mins(30));
let handler = Arc::new(ProbeHandler::new(
server_id,
7,
Arc::clone(&metrics),
limiter,
permissive_probe_rate_limiter(&metrics),
cache,
Arc::clone(&signer),
domain.clone(),
None,
true,
Some(stale),
));
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0xabc_def,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(
!resp.body.has_blob,
"a chain-stale node must not advertise a held blob"
);
anyhow::ensure!(
resp_ext.total_bytes.is_none(),
"a suppressed advertisement carries no size, got {:?}",
resp_ext.total_bytes
);
anyhow::ensure!(
resp_ext.consistent_with(resp.body.has_blob),
"has_blob and coverage.is_empty() must be a biconditional"
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_has_blob_true_and_partial_coverage_for_cached_partial_holder() -> anyhow::Result<()>
{
let (cache, hash, _cache_tmp) = cache_with_partial_two_block_blob().await?;
let direct_coverage = cache.coverage(hash).await?;
anyhow::ensure!(direct_coverage.covers(0) && !direct_coverage.covers(1));
let (_, direct_size) = cache.coverage_sized(hash).await?;
anyhow::ensure!(direct_size.is_some(), "the fixture partial knows its size");
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (handler, signer, domain) = build_handler(server_id, 7, &metrics, limiter, cache);
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0xf00d,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(
resp.body.has_blob,
"a partial holder covering >=1 block must report has_blob=true (#1506), \
not just a Complete holder"
);
anyhow::ensure!(
resp_ext.coverage.covers(0),
"block 0 was admitted in full and must be advertised as covered"
);
anyhow::ensure!(
!resp_ext.coverage.covers(1),
"block 1's middle group was never admitted and must not be advertised"
);
anyhow::ensure!(
resp_ext.coverage == direct_coverage,
"the handler's advertised coverage must match the cache's own derivation exactly"
);
anyhow::ensure!(
resp_ext.total_bytes == direct_size,
"a partial holder must advertise its size {direct_size:?}, got {:?}",
resp_ext.total_bytes
);
anyhow::ensure!(
resp_ext.consistent_with(resp.body.has_blob),
"has_blob and coverage.is_empty() must be a biconditional"
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_origin_held_advertises_full_coverage_without_caching() -> anyhow::Result<()> {
let payload = b"origin-held content this node has never pulled into its own cache";
let origin_dir = tempfile::tempdir()?;
let hash = Hash::new(payload);
let hex = hash.to_hex();
let shard = hex
.get(..2)
.ok_or_else(|| anyhow::anyhow!("hex too short"))?;
let dir = origin_dir.path().join(shard);
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join(hex.as_str()), payload)?;
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(FilesystemOrigin::new(origin_dir.path()).await?);
let cache = CacheEngine::open(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
anyhow::ensure!(
!cache.has(hash).await?,
"the store must hold nothing for this test to exercise the origin-held path"
);
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (handler, signer, domain) = build_handler(server_id, 7, &metrics, limiter, cache);
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0xbeef,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(
resp.body.has_blob,
"an origin-held blob must be advertised even though nothing is cached"
);
anyhow::ensure!(
resp_ext.total_bytes == Some(payload.len() as u64),
"origin content's total_bytes should report the backend size, got {:?}",
resp_ext.total_bytes
);
anyhow::ensure!(
resp_ext.coverage == Coverage::full(num_blocks(payload.len() as u64)),
"an origin can serve every block, so coverage must be all-ones"
);
anyhow::ensure!(
resp_ext.consistent_with(resp.body.has_blob),
"has_blob and coverage.is_empty() must be a biconditional"
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn origin_only_probe_advertises_backend_only() -> anyhow::Result<()> {
let own_payload = b"origin-only: own fs-origin content";
let foreign_payload = b"origin-only: leftover store content, now foreign";
let (cache, own_hash, foreign_hash, _origin_tmp, _cache_tmp) =
cache_with_own_and_foreign(own_payload, foreign_payload).await?;
let metrics = Arc::new(Metrics::new());
let (handler, _signer, _domain) = build_handler_origin_only(
fresh_key().public(),
7,
&metrics,
permissive_limiter(&metrics),
cache,
);
let foreign_req = ProbeRequest {
hash: *foreign_hash.as_bytes(),
timestamp_us: 1,
};
let (foreign_resp, foreign_resp_ext) =
run_one_probe(fresh_key(), Arc::clone(&handler), foreign_req).await?;
anyhow::ensure!(
!foreign_resp.body.has_blob,
"origin-only node must not advertise a store hit outside its own origin"
);
anyhow::ensure!(
foreign_resp_ext.total_bytes.is_none(),
"a non-advertised hash must carry no total_bytes hint, got {:?}",
foreign_resp_ext.total_bytes
);
let own_req = ProbeRequest {
hash: *own_hash.as_bytes(),
timestamp_us: 2,
};
let (own_resp, own_resp_ext) = run_one_probe(fresh_key(), handler, own_req).await?;
anyhow::ensure!(
own_resp.body.has_blob,
"origin-only node must still advertise its own backend-held content"
);
anyhow::ensure!(
own_resp_ext.total_bytes == Some(own_payload.len() as u64),
"own content's total_bytes should report the backend size, got {:?}",
own_resp_ext.total_bytes
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_holds_disabled_signs_has_blob_false_and_counts_disabled() -> anyhow::Result<()> {
let payload = b"present but un-holdable";
let (cache, hash, _cache_tmp) = cache_with_blob(payload).await?;
cache.set_max_probe_holds(0);
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (handler, signer, domain) = build_handler(server_id, 7, &metrics, limiter, cache);
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0x1234,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(
!resp.body.has_blob,
"holds-disabled must yield has_blob=false even though the blob is cached"
);
anyhow::ensure!(
resp_ext.total_bytes.is_none(),
"no size advertised when has_blob=false, got {:?}",
resp_ext.total_bytes
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
let text = metrics.encode()?;
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"disabled\"}"
) == Some(1),
"holds-disabled probe must bump the reason=disabled child of \
decdn_probe_hold_unavailable_total:\n{text}"
);
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"exhausted\"}"
) == Some(0),
"an intentional disable must NOT inflate the budget-pressure counter:\n{text}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_budget_exhausted_still_advertises_and_counts_exhausted() -> anyhow::Result<()> {
let a: &[u8] = b"first popular blob";
let b: &[u8] = b"second popular blob";
let (cache, ha, hb, _cache_tmp) = cache_with_two_blobs(a, b).await?;
cache.set_max_probe_holds(1);
anyhow::ensure!(
cache.try_probe_hold(ha).await? == decdn_cache::ProbeHoldOutcome::Held,
"first hold should fit the budget"
);
let cache_probe = cache.clone();
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (handler, signer, domain) = build_handler(server_id, 7, &metrics, limiter, cache);
let req = ProbeRequest {
hash: *hb.as_bytes(),
timestamp_us: 0x55,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(
resp.body.has_blob,
"budget-exhausted must still advertise has_blob=true — the hold is forgone, not the answer"
);
anyhow::ensure!(
resp_ext.total_bytes == Some(b.len() as u64),
"the advertised blob must carry its size, got {:?}",
resp_ext.total_bytes
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
anyhow::ensure!(
cache_probe.probe_hold_slots_used() == 1,
"budget-exhausted must forgo the hold, leaving max_probe_holds=1 slot \
in use; found {}",
cache_probe.probe_hold_slots_used()
);
let text = metrics.encode()?;
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"exhausted\"}"
) == Some(1),
"genuine budget exhaustion must bump the reason=exhausted child of \
decdn_probe_hold_unavailable_total:\n{text}"
);
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"disabled\"}"
) == Some(0),
"budget pressure must NOT be counted as a config disable:\n{text}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_end_client_reserved_out_still_advertises() -> anyhow::Result<()> {
let payload = b"reserved-for-stake-lane content";
let (cache, hash, _cache_tmp) = cache_with_blob(payload).await?;
cache.set_max_probe_holds(1);
let cache_probe = cache.clone();
let server_sk = fresh_key();
let server_id = server_sk.public();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let (handler, signer, domain) = build_handler_with_lane(
server_id,
7,
&metrics,
limiter,
cache,
std::collections::HashSet::new(),
NonZeroUsize::MIN,
1,
);
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0x9001,
};
let (resp, resp_ext) = run_one_probe(server_sk, handler, req).await?;
anyhow::ensure!(
resp.body.has_blob,
"an end-client under a stake-lane reservation must still get has_blob=true (hold shed, not the answer)"
);
anyhow::ensure!(
resp_ext.total_bytes == Some(payload.len() as u64),
"the advertised blob must carry its size, got {:?}",
resp_ext.total_bytes
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
anyhow::ensure!(
cache_probe.probe_hold_slots_used() == 0,
"a reserved-out end-client must place no hold; {} slot(s) in use",
cache_probe.probe_hold_slots_used()
);
let text = metrics.encode()?;
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"stake_lane_reserved\"}"
) == Some(1),
"reservation refusal must bump the reason=stake_lane_reserved child of \
decdn_probe_hold_unavailable_total:\n{text}"
);
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"exhausted\"}"
) == Some(0),
"a stake-lane reservation must NOT be counted as budget pressure:\n{text}"
);
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"disabled\"}"
) == Some(0),
"a stake-lane reservation must NOT be counted as a config disable:\n{text}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_stake_lane_requester_keeps_reserved_headroom() -> anyhow::Result<()> {
let payload = b"served to a node-to-node requester";
let (cache, hash, _cache_tmp) = cache_with_blob(payload).await?;
cache.set_max_probe_holds(1);
let cache_probe = cache.clone();
let server_sk = fresh_key();
let server_id = server_sk.public();
let client_sk = fresh_key();
let client_node_id = NodeId::from_bytes(*client_sk.public().as_bytes());
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let mut stakers = std::collections::HashSet::new();
stakers.insert(client_node_id);
let (handler, signer, domain) = build_handler_with_lane(
server_id,
7,
&metrics,
limiter,
cache,
stakers,
NonZeroUsize::MIN,
1,
);
let req = ProbeRequest {
hash: *hash.as_bytes(),
timestamp_us: 0x9002,
};
let (resp, resp_ext) = run_one_probe_as(client_sk, server_sk, handler, req).await?;
anyhow::ensure!(
resp.body.has_blob,
"a stake-lane requester must keep its reserved headroom -> has_blob=true"
);
anyhow::ensure!(
resp_ext.total_bytes == Some(payload.len() as u64),
"stake-lane requester should be served the blob size, got {:?}",
resp_ext.total_bytes
);
assert_slash_sig_valid(&resp, &signer, &domain)?;
anyhow::ensure!(
cache_probe.probe_hold_slots_used() == 1,
"a stake-lane requester must consume its reserved hold slot; {} in use",
cache_probe.probe_hold_slots_used()
);
let text = metrics.encode()?;
anyhow::ensure!(
metric_value(
&text,
"decdn_probe_hold_unavailable_total{reason=\"stake_lane_reserved\"}"
) == Some(0),
"a stake-lane requester must NOT trip the reservation counter, and the \
series must be present at zero rather than absent:\n{text}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn idle_timeout_closes_quiet_connection() -> anyhow::Result<()> {
let idle = Duration::from_millis(300);
let build_cfg = || -> anyhow::Result<QuicTransportConfig> {
let it: IdleTimeout = idle
.try_into()
.map_err(|e| anyhow::anyhow!("idle timeout: {e}"))?;
Ok(QuicTransportConfig::builder()
.max_idle_timeout(Some(it))
.keep_alive_interval(Duration::from_mins(1))
.max_concurrent_bidi_streams(VarInt::from_u32(100))
.build())
};
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_bind = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
let server_ep = Endpoint::builder(presets::Minimal)
.secret_key(server_sk)
.transport_config(build_cfg()?)
.alpns(vec![ALPN_PROBE.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(server_bind)
.map_err(|e| anyhow::anyhow!("server bind_addr: {e}"))?
.bind()
.await
.map_err(|e| anyhow::anyhow!("server bind: {e}"))?;
let server_addr = server_ep
.bound_sockets()
.into_iter()
.find(SocketAddr::is_ipv4)
.ok_or_else(|| anyhow::anyhow!("no IPv4 bound socket"))?;
let server_addr = match server_addr {
SocketAddr::V4(v4) if v4.ip().is_unspecified() => {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, v4.port()))
}
other => other,
};
let server_ep_bg = server_ep.clone();
let accept_task = tokio::spawn(async move {
if let Some(incoming) = server_ep_bg.accept().await {
let connecting = incoming
.accept()
.map_err(|e| anyhow::anyhow!("accept: {e}"))?;
let conn = connecting
.await
.map_err(|e| anyhow::anyhow!("handshake: {e}"))?;
let _ = conn.closed().await;
}
Ok::<_, anyhow::Error>(())
});
let client_bind = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
let client_ep = Endpoint::builder(presets::Minimal)
.secret_key(fresh_key())
.transport_config(build_cfg()?)
.relay_mode(RelayMode::Disabled)
.bind_addr(client_bind)
.map_err(|e| anyhow::anyhow!("client bind_addr: {e}"))?
.bind()
.await
.map_err(|e| anyhow::anyhow!("client bind: {e}"))?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let client_conn = client_ep
.connect(target, ALPN_PROBE)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let close_err = tokio::time::timeout(Duration::from_secs(3), client_conn.closed())
.await
.map_err(|_| {
anyhow::anyhow!("connection did not idle-close within 3s (idle window: {idle:?})")
})?;
match close_err {
ConnectionError::TimedOut => {}
other => anyhow::bail!("expected ConnectionError::TimedOut, got {other:?}"),
}
shutdown([], [&client_ep]).await?;
support::reap("accept", accept_task).await??;
shutdown([], [&server_ep]).await?;
Ok(())
}