use std::sync::Arc;
use std::time::{Duration, Instant};
use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::{Address, B256, U256};
use alloy::signers::SignerSync;
use alloy::signers::local::PrivateKeySigner;
use async_trait::async_trait;
use decdn_cache::{
CacheEngine, CacheMetrics, CircuitBreakerPolicy, Hash, PinnedHashes, RetryPolicy,
};
use decdn_client::{
BlobTooLarge, Cumulative, HashMismatch, PoolContext, PoolLedger, PullDeadlines,
RateAboveCeiling, UpstreamVoucherRejected, VoucherProgress, WarmConnection,
sign_client_binding, stream_fetch, stream_fetch_on, stream_fetch_shared, stream_fetch_tracked,
stream_fetch_tracked_with_progress,
};
use decdn_incentive::{
EPHEMERAL_BINDING_NONCE, LaneKey, LaneState, MemoryPoolStateStore, PoolStateStore, Voucher,
bind_node_id_domain, binding_signing_hash, min_payment, signed_to_wire_voucher,
slash_judge_domain, stream_sig::StreamSlashData, voucher_domain,
};
use decdn_node::dispatch::ConnectionLimiter;
use decdn_node::handlers::client::{ClientHandler, ClientHandlerDeps, ClientProtocol};
use decdn_node::metrics::Metrics;
use decdn_protocol::client::{
CHUNK_BYTES, ChunkData, ClientBinding, ClientMessage, StreamError, StreamRequest,
StreamRequestExt, StreamResponse, StreamResponseBody, StreamResponseExt, VoucherRejectReason,
encode_stream_response,
};
use decdn_protocol::{ALPN_CLIENT, decode_message, encode_stream_request, read_frame, write_frame};
use iroh::endpoint::{
ApplicationClose, Connection, ConnectionError, ReadError, ReadToEndError, RecvStream,
SendStream, VarInt,
};
use iroh::protocol::ProtocolHandler as _;
use iroh::{Endpoint, EndpointAddr};
mod support;
use decdn_node::receipt_log::{DownloadReceipt, spawn_receipt_writer};
use support::{
BlockingReceiptLog, BufferedClientReader, FailingReceiptLog, FailingRecordStore,
HandlerDomains, VecReceiptLog, build_handler_full, build_handler_full_configured,
build_handler_full_with_receipts, build_handler_full_with_sink, cache_with_blob, empty_cache,
fresh_key, local_endpoint, permissive_limiter, read_client_msg, read_stream_response, shutdown,
spawn_server, spawn_server_counting, write_client_msg,
};
const CHAIN_ID: u64 = 421_614;
const RATE_PER_MB: u64 = 10;
fn hex_lower(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes.iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
fn slash_domain() -> Eip712Domain {
slash_judge_domain(CHAIN_ID, Address::repeat_byte(0x11))
}
fn payment_domain() -> Eip712Domain {
voucher_domain(CHAIN_ID, Address::repeat_byte(0x34))
}
fn binding_domain() -> Eip712Domain {
bind_node_id_domain(CHAIN_ID, Address::repeat_byte(0x99))
}
fn loopback_domains() -> HandlerDomains {
HandlerDomains {
slash: slash_domain(),
voucher: payment_domain(),
binding: binding_domain(),
}
}
const fn pool_id() -> B256 {
B256::repeat_byte(0xC1)
}
#[allow(clippy::expect_used)] fn operator_signer() -> Arc<PrivateKeySigner> {
Arc::new(
PrivateKeySigner::from_bytes(&B256::repeat_byte(0x42))
.expect("0x42-repeated is a valid secp256k1 scalar"),
)
}
fn operator_addr() -> Address {
operator_signer().address()
}
fn lane_key(signer: Address) -> LaneKey {
LaneKey {
pool_id: pool_id(),
signer,
provider: operator_addr(),
}
}
fn fresh_lane(signer: Address, cap: U256) -> LaneState {
LaneState::hydrate(
pool_id(),
signer,
operator_addr(),
cap,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
)
}
fn build_handler(
server_id: iroh::PublicKey,
server_eth: &Arc<PrivateKeySigner>,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
rate: u64,
) -> anyhow::Result<Arc<ClientHandler>> {
build_handler_limited(
server_id, server_eth, metrics, limiter, cache, store, rate, 16,
)
}
#[allow(clippy::too_many_arguments)]
fn build_handler_limited(
server_id: iroh::PublicKey,
server_eth: &Arc<PrivateKeySigner>,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
rate: u64,
max_concurrent_streams: usize,
) -> anyhow::Result<Arc<ClientHandler>> {
build_handler_full(
server_id,
server_eth,
metrics,
limiter,
cache,
store,
rate,
&loopback_domains(),
max_concurrent_streams,
)
}
#[allow(clippy::too_many_arguments)]
fn build_handler_configured(
server_id: iroh::PublicKey,
server_eth: &Arc<PrivateKeySigner>,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
rate: u64,
configure: impl FnOnce(&mut ClientHandlerDeps),
) -> anyhow::Result<Arc<ClientHandler>> {
build_handler_limited_configured(
server_id, server_eth, metrics, limiter, cache, store, rate, 16, configure,
)
}
#[allow(clippy::too_many_arguments)]
fn build_handler_limited_configured(
server_id: iroh::PublicKey,
server_eth: &Arc<PrivateKeySigner>,
metrics: &Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
rate: u64,
max_concurrent_streams: usize,
configure: impl FnOnce(&mut ClientHandlerDeps),
) -> anyhow::Result<Arc<ClientHandler>> {
build_handler_full_configured(
server_id,
server_eth,
metrics,
limiter,
cache,
store,
rate,
&loopback_domains(),
max_concurrent_streams,
configure,
)
}
#[allow(clippy::expect_used)] fn pool_context(
client_ep: &Endpoint,
client_signer: Arc<PrivateKeySigner>,
deposit: U256,
) -> PoolContext {
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&client_signer, own_node_id, &binding_domain())
.expect("sign client binding over the loopback client node id");
PoolContext {
pool_id: pool_id(),
provider: operator_addr(),
deposit,
client_signer,
voucher_domain: payment_domain(),
prior_bytes_delivered: U256::ZERO,
prior_amount: U256::ZERO,
client_binding: Some(binding),
capability: None,
}
}
fn unbound_context(client_signer: Arc<PrivateKeySigner>, deposit: U256) -> PoolContext {
PoolContext {
pool_id: pool_id(),
provider: operator_addr(),
deposit,
client_signer,
voucher_domain: payment_domain(),
prior_bytes_delivered: U256::ZERO,
prior_amount: U256::ZERO,
client_binding: None,
capability: None,
}
}
#[tokio::test(flavor = "multi_thread")]
async fn client_delivery_roundtrip_advances_lane_state() -> anyhow::Result<()> {
let spans = support::capture_spans();
let payload = vec![0xABu8; 1_572_864]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
let persisted = store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let wire = support::bao_wire_len_whole(payload.len() as u64);
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(wire),
"bytes_delivered: {} (expected {wire})",
only.last_bytes_delivered()
);
anyhow::ensure!(only.last_amount() > U256::ZERO, "amount must be non-zero");
let key = hash.to_string();
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while spans.matching("serve_stream", "hash", &key).is_empty()
&& std::time::Instant::now() < deadline
{
tokio::time::sleep(Duration::from_millis(20)).await;
}
shutdown([server_task], [&client_ep, &server_ep]).await?;
assert_one_completed_inbound_stream(&metrics, 2)?;
let served = spans.matching("serve_stream", "hash", &key);
let [span] = served.as_slice() else {
anyhow::bail!("expected one serve_stream span for the hash, got {served:?}");
};
anyhow::ensure!(
span.fields.get("outcome").map(String::as_str) == Some("completed"),
"outcome: {span:?}"
);
anyhow::ensure!(
span.fields.get("bytes").is_some_and(|b| b != "0"),
"bytes: {span:?}"
);
anyhow::ensure!(
span.fields.get("peer").map(String::as_str) == Some(client_ep.id().to_string().as_str()),
"peer: {span:?}"
);
support::assert_serve_waits_recorded(span, payload.len() as u64)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn two_hashes_reuse_one_warm_connection() -> anyhow::Result<()> {
let payload_a = vec![0x11u8; 400_000];
let payload_b = vec![0x22u8; 500_000];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(50_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let (server_task, accepted) = spawn_server_counting(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let warm = WarmConnection::connect(&client_ep, target, Duration::from_secs(20), None).await?;
let got_a = stream_fetch_on(
&warm,
&ctx,
&slash_domain(),
server_eth.address(),
*hash_a.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got_a.as_ref() == payload_a.as_slice(),
"hash A bytes mismatch"
);
let after_a = store.load_all()?;
let lane_a = after_a
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane after hash A"))?;
let ctx_b = PoolContext {
prior_bytes_delivered: lane_a.last_bytes_delivered(),
prior_amount: lane_a.last_amount(),
..ctx
};
let got_b = stream_fetch_on(
&warm,
&ctx_b,
&slash_domain(),
server_eth.address(),
*hash_b.as_bytes(),
0,
0x00c0_ffef,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got_b.as_ref() == payload_b.as_slice(),
"hash B bytes mismatch"
);
let dialed = accepted.load(std::sync::atomic::Ordering::SeqCst);
anyhow::ensure!(
dialed == 1,
"expected exactly one accepted connection across two hash fetches, got {dialed}"
);
drop(warm);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[allow(
clippy::too_many_lines,
reason = "one fixture: a paid handler, a lane, two drives and a refusal"
)]
#[tokio::test(flavor = "multi_thread")]
async fn peer_source_legs_share_one_connection_across_a_refusal() -> anyhow::Result<()> {
use decdn_client::driver::drive;
use decdn_client::source::BlobSource as _;
use decdn_client::{BudgetPacer, ClientRangedStore, Connections, PeerSource};
let payload_a = vec![0x31u8; 400_000];
let payload_b = vec![0x42u8; 500_000];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(50_000_000u64);
let pool_store = Arc::new(MemoryPoolStateStore::new());
pool_store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = pool_store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let (server_task, accepted) = spawn_server_counting(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let ledger = Arc::new(ctx.new_ledger());
let ctx = Arc::new(std::sync::Mutex::new(ctx));
let slash = slash_domain();
let connections = Connections::new(client_ep.clone());
let source = PeerSource::new(
&client_ep,
target,
Arc::clone(&ctx),
Arc::clone(&ledger),
&slash,
server_eth.address(),
decdn_protocol::client::NO_NAMESPACE,
0,
u64::MAX,
PullDeadlines::new(Duration::from_secs(10), Duration::from_secs(10), 0)?,
Some(connections.clone()),
);
let pacer = BudgetPacer::new();
let store_dir = tempfile::tempdir()?;
let fetch = |name: &'static str, hash: Hash, len: usize| {
let (source, pacer, ctx, ledger) = (&source, &pacer, &ctx, &ledger);
let dir = store_dir.path().to_path_buf();
async move {
let total = u64::try_from(len)?;
let store = ClientRangedStore::create(&dir, name, *hash.as_bytes(), total)?;
drive(
&store,
source,
pacer,
ctx,
ledger,
*hash.as_bytes(),
0,
0,
None,
None,
None,
None,
)
.await?;
Ok::<_, anyhow::Error>(std::fs::read(dir.join(name))?)
}
};
anyhow::ensure!(fetch("a", hash_a, payload_a.len()).await? == payload_a);
let known_pool = {
let mut ctx = ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::replace(&mut ctx.pool_id, B256::repeat_byte(0xEE))
};
let range = decdn_bao_range::align_range(0, 0, u64::try_from(payload_a.len())?)?;
let refused = source
.open(*hash_a.as_bytes(), range)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a leg on an unknown pool must be refused"))?;
ctx.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.pool_id = known_pool;
anyhow::ensure!(
refused
.downcast_ref::<decdn_client::UpstreamRefused>()
.is_some_and(|r| *r.error() == StreamError::NotFound),
"expected a NotFound refusal, got: {refused:#}"
);
anyhow::ensure!(fetch("b", hash_b, payload_b.len()).await? == payload_b);
anyhow::ensure!(
connections.dials() == 1,
"every leg must share one dial, got {}",
connections.dials()
);
let served = accepted.load(std::sync::atomic::Ordering::SeqCst);
anyhow::ensure!(
served == 1,
"the node must accept one connection, got {served}"
);
drop(source);
drop(connections);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[allow(
clippy::too_many_lines,
reason = "one fixture: a paid handler, a lane, and the two drives it serves"
)]
async fn drive_scattered_as_a_restarted_payer() -> anyhow::Result<()> {
use decdn_client::driver::drive;
use decdn_client::{BudgetPacer, ClientRangedStore, PeerSource, SharedPool};
const GROUP: u64 = 16 * 1024;
let payload: Vec<u8> = (0..32 * GROUP).map(|i| (i % 251) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let total = u64::try_from(payload.len())?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(1_000_000_000u64);
let pool_store = Arc::new(MemoryPoolStateStore::new());
pool_store.record(&lane_with_proved_reveals(&client_signer, deposit)?)?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = pool_store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let (server_task, _accepted) = spawn_server_counting(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let ledger = Arc::new(ctx.new_ledger());
let ctx = Arc::new(std::sync::Mutex::new(ctx));
let slash = slash_domain();
let source = PeerSource::new(
&client_ep,
target,
Arc::clone(&ctx),
Arc::clone(&ledger),
&slash,
server_eth.address(),
decdn_protocol::client::NO_NAMESPACE,
0,
u64::MAX,
PullDeadlines::new(Duration::from_secs(10), Duration::from_secs(10), 0)?,
None,
);
let store_dir = tempfile::tempdir()?;
let stores = (0..4)
.map(|i| {
ClientRangedStore::create(
store_dir.path(),
&format!("blob{i}"),
*hash.as_bytes(),
total,
)
})
.collect::<Result<Vec<_>, _>>()?;
let spent_ledger = Arc::clone(&ledger);
let spent = move || spent_ledger.committed().amount;
let pool = SharedPool {
spent: &spent,
quotes: None,
};
let scattered: Vec<(u64, u64)> = (0..32).step_by(2).map(|g| (g * GROUP, GROUP)).collect();
let pacer = BudgetPacer::new();
let range = |store, (offset, len): (u64, u64)| {
drive(
store,
&source,
&pacer,
&ctx,
&ledger,
*hash.as_bytes(),
offset,
len,
None,
None,
None,
Some(&pool),
)
};
let siblings = stores.iter().enumerate().map(|(i, store)| {
let mine: Vec<(u64, u64)> = scattered.iter().copied().skip(i).step_by(4).collect();
async move {
for r in mine {
range(store, r).await?;
}
anyhow::Ok(())
}
});
for driven in futures_util::future::join_all(siblings).await {
driven?;
}
let first = stores.first().ok_or_else(|| anyhow::anyhow!("no store"))?;
range(first, (0, 0)).await?;
anyhow::ensure!(
std::fs::read(store_dir.path().join("blob0"))? == payload,
"the range set and the rest assemble the blob byte-exact"
);
drop(source);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_restarted_payer_heals_a_concurrent_range_set() -> anyhow::Result<()> {
Box::pin(drive_scattered_as_a_restarted_payer()).await
}
#[tokio::test(flavor = "multi_thread")]
async fn a_restarted_payer_heals_a_sub_chunk_transfer() -> anyhow::Result<()> {
restarted_sub_chunk_transfer(2).await
}
#[tokio::test(flavor = "multi_thread")]
async fn a_restarted_payer_heals_a_transfer_that_repeats_the_anchor() -> anyhow::Result<()> {
restarted_sub_chunk_transfer(1).await
}
async fn restarted_sub_chunk_transfer(earlier: u64) -> anyhow::Result<()> {
let payload = vec![0x3Cu8; 256 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let fetch = |ctx: PoolContext, timestamp_us: u64, budget: Duration| {
let target = target.clone();
let client_ep = &client_ep;
let server = server_eth.address();
async move {
stream_fetch(
client_ep,
target,
&ctx,
&slash_domain(),
server,
*hash.as_bytes(),
0,
timestamp_us,
budget,
)
.await
}
};
for timestamp_us in 0x7771..0x7771 + earlier {
let mut ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
if let Some(lane) = store.load_all()?.first() {
ctx.prior_amount = lane.last_amount();
ctx.prior_bytes_delivered = lane.last_bytes_delivered();
}
let got = fetch(ctx, timestamp_us, Duration::from_secs(20)).await?;
anyhow::ensure!(got.as_ref() == payload.as_slice());
}
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let got = fetch(ctx, 0x7770, Duration::from_secs(8)).await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"the restarted payer must reseed and recover the blob"
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_voucher_rejected_total")? == 1,
"the heal costs exactly one rejected voucher"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
fn assert_first_byte_class(metrics: &Metrics, class: &str) -> anyhow::Result<()> {
let encoded = metrics.encode()?;
for sibling in ["hit", "miss"] {
let line = format!(
"decdn_serve_first_byte_{sibling}_seconds_count {}",
u64::from(sibling == class)
);
anyhow::ensure!(
metric_line_present(&encoded, &line),
"missing `{line}`:\n{encoded}"
);
}
Ok(())
}
fn assert_one_completed_inbound_stream(metrics: &Metrics, vouchers: u64) -> anyhow::Result<()> {
let encoded = metrics.encode()?;
for line in [
"decdn_streams_completed_total{direction=\"inbound\"} 1".to_string(),
"decdn_streams_failed_total{direction=\"inbound\"} 0".to_string(),
format!("decdn_vouchers_received_total {vouchers}"),
"decdn_serve_cache_hit_total 1".to_string(),
"decdn_serve_cache_partial_hit_total 0".to_string(),
"decdn_serve_cache_miss_total 0".to_string(),
"decdn_serve_first_byte_hit_seconds_count 1".to_string(),
"decdn_serve_first_byte_miss_seconds_count 0".to_string(),
] {
anyhow::ensure!(
metric_line_present(&encoded, &line),
"missing `{line}`:\n{encoded}"
);
}
anyhow::ensure!(
!metric_line_present(&encoded, "decdn_bytes_served_total 0"),
"bytes served must count the delivered frames:\n{encoded}"
);
Ok(())
}
const HARNESS_INTERVAL_BYTES: u64 = decdn_protocol::client::CHUNK_BYTES;
const HARNESS_LAG_BYTES: u64 = decdn_client::VERIFY_LAG_BYTES;
const HARNESS_FLOOR_BYTES: u64 = HARNESS_INTERVAL_BYTES + HARNESS_LAG_BYTES;
async fn spawn_pipelined_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
credit_max: u64,
credit_ramp_divisor: u64,
) -> anyhow::Result<(
EndpointAddr,
Arc<PrivateKeySigner>,
Endpoint,
tokio::task::JoinHandle<()>,
)> {
let (target, server_eth, server_ep, server_task, _metrics) =
spawn_pipelined_server_with_metrics(cache, store, credit_max, credit_ramp_divisor).await?;
Ok((target, server_eth, server_ep, server_task))
}
async fn spawn_pipelined_server_with_metrics(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
credit_max: u64,
credit_ramp_divisor: u64,
) -> anyhow::Result<(
EndpointAddr,
Arc<PrivateKeySigner>,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_full_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
&loopback_domains(),
16,
|deps| {
deps.credit_max = credit_max;
deps.credit_ramp_divisor = credit_ramp_divisor;
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_eth, server_ep, server_task, metrics))
}
async fn open_paid_stream(
conn: &Connection,
hash: [u8; 32],
ext: Option<&StreamRequestExt>,
) -> anyhow::Result<(SendStream, RecvStream)> {
open_paid_range_stream(conn, hash, ext, 0, 0).await
}
async fn open_paid_range_stream(
conn: &Connection,
hash: [u8; 32],
ext: Option<&StreamRequestExt>,
byte_offset: u64,
byte_len: u64,
) -> anyhow::Result<(SendStream, RecvStream)> {
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let req = StreamRequest {
hash,
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset,
byte_len,
timestamp_us: 0x0012_61a0,
};
let payload =
encode_stream_request(&req, ext).map_err(|e| anyhow::anyhow!("encode request: {e}"))?;
write_frame(&mut send, &payload)
.await
.map_err(|e| anyhow::anyhow!("write request: {e}"))?;
let (resp, resp_ext) = read_stream_response(&mut recv).await?;
anyhow::ensure!(resp.body.ok, "delivery refused: {:?}", resp_ext.error);
Ok((send, recv))
}
async fn read_exact_chunks(recv: &mut RecvStream, expect: u64) -> anyhow::Result<()> {
let mut got: u64 = 0;
while got < expect {
let msg = tokio::time::timeout(Duration::from_secs(10), read_client_msg(recv))
.await
.map_err(|_| {
anyhow::anyhow!("server stalled after {got} of {expect} expected wire bytes")
})??;
match msg {
ClientMessage::ChunkData(chunk) => {
got = got.saturating_add(chunk.bytes().len() as u64);
}
other => anyhow::bail!("expected ChunkData, got {other:?}"),
}
}
anyhow::ensure!(
got == expect,
"read {got} wire bytes, expected exactly {expect} (server overshot the credit window)"
);
Ok(())
}
async fn assert_parked_awaiting_voucher(recv: &mut RecvStream) -> anyhow::Result<()> {
match tokio::time::timeout(Duration::from_secs(2), read_client_msg(recv)).await {
Err(_elapsed) => Ok(()),
Ok(Ok(ClientMessage::ChunkData(chunk))) => anyhow::bail!(
"server sent {} more wire bytes past the credit window instead of parking for a voucher",
chunk.bytes().len()
),
Ok(other) => anyhow::bail!("expected the server to park awaiting a voucher; got {other:?}"),
}
}
async fn pay_cumulative(
send: &mut SendStream,
signer: &PrivateKeySigner,
bytes_delivered: u64,
) -> anyhow::Result<()> {
let amount = min_payment(bytes_delivered, RATE_PER_MB);
let voucher = Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount,
bytes_delivered: U256::from(bytes_delivered),
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
}
.sign(signer, &payment_domain())
.map_err(|e| anyhow::anyhow!("sign voucher: {e}"))?;
write_client_msg(
send,
&ClientMessage::Voucher(signed_to_wire_voucher(&voucher)?),
)
.await
}
const CHAIN_SEED: B256 = B256::repeat_byte(0x5E);
const ROLLED_SEED: B256 = B256::repeat_byte(0x6E);
fn chain_root() -> B256 {
decdn_incentive::root_from_seed(CHAIN_SEED)
}
fn rolled_root() -> B256 {
decdn_incentive::root_from_seed(ROLLED_SEED)
}
async fn open_chain(
send: &mut SendStream,
signer: &PrivateKeySigner,
root: B256,
chunk_price: u64,
amount: U256,
bytes_delivered: u64,
) -> anyhow::Result<()> {
let voucher = Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount,
bytes_delivered: U256::from(bytes_delivered),
chain_root: root,
chunk_price: U256::from(chunk_price),
}
.sign(signer, &payment_domain())
.map_err(|e| anyhow::anyhow!("sign voucher: {e}"))?;
write_client_msg(
send,
&ClientMessage::Voucher(signed_to_wire_voucher(&voucher)?),
)
.await
}
async fn release(send: &mut SendStream, index: u8) -> anyhow::Result<()> {
release_from(send, CHAIN_SEED, index).await
}
async fn release_from(send: &mut SendStream, chain_seed: B256, index: u8) -> anyhow::Result<()> {
write_client_msg(
send,
&ClientMessage::ChunkPreimage(decdn_protocol::client::ChunkPreimage {
preimage: decdn_incentive::preimage_at(chain_seed, index).into(),
index,
}),
)
.await
}
async fn expect_reject(recv: &mut RecvStream, expected: VoucherRejectReason) -> anyhow::Result<()> {
loop {
let msg = tokio::time::timeout(Duration::from_secs(10), read_client_msg(recv))
.await
.map_err(|_| anyhow::anyhow!("node never answered with a rejection"))??;
match msg {
ClientMessage::ChunkData(_) => {}
ClientMessage::StreamError(StreamError::VoucherRejected { reason, bundle }) => {
anyhow::ensure!(reason == expected, "expected {expected:?}, got {reason:?}");
anyhow::ensure!(
bundle.is_none(),
"a chain rejection carries no watermark bundle: a preimage has no \
signature of its own, so a payment watermark cannot repair it"
);
return Ok(());
}
other => anyhow::bail!("expected a VoucherRejected, got {other:?}"),
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn the_first_frame_is_small_and_the_interval_still_ends_on_its_boundary() -> anyhow::Result<()>
{
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
const FIRST_FRAME_MAX: u64 = 64 * 1024;
let payload = vec![0x45u8; 8 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, _metrics) =
spawn_pipelined_server_with_metrics(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (_send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
let first = tokio::time::timeout(Duration::from_secs(10), read_client_msg(&mut recv))
.await
.map_err(|_| anyhow::anyhow!("no first frame"))??;
let ClientMessage::ChunkData(first) = first else {
anyhow::bail!("expected ChunkData, got {first:?}");
};
let first_len = first.bytes().len() as u64;
anyhow::ensure!(
first_len <= FIRST_FRAME_MAX,
"the first frame carries {first_len} bytes, over {FIRST_FRAME_MAX}"
);
read_exact_chunks(&mut recv, HARNESS_FLOOR_BYTES - first_len).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_released_preimage_pays_for_a_chunk_and_resumes_delivery() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x44u8; 8 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_pipelined_server_with_metrics(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
open_chain(&mut send, &signer, chain_root(), RATE_PER_MB, U256::ZERO, 0).await?;
release(&mut send, 1).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
let lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
anyhow::ensure!(
lane.chain().verified_index == 1,
"the lane's chain frontier must record the reveal"
);
anyhow::ensure!(
lane.owed() == U256::from(RATE_PER_MB),
"one chunk at the quoted rate, owed on top of a zero anchor: {}",
lane.owed()
);
anyhow::ensure!(counter(&metrics, "decdn_vouchers_received_total")? == 1);
anyhow::ensure!(counter(&metrics, "decdn_preimage_reveals_received_total")? == 1);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_preimage_before_its_epoch_voucher_is_unanchored() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x45u8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
release(&mut send, 1).await?;
expect_reject(&mut recv, VoucherRejectReason::UnanchoredPreimage).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_preimage_at_index_zero_is_refused() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x46u8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
open_chain(&mut send, &signer, chain_root(), RATE_PER_MB, U256::ZERO, 0).await?;
release(&mut send, 0).await?;
expect_reject(&mut recv, VoucherRejectReason::ChainIndexZero).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_preimage_inside_the_expiry_margin_is_refused() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x49u8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let cutoff = unix_now()?.saturating_add(2);
let mut lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
lane.expiry = cutoff.saturating_add(DEFAULT_EXPIRY_MARGIN);
store.record(&lane)?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
open_chain(&mut send, &signer, chain_root(), RATE_PER_MB, U256::ZERO, 0).await?;
anyhow::ensure!(
unix_now()? < cutoff,
"the anchor must land before the margin"
);
while unix_now()? < cutoff {
tokio::time::sleep(Duration::from_millis(100)).await;
}
release(&mut send, 1).await?;
expect_reject(&mut recv, VoucherRejectReason::CapabilityExpired).await?;
let lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
anyhow::ensure!(
lane.chain().verified_index == 0,
"the refused reveal must not extend the claim"
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_foreign_preimage_is_refused() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x47u8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
open_chain(&mut send, &signer, chain_root(), RATE_PER_MB, U256::ZERO, 0).await?;
write_client_msg(
&mut send,
&ClientMessage::ChunkPreimage(decdn_protocol::client::ChunkPreimage {
preimage: decdn_incentive::preimage_at(B256::repeat_byte(0x5F), 1).into(),
index: 1,
}),
)
.await?;
expect_reject(&mut recv, VoucherRejectReason::BadPreimage).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
fn assert_one_reject_line(
spans: &support::SpanCapture,
signer: &PrivateKeySigner,
reason: &str,
with_watermark: bool,
) -> anyhow::Result<()> {
let lines = spans.events(
"rejecting a payment proof",
"signer",
&signer.address().to_string(),
);
let [line] = lines.as_slice() else {
anyhow::bail!("expected one proof-reject line for the signer, got {lines:?}");
};
let with_watermark = with_watermark.to_string();
anyhow::ensure!(
line.get("reason").map(String::as_str) == Some(reason)
&& line.get("with_watermark") == Some(&with_watermark),
"expected reason={reason} with_watermark={with_watermark}: {line:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_chain_opened_below_the_quoted_rate_is_refused() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let spans = support::capture_spans();
let payload = vec![0x48u8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
open_chain(&mut send, &signer, chain_root(), 1, U256::ZERO, 0).await?;
expect_reject(&mut recv, VoucherRejectReason::ChunkPriceMismatch).await?;
assert_one_reject_line(&spans, &signer, "ChunkPriceMismatch", false)?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_sealed_voucher_carrying_a_price_is_refused() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x49u8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
open_chain(
&mut send,
&signer,
B256::ZERO,
RATE_PER_MB,
min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB),
HARNESS_INTERVAL_BYTES,
)
.await?;
expect_reject(&mut recv, VoucherRejectReason::ChunkPriceMismatch).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn two_streams_on_one_lane_converge_on_the_deepest_index() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x4Au8; 8 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send_a, mut recv_a) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
let (mut send_b, mut recv_b) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv_a, HARNESS_FLOOR_BYTES).await?;
read_exact_chunks(&mut recv_b, HARNESS_FLOOR_BYTES).await?;
open_chain(
&mut send_a,
&signer,
chain_root(),
RATE_PER_MB,
U256::ZERO,
0,
)
.await?;
open_chain(
&mut send_b,
&signer,
chain_root(),
RATE_PER_MB,
U256::ZERO,
0,
)
.await?;
release(&mut send_a, 3).await?;
read_exact_chunks(&mut recv_a, HARNESS_INTERVAL_BYTES).await?;
release(&mut send_b, 1).await?;
release(&mut send_b, 2).await?;
release(&mut send_b, 5).await?;
read_exact_chunks(&mut recv_b, HARNESS_INTERVAL_BYTES).await?;
let deadline = Instant::now() + Duration::from_secs(10);
let lane = loop {
let lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
if lane.chain().verified_index >= 5 || Instant::now() >= deadline {
break lane;
}
tokio::time::sleep(Duration::from_millis(20)).await;
};
anyhow::ensure!(
lane.chain().verified_index == 5,
"the lane converges on the deepest index proved, got {}",
lane.chain().verified_index
);
anyhow::ensure!(
lane.owed() == U256::from(5 * RATE_PER_MB),
"the lane's claim is a MAXIMUM over the streams, never a sum: {}",
lane.owed()
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_below_frontier_reveal_is_credited_from_lane_headroom() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x4Bu8; 8 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send_a, mut recv_a) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
let (mut send_b, mut recv_b) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv_a, HARNESS_FLOOR_BYTES).await?;
read_exact_chunks(&mut recv_b, HARNESS_FLOOR_BYTES).await?;
open_chain(
&mut send_a,
&signer,
chain_root(),
RATE_PER_MB,
U256::ZERO,
0,
)
.await?;
release(&mut send_a, 5).await?;
read_exact_chunks(&mut recv_a, HARNESS_INTERVAL_BYTES).await?;
open_chain(
&mut send_b,
&signer,
chain_root(),
RATE_PER_MB,
U256::ZERO,
0,
)
.await?;
assert_parked_awaiting_voucher(&mut recv_b).await?;
release(&mut send_b, 3).await?;
read_exact_chunks(&mut recv_b, HARNESS_INTERVAL_BYTES).await?;
release(&mut send_b, 4).await?;
read_exact_chunks(&mut recv_b, HARNESS_INTERVAL_BYTES).await?;
let lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
anyhow::ensure!(
lane.chain().verified_index == 5,
"the lane stays at the deepest index proved, got {}",
lane.chain().verified_index
);
anyhow::ensure!(
lane.owed() == U256::from(5 * RATE_PER_MB),
"the below-frontier reveals must take no new money: {}",
lane.owed()
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_sibling_roll_does_not_take_a_retired_reveals_credit() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x4Cu8; 8 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send_x, mut recv_x) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
let (mut send_y, mut recv_y) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv_x, HARNESS_FLOOR_BYTES).await?;
read_exact_chunks(&mut recv_y, HARNESS_FLOOR_BYTES).await?;
open_chain(
&mut send_x,
&signer,
chain_root(),
RATE_PER_MB,
U256::ZERO,
0,
)
.await?;
release(&mut send_x, 1).await?;
read_exact_chunks(&mut recv_x, HARNESS_INTERVAL_BYTES).await?;
open_chain(
&mut send_y,
&signer,
chain_root(),
RATE_PER_MB,
U256::ZERO,
0,
)
.await?;
assert_parked_awaiting_voucher(&mut recv_y).await?;
open_chain(
&mut send_x,
&signer,
rolled_root(),
RATE_PER_MB,
min_payment(2 * HARNESS_INTERVAL_BYTES, RATE_PER_MB),
2 * HARNESS_INTERVAL_BYTES,
)
.await?;
assert_parked_awaiting_voucher(&mut recv_x).await?;
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
if lane.chain().chain_root == rolled_root() {
break;
}
anyhow::ensure!(
Instant::now() < deadline,
"the lane never adopted the rolled root"
);
tokio::time::sleep(Duration::from_millis(20)).await;
}
release(&mut send_y, 2).await?;
tokio::time::timeout(
Duration::from_secs(3),
read_exact_chunks(&mut recv_y, HARNESS_INTERVAL_BYTES),
)
.await
.map_err(|_| anyhow::anyhow!("Y's retired reveal was not credited"))??;
release_from(&mut send_x, ROLLED_SEED, 1).await?;
read_exact_chunks(&mut recv_x, HARNESS_INTERVAL_BYTES).await?;
let lane = store
.get(lane_key(signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
anyhow::ensure!(
lane.chain().chain_root == rolled_root() && lane.chain().verified_index == 1,
"the lane meters the rolled chain at index 1"
);
anyhow::ensure!(
lane.owed() == U256::from(3 * RATE_PER_MB),
"three chunks at the quoted rate: {}",
lane.owed()
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn unpaid_stream_is_served_only_the_floor_then_pauses() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x22u8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (_send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn paying_grows_the_window_to_paid_over_divisor() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x33u8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 1).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
pay_cumulative(&mut send, &signer, HARNESS_INTERVAL_BYTES).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
pay_cumulative(&mut send, &signer, 2 * HARNESS_INTERVAL_BYTES).await?;
read_exact_chunks(&mut recv, 2 * HARNESS_INTERVAL_BYTES - HARNESS_LAG_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn pay_range_to_end(
conn: &Connection,
hash: [u8; 32],
ext: &StreamRequestExt,
signer: &PrivateKeySigner,
range: u64,
mut paid: u64,
) -> anyhow::Result<u64> {
let (mut send, mut recv) = open_paid_range_stream(conn, hash, Some(ext), 0, range).await?;
let mut reader = BufferedClientReader::default();
let mut received = paid;
loop {
match tokio::time::timeout(Duration::from_secs(5), reader.read(&mut recv)).await {
Ok(msg) => match msg? {
ClientMessage::ChunkData(chunk) => {
received = received.saturating_add(chunk.bytes().len() as u64);
if received.saturating_sub(paid) >= HARNESS_INTERVAL_BYTES {
pay_cumulative(&mut send, signer, received).await?;
paid = received;
}
}
ClientMessage::StreamEnd => break,
other => anyhow::bail!("expected ChunkData or StreamEnd, got {other:?}"),
},
Err(_elapsed) => {
anyhow::ensure!(received > paid, "the stream stalled with nothing owed");
pay_cumulative(&mut send, signer, received).await?;
paid = received;
}
}
}
let _ = send.finish();
Ok(paid)
}
async fn read_unpaid_opening(
conn: &Connection,
hash: [u8; 32],
ext: &StreamRequestExt,
) -> anyhow::Result<(u64, SendStream, RecvStream)> {
let (send, mut recv) = open_paid_stream(conn, hash, Some(ext)).await?;
let mut unpaid: u64 = 0;
while let Ok(msg) =
tokio::time::timeout(Duration::from_secs(2), read_client_msg(&mut recv)).await
{
match msg? {
ClientMessage::ChunkData(chunk) => {
unpaid = unpaid.saturating_add(chunk.bytes().len() as u64);
}
other => anyhow::bail!("expected ChunkData, got {other:?}"),
}
}
Ok((unpaid, send, recv))
}
#[tokio::test(flavor = "multi_thread")]
async fn a_back_to_back_stream_opens_at_the_lanes_banked_ramp() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
const FIRST_RANGE: u64 = 4 * HARNESS_INTERVAL_BYTES;
let payload = vec![0x5Au8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 1).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let paid = pay_range_to_end(&conn, *hash.as_bytes(), &ext, &signer, FIRST_RANGE, 0).await?;
anyhow::ensure!(paid >= FIRST_RANGE, "stream 1 paid {paid} wire bytes");
let (unpaid, _send, _recv) = read_unpaid_opening(&conn, *hash.as_bytes(), &ext).await?;
anyhow::ensure!(
unpaid >= FIRST_RANGE,
"stream 2 received {unpaid} unpaid wire bytes; the banked ramp opens {FIRST_RANGE}"
);
anyhow::ensure!(
unpaid <= paid.saturating_add(HARNESS_INTERVAL_BYTES),
"stream 2 received {unpaid} unpaid wire bytes past its {paid}-byte window"
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn an_abandoned_stream_forfeits_its_lanes_ramp() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
const FIRST_RANGE: u64 = 4 * HARNESS_INTERVAL_BYTES;
let payload = vec![0x5Cu8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_pipelined_server_with_metrics(cache, Arc::clone(&store), CREDIT_MAX, 1).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
pay_range_to_end(&conn, *hash.as_bytes(), &ext, &signer, FIRST_RANGE, 0).await?;
let (unpaid, mut send, mut recv) = read_unpaid_opening(&conn, *hash.as_bytes(), &ext).await?;
anyhow::ensure!(
unpaid >= FIRST_RANGE,
"stream 2 opened at {unpaid} wire bytes"
);
let ended = || -> anyhow::Result<u64> {
Ok(
counter(&metrics, "decdn_serve_stream_client_declined_total")?
+ counter(&metrics, "decdn_serve_stream_client_abandoned_total")?,
)
};
let before = ended()?;
let _ = send.reset(0u32.into());
let _ = recv.stop(0u32.into());
let deadline = Instant::now() + Duration::from_secs(15);
while ended()? == before {
anyhow::ensure!(Instant::now() < deadline, "the node never ended stream 2");
tokio::time::sleep(Duration::from_millis(20)).await;
}
let (unpaid, _send, _recv) = read_unpaid_opening(&conn, *hash.as_bytes(), &ext).await?;
anyhow::ensure!(
unpaid == HARNESS_FLOOR_BYTES,
"stream 3 received {unpaid} unpaid wire bytes; the forfeited lane opens at the floor"
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn divisor_zero_serves_the_full_credit_max_immediately() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 8 * 1024 * 1024;
let payload = vec![0x11u8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_pipelined_server(cache, Arc::clone(&store), CREDIT_MAX, 0).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (_send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, CREDIT_MAX).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn idle_connection_is_closed_by_the_app_layer() -> anyhow::Result<()> {
let (cache, _cache_tmp) = empty_cache().await?;
let store: Arc<dyn PoolStateStore> = Arc::new(MemoryPoolStateStore::new());
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
|deps| deps.idle_timeout = Some(Duration::from_millis(300)),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
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_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let err = tokio::time::timeout(Duration::from_secs(10), conn.closed())
.await
.map_err(|_| anyhow::anyhow!("connection was not idle-closed within 10s"))?;
ensure_graceful_idle_close(&err)?;
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(&encoded, "decdn_client_idle_close_total 1"),
"idle-close must bump decdn_client_idle_close_total; got:\n{encoded}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn in_flight_stream_defers_idle_close_then_reaps_on_completion() -> anyhow::Result<()> {
const BODY_LEN: u8 = 64;
let (cache, _cache_tmp) = empty_cache().await?;
let store: Arc<dyn PoolStateStore> = Arc::new(MemoryPoolStateStore::new());
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let idle = Duration::from_millis(150);
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
|deps| deps.idle_timeout = Some(idle),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
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_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let (mut send, _recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
send.write_all(&[BODY_LEN])
.await
.map_err(|e| anyhow::anyhow!("write frame len: {e}"))?;
let premature = tokio::time::timeout(idle * 5, conn.closed()).await;
anyhow::ensure!(
premature.is_err(),
"connection was idle-closed while a stream was in flight: {premature:?}"
);
send.write_all(&[0xFFu8; BODY_LEN as usize])
.await
.map_err(|e| anyhow::anyhow!("write frame body: {e}"))?;
let err = tokio::time::timeout(Duration::from_secs(10), conn.closed())
.await
.map_err(|_| {
anyhow::anyhow!("connection was not idle-closed after the stream completed")
})?;
ensure_graceful_idle_close(&err)?;
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(&encoded, "decdn_client_idle_close_total 1"),
"post-stream idle-close must bump decdn_client_idle_close_total; got:\n{encoded}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[derive(Clone, Copy)]
struct IdleBlob {
hash: Hash,
wire_bytes: u64,
}
struct IdleFixture {
target: EndpointAddr,
client_signer: Arc<PrivateKeySigner>,
store: Arc<MemoryPoolStateStore>,
blobs: Vec<IdleBlob>,
metrics: Arc<Metrics>,
server_ep: Endpoint,
server_task: tokio::task::JoinHandle<()>,
_cache_tmp: tempfile::TempDir,
}
impl IdleFixture {
fn blob(&self, index: usize) -> anyhow::Result<IdleBlob> {
self.blobs
.get(index)
.copied()
.ok_or_else(|| anyhow::anyhow!("idle fixture has no blob at index {index}"))
}
}
async fn idle_fixture(payload: &[u8], idle: Duration) -> anyhow::Result<IdleFixture> {
let (cache, hash, cache_tmp) = cache_with_blob(payload).await?;
idle_fixture_with_cache(
cache,
vec![IdleBlob {
hash,
wire_bytes: support::bao_wire_len_whole(payload.len() as u64),
}],
cache_tmp,
idle,
)
.await
}
async fn idle_fixture_with_two_blobs(
a: &[u8],
b: &[u8],
idle: Duration,
) -> anyhow::Result<IdleFixture> {
let (cache, hash_a, hash_b, cache_tmp) = cache_with_two_blobs(a, b).await?;
idle_fixture_with_cache(
cache,
vec![
IdleBlob {
hash: hash_a,
wire_bytes: support::bao_wire_len_whole(a.len() as u64),
},
IdleBlob {
hash: hash_b,
wire_bytes: support::bao_wire_len_whole(b.len() as u64),
},
],
cache_tmp,
idle,
)
.await
}
async fn idle_fixture_with_cache(
cache: CacheEngine,
blobs: Vec<IdleBlob>,
cache_tmp: tempfile::TempDir,
idle: Duration,
) -> anyhow::Result<IdleFixture> {
let client_signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|deps| deps.idle_timeout = Some(idle),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
Ok(IdleFixture {
target: EndpointAddr::new(server_id).with_ip_addr(server_addr),
client_signer,
store,
blobs,
metrics,
server_ep,
server_task,
_cache_tmp: cache_tmp,
})
}
struct StalledDelivery {
send: SendStream,
recv: RecvStream,
wire_bytes: u64,
}
#[derive(Clone, Copy, Default)]
struct VoucherTotals {
wire_bytes: u64,
amount: U256,
}
async fn stall_delivery_at_closing_voucher(
conn: &Connection,
hash: [u8; 32],
expected_wire: u64,
ext: Option<&StreamRequestExt>,
) -> anyhow::Result<StalledDelivery> {
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let req = StreamRequest {
hash,
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0012_61a0,
};
let payload =
encode_stream_request(&req, ext).map_err(|e| anyhow::anyhow!("encode request: {e}"))?;
write_frame(&mut send, &payload)
.await
.map_err(|e| anyhow::anyhow!("write request: {e}"))?;
let (resp, resp_ext) = read_stream_response(&mut recv).await?;
anyhow::ensure!(resp.body.ok, "delivery refused: {:?}", resp_ext.error);
let mut wire_bytes: u64 = 0;
while wire_bytes < expected_wire {
match read_client_msg(&mut recv).await? {
ClientMessage::ChunkData(chunk) => {
wire_bytes = wire_bytes.saturating_add(chunk.bytes().len() as u64);
}
other => anyhow::bail!("expected ChunkData mid-delivery, got {other:?}"),
}
}
anyhow::ensure!(
wire_bytes == expected_wire,
"read {wire_bytes} wire bytes, expected exactly {expected_wire}"
);
Ok(StalledDelivery {
send,
recv,
wire_bytes,
})
}
impl StalledDelivery {
async fn pay_and_finish(
mut self,
signer: &PrivateKeySigner,
prior: VoucherTotals,
) -> anyhow::Result<(Instant, VoucherTotals)> {
let amount_delta = min_payment(self.wire_bytes, RATE_PER_MB);
let settled = VoucherTotals {
wire_bytes: prior
.wire_bytes
.checked_add(self.wire_bytes)
.ok_or_else(|| anyhow::anyhow!("voucher wire-byte total overflow"))?,
amount: prior
.amount
.checked_add(amount_delta)
.ok_or_else(|| anyhow::anyhow!("voucher amount overflow"))?,
};
let voucher = Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount: settled.amount,
bytes_delivered: U256::from(settled.wire_bytes),
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
}
.sign(signer, &payment_domain())
.map_err(|e| anyhow::anyhow!("sign voucher: {e}"))?;
write_client_msg(
&mut self.send,
&ClientMessage::Voucher(signed_to_wire_voucher(&voucher)?),
)
.await?;
match read_client_msg(&mut self.recv).await? {
ClientMessage::StreamEnd => {}
other => anyhow::bail!("expected StreamEnd, got {other:?}"),
}
Ok((Instant::now(), settled))
}
async fn send_cumulative_voucher(
&mut self,
signer: &PrivateKeySigner,
bytes_delivered: u64,
amount: U256,
) -> anyhow::Result<ClientMessage> {
let voucher = Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount,
bytes_delivered: U256::from(bytes_delivered),
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
}
.sign(signer, &payment_domain())
.map_err(|e| anyhow::anyhow!("sign voucher: {e}"))?;
write_client_msg(
&mut self.send,
&ClientMessage::Voucher(signed_to_wire_voucher(&voucher)?),
)
.await?;
read_client_msg(&mut self.recv).await
}
}
async fn open_expecting_refusal(
conn: &Connection,
hash: [u8; 32],
ext: Option<&StreamRequestExt>,
) -> anyhow::Result<(
decdn_protocol::client::StreamResponse,
decdn_protocol::client::StreamResponseExt,
)> {
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let req = StreamRequest {
hash,
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0012_61a0,
};
let payload =
encode_stream_request(&req, ext).map_err(|e| anyhow::anyhow!("encode request: {e}"))?;
write_frame(&mut send, &payload)
.await
.map_err(|e| anyhow::anyhow!("write request: {e}"))?;
read_stream_response(&mut recv).await
}
async fn race_two_same_lane_opens(
conn: &Connection,
hash_a: [u8; 32],
hash_b: [u8; 32],
ext: &StreamRequestExt,
) -> anyhow::Result<(bool, bool)> {
let (mut send_a, mut recv_a) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi a: {e}"))?;
let (mut send_b, mut recv_b) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi b: {e}"))?;
let req_a = StreamRequest {
hash: hash_a,
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0012_61a0,
};
let req_b = StreamRequest {
hash: hash_b,
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0012_61a0,
};
let payload_a = encode_stream_request(&req_a, Some(ext))
.map_err(|e| anyhow::anyhow!("encode request a: {e}"))?;
let payload_b = encode_stream_request(&req_b, Some(ext))
.map_err(|e| anyhow::anyhow!("encode request b: {e}"))?;
write_frame(&mut send_a, &payload_a)
.await
.map_err(|e| anyhow::anyhow!("write request a: {e}"))?;
write_frame(&mut send_b, &payload_b)
.await
.map_err(|e| anyhow::anyhow!("write request b: {e}"))?;
let ok_a = match read_client_msg(&mut recv_a).await? {
ClientMessage::StreamResponse(resp) => resp.body.ok,
other => anyhow::bail!("expected a StreamResponse for a, got {other:?}"),
};
let ok_b = match read_client_msg(&mut recv_b).await? {
ClientMessage::StreamResponse(resp) => resp.body.ok,
other => anyhow::bail!("expected a StreamResponse for b, got {other:?}"),
};
Ok((ok_a, ok_b))
}
fn ensure_graceful_idle_close(err: &ConnectionError) -> anyhow::Result<()> {
match err {
ConnectionError::ApplicationClosed(ac) => {
anyhow::ensure!(
ac.error_code.into_inner() == 0,
"idle close must use the no-error code, got {}",
ac.error_code.into_inner()
);
anyhow::ensure!(
ac.reason.as_ref() == b"idle",
"idle close reason: {:?}",
ac.reason
);
Ok(())
}
other => anyhow::bail!("expected a graceful application idle-close, got {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn active_delivery_stream_defers_idle_close() -> anyhow::Result<()> {
let payload = vec![0x3Du8; 64 * 1024];
let idle = Duration::from_millis(150);
let fx = idle_fixture(&payload, idle).await?;
let blob = fx.blob(0)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let stalled = stall_delivery_at_closing_voucher(
&conn,
*blob.hash.as_bytes(),
blob.wire_bytes,
Some(&ext),
)
.await?;
let premature = tokio::time::timeout(idle * 5, conn.closed()).await;
anyhow::ensure!(
premature.is_err(),
"connection was idle-closed while a paid delivery was in flight: {premature:?}"
);
let (_completed_at, _totals) = stalled
.pay_and_finish(&fx.client_signer, VoucherTotals::default())
.await?;
let persisted = fx.store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(blob.wire_bytes),
"bytes_delivered: {} (expected {})",
only.last_bytes_delivered(),
blob.wire_bytes
);
drop(conn);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn idle_clock_re_arms_from_last_stream_close() -> anyhow::Result<()> {
let payload = vec![0x4Eu8; 64 * 1024];
let idle = Duration::from_millis(400);
let fx = idle_fixture(&payload, idle).await?;
let blob = fx.blob(0)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let stalled = stall_delivery_at_closing_voucher(
&conn,
*blob.hash.as_bytes(),
blob.wire_bytes,
Some(&ext),
)
.await?;
tokio::time::sleep(idle * 3).await;
let (completed_at, _totals) = stalled
.pay_and_finish(&fx.client_signer, VoucherTotals::default())
.await?;
let err = tokio::time::timeout(Duration::from_secs(10), conn.closed())
.await
.map_err(|_| {
anyhow::anyhow!("connection was not idle-closed after the stream completed")
})?;
let since_completion = completed_at.elapsed();
ensure_graceful_idle_close(&err)?;
let floor = idle.mul_f64(0.8);
anyhow::ensure!(
since_completion >= floor,
"idle clock did not re-arm from the stream's close: closed {since_completion:?} after \
completion, expected at least {floor:?} (a hoisted timer counts from connection accept)"
);
anyhow::ensure!(
metric_line_present(&fx.metrics.encode()?, "decdn_client_idle_close_total 1"),
"the re-armed reap must bump decdn_client_idle_close_total"
);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn idle_clock_re_arms_after_each_completed_stream() -> anyhow::Result<()> {
let payload = vec![0x6Au8; 64 * 1024];
let idle = Duration::from_secs(1);
let activity_cadence = idle.mul_f64(0.6);
let fx = idle_fixture(&payload, idle).await?;
let blob = fx.blob(0)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let mut totals = VoucherTotals::default();
let mut final_completion = Instant::now();
for round in 1..=3 {
let stalled = stall_delivery_at_closing_voucher(
&conn,
*blob.hash.as_bytes(),
blob.wire_bytes,
Some(&ext),
)
.await?;
let (completed_at, settled) = stalled.pay_and_finish(&fx.client_signer, totals).await?;
totals = settled;
final_completion = completed_at;
if round < 3 {
let premature = tokio::time::timeout(activity_cadence, conn.closed()).await;
anyhow::ensure!(
premature.is_err(),
"connection closed after re-arm round {round}, before the next activity: \
{premature:?}"
);
}
}
let err = tokio::time::timeout(Duration::from_secs(10), conn.closed())
.await
.map_err(|_| anyhow::anyhow!("connection was not reaped after the final idle window"))?;
let since_completion = final_completion.elapsed();
ensure_graceful_idle_close(&err)?;
let floor = idle.mul_f64(0.8);
anyhow::ensure!(
since_completion >= floor,
"final idle window was not freshly armed: closed {since_completion:?} after completion, \
expected at least {floor:?}"
);
let persisted = fx.store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(totals.wire_bytes),
"bytes_delivered: {} (expected {})",
only.last_bytes_delivered(),
totals.wire_bytes
);
anyhow::ensure!(
metric_line_present(&fx.metrics.encode()?, "decdn_client_idle_close_total 1"),
"repeated re-arms must end in exactly one metered idle-close"
);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_streams_all_finish_before_idle_clock_arms() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 48 * 1024];
let payload_b = vec![0x82u8; 64 * 1024];
let idle = Duration::from_millis(300);
let fx = idle_fixture_with_two_blobs(&payload_a, &payload_b, idle).await?;
let blob_a = fx.blob(0)?;
let blob_b = fx.blob(1)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let stalled_a = stall_delivery_at_closing_voucher(
&conn,
*blob_a.hash.as_bytes(),
blob_a.wire_bytes,
Some(&ext),
)
.await?;
let stalled_b = stall_delivery_at_closing_voucher(
&conn,
*blob_b.hash.as_bytes(),
blob_b.wire_bytes,
Some(&ext),
)
.await?;
let (_first_completed_at, totals) = stalled_a
.pay_and_finish(&fx.client_signer, VoucherTotals::default())
.await?;
let premature = tokio::time::timeout(idle * 3, conn.closed()).await;
anyhow::ensure!(
premature.is_err(),
"connection was idle-closed while the second stream was still in flight: {premature:?}"
);
let (second_completed_at, _totals) =
stalled_b.pay_and_finish(&fx.client_signer, totals).await?;
let err = tokio::time::timeout(Duration::from_secs(10), conn.closed())
.await
.map_err(|_| {
anyhow::anyhow!("connection was not reaped after both concurrent streams completed")
})?;
let since_completion = second_completed_at.elapsed();
ensure_graceful_idle_close(&err)?;
let floor = idle.mul_f64(0.8);
anyhow::ensure!(
since_completion >= floor,
"idle window started before the second stream closed: reaped {since_completion:?} after \
completion, expected at least {floor:?}"
);
let expected_wire_bytes = blob_a
.wire_bytes
.checked_add(blob_b.wire_bytes)
.ok_or_else(|| anyhow::anyhow!("expected wire-byte total overflow"))?;
let persisted = fx.store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(expected_wire_bytes),
"persisted bytes: {}, expected {expected_wire_bytes}",
only.last_bytes_delivered(),
);
anyhow::ensure!(
metric_line_present(&fx.metrics.encode()?, "decdn_client_idle_close_total 1"),
"the post-concurrency idle reap must be metered exactly once"
);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_same_lane_streams_aggregate_across_the_chunk_boundary() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 768 * 1024];
let payload_b = vec![0x82u8; 512 * 1024];
let idle = Duration::from_secs(30);
let fx = idle_fixture_with_two_blobs(&payload_a, &payload_b, idle).await?;
let blob_a = fx.blob(0)?;
let blob_b = fx.blob(1)?;
anyhow::ensure!(
blob_a.wire_bytes < CHUNK_BYTES && blob_b.wire_bytes < CHUNK_BYTES,
"each blob must stay under one chunk: a={}, b={}, chunk={}",
blob_a.wire_bytes,
blob_b.wire_bytes,
CHUNK_BYTES,
);
let aggregate_wire = blob_a
.wire_bytes
.checked_add(blob_b.wire_bytes)
.ok_or_else(|| anyhow::anyhow!("aggregate wire-byte overflow"))?;
anyhow::ensure!(
aggregate_wire > CHUNK_BYTES,
"the two streams must aggregate past one interval: {aggregate_wire} <= {CHUNK_BYTES}",
);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let stalled_a = stall_delivery_at_closing_voucher(
&conn,
*blob_a.hash.as_bytes(),
blob_a.wire_bytes,
Some(&ext),
)
.await?;
let stalled_b = stall_delivery_at_closing_voucher(
&conn,
*blob_b.hash.as_bytes(),
blob_b.wire_bytes,
Some(&ext),
)
.await?;
let (_b_completed_at, after_b) = stalled_b
.pay_and_finish(&fx.client_signer, VoucherTotals::default())
.await?;
anyhow::ensure!(
after_b.wire_bytes == blob_b.wire_bytes,
"after paying B first the cumulative wire bytes must be exactly B's: {} vs {}",
after_b.wire_bytes,
blob_b.wire_bytes,
);
let (_a_completed_at, after_a) = stalled_a.pay_and_finish(&fx.client_signer, after_b).await?;
anyhow::ensure!(
after_a.wire_bytes == aggregate_wire,
"the final cumulative wire bytes must be the exact aggregate: {} vs {aggregate_wire}",
after_a.wire_bytes,
);
let persisted = fx.store.load_all()?;
anyhow::ensure!(
persisted.len() == 1,
"the two streams share one lane, but {} were persisted",
persisted.len(),
);
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(aggregate_wire),
"shared lane watermark: {} (expected the {aggregate_wire}-byte aggregate)",
only.last_bytes_delivered(),
);
anyhow::ensure!(
only.last_amount() == after_a.amount,
"cumulative amount: {} (expected {})",
only.last_amount(),
after_a.amount,
);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn second_same_lane_stream_refused_when_budget_covers_one() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 512 * 1024];
let payload_b = vec![0x82u8; 256 * 1024];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let reserved_a = min_payment(payload_a.len() as u64, RATE_PER_MB);
let reserved_b = min_payment(payload_b.len() as u64, RATE_PER_MB);
let remaining = reserved_a + reserved_b - U256::from(1u64);
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
remaining,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let wire_a = support::bao_wire_len_whole(payload_a.len() as u64);
let first =
stall_delivery_at_closing_voucher(&conn, *hash_a.as_bytes(), wire_a, Some(&ext)).await?;
let (refusal, refusal_ext) =
open_expecting_refusal(&conn, *hash_b.as_bytes(), Some(&ext)).await?;
anyhow::ensure!(
!refusal.body.ok,
"second same-lane stream must be refused while budget covers only one floor"
);
anyhow::ensure!(
matches!(
refusal_ext.error,
Some(decdn_protocol::client::StreamError::Unfunded)
),
"expected the Unfunded wire code, got {:?}",
refusal_ext.error
);
let _ = first
.pay_and_finish(&signer, VoucherTotals::default())
.await?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn finished_stream_releases_its_floor_reservation() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 512 * 1024];
let payload_b = vec![0x82u8; 256 * 1024];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let remaining = min_payment(CHUNK_BYTES, RATE_PER_MB) + U256::from(2u64);
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
remaining,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let wire_a = support::bao_wire_len_whole(payload_a.len() as u64);
let first =
stall_delivery_at_closing_voucher(&conn, *hash_a.as_bytes(), wire_a, Some(&ext)).await?;
let (_first_completed_at, totals) = first
.pay_and_finish(&signer, VoucherTotals::default())
.await?;
let wire_b = support::bao_wire_len_whole(payload_b.len() as u64);
let second =
stall_delivery_at_closing_voucher(&conn, *hash_b.as_bytes(), wire_b, Some(&ext)).await?;
let _ = second.pay_and_finish(&signer, totals).await?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn single_same_lane_stream_admitted_unchanged() -> anyhow::Result<()> {
let payload = vec![0x93u8; 512 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let remaining = min_payment(CHUNK_BYTES, RATE_PER_MB) + U256::from(2u64);
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
remaining,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let wire = support::bao_wire_len_whole(payload.len() as u64);
let only = stall_delivery_at_closing_voucher(&conn, *hash.as_bytes(), wire, Some(&ext)).await?;
let (_completed_at, totals) = only
.pay_and_finish(&signer, VoucherTotals::default())
.await?;
anyhow::ensure!(
totals.wire_bytes == wire,
"settled wire bytes: {} (expected the whole blob's {wire})",
totals.wire_bytes
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_opens_admit_exactly_one() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 2 * 1024 * 1024];
let payload_b = vec![0x82u8; 2 * 1024 * 1024];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let remaining = min_payment(CHUNK_BYTES, RATE_PER_MB) + U256::from(2u64);
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
remaining,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let (a_ok, b_ok) =
race_two_same_lane_opens(&conn, *hash_a.as_bytes(), *hash_b.as_bytes(), &ext).await?;
assert_ne!(
a_ok, b_ok,
"exactly one of the two concurrent opens is admitted"
);
drop(conn);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn skip_ahead_voucher_on_a_concurrent_lane_is_accepted() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 512 * 1024];
let payload_b = vec![0x82u8; 384 * 1024];
let idle = Duration::from_secs(30);
let fx = idle_fixture_with_two_blobs(&payload_a, &payload_b, idle).await?;
let blob_a = fx.blob(0)?;
let blob_b = fx.blob(1)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let _stalled_a = stall_delivery_at_closing_voucher(
&conn,
*blob_a.hash.as_bytes(),
blob_a.wire_bytes,
Some(&ext),
)
.await?;
let mut stalled_b = stall_delivery_at_closing_voucher(
&conn,
*blob_b.hash.as_bytes(),
blob_b.wire_bytes,
Some(&ext),
)
.await?;
let skip_ahead_bytes = blob_a
.wire_bytes
.checked_add(blob_b.wire_bytes)
.ok_or_else(|| anyhow::anyhow!("skip-ahead wire-byte overflow"))?;
let skip_ahead_amount = min_payment(blob_a.wire_bytes, RATE_PER_MB)
.checked_add(min_payment(blob_b.wire_bytes, RATE_PER_MB))
.ok_or_else(|| anyhow::anyhow!("skip-ahead amount overflow"))?;
let reply = stalled_b
.send_cumulative_voucher(&fx.client_signer, skip_ahead_bytes, skip_ahead_amount)
.await?;
match reply {
ClientMessage::StreamError(err) => {
anyhow::bail!("skip-ahead voucher must be accepted, got StreamError {err:?}")
}
ClientMessage::StreamEnd | ClientMessage::ChunkData(_) => {}
other => anyhow::bail!("unexpected reply to an accepted voucher: {other:?}"),
}
let persisted = fx.store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("lane state must persist after an accepted voucher"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(skip_ahead_bytes),
"an accepted skip-ahead voucher lands the aggregate watermark, got {}",
only.last_bytes_delivered(),
);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn lower_voucher_after_higher_sibling_is_already_satisfied() -> anyhow::Result<()> {
let payload_a = vec![0x71u8; 512 * 1024];
let payload_b = vec![0x82u8; 384 * 1024];
let idle = Duration::from_secs(30);
let fx = idle_fixture_with_two_blobs(&payload_a, &payload_b, idle).await?;
let blob_a = fx.blob(0)?;
let blob_b = fx.blob(1)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let mut stalled_a = stall_delivery_at_closing_voucher(
&conn,
*blob_a.hash.as_bytes(),
blob_a.wire_bytes,
Some(&ext),
)
.await?;
let mut stalled_b = stall_delivery_at_closing_voucher(
&conn,
*blob_b.hash.as_bytes(),
blob_b.wire_bytes,
Some(&ext),
)
.await?;
let agg_bytes = blob_a.wire_bytes + blob_b.wire_bytes;
let agg_amount =
min_payment(blob_a.wire_bytes, RATE_PER_MB) + min_payment(blob_b.wire_bytes, RATE_PER_MB);
let reply_b = stalled_b
.send_cumulative_voucher(&fx.client_signer, agg_bytes, agg_amount)
.await?;
anyhow::ensure!(
!matches!(reply_b, ClientMessage::StreamError(_)),
"aggregate voucher on B must be accepted, got {reply_b:?}"
);
let reply_a = stalled_a
.send_cumulative_voucher(
&fx.client_signer,
blob_a.wire_bytes,
min_payment(blob_a.wire_bytes, RATE_PER_MB),
)
.await?;
anyhow::ensure!(
!matches!(reply_a, ClientMessage::StreamError(_)),
"a superseded lower voucher must be already-satisfied, got StreamError: {reply_a:?}"
);
let persisted = fx.store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("lane state must persist"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(agg_bytes),
"watermark must stay at the aggregate, got {}",
only.last_bytes_delivered(),
);
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_delivers_empty_blob() -> anyhow::Result<()> {
let payload: Vec<u8> = Vec::new();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(got.as_ref().is_empty(), "empty blob delivers empty bytes");
let persisted = store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
only.last_bytes_delivered() == U256::ZERO,
"0 bytes delivered, got {}",
only.last_bytes_delivered()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn tracked_watermark_survives_post_ack_error() -> anyhow::Result<()> {
let payload = vec![0xABu8; 6 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let mut progress = VoucherProgress::default();
let result = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c0_ffee,
PullDeadlines::whole_transfer(Duration::from_secs(20)),
0,
0,
&mut progress,
)
.await;
let err = result
.err()
.ok_or_else(|| anyhow::anyhow!("fetch must error when voucher 2 is over-deposit"))?;
let rejected = err
.downcast_ref::<UpstreamVoucherRejected>()
.ok_or_else(|| anyhow::anyhow!("expected UpstreamVoucherRejected, got: {err:?}"))?;
anyhow::ensure!(
matches!(
rejected.reason,
decdn_protocol::client::VoucherRejectReason::SpendingCapExhausted
),
"the exhausted cap must surface its own rejection reason; got {:?}",
rejected.reason
);
anyhow::ensure!(
rejected.bundle.is_some(),
"the rejection should still carry a watermark bundle; \
without one this test would pass vacuously"
);
let advanced = progress.advanced().ok_or_else(|| {
anyhow::anyhow!("advanced watermark must survive a post-ack error, got None")
})?;
anyhow::ensure!(
advanced.1 > U256::ZERO,
"at least one voucher should have cleared before the rejection; amount = {}",
advanced.1
);
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 0"
),
"an exhausted cap must not retry at all, so nothing should reach the \
pre-serve deposit gate"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn accepted_voucher_advances_lane_activity_clock() -> anyhow::Result<()> {
let payload = vec![0xABu8; 1_572_864]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|_deps| {},
)?;
let activity = handler.lane_activity_clock();
assert!(
!activity
.ages()
.await
.contains_key(&lane_key(client_signer.address())),
"no voucher accepted yet → clock must report the lane as absent (never)"
);
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
assert!(
activity
.ages()
.await
.contains_key(&lane_key(client_signer.address())),
"an accepted voucher must advance the lane's last-voucher clock"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn voucher_acceptance_appends_download_receipt() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 6 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let receipts = Arc::new(VecReceiptLog::default());
let handler = build_handler_full_with_receipts(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
Arc::clone(&receipts) as Arc<dyn decdn_node::receipt_log::ReceiptLog>,
RATE_PER_MB,
&loopback_domains(),
16,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let client_sk = fresh_key();
let client_node_id = client_sk.public();
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
assert_receipts_cover_the_delivery(&receipts.snapshot(), hash, client_node_id, &payload)?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
fn assert_receipts_cover_the_delivery(
recorded: &[DownloadReceipt],
hash: Hash,
client_node_id: iroh::PublicKey,
payload: &[u8],
) -> anyhow::Result<()> {
let want_hash = hex_lower(hash.as_bytes());
let want_node = hex_lower(client_node_id.as_bytes());
let total: u64 = recorded.iter().map(DownloadReceipt::size).sum();
let wire = support::bao_wire_len_whole(payload.len() as u64);
let chunk = decdn_protocol::client::CHUNK_BYTES;
let expected = (wire / chunk) + u64::from(!wire.is_multiple_of(chunk));
anyhow::ensure!(
recorded.len() as u64 == expected,
"expected {expected} receipts (one per chunk plus the closing residual), got {}: \
{recorded:?}",
recorded.len()
);
anyhow::ensure!(
recorded.iter().all(|r| r.size() > 0),
"a proof that credited nothing must record no receipt: {recorded:?}"
);
anyhow::ensure!(
total == wire,
"receipt sizes sum to {total}, expected {wire}"
);
for (i, r) in recorded.iter().enumerate() {
anyhow::ensure!(
r.hash() == want_hash,
"receipt[{i}] hash {} != {want_hash}",
r.hash()
);
anyhow::ensure!(
r.client_node_id() == want_node,
"receipt[{i}] client_node_id {} != {want_node}",
r.client_node_id()
);
anyhow::ensure!(r.size() > 0, "receipt[{i}] size must be > 0");
anyhow::ensure!(r.timestamp_secs() > 0, "receipt[{i}] timestamp must be set");
}
let amounts: Vec<u128> = recorded
.iter()
.map(|r| r.voucher_amount().parse::<u128>().unwrap_or(0))
.collect();
anyhow::ensure!(
amounts.windows(2).all(|w| w.first() < w.last()),
"receipt amounts must rise strictly, got {amounts:?}"
);
anyhow::ensure!(
amounts.first() == Some(&u128::from(RATE_PER_MB)),
"the first reveal claims exactly one chunk at the quoted rate, got {amounts:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delivery_completes_while_receipt_writer_is_stalled() -> anyhow::Result<()> {
let payload = vec![0x3Cu8; 1_572_864]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let blocking_log = Arc::new(BlockingReceiptLog::default());
let (receipt_sink, writer_handle) = spawn_receipt_writer(
Arc::clone(&blocking_log) as Arc<dyn decdn_node::receipt_log::ReceiptLog>,
Arc::clone(&metrics),
);
let handler = build_handler_full_with_sink(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
receipt_sink,
RATE_PER_MB,
&loopback_domains(),
16,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = tokio::time::timeout(
Duration::from_secs(20),
stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
),
)
.await
.map_err(|_| anyhow::anyhow!("delivery hung while the receipt writer was stalled (#803)"))??;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
blocking_log.release();
tokio::time::timeout(Duration::from_secs(10), writer_handle.shutdown())
.await
.map_err(|_| anyhow::anyhow!("receipt writer did not drain after release"))??;
let recorded = blocking_log.snapshot();
let total: u64 = recorded.iter().map(DownloadReceipt::size).sum();
let wire = support::bao_wire_len_whole(payload.len() as u64);
anyhow::ensure!(
total == wire,
"drained receipt sizes sum to {total}, expected {wire} ({} receipts)",
recorded.len()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn receipt_log_write_failure_does_not_fail_delivery() -> anyhow::Result<()> {
let payload = vec![0x7Eu8; 1_572_864]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler_full_with_receipts(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
Arc::new(FailingReceiptLog) as Arc<dyn decdn_node::receipt_log::ReceiptLog>,
RATE_PER_MB,
&loopback_domains(),
16,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivery must succeed despite receipt-log failure"
);
let persisted = store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let wire = support::bao_wire_len_whole(payload.len() as u64);
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(wire),
"lane state must still advance: bytes_delivered={} (expected {wire})",
only.last_bytes_delivered()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_reused_lane_resumes() -> anyhow::Result<()> {
let payload = vec![0x33u8; 4096];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx1 = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got1 = stream_fetch(
&client_ep,
target.clone(),
&ctx1,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x1111,
Duration::from_secs(15),
)
.await?;
anyhow::ensure!(got1.as_ref() == payload.as_slice());
let after1 = store.load_all()?;
let s1 = after1
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let wire = support::bao_wire_len_whole(payload.len() as u64);
anyhow::ensure!(
s1.last_bytes_delivered() == U256::from(wire),
"stream-1 bytes: {} (expected {wire})",
s1.last_bytes_delivered()
);
let ctx2 = PoolContext {
prior_bytes_delivered: s1.last_bytes_delivered(),
prior_amount: s1.last_amount(),
..pool_context(&client_ep, Arc::clone(&client_signer), deposit)
};
let got2 = stream_fetch(
&client_ep,
target,
&ctx2,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x2222,
Duration::from_secs(15),
)
.await?;
anyhow::ensure!(got2.as_ref() == payload.as_slice());
let after2 = store.load_all()?;
let s2 = after2
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
s2.last_bytes_delivered() == U256::from(2 * wire),
"cumulative bytes: {}",
s2.last_bytes_delivered()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_restart_with_stale_watermark_heals_and_resumes() -> anyhow::Result<()> {
let payload: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(1_000_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx1 = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got1 = stream_fetch(
&client_ep,
target.clone(),
&ctx1,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x1111,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(got1.as_ref() == payload.as_slice());
let after1 = store.load_all()?;
let s1 = after1
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let wire = support::bao_wire_len_whole(payload.len() as u64);
anyhow::ensure!(
s1.last_bytes_delivered() == U256::from(wire),
"stream-1 advanced the node lane to {wire}, got {}",
s1.last_bytes_delivered()
);
let ctx2 = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got2 = stream_fetch(
&client_ep,
target,
&ctx2,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x2222,
Duration::from_secs(8),
)
.await?;
anyhow::ensure!(
got2.as_ref() == payload.as_slice(),
"restarted client must recover the full blob, got {} of {} bytes",
got2.len(),
payload.len()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_ahead_of_the_node_rebases_and_resumes() -> anyhow::Result<()> {
let payload: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(1_000_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx1 = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got1 = stream_fetch(
&client_ep,
target.clone(),
&ctx1,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x3333,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(got1.as_ref() == payload.as_slice());
let node_lane = store
.load_all()?
.first()
.map(|l| (l.last_amount(), l.last_bytes_delivered()))
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let mut ctx2 = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
ctx2.prior_amount = node_lane.0 + U256::from(1u64);
ctx2.prior_bytes_delivered = node_lane.1 + U256::from(64u64 * 1024 * 1024);
let got2 = stream_fetch(
&client_ep,
target,
&ctx2,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x4444,
Duration::from_secs(8),
)
.await?;
anyhow::ensure!(
got2.as_ref() == payload.as_slice(),
"the client ahead of the node must rebase and recover the full blob, got {} of {} \
bytes",
got2.len(),
payload.len()
);
let healed = store
.load_all()?
.first()
.map(|l| (l.last_amount(), l.last_bytes_delivered()))
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
healed.0 > node_lane.0 && healed.1 > node_lane.1,
"the node must credit the healed stream from its own watermark: {node_lane:?} -> \
{healed:?}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_under_folding_a_live_chain_reseeds_and_resumes() -> anyhow::Result<()> {
resume_under_a_live_chain(1).await
}
#[tokio::test(flavor = "multi_thread")]
async fn client_resuming_at_the_anchor_under_a_live_chain_heals() -> anyhow::Result<()> {
resume_under_a_live_chain(0).await
}
#[tokio::test(flavor = "multi_thread")]
async fn a_restarted_payer_beside_a_live_sibling_heals_below_the_anchor() -> anyhow::Result<()> {
let payload: Vec<u8> = (0..8 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let sibling_payload = vec![0x82u8; 256 * 1024];
let (cache, hash, sibling_hash, _cache_tmp) =
cache_with_two_blobs(&payload, &sibling_payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(1_000_000_000u64);
let lane = lane_with_proved_reveals(&client_signer, deposit)?;
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&lane)?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let accept_ep = server_ep.clone();
let server_task = tokio::spawn(async move {
while let Some(incoming) = accept_ep.accept().await {
let Ok(connecting) = incoming.accept() else {
continue;
};
let Ok(conn) = connecting.await else { continue };
let handler = Arc::clone(&handler);
tokio::spawn(async move {
let _ = ClientProtocol::new(handler).accept(conn).await;
});
}
});
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (sibling_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let sibling_conn = sibling_ep
.connect(target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let sibling_ext = binding_ext(&client_signer, B256::from(*sibling_ep.id().as_bytes()))?;
let _sibling = stall_delivery_at_closing_voucher(
&sibling_conn,
*sibling_hash.as_bytes(),
support::bao_wire_len_whole(sibling_payload.len() as u64),
Some(&sibling_ext),
)
.await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x5555,
Duration::from_secs(8),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"the restarted client must reseed and recover the full blob, got {} of {} bytes",
got.len(),
payload.len()
);
let healed = store
.load_all()?
.first()
.map(LaneState::owed)
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
healed > lane.owed(),
"the healed stream must pay past the live claim: {} -> {healed}",
lane.owed()
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_voucher_rejected_total")? == 1,
"the heal costs exactly one rejected proof"
);
drop(sibling_conn);
shutdown([server_task], [&client_ep, &sibling_ep, &server_ep]).await?;
Ok(())
}
fn lane_with_proved_reveals(signer: &PrivateKeySigner, deposit: U256) -> anyhow::Result<LaneState> {
lane_with_proved_frontier(signer, deposit, 5)
}
fn lane_with_proved_frontier(
signer: &PrivateKeySigner,
deposit: U256,
verified_index: u8,
) -> anyhow::Result<LaneState> {
use decdn_incentive::chain::{CHUNK_BYTES, preimage_at, root_from_seed};
let price = decdn_incentive::min_payment(CHUNK_BYTES, RATE_PER_MB);
let anchor_amount = price * U256::from(3u64);
let anchor_bytes = U256::from(3 * CHUNK_BYTES);
let chain_seed = B256::repeat_byte(0x5E);
let chain = decdn_incentive::LaneChain {
chain_root: root_from_seed(chain_seed),
chunk_price: price,
verified_index,
tip: preimage_at(chain_seed, verified_index),
};
let anchor = decdn_incentive::Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount: anchor_amount,
bytes_delivered: anchor_bytes,
chain_root: chain.chain_root,
chunk_price: price,
}
.sign(signer, &payment_domain())?;
Ok(LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
deposit,
0,
anchor_amount,
anchor_bytes,
Some(anchor.signature.as_bytes()),
chain,
))
}
async fn resume_under_a_live_chain(above_anchor: u64) -> anyhow::Result<()> {
let payload: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(1_000_000_000u64);
let lane = lane_with_proved_reveals(&client_signer, deposit)?;
let anchor_amount = lane.last_amount();
let anchor_bytes = lane.last_bytes_delivered();
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&lane)?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let mut ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
ctx.prior_amount = anchor_amount + U256::from(above_anchor);
ctx.prior_bytes_delivered = anchor_bytes;
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x5555,
Duration::from_secs(8),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"the under-folding client must reseed and recover the full blob, got {} of {} bytes",
got.len(),
payload.len()
);
let healed = store
.load_all()?
.first()
.map(LaneState::owed)
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
healed > lane.owed(),
"the healed stream must pay past the live claim: {} -> {healed}",
lane.owed()
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_client_declined_total")? == 0,
"no stream may stall on a proof the client never sends"
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_voucher_rejected_total")? == 1,
"the heal costs exactly one rejected voucher"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn an_unadopted_chain_past_the_bundle_is_retired_and_the_pull_completes() -> anyhow::Result<()>
{
let payload: Vec<u8> = (0..8 * 1024 * 1024u32).map(|i| (i % 239) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(1_000_000_000u64);
let lane = lane_with_proved_frontier(&client_signer, deposit, 1)?;
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&lane)?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|deps| deps.credit_ramp_divisor = 0,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let mut ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
ctx.prior_amount = lane.last_amount();
ctx.prior_bytes_delivered = lane.last_bytes_delivered();
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x2257,
Duration::from_secs(8),
)
.await
.map_err(|e| anyhow::anyhow!("resumed fetch: {e:#}"))?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"the resumed fetch must recover the full blob, got {} of {} bytes",
got.len(),
payload.len()
);
let healed = store
.load_all()?
.first()
.map(LaneState::owed)
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
healed > lane.owed(),
"the healed stream must pay past the live claim: {} -> {healed}",
lane.owed()
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_voucher_rejected_total")? == 1,
"the heal costs exactly one rejected voucher"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_process_that_exits_before_its_last_persist_heals_in_one_rejection() -> anyhow::Result<()>
{
let payload_a: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let payload_b: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 241) as u8).collect();
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let sd = slash_domain();
let deadlines = PullDeadlines::whole_transfer(Duration::from_secs(15));
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let ledger = Arc::new(PoolLedger::new(Cumulative::default()));
let fetch_shared = |hash: decdn_cache::Hash, ts: u64| {
stream_fetch_shared(
&client_ep,
target.clone(),
&ctx,
&ledger,
&sd,
server_eth.address(),
*hash.as_bytes(),
0,
ts,
deadlines,
0,
0,
)
};
fetch_shared(hash_a, 0x2169_0001)
.await
.map_err(|e| anyhow::anyhow!("entry A: {e:#}"))?;
let persisted = ledger.committed();
fetch_shared(hash_b, 0x2169_0002)
.await
.map_err(|e| anyhow::anyhow!("entry B: {e:#}"))?;
let node_claim = store
.load_all()?
.first()
.map(LaneState::owed)
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
node_claim > persisted.amount,
"the node proved past the persisted watermark: {node_claim} vs {}",
persisted.amount
);
let rejected_before = counter(&metrics, "decdn_serve_stream_voucher_rejected_total")?;
let mut next = pool_context(&client_ep, Arc::clone(&signer), deposit);
next.prior_amount = persisted.amount;
next.prior_bytes_delivered = persisted.bytes;
let got = stream_fetch(
&client_ep,
target,
&next,
&sd,
server_eth.address(),
*hash_a.as_bytes(),
0,
0x2169_0003,
Duration::from_secs(8),
)
.await
.map_err(|e| anyhow::anyhow!("resumed fetch: {e:#}"))?;
anyhow::ensure!(
got.as_ref() == payload_a.as_slice(),
"the resumed fetch recovers blob A"
);
let healed = store
.load_all()?
.first()
.map(LaneState::owed)
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
healed > node_claim,
"the healed stream pays past the node's claim: {node_claim} -> {healed}"
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_voucher_rejected_total")? == rejected_before + 1,
"the heal costs exactly one rejected voucher"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_byte_offset_returns_suffix_multi_group() -> anyhow::Result<()> {
let payload: Vec<u8> = (0..204_800u32).map(|i| (i % 251) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let offset = 70 * 1024u64;
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
offset,
0xfeed,
Duration::from_secs(15),
)
.await?;
let off = usize::try_from(offset)?;
let suffix = payload
.get(off..)
.ok_or_else(|| anyhow::anyhow!("offset past payload"))?;
anyhow::ensure!(
got.as_ref() == suffix,
"multi-group offset fetch must return the trimmed suffix"
);
let persisted = store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let wire = support::bao_wire_len(payload.len() as u64, offset, 0);
let content_suffix = payload.len() as u64 - offset;
anyhow::ensure!(
wire > content_suffix,
"a multi-group range must carry proof parents (wire {wire} > content {content_suffix})"
);
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(wire),
"bytes_delivered should be the aligned bao wire size {wire}, got {}",
only.last_bytes_delivered()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_byte_offset_returns_suffix() -> anyhow::Result<()> {
let payload: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let offset = 1000u64;
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
offset,
0xfeed,
Duration::from_secs(15),
)
.await?;
let off = usize::try_from(offset)?;
let suffix = payload
.get(off..)
.ok_or_else(|| anyhow::anyhow!("offset past payload"))?;
anyhow::ensure!(
got.as_ref() == suffix,
"offset fetch must return the suffix"
);
let persisted = store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
let wire = support::bao_wire_len(payload.len() as u64, offset, 0);
anyhow::ensure!(
only.last_bytes_delivered() == U256::from(wire),
"bytes_delivered should be the aligned bao wire size {wire}, got {}",
only.last_bytes_delivered()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_rejects_zero_rate_response() -> anyhow::Result<()> {
let payload = b"zero-rate must be refused on receive".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
0,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, client_signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x1234,
Duration::from_secs(10),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("zero-rate response must be rejected"))?;
anyhow::ensure!(
err.to_string().contains("rate") || err.to_string().contains("zero"),
"error should mention the zero rate: {err}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_unknown_lane_is_rejected() -> anyhow::Result<()> {
let payload = vec![0xABu8; 1_572_864]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let store: Arc<dyn PoolStateStore> = Arc::new(MemoryPoolStateStore::new());
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let signer = PrivateKeySigner::random();
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x5678,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(
!resp.body.ok,
"unknown lane must be refused pre-serve, not served"
);
anyhow::ensure!(
matches!(
resp_ext.error,
Some(decdn_protocol::client::StreamError::NotFound)
),
"expected NotFound, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a pre-serve StreamResponse refusal, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_unknown_lane_total 1"
),
"unknown-lane refusal must bump its reason counter"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_underfunded_pool_is_refused_pre_serve() -> anyhow::Result<()> {
let payload = vec![0xABu8; 6 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let window_cost = min_payment(HARNESS_FLOOR_BYTES, RATE_PER_MB);
let deposit = window_cost.saturating_sub(U256::from(1u64));
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
deposit,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x1516,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(
!resp.body.ok,
"an underfunded pool must be refused pre-serve, not served"
);
anyhow::ensure!(
matches!(
resp_ext.error,
Some(decdn_protocol::client::StreamError::Unfunded)
),
"expected the Unfunded wire code, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a pre-serve StreamResponse refusal, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 1"
),
"the refusal must be distinguishable server-side"
);
let persisted = store.load_all()?;
let state = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("the seeded lane must still be persisted"))?;
anyhow::ensure!(
state.last_bytes_delivered() == U256::ZERO && state.last_amount() == U256::ZERO,
"a refused request must deliver zero bytes; got {} bytes / {} owed",
state.last_bytes_delivered(),
state.last_amount()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_sub_interval_blob_serves_below_one_interval_cost() -> anyhow::Result<()> {
let payload = vec![0x2Cu8; 262_144]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(5u64); let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store_dyn, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x2C01,
Duration::from_secs(10),
)
.await?;
anyhow::ensure!(got == payload, "the sub-interval blob must transfer intact");
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 0"
),
"a funded sub-interval request must not trip the deposit gate"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_deposit_gate_reserves_only_the_floor_by_default() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024; let payload = vec![0x77u8; 8 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(HARNESS_FLOOR_COST),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
U256::from(HARNESS_FLOOR_COST),
CREDIT_MAX,
2,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x1477,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), _resp_ext) => anyhow::ensure!(
resp.body.ok,
"a deposit covering the floor must clear the gate regardless of credit_max"
),
other => anyhow::bail!("expected a pre-serve StreamResponse acceptance, got {other:?}"),
}
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_deposit_gate_scales_with_credit_max_when_ramp_disabled() -> anyhow::Result<()> {
const CREDIT_MAX: u64 = 8 * 1024 * 1024; let payload = vec![0x77u8; 12 * 1024 * 1024]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(HARNESS_FLOOR_COST),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
U256::from(HARNESS_FLOOR_COST),
CREDIT_MAX,
0,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x1477,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), _resp_ext) => anyhow::ensure!(
!resp.body.ok,
"a deposit covering one interval must not unlock the whole ceiling"
),
other => anyhow::bail!("expected a pre-serve StreamResponse refusal, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 1"
),
"the credit-window refusal must bump the deposit counter"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_spent_down_pool_is_refused_pre_serve() -> anyhow::Result<()> {
let payload = vec![0x5Du8; 1_572_864]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(100u64),
0,
U256::from(95u64),
U256::from(9_961_472u64),
Some([0x22; 65]),
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
U256::from(5u64),
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x5D01,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), _resp_ext) => anyhow::ensure!(
!resp.body.ok,
"a spent-down pool must be refused on headroom, not waved through on gross deposit"
),
other => anyhow::bail!("expected a pre-serve StreamResponse refusal, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 1"
),
"the spent-down refusal must bump the deposit counter"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_resumed_range_is_priced_on_the_tail_not_the_whole_blob() -> anyhow::Result<()> {
const TAIL_OFFSET: u64 = 1_048_576;
let payload = vec![0x9Eu8; 1_468_006]; let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(5u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
U256::from(5u64),
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: TAIL_OFFSET,
byte_len: 0,
timestamp_us: 0x9E01,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => anyhow::ensure!(
resp.body.ok,
"a resume funded for its tail must be served, not refused: {:?}",
resp_ext.error
),
other => anyhow::bail!("expected a signed StreamResponse, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 0"
),
"the resume must not trip the deposit gate"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_headroom_equal_to_the_ceiling_is_served() -> anyhow::Result<()> {
let payload = vec![0xB0u8; 1_572_864];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let window_cost = min_payment(HARNESS_FLOOR_BYTES, RATE_PER_MB);
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
window_cost,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
window_cost,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0xB001,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => anyhow::ensure!(
resp.body.ok,
"headroom exactly equal to the ceiling must be served: {:?}",
resp_ext.error
),
other => anyhow::bail!("expected a signed StreamResponse, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 0"
),
"an exactly-funded pool must not trip the deposit gate"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_store_record_failure_aborts_the_stream() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 4096];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let inner = MemoryPoolStateStore::new();
inner.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store: Arc<dyn PoolStateStore> = Arc::new(FailingRecordStore { inner });
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
Arc::clone(&store),
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, client_signer.clone(), deposit);
anyhow::ensure!(
stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x9abc,
Duration::from_secs(10),
)
.await
.is_err(),
"a store record failure must not complete the fetch"
);
let persisted = store
.get(lane_key(client_signer.address()))?
.ok_or_else(|| anyhow::anyhow!("lane row missing"))?;
anyhow::ensure!(
persisted.last_bytes_delivered() == U256::ZERO,
"a failed record must not advance the watermark, got {}",
persisted.last_bytes_delivered()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn fetch_on_a_lane_expiring_at(expiry: u64) -> anyhow::Result<Option<String>> {
let payload = vec![0x5Au8; 4096];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
let mut lane = LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
);
lane.expiry = expiry;
store.record(&lane)?;
let store_dyn: Arc<dyn PoolStateStore> = store;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, client_signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x9abd,
Duration::from_secs(10),
)
.await
.err()
.map(|e| e.to_string());
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(err)
}
const DEFAULT_EXPIRY_MARGIN: u64 = decdn_common::config::capability_expiry_margin_secs(
decdn_common::config::DEFAULT_REDEEM_INTERVAL_SECS,
);
fn unix_now() -> anyhow::Result<u64> {
Ok(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_expired_capability_is_refused_unfunded() -> anyhow::Result<()> {
let err = fetch_on_a_lane_expiring_at(1)
.await?
.ok_or_else(|| anyhow::anyhow!("an expired lane must be refused"))?;
anyhow::ensure!(
err.contains("Unfunded"),
"an expired grant should be refused Unfunded: {err}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_capability_inside_the_expiry_margin_is_refused_unfunded() -> anyhow::Result<()> {
let expiry = unix_now()?
.saturating_add(DEFAULT_EXPIRY_MARGIN)
.saturating_sub(1);
let err = fetch_on_a_lane_expiring_at(expiry)
.await?
.ok_or_else(|| anyhow::anyhow!("a capability inside the margin must be refused"))?;
anyhow::ensure!(
err.contains("Unfunded"),
"a capability inside the margin should be refused Unfunded: {err}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_capability_beyond_the_expiry_margin_is_served() -> anyhow::Result<()> {
let expiry = unix_now()?
.saturating_add(DEFAULT_EXPIRY_MARGIN)
.saturating_add(60);
let err = fetch_on_a_lane_expiring_at(expiry).await?;
anyhow::ensure!(
err.is_none(),
"a capability beyond the margin must be served: {err:?}"
);
Ok(())
}
fn seeded_store() -> anyhow::Result<(Arc<dyn PoolStateStore>, Arc<PrivateKeySigner>, U256)> {
let signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
Ok((store, signer, deposit))
}
async fn spawn_handler_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
rate: u64,
max_streams: usize,
) -> anyhow::Result<(
EndpointAddr,
Arc<PrivateKeySigner>,
Endpoint,
tokio::task::JoinHandle<()>,
)> {
let (target, server_eth, server_ep, server_task, _metrics) =
spawn_handler_server_with_metrics(cache, store, rate, max_streams).await?;
Ok((target, server_eth, server_ep, server_task))
}
async fn spawn_handler_server_with_metrics(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
rate: u64,
max_streams: usize,
) -> anyhow::Result<(
EndpointAddr,
Arc<PrivateKeySigner>,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
rate,
max_streams,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_eth, server_ep, server_task, metrics))
}
#[derive(Debug)]
struct FixedRemainingPoolView {
owner: Address,
remaining: U256,
}
#[async_trait]
impl decdn_node::pool_view::PoolView for FixedRemainingPoolView {
async fn status(&self, _pool_id: B256) -> Option<decdn_node::pool_view::PoolStatus> {
Some(decdn_node::pool_view::PoolStatus {
owner: self.owner,
remaining: self.remaining,
lifecycle: decdn_node::pool_view::Lifecycle::Open,
})
}
}
#[derive(Debug)]
struct DrainedSignerPoolView {
owner: Address,
cap: u64,
}
#[async_trait]
impl decdn_node::pool_view::PoolView for DrainedSignerPoolView {
async fn status(&self, _pool_id: B256) -> Option<decdn_node::pool_view::PoolStatus> {
Some(decdn_node::pool_view::PoolStatus {
owner: self.owner,
remaining: U256::from(50_000_000u64),
lifecycle: decdn_node::pool_view::Lifecycle::Open,
})
}
async fn signer_authorization(
&self,
_pool_id: B256,
_signer: Address,
) -> Option<decdn_incentive::payment_pool::SignerAuthorization> {
Some(self.registered())
}
}
impl DrainedSignerPoolView {
const fn registered(&self) -> decdn_incentive::payment_pool::SignerAuthorization {
decdn_incentive::payment_pool::SignerAuthorization::Registered {
cap: self.cap,
expiry: u64::MAX,
spent: self.cap,
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_drained_signer_refused_at_admission_is_unfunded() -> anyhow::Result<()> {
use decdn_client::PeerSource;
use decdn_client::source::BlobSource as _;
let payload = vec![0x5Du8; 300_000];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(50_000_000u64);
let pool_store = Arc::new(MemoryPoolStateStore::new());
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = pool_store;
let view = Arc::new(DrainedSignerPoolView {
owner: operator_addr(),
cap: 40,
});
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|deps| deps.pool_view = Some(Arc::clone(&view) as _),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, Arc::clone(&client_signer), deposit);
let ledger = Arc::new(ctx.new_ledger());
let ctx = Arc::new(std::sync::Mutex::new(ctx));
let slash = slash_domain();
let deadlines = PullDeadlines::new(Duration::from_secs(10), Duration::from_secs(10), 0)?;
let source = || {
PeerSource::new(
&client_ep,
target.clone(),
Arc::clone(&ctx),
Arc::clone(&ledger),
&slash,
server_eth.address(),
decdn_protocol::client::NO_NAMESPACE,
0,
u64::MAX,
deadlines,
None,
)
};
let range = decdn_bao_range::align_range(0, 0, u64::try_from(payload.len())?)?;
let refused = source()
.open(*hash.as_bytes(), range)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("the node must refuse the drained signer"))?;
anyhow::ensure!(
refused
.downcast_ref::<decdn_client::UpstreamRefused>()
.is_some_and(|r| *r.error() == StreamError::Unfunded),
"a drained signer's admission refusal is Unfunded, got: {refused:#}"
);
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(
&encoded,
"decdn_serve_stream_rejected_signer_cap_exhausted_total 1"
),
"the refusal must count as signer_cap_exhausted:\n{encoded}"
);
shutdown([server_task], [&server_ep, &client_ep]).await?;
Ok(())
}
#[derive(Debug)]
struct ClosingPoolView {
owner: Address,
}
#[async_trait]
impl decdn_node::pool_view::PoolView for ClosingPoolView {
async fn status(&self, _pool_id: B256) -> Option<decdn_node::pool_view::PoolStatus> {
Some(decdn_node::pool_view::PoolStatus {
owner: self.owner,
remaining: U256::from(50_000_000u64),
lifecycle: decdn_node::pool_view::Lifecycle::Closing { deadline: u64::MAX },
})
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_closing_pool_is_unfunded_to_its_lane_and_not_found_to_anyone_else() -> anyhow::Result<()>
{
let payload = vec![0x6Cu8; 300_000];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
u64::MAX,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let owner = operator_addr();
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|deps| deps.pool_view = Some(Arc::new(ClosingPoolView { owner })),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let (proven, proven_ext) = open_expecting_refusal(&conn, *hash.as_bytes(), Some(&ext)).await?;
anyhow::ensure!(!proven.body.ok, "a closing pool must not be served");
anyhow::ensure!(
matches!(proven_ext.error, Some(StreamError::Unfunded)),
"the lane's requester must hear Unfunded, got {:?}",
proven_ext.error
);
conn.close(0u32.into(), b"done");
let stranger = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let (unbound, unbound_ext) = open_expecting_refusal(&stranger, *hash.as_bytes(), None).await?;
anyhow::ensure!(!unbound.body.ok, "a closing pool must not be served");
anyhow::ensure!(
matches!(unbound_ext.error, Some(StreamError::NotFound)),
"an unbound requester must hear NotFound, got {:?}",
unbound_ext.error
);
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(&encoded, "decdn_serve_stream_rejected_pool_closing_total 2"),
"both refusals must count as pool_closing:\n{encoded}"
);
stranger.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
const HARNESS_FLOOR_COST: u64 =
(HARNESS_FLOOR_BYTES * RATE_PER_MB).div_ceil(decdn_incentive::BYTES_PER_MB);
async fn spawn_handler_server_with_pool(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
remaining: U256,
credit_max: u64,
credit_ramp_divisor: u64,
) -> anyhow::Result<(
EndpointAddr,
Arc<PrivateKeySigner>,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let owner = operator_addr();
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
|deps| {
deps.pool_view = Some(Arc::new(FixedRemainingPoolView { owner, remaining }));
deps.credit_max = credit_max;
deps.credit_ramp_divisor = credit_ramp_divisor;
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_eth, server_ep, server_task, metrics))
}
#[derive(Debug)]
struct DrainingPoolView {
owner: Address,
high: U256,
low: U256,
drained: Arc<std::sync::atomic::AtomicBool>,
}
#[async_trait]
impl decdn_node::pool_view::PoolView for DrainingPoolView {
async fn status(&self, _pool_id: B256) -> Option<decdn_node::pool_view::PoolStatus> {
let remaining = if self.drained.load(std::sync::atomic::Ordering::Relaxed) {
self.low
} else {
self.high
};
Some(decdn_node::pool_view::PoolStatus {
owner: self.owner,
remaining,
lifecycle: decdn_node::pool_view::Lifecycle::Open,
})
}
}
async fn spawn_handler_server_with_draining_pool(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
high: U256,
low: U256,
drained: Arc<std::sync::atomic::AtomicBool>,
recheck_interval: Duration,
) -> anyhow::Result<(
EndpointAddr,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let owner = operator_addr();
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
|deps| {
deps.pool_view = Some(Arc::new(DrainingPoolView {
owner,
high,
low,
drained,
}));
deps.pool_recheck_interval = Some(recheck_interval);
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_ep, server_task, metrics))
}
#[tokio::test(flavor = "multi_thread")]
async fn mid_stream_pool_drain_stops_with_pool_exhausted() -> anyhow::Result<()> {
let payload_a = vec![0xA1u8; 6 * 1024 * 1024];
let payload_b = vec![0xB2u8; 16 * 1024 * 1024];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let signer_a = Arc::new(PrivateKeySigner::random());
let signer_b = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
for signer in [&signer_a, &signer_b] {
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
}
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let drained = Arc::new(std::sync::atomic::AtomicBool::new(false));
let (target, server_ep, server_task, metrics) = spawn_handler_server_with_draining_pool(
cache,
store_dyn,
U256::from(10_000u64),
U256::from(HARNESS_FLOOR_COST - 1),
Arc::clone(&drained),
Duration::ZERO,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext_a = binding_ext(&signer_a, client_node_id)?;
let ext_b = binding_ext(&signer_b, client_node_id)?;
let (_send_a, mut recv_a) = open_paid_stream(&conn, *hash_a.as_bytes(), Some(&ext_a)).await?;
read_exact_chunks(&mut recv_a, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv_a).await?;
let (mut send_b, mut recv_b) =
open_paid_stream(&conn, *hash_b.as_bytes(), Some(&ext_b)).await?;
read_exact_chunks(&mut recv_b, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv_b).await?;
drained.store(true, std::sync::atomic::Ordering::Relaxed);
pay_cumulative(&mut send_b, &signer_b, HARNESS_INTERVAL_BYTES).await?;
match tokio::time::timeout(Duration::from_secs(10), read_client_msg(&mut recv_b)).await?? {
ClientMessage::StreamError(decdn_protocol::client::StreamError::VoucherRejected {
reason,
bundle,
}) => {
anyhow::ensure!(
reason == VoucherRejectReason::PoolExhausted,
"expected PoolExhausted, got {reason:?}"
);
anyhow::ensure!(
bundle.is_none(),
"PoolExhausted is not watermark-gated, so no bundle attaches"
);
}
other => anyhow::bail!("expected VoucherRejected {{ PoolExhausted }}, got {other:?}"),
}
anyhow::ensure!(
tokio::time::timeout(Duration::from_secs(5), read_client_msg(&mut recv_b))
.await?
.is_err(),
"after PoolExhausted the server finishes the stream; no further frame is sent"
);
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
counter(
&metrics,
"decdn_serve_stream_midstream_pool_exhausted_total"
)? == 1,
"the mid-stream stop must be counted"
);
conn.close(0u32.into(), b"done");
support::assert_inbound_failures_attributed(&metrics, 2).await?;
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_client_declined_total")? == 1,
"the parked holder ends unpaid"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
struct RecheckFixture {
conn: Connection,
client_ep: Endpoint,
server_ep: Endpoint,
server_task: tokio::task::JoinHandle<()>,
_send_a: SendStream,
_recv_a: RecvStream,
send_b: SendStream,
recv_b: RecvStream,
signer_b: Arc<PrivateKeySigner>,
drained: Arc<std::sync::atomic::AtomicBool>,
}
async fn setup_recheck_holder_and_driven(
recheck_interval: Duration,
) -> anyhow::Result<RecheckFixture> {
let payload_a = vec![0xA1u8; 6 * 1024 * 1024];
let payload_b = vec![0xB2u8; 16 * 1024 * 1024];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let signer_a = Arc::new(PrivateKeySigner::random());
let signer_b = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
for signer in [&signer_a, &signer_b] {
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
}
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let drained = Arc::new(std::sync::atomic::AtomicBool::new(false));
let (target, server_ep, server_task, _metrics) = spawn_handler_server_with_draining_pool(
cache,
store_dyn,
U256::from(10_000u64),
U256::from(HARNESS_FLOOR_COST - 1),
Arc::clone(&drained),
recheck_interval,
)
.await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext_a = binding_ext(&signer_a, client_node_id)?;
let ext_b = binding_ext(&signer_b, client_node_id)?;
let (send_a, mut recv_a) = open_paid_stream(&conn, *hash_a.as_bytes(), Some(&ext_a)).await?;
read_exact_chunks(&mut recv_a, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv_a).await?;
let (send_b, mut recv_b) = open_paid_stream(&conn, *hash_b.as_bytes(), Some(&ext_b)).await?;
read_exact_chunks(&mut recv_b, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv_b).await?;
Ok(RecheckFixture {
conn,
client_ep,
server_ep,
server_task,
_send_a: send_a,
_recv_a: recv_a,
send_b,
recv_b,
signer_b,
drained,
})
}
async fn pay_and_read_to_end(
send: &mut SendStream,
recv: &mut RecvStream,
signer: &PrivateKeySigner,
) -> anyhow::Result<u64> {
let mut received = HARNESS_FLOOR_BYTES;
let mut paid = HARNESS_INTERVAL_BYTES;
pay_cumulative(send, signer, paid).await?;
let mut reader = BufferedClientReader::default();
loop {
match tokio::time::timeout(Duration::from_secs(5), reader.read(recv)).await {
Ok(msg) => match msg? {
ClientMessage::ChunkData(chunk) => {
received = received.saturating_add(chunk.bytes().len() as u64);
if received.saturating_sub(paid) >= HARNESS_INTERVAL_BYTES {
paid = received - received % HARNESS_INTERVAL_BYTES;
pay_cumulative(send, signer, paid).await?;
}
}
ClientMessage::StreamEnd => return Ok(received),
other => {
anyhow::bail!("expected ChunkData or StreamEnd while draining, got {other:?}")
}
},
Err(_elapsed) => {
anyhow::ensure!(
received > paid,
"stream stalled with nothing left to pay ({received} wire bytes received)"
);
pay_cumulative(send, signer, received).await?;
paid = received;
}
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn pool_recheck_gated_by_interval_lets_drained_stream_continue() -> anyhow::Result<()> {
let mut fx = setup_recheck_holder_and_driven(Duration::from_secs(30)).await?;
fx.drained.store(true, std::sync::atomic::Ordering::Relaxed);
let received = pay_and_read_to_end(&mut fx.send_b, &mut fx.recv_b, &fx.signer_b).await?;
anyhow::ensure!(
received >= 16 * 1024 * 1024,
"expected at least the full 16 MiB blob (wire = content + bao proof), received {received} bytes"
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn pool_recheck_fires_after_interval_on_drained_pool() -> anyhow::Result<()> {
let interval = Duration::from_millis(300);
let mut fx = setup_recheck_holder_and_driven(interval).await?;
fx.drained.store(true, std::sync::atomic::Ordering::Relaxed);
tokio::time::sleep(interval + Duration::from_millis(200)).await;
pay_cumulative(&mut fx.send_b, &fx.signer_b, HARNESS_INTERVAL_BYTES).await?;
match tokio::time::timeout(Duration::from_secs(10), read_client_msg(&mut fx.recv_b)).await?? {
ClientMessage::StreamError(decdn_protocol::client::StreamError::VoucherRejected {
reason,
bundle,
}) => {
anyhow::ensure!(
reason == VoucherRejectReason::PoolExhausted,
"expected PoolExhausted, got {reason:?}"
);
anyhow::ensure!(bundle.is_none(), "PoolExhausted attaches no bundle");
}
other => anyhow::bail!("expected VoucherRejected {{ PoolExhausted }}, got {other:?}"),
}
anyhow::ensure!(
tokio::time::timeout(Duration::from_secs(5), read_client_msg(&mut fx.recv_b))
.await?
.is_err(),
"after PoolExhausted the server finishes the stream; no further frame is sent"
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn solvent_pool_stream_completes_across_recheck_interval() -> anyhow::Result<()> {
let interval = Duration::from_millis(300);
let mut fx = setup_recheck_holder_and_driven(interval).await?;
tokio::time::sleep(interval + Duration::from_millis(200)).await;
let received = pay_and_read_to_end(&mut fx.send_b, &mut fx.recv_b, &fx.signer_b).await?;
anyhow::ensure!(
received >= 16 * 1024 * 1024,
"expected at least the full 16 MiB blob (wire = content + bao proof), received {received} bytes"
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
fn store_with_distinct_lanes(
n: usize,
) -> anyhow::Result<(Arc<MemoryPoolStateStore>, Vec<Arc<PrivateKeySigner>>)> {
let store = Arc::new(MemoryPoolStateStore::new());
let mut signers = Vec::with_capacity(n);
for _ in 0..n {
let signer = Arc::new(PrivateKeySigner::random());
store.record(&fresh_lane(signer.address(), U256::from(10_000_000u64)))?;
signers.push(signer);
}
Ok((store, signers))
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_distinct_lanes_bounded_to_pool_deposit() -> anyhow::Result<()> {
let payload = vec![0x6Bu8; 6 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let affordable_floors = 3usize;
let remaining = U256::from(u128::from(HARNESS_FLOOR_COST) * affordable_floors as u128 + 2);
let (store, signers) = store_with_distinct_lanes(affordable_floors + 1)?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
remaining,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let mut held = Vec::new();
for (i, signer) in signers.iter().take(affordable_floors).enumerate() {
let ext = binding_ext(signer, client_node_id)?;
let (send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_FLOOR_BYTES).await?;
assert_parked_awaiting_voucher(&mut recv).await?;
held.push((i, send, recv));
}
let fourth = signers
.get(affordable_floors)
.ok_or_else(|| anyhow::anyhow!("missing the fourth lane signer"))?;
let ext = binding_ext(fourth, client_node_id)?;
let (refusal, refusal_ext) =
open_expecting_refusal(&conn, *hash.as_bytes(), Some(&ext)).await?;
anyhow::ensure!(
!refusal.body.ok,
"the fourth concurrent lane must be refused while three live floors are held"
);
anyhow::ensure!(
matches!(
refusal_ext.error,
Some(decdn_protocol::client::StreamError::Unfunded)
),
"expected the Unfunded wire code, got {:?}",
refusal_ext.error
);
drop(held);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn topped_up_pool_serves_many_lanes() -> anyhow::Result<()> {
let payload = vec![0x7Cu8; 6 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let lanes = 6usize;
let remaining = U256::from(10_000_000u64);
let (store, signers) = store_with_distinct_lanes(lanes)?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, _server_eth, server_ep, server_task, _metrics) = spawn_handler_server_with_pool(
cache,
store_dyn,
remaining,
decdn_common::config::DEFAULT_CREDIT_MAX,
decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
)
.await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let mut held = Vec::new();
for (i, signer) in signers.iter().enumerate() {
let ext = binding_ext(signer, client_node_id)?;
let (send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext))
.await
.map_err(|e| anyhow::anyhow!("lane {i} must be admitted on a topped-up pool: {e}"))?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
held.push((send, recv));
}
anyhow::ensure!(
held.len() == lanes,
"every one of the {lanes} distinct lanes must be served on a topped-up pool, served {}",
held.len()
);
drop(held);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
fn metric_line_present(encoded: &str, line: &str) -> bool {
encoded.lines().any(|l| l == line)
}
async fn raw_request(
client_ep: &Endpoint,
target: EndpointAddr,
req: &StreamRequest,
ext: Option<&StreamRequestExt>,
) -> anyhow::Result<(ClientMessage, decdn_protocol::StreamResponseExt)> {
let conn = client_ep
.connect(target, ALPN_CLIENT)
.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 payload = encode_stream_request(req, ext)?;
write_frame(&mut send, &payload)
.await
.map_err(|e| anyhow::anyhow!("write: {e}"))?;
let frame = read_frame(&mut recv)
.await
.map_err(|e| anyhow::anyhow!("read frame (stream reset?): {e}"))?;
let (msg, rest) =
decode_message::<ClientMessage>(&frame).map_err(|e| anyhow::anyhow!("decode: {e}"))?;
let ext = decdn_protocol::parse_stream_response_ext(rest)
.map_err(|e| anyhow::anyhow!("decode response ext: {e}"))?;
Ok((msg, ext))
}
fn sign_binding_for(signer: &PrivateKeySigner, client_node_id: B256) -> anyhow::Result<Vec<u8>> {
let hash = binding_signing_hash(client_node_id, EPHEMERAL_BINDING_NONCE, &binding_domain());
Ok(signer.sign_hash_sync(&hash)?.as_bytes().to_vec())
}
#[tokio::test(flavor = "multi_thread")]
async fn buyer_aborts_oversized_small_blob_without_paying() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 8192];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let mut progress = VoucherProgress::default();
let err = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c1,
PullDeadlines::whole_transfer(Duration::from_secs(10)),
4096,
0,
&mut progress,
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("received bytes over the ceiling must abort the fetch"))?;
anyhow::ensure!(
err.to_string().contains("BlobTooLarge"),
"error should surface the buyer-side BlobTooLarge ceiling: {err}"
);
anyhow::ensure!(
progress.advanced().is_none(),
"a sub-interval oversized blob aborts before the first voucher: {:?}",
progress.advanced()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn buyer_pays_for_received_bytes_then_aborts_over_ceiling() -> anyhow::Result<()> {
let payload = vec![0x5Bu8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let mut progress = VoucherProgress::default();
let ceiling: u64 = 2_500_000;
let err = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c2,
PullDeadlines::whole_transfer(Duration::from_secs(10)),
ceiling,
0,
&mut progress,
)
.await
.err()
.ok_or_else(|| {
anyhow::anyhow!("an oversized blob must abort once received bytes cross the ceiling")
})?;
anyhow::ensure!(
err.to_string().contains("BlobTooLarge"),
"the received-byte cap must surface BlobTooLarge: {err}"
);
let (bytes, _amount) = progress.advanced().ok_or_else(|| {
anyhow::anyhow!("the buyer must pay for the bytes it actually received before aborting")
})?;
anyhow::ensure!(
bytes <= U256::from(ceiling),
"the buyer must not pay past the ceiling, paid {bytes}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn buyer_rejects_over_ceiling_rate() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 4096];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let mut progress = VoucherProgress::default();
let buyer_ceiling = RATE_PER_MB - 1;
let err = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c4,
PullDeadlines::whole_transfer(Duration::from_secs(10)),
0,
buyer_ceiling,
&mut progress,
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("an over-ceiling rate must be refused by the buyer"))?;
let refused = err
.downcast_ref::<RateAboveCeiling>()
.ok_or_else(|| anyhow::anyhow!("must surface a typed RateAboveCeiling, got: {err:#}"))?;
anyhow::ensure!(
refused.quoted_rate_per_mb() == RATE_PER_MB
&& refused.ceiling_rate_per_mb() == buyer_ceiling,
"the error must record the quoted rate {} and the ceiling {}, got {} / {}",
RATE_PER_MB,
buyer_ceiling,
refused.quoted_rate_per_mb(),
refused.ceiling_rate_per_mb(),
);
let evidence = refused.evidence();
anyhow::ensure!(
evidence.body.rate_per_mb == RATE_PER_MB && evidence.body.ok,
"the retained evidence must be the signed ok:true over-quote"
);
let recovered = alloy::primitives::Signature::try_from(evidence.slash_sig.as_slice())?;
StreamSlashData::from_response_body(&evidence.body).verify_signer(
&recovered,
server_eth.address(),
&slash_domain(),
)?;
anyhow::ensure!(
progress.advanced().is_none(),
"no voucher may be paid when the buyer refuses the rate up front: {:?}",
progress.advanced()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn buyer_accepts_blob_at_exact_ceiling() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 3 * 16 * 1024 + 123];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let mut progress = VoucherProgress::default();
let got = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c2,
PullDeadlines::whole_transfer(Duration::from_secs(10)),
u64::try_from(payload.len())?,
0,
&mut progress,
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"exact-ceiling blob must deliver intact"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn read_groups(
reader: &mut decdn_client::sink::PullReader,
wire_len: u64,
until: u64,
) -> anyhow::Result<u64> {
use iroh_io::AsyncStreamReader as _;
let mut read = 0u64;
while read < until.min(wire_len) {
let want = (wire_len - read).min(decdn_bao_range::CHUNK_GROUP_BYTES);
let bytes = reader.read_bytes(usize::try_from(want)?).await?;
if bytes.is_empty() {
break;
}
read = read.saturating_add(u64::try_from(bytes.len())?);
}
Ok(read)
}
#[tokio::test(flavor = "multi_thread")]
async fn a_pull_stopped_at_a_split_pays_what_it_verified_and_the_lane_pulls_on()
-> anyhow::Result<()> {
use decdn_client::PeerSource;
use decdn_client::source::BlobSource as _;
let payload = vec![0x3Au8; 8 * 1024 * 1024];
let total = u64::try_from(payload.len())?;
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let ledger = Arc::new(ctx.new_ledger());
let slash = slash_domain();
let source = PeerSource::new(
&client_ep,
target,
Arc::new(std::sync::Mutex::new(ctx)),
Arc::clone(&ledger),
&slash,
server_eth.address(),
decdn_protocol::client::NO_NAMESPACE,
0,
u64::MAX,
PullDeadlines::new(Duration::from_secs(10), Duration::from_secs(10), 0)?,
None,
);
let range = decdn_bao_range::align_range(0, 4 * 1024 * 1024, total)?;
let (_header, mut reader) = source.open(*hash.as_bytes(), range).await?;
let wire_len = support::bao_wire_len(total, 0, 4 * 1024 * 1024);
let verified = read_groups(&mut reader, wire_len, 3 * 512 * 1024).await?;
anyhow::ensure!(verified >= 3 * 512 * 1024, "the leg ended before the split");
tokio::time::sleep(Duration::from_millis(200)).await;
let stopped = source.stop(reader).await?;
let (stopped_bytes, _) = stopped
.advanced()
.ok_or_else(|| anyhow::anyhow!("the stopped leg must have paid"))?;
anyhow::ensure!(
stopped_bytes == U256::from(verified),
"the stopped leg pays for exactly the {verified} wire bytes it verified, none of the \
frames queued past the split: paid {stopped_bytes}"
);
let range = decdn_bao_range::align_range(4 * 1024 * 1024, 4 * 1024 * 1024, total)?;
let (_header, mut reader) = source.open(*hash.as_bytes(), range).await?;
let wire_len = support::bao_wire_len(total, 4 * 1024 * 1024, 4 * 1024 * 1024);
let next = read_groups(&mut reader, wire_len, u64::MAX).await?;
anyhow::ensure!(
next == wire_len,
"the next leg delivers its whole wire ({next} of {wire_len})"
);
let finished = source.finish(reader).await?;
let (finished_bytes, _) = finished
.advanced()
.ok_or_else(|| anyhow::anyhow!("the next leg must have paid"))?;
anyhow::ensure!(
finished_bytes == stopped_bytes + U256::from(next),
"the next leg pays on from the stopped one: {finished_bytes} of {stopped_bytes} + {next}"
);
drop(source);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn gap_scoped_leg_enforces_the_received_byte_ceiling_from_its_own_offset()
-> anyhow::Result<()> {
use decdn_client::PeerSource;
use decdn_client::sink::StashedFault as _;
use decdn_client::source::BlobSource as _;
let payload = vec![0x5Cu8; 4 * 1024 * 1024];
let total = u64::try_from(payload.len())?;
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let ledger = Arc::new(ctx.new_ledger());
let ceiling: u64 = 2_500_000;
let slash = slash_domain();
let source = PeerSource::new(
&client_ep,
target,
Arc::new(std::sync::Mutex::new(ctx)),
Arc::clone(&ledger),
&slash,
server_eth.address(),
decdn_protocol::client::NO_NAMESPACE,
ceiling,
u64::MAX,
PullDeadlines::new(Duration::from_secs(10), Duration::from_secs(10), 0)?,
None,
);
let range = decdn_bao_range::align_range(0, 2 * 1024 * 1024, total)?;
let (header, mut reader) = source.open(*hash.as_bytes(), range).await?;
anyhow::ensure!(
header.total_bytes == total,
"the signed claim exceeds the ceiling by design"
);
let expected = support::bao_wire_len(total, 0, 2 * 1024 * 1024);
let received = read_groups(&mut reader, expected, u64::MAX).await?;
anyhow::ensure!(
received == expected,
"the under-ceiling leg must deliver its whole wire ({received} of {expected})"
);
let paid_first = source.finish(reader).await?;
let (paid_first, _) = paid_first
.advanced()
.ok_or_else(|| anyhow::anyhow!("the completed leg must have paid"))?;
let ceiling_wire_from_offset = support::bao_wire_len(ceiling, 2 * 1024 * 1024, 0);
anyhow::ensure!(
ceiling_wire_from_offset < HARNESS_INTERVAL_BYTES,
"the ceiling's wire from the offset must sit under one interval"
);
let range = decdn_bao_range::align_range(2 * 1024 * 1024, 2 * 1024 * 1024, total)?;
let (_header, mut reader) = source.open(*hash.as_bytes(), range).await?;
let wire_len = support::bao_wire_len(total, 2 * 1024 * 1024, 2 * 1024 * 1024);
read_groups(&mut reader, wire_len, u64::MAX).await?;
let err = reader
.take_fault()
.ok_or_else(|| anyhow::anyhow!("the crossing leg must not complete"))?;
anyhow::ensure!(
err.downcast_ref::<BlobTooLarge>().is_some(),
"the crossing leg must abort with BlobTooLarge, got: {err:#}"
);
drop(reader);
let paid_total = ledger.settlement().bytes;
anyhow::ensure!(
paid_total == paid_first,
"the crossing leg paid {} past the first leg's {paid_first}; it verified less than \
one interval ({ceiling_wire_from_offset} wire bytes at most) and owes nothing",
paid_total - paid_first
);
drop(source);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn buyer_accepts_rate_at_exact_ceiling() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 4096];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let mut progress = VoucherProgress::default();
let got = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c5,
PullDeadlines::whole_transfer(Duration::from_secs(10)),
0,
RATE_PER_MB, &mut progress,
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"a quote exactly at the buyer ceiling must deliver intact"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn progress_callback_reports_monotonic_delivery() -> anyhow::Result<()> {
let payload = vec![0xABu8; 256 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let mut progress = VoucherProgress::default();
let observations = Arc::new(std::sync::Mutex::new(Vec::<(u64, u64)>::new()));
let sink = Arc::clone(&observations);
let record = move |received: u64, expected: u64| {
if let Ok(mut v) = sink.lock() {
v.push((received, expected));
}
};
let got = stream_fetch_tracked_with_progress(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c3,
PullDeadlines::whole_transfer(Duration::from_secs(10)),
0, 0, &mut progress,
Some(&record),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"progress-tracked fetch must still deliver the blob intact"
);
let obs: Vec<(u64, u64)> = observations
.lock()
.map_err(|_| anyhow::anyhow!("progress mutex poisoned"))?
.clone();
anyhow::ensure!(!obs.is_empty(), "progress callback must fire at least once");
let expected = obs.last().map_or(0, |&(_, e)| e);
anyhow::ensure!(
expected == u64::try_from(payload.len())?,
"expected must be the blob's content size, got {expected}"
);
anyhow::ensure!(
obs.iter().all(|&(_, e)| e == expected),
"expected total must stay constant across the pull"
);
let mut prev = 0u64;
for &(received, _) in &obs {
anyhow::ensure!(
received >= prev,
"progress must be monotonically non-decreasing ({received} < {prev})"
);
anyhow::ensure!(
received <= expected,
"progress ({received}) must never exceed the expected total ({expected})"
);
prev = received;
}
anyhow::ensure!(
prev == expected,
"final progress ({prev}) must reach the content size ({expected})"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
fn denied_hashes(hashes: &[Hash]) -> decdn_cache::DeniedHashes {
decdn_cache::DeniedHashes::new(
hashes
.iter()
.map(|h| decdn_cache::LeafHash::from_bytes(*h.as_bytes()))
.collect(),
)
}
fn content_with_origins(
origins: &[alloy::primitives::Address],
) -> decdn_common::config::ResolvedContent {
decdn_common::config::ResolvedContent {
denied_origins: origins.iter().copied().collect(),
..Default::default()
}
}
async fn spawn_handler_server_with_deny(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
deny: Arc<decdn_node::content_deny::ContentDenylist>,
owner: Address,
) -> anyhow::Result<(
EndpointAddr,
Arc<PrivateKeySigner>,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| {
deps.content_deny = deny;
deps.pool_view = Some(Arc::new(FixedRemainingPoolView {
owner,
remaining: U256::MAX,
}));
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_eth, server_ep, server_task, metrics))
}
#[tokio::test(flavor = "multi_thread")]
async fn denylisted_hash_is_refused_even_when_held() -> anyhow::Result<()> {
let payload = b"content under a local takedown order".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
anyhow::ensure!(cache.has(hash).await?, "held before the takedown");
cache.set_denied(&denied_hashes(&[hash]));
anyhow::ensure!(
!cache.has(hash).await?,
"a denied blob must report absent, exactly as an evicted one does — \
defence in depth behind the serve gate"
);
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00e2,
Duration::from_secs(10),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a denylisted blob must be refused even when held"))?;
anyhow::ensure!(
err.to_string().contains("Declined"),
"error should surface Declined: {err}"
);
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_hash_denied_total 1"
),
"the takedown refusal must bump its own reason counter, not the eviction one"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn governance_denied_hash_is_refused_as_hash_blacklisted() -> anyhow::Result<()> {
let payload = b"content under a governance takedown".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
cache.set_chain_denied_one(hash, true);
anyhow::ensure!(!cache.is_denied(hash), "not on the LOCAL list");
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00e4,
Duration::from_secs(10),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a governance-blacklisted blob must be refused"))?;
anyhow::ensure!(
err.to_string().contains("Declined"),
"a governance takedown must sign Declined: {err}"
);
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(
&encoded,
"decdn_serve_stream_rejected_chain_hash_denied_total 1"
),
"the governance refusal has its own operator-side counter"
);
anyhow::ensure!(
metric_line_present(&encoded, "decdn_serve_stream_rejected_hash_denied_total 0"),
"...and must not be miscounted as a local denylist hit"
);
anyhow::ensure!(
metric_line_present(
&encoded,
"decdn_serve_stream_rejected_evicted_since_probe_total 0"
),
"...nor land on the eviction counter, which is now non-takedown evictions only"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn takedown_mid_stream_terminates_the_delivery() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let deny_cache = cache.clone();
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let fetch_ep = client_ep.clone();
let server_addr = server_eth.address();
let hash_bytes = *hash.as_bytes();
let fetch = tokio::spawn(async move {
stream_fetch(
&fetch_ep,
target,
&ctx,
&slash_domain(),
server_addr,
hash_bytes,
0,
0x00e5,
Duration::from_secs(30),
)
.await
});
let deadline = Instant::now() + Duration::from_secs(20);
while !store
.load_all()?
.iter()
.any(|state| state.owed() > U256::ZERO)
{
anyhow::ensure!(
Instant::now() < deadline,
"no voucher was ever accepted; the test never reached the mid-stream path"
);
tokio::time::sleep(Duration::from_millis(1)).await;
}
deny_cache.set_chain_denied_one(hash, true);
let err = fetch
.await?
.err()
.ok_or_else(|| anyhow::anyhow!("delivery must not complete through a takedown"))?;
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_terminated_takedown_total 1"
),
"the cut-off must be metered as an in-flight termination, not a completed \
delivery ({err})"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn blacklisted_funder_is_refused_on_a_cache_miss() -> anyhow::Result<()> {
let cache_dir = tempfile::tempdir()?;
let cache = CacheEngine::open(cache_dir.path(), vec![], 16).await?;
let (store, signer, deposit) = seeded_store()?;
let funder = signer.address();
let deny = Arc::new(decdn_node::content_deny::ContentDenylist::new(
&content_with_origins(&[funder]),
));
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_deny(cache, store, deny, funder).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
[0x5a; 32], 0,
0x00e3,
Duration::from_secs(10),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a blacklisted funder must be refused"))?;
anyhow::ensure!(
err.to_string().contains("Declined"),
"error should surface Declined, not NotFound: {err}"
);
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_origin_denied_total 1"
),
"the compliance gauge must count a miss-path refusal too"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_evicted_since_probe_is_refused() -> anyhow::Result<()> {
let payload = b"evicted between probe and stream".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
cache.evict(hash).await?; let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00e1,
Duration::from_secs(10),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("evicted blob must be refused"))?;
anyhow::ensure!(
err.to_string().contains("Declined"),
"error should surface Declined: {err}"
);
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_evicted_since_probe_total 1"
),
"evicted-since-probe refusal must bump its reason counter (#876)"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_binding_address_mismatch_resets() -> anyhow::Result<()> {
let payload = b"binding mismatch never delivers".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, _owner, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let attacker = PrivateKeySigner::random();
let claimed = PrivateKeySigner::random();
let claimed_addr: [u8; 20] = claimed.address().into();
let ext = StreamRequestExt {
binding: Some(ClientBinding {
ethereum_address: claimed_addr,
binding_signature: sign_binding_for(&attacker, client_node_id)?,
}),
..Default::default()
};
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00ba_d001,
};
let res = raw_request(&client_ep, target, &req, Some(&ext)).await;
anyhow::ensure!(
res.is_err(),
"a binding recovering a different address must reset the stream, got {res:?}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_binding_for_other_owner_is_not_found() -> anyhow::Result<()> {
let payload = b"valid binding, wrong pool owner".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, _owner, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let intruder = PrivateKeySigner::random();
let intruder_addr: [u8; 20] = intruder.address().into();
let ext = StreamRequestExt {
binding: Some(ClientBinding {
ethereum_address: intruder_addr,
binding_signature: sign_binding_for(&intruder, client_node_id)?,
}),
..Default::default()
};
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00ba_d002,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(
!resp.body.ok,
"expected ok:false for an unauthorized binding"
);
anyhow::ensure!(
matches!(
resp_ext.error,
Some(decdn_protocol::client::StreamError::NotFound)
),
"expected NotFound, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a StreamResponse, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_unknown_lane_total 1"
),
"a bound-but-laneless request must bump the unknown-lane counter"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
type DelegateSignerFixture = (
Arc<dyn PoolStateStore>,
Arc<PrivateKeySigner>,
Arc<PrivateKeySigner>,
);
fn delegate_signer_store() -> anyhow::Result<DelegateSignerFixture> {
let funder = Arc::new(PrivateKeySigner::random());
let delegate = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
delegate.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
Ok((store, funder, delegate))
}
fn binding_ext(
signer: &PrivateKeySigner,
client_node_id: B256,
) -> anyhow::Result<StreamRequestExt> {
Ok(StreamRequestExt {
binding: Some(ClientBinding {
ethereum_address: signer.address().into(),
binding_signature: sign_binding_for(signer, client_node_id)?,
}),
..Default::default()
})
}
#[tokio::test(flavor = "multi_thread")]
async fn binding_matching_the_delegate_signer_is_authorized() -> anyhow::Result<()> {
let payload = b"delegate-signed lane is served".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, _funder, delegate) = delegate_signer_store()?;
let (target, _server_eth, server_ep, server_task, _metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&delegate, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00de_1e01,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(
resp.body.ok,
"the pinned voucher signer must be authorized, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a StreamResponse, got {other:?}"),
}
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn binding_matching_only_the_funder_is_refused() -> anyhow::Result<()> {
let payload = b"the funder cannot spend a delegated lane".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, funder, _delegate) = delegate_signer_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, store, RATE_PER_MB, 16).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&funder, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00de_1e02,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(!resp.body.ok, "expected ok:false for the funder binding");
anyhow::ensure!(
matches!(
resp_ext.error,
Some(decdn_protocol::client::StreamError::NotFound)
),
"expected NotFound, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a StreamResponse, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_unknown_lane_total 1"
),
"the funder binding must bump the unknown-lane counter"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn blacklisted_funder_is_refused_even_behind_a_clean_delegate() -> anyhow::Result<()> {
let payload = b"a clean delegate does not launder a blacklisted funder".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, funder, delegate) = delegate_signer_store()?;
let deny = Arc::new(decdn_node::content_deny::ContentDenylist::new(
&content_with_origins(&[funder.address()]),
));
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_deny(cache, store, deny, funder.address()).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&delegate, client_node_id)?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00de_1e03,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(
!resp.body.ok,
"a blacklisted funder must be refused behind a clean delegate"
);
anyhow::ensure!(
matches!(
resp_ext.error,
Some(decdn_protocol::client::StreamError::Declined)
),
"expected Declined, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a StreamResponse, got {other:?}"),
}
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_origin_denied_total 1"
),
"the compliance gauge must count the delegated-lane refusal"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delegate_signed_vouchers_carry_a_delivery_to_completion() -> anyhow::Result<()> {
let payload = vec![0xC3u8; 1_572_864];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, _funder, delegate) = delegate_signer_store()?;
let (target, server_eth, server_ep, server_task, _metrics) =
spawn_handler_server_with_metrics(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&delegate), U256::from(10_000_000u64));
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00de_1e05,
Duration::from_secs(30),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delegate-signed delivery returned {} bytes, expected {}",
got.len(),
payload.len()
);
let states = store.load_all()?;
let state = states
.first()
.ok_or_else(|| anyhow::anyhow!("the lane must still be persisted"))?;
let wire = support::bao_wire_len_whole(payload.len() as u64);
anyhow::ensure!(
state.last_bytes_delivered() == U256::from(wire),
"the persisted byte count must cover the whole blob: {} (expected {wire})",
state.last_bytes_delivered()
);
anyhow::ensure!(
state.signer == delegate.address() && state.provider == operator_addr(),
"the delegate is the lane signer across a completed delivery"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn blacklisting_the_funder_mid_stream_cuts_off_a_delegated_delivery() -> anyhow::Result<()> {
let payload = vec![0x6Bu8; 16 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, funder, delegate) = delegate_signer_store()?;
let deny = Arc::new(decdn_node::content_deny::ContentDenylist::empty());
let (target, server_eth, server_ep, server_task, metrics) = spawn_handler_server_with_deny(
cache,
Arc::clone(&store),
Arc::clone(&deny),
funder.address(),
)
.await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&delegate), U256::from(10_000_000u64));
let fetch_ep = client_ep.clone();
let server_addr = server_eth.address();
let hash_bytes = *hash.as_bytes();
let fetch = tokio::spawn(async move {
stream_fetch(
&fetch_ep,
target,
&ctx,
&slash_domain(),
server_addr,
hash_bytes,
0,
0x00de_1e06,
Duration::from_secs(30),
)
.await
});
let deadline = Instant::now() + Duration::from_secs(20);
while !store
.load_all()?
.iter()
.any(|state| state.owed() > U256::ZERO)
{
anyhow::ensure!(
Instant::now() < deadline,
"no delegate-signed voucher was ever accepted; the test never reached the \
mid-stream path"
);
tokio::time::sleep(Duration::from_millis(1)).await;
}
deny.apply_chain_origin(funder.address(), true);
let err = fetch.await?.err().ok_or_else(|| {
anyhow::anyhow!(
"delivery must not complete once the funder is blacklisted — the mid-stream \
re-check has been re-keyed onto the voucher signer"
)
})?;
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_terminated_takedown_total 1"
),
"the cut-off must be metered as an in-flight termination, not a completed \
delivery ({err})"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn cache_with_two_blobs(
a: &[u8],
b: &[u8],
) -> anyhow::Result<(
CacheEngine,
decdn_cache::Hash,
decdn_cache::Hash,
tempfile::TempDir,
)> {
let hash_a = decdn_cache::Hash::new(a);
let hash_b = decdn_cache::Hash::new(b);
let origin_dir = tempfile::tempdir()?;
for (payload, hash) in [(a, hash_a), (b, hash_b)] {
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(decdn_cache::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_a).await?; let _ = cache.get(hash_b).await?;
drop(origin_dir);
Ok((cache, hash_a, hash_b, cache_dir))
}
#[tokio::test(flavor = "multi_thread")]
async fn client_concurrent_same_lane_both_succeed() -> anyhow::Result<()> {
let payload_a = vec![0x5Au8; 4096];
let payload_b = vec![0xA5u8; 8192];
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task) =
spawn_handler_server(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let ledger = Arc::new(PoolLedger::new(Cumulative::default()));
let server_addr = server_eth.address();
let sd = slash_domain();
let (ra, rb) = tokio::join!(
stream_fetch_shared(
&client_ep,
target.clone(),
&ctx,
&ledger,
&sd,
server_addr,
*hash_a.as_bytes(),
0,
0x00aa,
PullDeadlines::whole_transfer(Duration::from_secs(15)),
0,
0,
),
stream_fetch_shared(
&client_ep,
target.clone(),
&ctx,
&ledger,
&sd,
server_addr,
*hash_b.as_bytes(),
0,
0x00bb,
PullDeadlines::whole_transfer(Duration::from_secs(15)),
0,
0,
),
);
let bytes_a = ra.map_err(|e| anyhow::anyhow!("pull A failed: {e:?}"))?;
let bytes_b = rb.map_err(|e| anyhow::anyhow!("pull B failed: {e:?}"))?;
anyhow::ensure!(bytes_a.as_ref() == payload_a.as_slice(), "blob A mismatch");
anyhow::ensure!(bytes_b.as_ref() == payload_b.as_slice(), "blob B mismatch");
let final_cum = ledger.committed();
anyhow::ensure!(
final_cum.amount > U256::ZERO,
"ledger amount: {} (expected a paid delivery)",
final_cum.amount
);
let total_bytes = U256::from(payload_a.len() + payload_b.len());
anyhow::ensure!(
final_cum.bytes == total_bytes,
"ledger bytes: {} (expected {})",
final_cum.bytes,
total_bytes
);
let persisted = store.load_all()?;
let only = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane"))?;
anyhow::ensure!(
only.last_bytes_delivered() == total_bytes,
"persisted bytes_delivered: {} (expected {})",
only.last_bytes_delivered(),
total_bytes
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn honest_concurrent_rollovers_draw_no_voucher_rejection() -> anyhow::Result<()> {
let payload_a: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let payload_b: Vec<u8> = (0..5 * 1024 * 1024 / 2u32)
.map(|i| (i % 241) as u8)
.collect();
let (cache, hash_a, hash_b, _cache_tmp) = cache_with_two_blobs(&payload_a, &payload_b).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let ledger = Arc::new(PoolLedger::new(Cumulative::default()));
let server_addr = server_eth.address();
let sd = slash_domain();
for round in 0..2u64 {
let (ra, rb) = tokio::join!(
stream_fetch_shared(
&client_ep,
target.clone(),
&ctx,
&ledger,
&sd,
server_addr,
*hash_a.as_bytes(),
0,
0x0a00 + round,
PullDeadlines::whole_transfer(Duration::from_secs(20)),
0,
0,
),
stream_fetch_shared(
&client_ep,
target.clone(),
&ctx,
&ledger,
&sd,
server_addr,
*hash_b.as_bytes(),
0,
0x0b00 + round,
PullDeadlines::whole_transfer(Duration::from_secs(20)),
0,
0,
),
);
let bytes_a = ra.map_err(|e| anyhow::anyhow!("round {round} pull A failed: {e:?}"))?;
let bytes_b = rb.map_err(|e| anyhow::anyhow!("round {round} pull B failed: {e:?}"))?;
anyhow::ensure!(bytes_a.as_ref() == payload_a.as_slice(), "blob A mismatch");
anyhow::ensure!(bytes_b.as_ref() == payload_b.as_slice(), "blob B mismatch");
}
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_voucher_rejected_total")? == 0,
"an honest payer's vouchers must all fold the lane"
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_client_declined_total")? == 0,
"no stream may stall on a proof the client never sends"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_sub_chunk_stream_on_a_live_chain_sends_only_its_closing_voucher() -> anyhow::Result<()> {
let big = vec![0x3Cu8; 3 * 1024 * 1024 + 512 * 1024];
let small = vec![0x4Du8; 400_000];
let (cache, big_hash, small_hash, _cache_tmp) = cache_with_two_blobs(&big, &small).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth, server_ep, server_task, metrics) =
spawn_handler_server_with_metrics(cache, Arc::clone(&store), RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let ledger = Arc::new(PoolLedger::new(Cumulative::default()));
let sd = slash_domain();
let fetch = |hash: decdn_cache::Hash, request_id: u64| {
stream_fetch_shared(
&client_ep,
target.clone(),
&ctx,
&ledger,
&sd,
server_eth.address(),
*hash.as_bytes(),
0,
request_id,
PullDeadlines::whole_transfer(Duration::from_secs(30)),
0,
0,
)
};
let got = fetch(big_hash, 0x2132_0001)
.await
.map_err(|e| anyhow::anyhow!("multi-chunk pull failed: {e:?}"))?;
anyhow::ensure!(got.as_ref() == big.as_slice(), "multi-chunk blob mismatch");
anyhow::ensure!(
ledger.chain_root().is_some(),
"the multi-chunk stream must leave the lane on a live chain"
);
let before = counter(&metrics, "decdn_vouchers_received_total")?;
let got = fetch(small_hash, 0x2132_0002)
.await
.map_err(|e| anyhow::anyhow!("sub-chunk pull failed: {e:?}"))?;
anyhow::ensure!(got.as_ref() == small.as_slice(), "sub-chunk blob mismatch");
let sent = counter(&metrics, "decdn_vouchers_received_total")? - before;
anyhow::ensure!(
sent == 1,
"a sub-chunk stream must pay with its closing voucher alone, sent {sent} vouchers"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
fn counter(metrics: &Metrics, name: &str) -> anyhow::Result<u64> {
let encoded = metrics.encode()?;
let prefix = format!("{name} ");
encoded
.lines()
.find_map(|line| line.strip_prefix(prefix.as_str()))
.ok_or_else(|| anyhow::anyhow!("no {name} sample"))?
.trim()
.parse()
.map_err(|e| anyhow::anyhow!("parse {name}: {e}"))
}
#[tokio::test(flavor = "multi_thread")]
async fn client_not_found_is_refused() -> anyhow::Result<()> {
let (cache, _cache_tmp) = empty_cache().await?;
let client_signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client_signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let ctx = pool_context(&client_ep, client_signer, deposit);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
[0x42u8; 32], 0,
0x9abc,
Duration::from_secs(10),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("absent blob must be refused"))?;
anyhow::ensure!(
err.to_string().contains("refused") || err.to_string().contains("NotFound"),
"error should surface the delivery refusal: {err}"
);
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_cache_miss_total 1"
),
"cache-miss refusal must bump its reason counter"
);
let text = metrics.encode()?;
for line in [
"decdn_serve_first_byte_hit_seconds_count 0",
"decdn_serve_first_byte_miss_seconds_count 0",
] {
anyhow::ensure!(
metric_line_present(&text, line),
"missing `{line}`:\n{text}"
);
}
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test]
async fn register_lane_is_idempotent_and_preserves_watermark() -> anyhow::Result<()> {
let (cache, _tmp) = empty_cache().await?;
let store = Arc::new(MemoryPoolStateStore::new());
let client = PrivateKeySigner::random().address();
let advanced = LaneState::hydrate(
pool_id(),
client,
operator_addr(),
U256::from(10_000_000u64),
0,
U256::from(4_321u64),
U256::from(2_048u64),
Some([0x11; 65]),
decdn_incentive::LaneChain::NONE,
);
store.record(&advanced)?;
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let server_eth = operator_signer();
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let handler = build_handler(
fresh_key().public(),
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
)?;
handler.register_lane(fresh_lane(client, U256::from(10_000_000u64)))?;
let after = store
.get(lane_key(client))?
.ok_or_else(|| anyhow::anyhow!("lane vanished"))?;
anyhow::ensure!(
after.last_bytes_delivered() == U256::from(2_048u64),
"re-observed registration reset the watermark bytes to {} (reopened #527 replay window)",
after.last_bytes_delivered()
);
anyhow::ensure!(after.last_amount() == U256::from(4_321u64));
Ok(())
}
#[derive(Debug)]
struct CountingOrigin {
hits: Arc<std::sync::atomic::AtomicUsize>,
}
impl decdn_cache::Origin for CountingOrigin {
fn fetch(
&self,
_hash: decdn_cache::Hash,
_max_bytes: u64,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<decdn_cache::origin::OriginFetch, decdn_cache::OriginPullError>,
> + Send
+ '_,
>,
> {
self.hits.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Box::pin(async { Ok(decdn_cache::origin::OriginFetch::NotFound) })
}
fn kind(&self) -> decdn_cache::OriginKind {
decdn_cache::OriginKind::Peer
}
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_through_gate_authorizes_only_lane_signer() -> anyhow::Result<()> {
let hits = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let cache_tmp = tempfile::tempdir()?;
let cache = CacheEngine::open(
cache_tmp.path(),
vec![Arc::new(CountingOrigin {
hits: Arc::clone(&hits),
}) as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let (store, owner, _deposit) = seeded_store()?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_full_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
&loopback_domains(),
16,
|deps| deps.pull_through = Some(std::time::Duration::from_secs(10)),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let miss_hash = [0xEEu8; 32];
let req = StreamRequest {
hash: miss_hash,
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0091_1001,
};
let (c1, _) = local_endpoint(fresh_key(), vec![]).await?;
let _ = raw_request(&c1, target.clone(), &req, None).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 0,
"an unbound request must NOT trigger a paid pull"
);
shutdown([], [&c1]).await?;
let intruder_sk = fresh_key();
let intruder_node_id = B256::from(*intruder_sk.public().as_bytes());
let (c2, _) = local_endpoint(intruder_sk, vec![]).await?;
let intruder = PrivateKeySigner::random();
let ext_intruder = StreamRequestExt {
binding: Some(ClientBinding {
ethereum_address: intruder.address().into(),
binding_signature: sign_binding_for(&intruder, intruder_node_id)?,
}),
..Default::default()
};
let _ = raw_request(&c2, target.clone(), &req, Some(&ext_intruder)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 0,
"a binding for a non-owner address must NOT trigger a paid pull"
);
shutdown([], [&c2]).await?;
let owner_sk = fresh_key();
let owner_node_id = B256::from(*owner_sk.public().as_bytes());
let (c3, _) = local_endpoint(owner_sk, vec![]).await?;
let ext_owner = StreamRequestExt {
binding: Some(ClientBinding {
ethereum_address: owner.address().into(),
binding_signature: sign_binding_for(&owner, owner_node_id)?,
}),
..Default::default()
};
let _ = raw_request(&c3, target.clone(), &req, Some(&ext_owner)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 1,
"the pool owner's bound request MUST trigger the pull exactly once, got {}",
hits.load(std::sync::atomic::Ordering::SeqCst)
);
shutdown([], [&c3]).await?;
shutdown([server_task], [&server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_through_authorizes_the_delegate_not_the_funder() -> anyhow::Result<()> {
let hits = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let cache_tmp = tempfile::tempdir()?;
let cache = CacheEngine::open(
cache_tmp.path(),
vec![Arc::new(CountingOrigin {
hits: Arc::clone(&hits),
}) as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let (store, funder, delegate) = delegate_signer_store()?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_full_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
&loopback_domains(),
16,
|deps| deps.pull_through = Some(std::time::Duration::from_secs(10)),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let req = StreamRequest {
hash: [0xEDu8; 32], namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00de_1e04,
};
let funder_sk = fresh_key();
let funder_node_id = B256::from(*funder_sk.public().as_bytes());
let (c1, _) = local_endpoint(funder_sk, vec![]).await?;
let ext_funder = binding_ext(&funder, funder_node_id)?;
let _ = raw_request(&c1, target.clone(), &req, Some(&ext_funder)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 0,
"the funder behind a delegated lane must NOT trigger a paid pull"
);
shutdown([], [&c1]).await?;
let delegate_sk = fresh_key();
let delegate_node_id = B256::from(*delegate_sk.public().as_bytes());
let (c2, _) = local_endpoint(delegate_sk, vec![]).await?;
let ext_delegate = binding_ext(&delegate, delegate_node_id)?;
let _ = raw_request(&c2, target.clone(), &req, Some(&ext_delegate)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 1,
"the pinned voucher signer MUST trigger the pull exactly once, got {}",
hits.load(std::sync::atomic::Ordering::SeqCst)
);
shutdown([], [&c2]).await?;
shutdown([server_task], [&server_ep]).await?;
Ok(())
}
async fn spawn_counting_pull_server_with_deny(
store: Arc<dyn PoolStateStore>,
denied: &[alloy::primitives::Address],
remaining: U256,
owner: Address,
) -> anyhow::Result<(
EndpointAddr,
Arc<std::sync::atomic::AtomicUsize>,
Endpoint,
tokio::task::JoinHandle<()>,
tempfile::TempDir,
Arc<Metrics>,
)> {
let hits = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let cache_tmp = tempfile::tempdir()?;
let cache = CacheEngine::open(
cache_tmp.path(),
vec![Arc::new(CountingOrigin {
hits: Arc::clone(&hits),
}) as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let deny = Arc::new(decdn_node::content_deny::ContentDenylist::new(
&content_with_origins(denied),
));
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_full_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
&loopback_domains(),
16,
|deps| {
deps.pull_through = Some(std::time::Duration::from_secs(10));
deps.content_deny = deny;
deps.pool_view = Some(Arc::new(FixedRemainingPoolView { owner, remaining }));
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, hits, server_ep, server_task, cache_tmp, metrics))
}
async fn spawn_counting_pull_server(
store: Arc<dyn PoolStateStore>,
remaining: U256,
owner: Address,
) -> anyhow::Result<(
EndpointAddr,
Arc<std::sync::atomic::AtomicUsize>,
Endpoint,
tokio::task::JoinHandle<()>,
tempfile::TempDir,
Arc<Metrics>,
)> {
spawn_counting_pull_server_with_deny(store, &[], remaining, owner).await
}
#[tokio::test(flavor = "multi_thread")]
async fn underfunded_pool_never_reaches_the_paid_pull() -> anyhow::Result<()> {
let window_cost = min_payment(HARNESS_FLOOR_BYTES, RATE_PER_MB);
let deposit = window_cost.saturating_sub(U256::from(1u64));
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
deposit,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, hits, server_ep, server_task, _cache_tmp, metrics) =
spawn_counting_pull_server(store_dyn, deposit, signer.address()).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: [0x19u8; 32], namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x1519,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), resp_ext) => {
anyhow::ensure!(!resp.body.ok, "an underfunded miss must be refused");
anyhow::ensure!(
matches!(
resp_ext.error,
Some(decdn_protocol::client::StreamError::Unfunded)
),
"expected the Unfunded wire code, got {:?}",
resp_ext.error
);
}
other => anyhow::bail!("expected a signed refusal, got {other:?}"),
}
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 0,
"the origin must never be contacted for a pool that cannot pay: {} hits",
hits.load(std::sync::atomic::Ordering::SeqCst)
);
let text = metrics.encode()?;
anyhow::ensure!(
metric_line_present(
&text,
"decdn_serve_stream_rejected_insufficient_deposit_total 1"
),
"the refusal must be attributed to the deposit floor"
);
anyhow::ensure!(
metric_line_present(&text, "decdn_serve_stream_rejected_cache_miss_total 0"),
"the refusal must come from the floor, not from the fill missing"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn funded_pool_still_reaches_the_paid_pull() -> anyhow::Result<()> {
let window_cost = min_payment(HARNESS_FLOOR_BYTES, RATE_PER_MB);
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
signer.address(),
operator_addr(),
window_cost,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, hits, server_ep, server_task, _cache_tmp, metrics) =
spawn_counting_pull_server(store_dyn, window_cost, signer.address()).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&signer, client_node_id)?;
let req = StreamRequest {
hash: [0x1Au8; 32],
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x151A,
};
match raw_request(&client_ep, target, &req, Some(&ext)).await? {
(ClientMessage::StreamResponse(resp), _resp_ext) => anyhow::ensure!(
!resp.body.ok,
"CountingOrigin holds nothing, so this must still end in a refusal"
),
other => anyhow::bail!("expected a signed StreamResponse, got {other:?}"),
}
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 1,
"a pool funded to exactly the floor must still reach the pull: {} hits",
hits.load(std::sync::atomic::Ordering::SeqCst)
);
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 0"
),
"an exactly-funded pool must not trip the deposit floor"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn spent_out_pool_never_reaches_the_paid_pull() -> anyhow::Result<()> {
let client = PrivateKeySigner::random();
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&LaneState::hydrate(
pool_id(),
client.address(),
operator_addr(),
U256::from(10_000_000u64),
0,
U256::from(9_999_995u64),
U256::from(1_048_575_488u64),
Some([0x33; 65]),
decdn_incentive::LaneChain::NONE,
))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let (target, hits, server_ep, server_task, _cache_tmp, metrics) =
spawn_counting_pull_server(store_dyn, U256::from(5u64), client.address()).await?;
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let ext = binding_ext(&Arc::new(client), client_node_id)?;
let req = StreamRequest {
hash: [0x33u8; 32],
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x3301,
};
let _ = raw_request(&client_ep, target, &req, Some(&ext)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 0,
"a spent-out pool must not reach the origin despite a large gross deposit: {} hits",
hits.load(std::sync::atomic::Ordering::SeqCst)
);
anyhow::ensure!(
metric_line_present(
&metrics.encode()?,
"decdn_serve_stream_rejected_insufficient_deposit_total 1"
),
"the refusal must be attributed to the deposit floor, not to the fill missing"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_through_refuses_a_blacklisted_funder_behind_a_clean_delegate() -> anyhow::Result<()> {
let (store, funder, delegate) = delegate_signer_store()?;
let (target, hits, server_ep, server_task, _cache_tmp, _metrics) =
spawn_counting_pull_server_with_deny(
store,
&[funder.address()],
U256::MAX,
funder.address(),
)
.await?;
let delegate_sk = fresh_key();
let delegate_node_id = B256::from(*delegate_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(delegate_sk, vec![]).await?;
let ext = binding_ext(&delegate, delegate_node_id)?;
let req = StreamRequest {
hash: [0xB1u8; 32], namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00b1_ac11,
};
let _ = raw_request(&client_ep, target, &req, Some(&ext)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 0,
"a blacklisted funder must NOT trigger a paid pull, clean delegate or not"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_through_allows_a_clean_funder_with_a_blacklisted_delegate() -> anyhow::Result<()> {
let (store, funder, delegate) = delegate_signer_store()?;
let (target, hits, server_ep, server_task, _cache_tmp, _metrics) =
spawn_counting_pull_server_with_deny(
store,
&[delegate.address()],
U256::MAX,
funder.address(),
)
.await?;
let delegate_sk = fresh_key();
let delegate_node_id = B256::from(*delegate_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(delegate_sk, vec![]).await?;
let ext = binding_ext(&delegate, delegate_node_id)?;
let req = StreamRequest {
hash: [0xB2u8; 32], namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x00b2_ac11,
};
let _ = raw_request(&client_ep, target, &req, Some(&ext)).await?;
anyhow::ensure!(
hits.load(std::sync::atomic::Ordering::SeqCst) == 1,
"a clean funder's delegated request MUST trigger the pull exactly once, got {}",
hits.load(std::sync::atomic::Ordering::SeqCst)
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[derive(Debug)]
struct SlowOrigin {
payload: Vec<u8>,
delay: Duration,
}
impl decdn_cache::Origin for SlowOrigin {
fn fetch(
&self,
_hash: decdn_cache::Hash,
_max_bytes: u64,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<decdn_cache::origin::OriginFetch, decdn_cache::OriginPullError>,
> + Send
+ '_,
>,
> {
let payload = self.payload.clone();
let delay = self.delay;
Box::pin(async move {
tokio::time::sleep(delay).await;
Ok(decdn_cache::origin::OriginFetch::found_one_shot(
payload.into(),
))
})
}
fn kind(&self) -> decdn_cache::OriginKind {
decdn_cache::OriginKind::Peer
}
}
async fn pull_through_fills_under_deadline(
outer_deadline: Duration,
origin_delay: Duration,
) -> anyhow::Result<bool> {
let payload = vec![0x5Au8; 4096];
let want = decdn_cache::Hash::new(&payload);
let cache_tmp = tempfile::tempdir()?;
let cache = CacheEngine::open(
cache_tmp.path(),
vec![Arc::new(SlowOrigin {
payload: payload.clone(),
delay: origin_delay,
}) as Arc<dyn decdn_cache::Origin>],
16,
)
.await?;
let cache_probe = cache.clone();
let (store, owner, _deposit) = seeded_store()?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_full_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
&loopback_domains(),
16,
|deps| deps.pull_through = Some(outer_deadline),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let req = StreamRequest {
hash: *want.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0091_1001,
};
let owner_sk = fresh_key();
let owner_node_id = B256::from(*owner_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(owner_sk, vec![]).await?;
let ext_owner = StreamRequestExt {
binding: Some(ClientBinding {
ethereum_address: owner.address().into(),
binding_signature: sign_binding_for(&owner, owner_node_id)?,
}),
..Default::default()
};
let _ = raw_request(&client_ep, target, &req, Some(&ext_owner)).await?;
shutdown([], [&client_ep]).await?;
let filled = cache_probe.has(want).await?;
shutdown([server_task], [&server_ep]).await?;
Ok(filled)
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_through_outer_deadline_accommodates_a_slow_pull() -> anyhow::Result<()> {
let per = Duration::from_secs(1);
let stall = Duration::from_secs(1);
let slow = Duration::from_millis(1500);
anyhow::ensure!(
!pull_through_fills_under_deadline(per, slow).await?,
"an outer deadline equal to the per-candidate budget must abandon the slow pull (the #859 bug)"
);
anyhow::ensure!(
pull_through_fills_under_deadline(
decdn_node::selection::outer_pull_deadline(per, stall),
slow
)
.await?,
"the derived outer deadline must let a pull exceeding one per-candidate budget complete"
);
Ok(())
}
async fn empty_cache_with_fs_origin(
payload: &[u8],
) -> anyhow::Result<(
CacheEngine,
decdn_cache::Hash,
Arc<CacheMetrics>,
tempfile::TempDir,
tempfile::TempDir,
)> {
let hash = decdn_cache::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(decdn_cache::FilesystemOrigin::new(origin_dir.path()).await?);
let cache_metrics = Arc::new(CacheMetrics::default());
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::clone(&cache_metrics)),
Duration::ZERO,
)
.await?;
anyhow::ensure!(!cache.has(hash).await?, "cache store must start empty");
Ok((cache, hash, cache_metrics, origin_dir, cache_dir))
}
async fn spawn_pull_through_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
) -> anyhow::Result<(
EndpointAddr,
Address,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| deps.pull_through = Some(Duration::from_secs(20)),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((
target,
server_eth.address(),
server_ep,
server_task,
metrics,
))
}
async fn spawn_origin_only_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
) -> anyhow::Result<(EndpointAddr, Address, Endpoint, tokio::task::JoinHandle<()>)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| {
deps.pull_through = Some(Duration::from_secs(20));
deps.relay_foreign_namespaces = false;
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_eth.address(), server_ep, server_task))
}
async fn spawn_origin_only_server_with_metrics(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
) -> anyhow::Result<(
EndpointAddr,
Address,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| {
deps.pull_through = Some(Duration::from_secs(20));
deps.relay_foreign_namespaces = false;
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((
target,
server_eth.address(),
server_ep,
server_task,
metrics,
))
}
#[tokio::test(flavor = "multi_thread")]
async fn origin_only_node_fault_is_internal_error_not_signed_not_found() -> anyhow::Result<()> {
let hash = decdn_cache::Hash::new(b"origin-only fault probe");
let cache_dir = tempfile::tempdir()?;
let unreachable = decdn_cache::parse_origin_url("http://127.0.0.1:1/")?;
let origin = Arc::new(decdn_cache::HttpOrigin::new(unreachable)?);
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::new(CacheMetrics::default())),
Duration::ZERO,
)
.await?;
anyhow::ensure!(!cache.has(hash).await?, "cache store must start empty");
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_origin_only_server_with_metrics(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("a faulting backend probe must not deliver"),
Err(e) => {
let msg = e.to_string();
anyhow::ensure!(
msg.contains("Declined"),
"a faulting backend probe must surface as Declined (InternalError), got: {e}"
);
anyhow::ensure!(
!msg.contains("NotFound"),
"a faulting backend probe must NOT sign an authoritative NotFound, got: {e}"
);
}
}
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(
&encoded,
"decdn_serve_stream_rejected_internal_error_total 1"
),
"expected the fault to land in the internal-error counter, not the \
foreign-decline one; counters were:\n{}",
encoded
.lines()
.filter(|l| l.starts_with("decdn_serve_stream_rejected"))
.collect::<Vec<_>>()
.join("\n")
);
anyhow::ensure!(
metric_line_present(
&encoded,
"decdn_serve_stream_rejected_foreign_declined_total 0"
),
"a backend fault must not be counted as a foreign decline"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn cache_with_foreign_blob_imported(
payload: &[u8],
) -> anyhow::Result<(
CacheEngine,
decdn_cache::Hash,
Arc<CacheMetrics>,
tempfile::TempDir,
)> {
let hash = decdn_cache::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(decdn_cache::FilesystemOrigin::new(origin_dir.path()).await?);
let cache_metrics = Arc::new(CacheMetrics::default());
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::clone(&cache_metrics)),
Duration::ZERO,
)
.await?;
let _ = cache.get(hash).await?; anyhow::ensure!(
cache.has(hash).await?,
"precondition: the blob must be in the local store"
);
drop(origin_dir);
Ok((cache, hash, cache_metrics, cache_dir))
}
#[tokio::test(flavor = "multi_thread")]
async fn origin_only_node_declines_a_foreign_hash() -> anyhow::Result<()> {
let payload = vec![0x11u8; 64 * 1024];
let (cache, _own_hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let foreign_hash = decdn_cache::Hash::from([0x9Au8; 32]);
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task) =
spawn_origin_only_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let res = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*foreign_hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await;
anyhow::ensure!(res.is_err(), "origin-only node must decline a foreign hash");
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 0,
"foreign decline must not touch the origin, got {}",
cache_metrics.origin_fetches.get()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn origin_only_node_serves_its_own_backend_hash() -> anyhow::Result<()> {
let payload = vec![0x22u8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task) =
spawn_origin_only_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch for own-backend hash"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 1,
"expected exactly 1 origin fetch to fill the own-backend hash, got {}",
cache_metrics.origin_fetches.get()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn origin_only_node_declines_a_foreign_blob_already_in_cache() -> anyhow::Result<()> {
let payload = vec![0x33u8; 32 * 1024];
let (cache, foreign_hash, cache_metrics, _cache_tmp) =
cache_with_foreign_blob_imported(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task) =
spawn_origin_only_server(cache, Arc::clone(&store)).await?;
let baseline_fetches = cache_metrics.origin_fetches.get();
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit);
let res = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*foreign_hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await;
anyhow::ensure!(
res.is_err(),
"origin-only node must decline a foreign hash even when it is already cached"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == baseline_fetches,
"the cache-hit gate must not touch the origin, got {} beyond baseline {}",
cache_metrics.origin_fetches.get(),
baseline_fetches
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn bound_client_fetch_triggers_reactive_origin_pull_through() -> anyhow::Result<()> {
let payload = vec![0x7Bu8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_pull_through_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 1,
"expected exactly 1 origin fetch, got {}",
cache_metrics.origin_fetches.get()
);
assert_first_byte_class(&metrics, "miss")?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn own_namespace_miss_ignores_serve_economics_gate() -> anyhow::Result<()> {
let payload = vec![0x9Eu8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let warming = Arc::new(decdn_node::warming_allowance::WarmingAllowance::new(
1000, 0,
));
warming.debit_speculative(
decdn_node::warming_allowance::SourceId::from_bytes([0xAAu8; 32]),
decdn_cache::Hash::from_bytes([0xBBu8; 32]),
u64::MAX,
);
anyhow::ensure!(
!warming.available(decdn_node::warming_allowance::SourceId::from_bytes(
[0xAAu8; 32]
)),
"precondition: the source must read as fully spent"
);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store.clone(),
RATE_PER_MB,
16,
|deps| {
deps.pull_through = Some(Duration::from_secs(20));
deps.warming_credit = Arc::new(
decdn_node::warming_allowance::DirectWarmingCreditSink::new(Arc::clone(&warming)),
);
deps.operator_shares = decdn_node::fee_shares::OperatorShares::new(0);
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 1,
"expected exactly 1 origin fetch (own-namespace path taken), got {}",
cache_metrics.origin_fetches.get()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_completed_serve_credits_the_source_through_the_background_aggregator()
-> anyhow::Result<()> {
const SOURCE: decdn_node::warming_allowance::SourceId =
decdn_node::warming_allowance::SourceId::from_bytes([0xA1u8; 32]);
const OP_BPS: u16 = 6000;
const BUDGET: u64 = 1_000_000;
let payload = vec![0x5Cu8; 64 * 1024];
let (cache, hash, _cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let warming = Arc::new(decdn_node::warming_allowance::WarmingAllowance::new(
BUDGET, 0,
));
warming.debit_speculative(SOURCE, hash, BUDGET);
anyhow::ensure!(
!warming.available(SOURCE),
"precondition: the speculative buy must leave the source spent"
);
let (warming_credit, creditor) = decdn_node::warming_allowance::spawn_warming_creditor(
Arc::clone(&warming),
Arc::clone(&metrics),
);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store.clone(),
RATE_PER_MB,
16,
|deps| {
deps.pull_through = Some(Duration::from_secs(20));
deps.warming_credit = warming_credit;
deps.operator_shares = decdn_node::fee_shares::OperatorShares::new(OP_BPS);
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_dec0,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
let mut credited = false;
for _ in 0..500 {
if warming.available(SOURCE) {
credited = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
anyhow::ensure!(
credited,
"the serve credit never reached the ledger through the channel sink"
);
anyhow::ensure!(
creditor.shutdown().await.is_ok(),
"the aggregator must exit cleanly"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn unbound_client_fetch_is_refused_on_origin_only_blob() -> anyhow::Result<()> {
let payload = vec![0x7Bu8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, _metrics) =
spawn_pull_through_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = unbound_context(Arc::clone(&signer), deposit);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("unbound fetch of an origin-only blob must be refused"),
Err(e) => anyhow::ensure!(
e.to_string().contains("delivery refused"),
"expected a delivery-refused error, got: {e}"
),
}
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 0,
"unbound request must not trigger an origin fetch, got {}",
cache_metrics.origin_fetches.get()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn origin_held_serve_miss_signs_a_refusal_rather_than_dropping() -> anyhow::Result<()> {
let payload = vec![0x3Cu8; 32 * 1024];
let (cache, hash, _cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
cache.rescan_origins().await;
anyhow::ensure!(
cache.origin_held_size(hash) == Some(payload.len() as u64),
"precondition: the hash must be indexed as origin-held"
);
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, _metrics) =
spawn_pull_through_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = unbound_context(Arc::clone(&signer), deposit);
let Err(err) = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
else {
anyhow::bail!("an unbound fetch of origin-held content must be refused")
};
let refused = err
.downcast_ref::<decdn_client::UpstreamRefused>()
.ok_or_else(|| anyhow::anyhow!("expected a typed UpstreamRefused, got: {err:#}"))?;
let evidence = refused.evidence().ok_or_else(|| {
anyhow::anyhow!("the refusal must carry the operator's signed StreamResponse")
})?;
anyhow::ensure!(
!evidence.body.ok,
"an origin-held serve miss must sign ok:false, got ok:{}",
evidence.body.ok
);
anyhow::ensure!(
evidence.body.hash == *hash.as_bytes(),
"the signed refusal must name the requested hash"
);
anyhow::ensure!(
!evidence.slash_sig.is_empty(),
"the refusal must be signed — an unsigned one is unattributable"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn spawn_local_populate_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
) -> anyhow::Result<(
EndpointAddr,
Address,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| deps.local_populate = Some(Duration::from_secs(20)),
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((
target,
server_eth.address(),
server_ep,
server_task,
metrics,
))
}
async fn spawn_local_and_window_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
) -> anyhow::Result<(EndpointAddr, Address, Endpoint, tokio::task::JoinHandle<()>)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| {
deps.local_populate = Some(Duration::from_secs(20));
deps.pull_through_origin = Some(Arc::new(decdn_node::node_origin::NodeOrigin::new()));
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((target, server_eth.address(), server_ep, server_task))
}
#[tokio::test(flavor = "multi_thread")]
async fn local_populate_serves_own_origin_with_node_to_node_off() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_local_populate_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 1,
"expected exactly 1 local origin fetch, got {}",
cache_metrics.origin_fetches.get()
);
assert_first_byte_class(&metrics, "miss")?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn unbound_local_populate_is_refused() -> anyhow::Result<()> {
let payload = vec![0x5Au8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, _metrics) =
spawn_local_populate_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = unbound_context(Arc::clone(&signer), deposit);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("unbound local-populate fetch must be refused"),
Err(e) => anyhow::ensure!(
e.to_string().contains("delivery refused"),
"expected a delivery-refused error, got: {e}"
),
}
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 0,
"unbound request must not trigger a local origin fetch, got {}",
cache_metrics.origin_fetches.get()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn local_origin_preferred_over_peer_window_path() -> anyhow::Result<()> {
let payload = vec![0x3Cu8; 64 * 1024];
let (cache, hash, cache_metrics, _origin_tmp, _cache_tmp) =
empty_cache_with_fs_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task) =
spawn_local_and_window_server(cache, Arc::clone(&store)).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
let got = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
got.as_ref() == payload.as_slice(),
"delivered bytes mismatch"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 1,
"the local origin must be preferred (served) over the peer window path, got {} fetches",
cache_metrics.origin_fetches.get()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn local_populate_miss_is_clean_cache_miss() -> anyhow::Result<()> {
let spans = support::capture_spans();
let payload = vec![0x2Bu8; 64 * 1024];
let hash = decdn_cache::Hash::new(&payload);
let origin_dir = tempfile::tempdir()?;
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(decdn_cache::FilesystemOrigin::new(origin_dir.path()).await?);
let cache_metrics = Arc::new(CacheMetrics::default());
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::clone(&cache_metrics)),
Duration::ZERO,
)
.await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_fault_server(cache, Arc::clone(&store), FaultTiers::LocalOnly).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("a blob absent from the local origin must be refused"),
Err(e) => anyhow::ensure!(
e.to_string().contains("delivery refused") && e.to_string().contains("NotFound"),
"expected a signed NotFound for a genuine absence, got: {e}"
),
}
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 1,
"local origin should be consulted once on the miss, got {}",
cache_metrics.origin_fetches.get()
);
assert_reject_reason(&metrics, 0, 1)?;
assert_one_miss_line(&spans, hash, "no_pull_through", "cache_miss")?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
let key = hash.to_string();
let field = |span: &support::CapturedSpan, name: &str| span.fields.get(name).cloned();
let served = spans.matching("serve_stream", "hash", &key);
let [serve] = served.as_slice() else {
anyhow::bail!("expected one serve_stream span for the hash, got {served:?}");
};
anyhow::ensure!(
field(serve, "outcome").as_deref() == Some("refused")
&& field(serve, "reason").as_deref() == Some("cache_miss"),
"serve_stream: {serve:?}"
);
let fills = spans.matching("pull_through", "hash", &key);
anyhow::ensure!(
fills
.iter()
.any(|s| field(s, "tier").as_deref() == Some("local")
&& field(s, "outcome").as_deref() == Some("clean_miss")
&& s.parent == Some("serve_stream")),
"pull_through: {fills:?}"
);
let walks = spans.matching("origin_pull", "hash", &key);
anyhow::ensure!(
walks
.iter()
.any(|s| field(s, "outcome").as_deref() == Some("not_found")
&& field(s, "otel.status_code").is_none()
&& s.parent == Some("pull_through")),
"origin_pull: {walks:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn unbound_miss_refuses_on_a_throttled_no_lane_line() -> anyhow::Result<()> {
let spans = support::capture_spans();
let payload = vec![0x5Au8; 64 * 1024];
let hash = decdn_cache::Hash::new(&payload);
let origin_dir = tempfile::tempdir()?;
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(decdn_cache::FilesystemOrigin::new(origin_dir.path()).await?);
let cache_metrics = Arc::new(CacheMetrics::default());
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::clone(&cache_metrics)),
Duration::ZERO,
)
.await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_fault_server(cache, Arc::clone(&store), FaultTiers::LocalOnly).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = unbound_context(Arc::clone(&signer), deposit);
for _ in 0..2 {
match stream_fetch(
&client_ep,
target.clone(),
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("an unbound miss must be refused"),
Err(e) => anyhow::ensure!(
e.to_string().contains("NotFound"),
"expected a signed NotFound, got: {e}"
),
}
}
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 0,
"an unbound request must not reach the local origin, got {}",
cache_metrics.origin_fetches.get()
);
assert_reject_reason(&metrics, 0, 2)?;
let line = assert_one_miss_line(&spans, hash, "no_lane", "cache_miss")?;
anyhow::ensure!(
line.get("suppressed").map(String::as_str) == Some("0"),
"the first line of a window suppresses nothing: {line:?}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum OriginHolds {
Nothing,
Blob,
BlobAndOutboard,
}
async fn lane_registered_mid_request_refuses_on_no_lane(
tiers: FaultTiers,
holds: OriginHolds,
) -> anyhow::Result<()> {
let spans = support::capture_spans();
let payload = vec![0x6Du8; 64 * 1024];
let hash = decdn_cache::Hash::new(&payload);
let origin_dir = tempfile::tempdir()?;
if holds != OriginHolds::Nothing {
let encoded = decdn_bao_range::encode_outboard(payload.as_slice(), payload.len() as u64)?;
anyhow::ensure!(
encoded.hash_hex == hash.to_hex().as_str(),
"encoder hash mismatch"
);
let shard = origin_dir.path().join(encoded.shard());
std::fs::create_dir_all(&shard)?;
std::fs::write(shard.join(&encoded.hash_hex), &payload)?;
if holds == OriginHolds::BlobAndOutboard {
std::fs::write(shard.join(encoded.obao4_name()), &encoded.outboard)?;
}
}
let cache_dir = tempfile::tempdir()?;
let origin = Arc::new(decdn_cache::FilesystemOrigin::new(origin_dir.path()).await?);
let cache_metrics = Arc::new(CacheMetrics::default());
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::clone(&cache_metrics)),
Duration::ZERO,
)
.await?;
let store: Arc<dyn PoolStateStore> = Arc::new(MemoryPoolStateStore::new());
let signer = Arc::new(PrivateKeySigner::random());
let deposit = U256::from(10_000_000u64);
let (target, server_eth_addr, server_ep, server_task, metrics, handler) =
spawn_fault_server_with_handler(cache, store, tiers).await?;
handler.register_lane_after_next_resolution(fresh_lane(signer.address(), deposit));
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
let slash = slash_domain();
let fetch = || {
stream_fetch(
&client_ep,
target.clone(),
&ctx,
&slash,
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
};
match fetch().await {
Ok(_) => anyhow::bail!("a request whose lane was unknown at resolution must be refused"),
Err(e) => anyhow::ensure!(
e.to_string().contains("delivery refused") && e.to_string().contains("NotFound"),
"expected a signed NotFound, got: {e}"
),
}
anyhow::ensure!(
metric_line_present(&metrics.encode()?, "decdn_lanes_open 1"),
"precondition: the armed lane must be registered mid-request"
);
anyhow::ensure!(
cache_metrics.origin_fetches.get() == 0,
"no fill tier may run for the request, got {} origin fetches",
cache_metrics.origin_fetches.get()
);
assert_reject_reason(&metrics, 0, 1)?;
assert_one_miss_line(&spans, hash, "no_lane", "cache_miss")?;
if holds != OriginHolds::Nothing {
let served = fetch().await?;
anyhow::ensure!(
served == payload,
"the next request must be served the blob"
);
}
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn lane_registered_mid_request_spends_no_origin_egress() -> anyhow::Result<()> {
lane_registered_mid_request_refuses_on_no_lane(FaultTiers::LocalOnly, OriginHolds::Blob).await
}
#[tokio::test(flavor = "multi_thread")]
async fn lane_registered_mid_request_skips_the_own_origin_spine() -> anyhow::Result<()> {
lane_registered_mid_request_refuses_on_no_lane(
FaultTiers::LocalOnly,
OriginHolds::BlobAndOutboard,
)
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn lane_registered_mid_request_skips_the_buffered_pull() -> anyhow::Result<()> {
lane_registered_mid_request_refuses_on_no_lane(FaultTiers::Buffered, OriginHolds::Blob).await
}
#[tokio::test(flavor = "multi_thread")]
async fn lane_registered_mid_request_is_not_an_internal_error() -> anyhow::Result<()> {
lane_registered_mid_request_refuses_on_no_lane(FaultTiers::LocalAndWindow, OriginHolds::Nothing)
.await
}
#[derive(Clone, Copy)]
enum FaultTiers {
LocalOnly,
LocalAndWindow,
Buffered,
}
async fn spawn_fault_server(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
tiers: FaultTiers,
) -> anyhow::Result<(
EndpointAddr,
Address,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
)> {
let (target, server_eth_addr, server_ep, server_task, metrics, _handler) =
spawn_fault_server_with_handler(cache, store, tiers).await?;
Ok((target, server_eth_addr, server_ep, server_task, metrics))
}
async fn spawn_fault_server_with_handler(
cache: CacheEngine,
store: Arc<dyn PoolStateStore>,
tiers: FaultTiers,
) -> anyhow::Result<(
EndpointAddr,
Address,
Endpoint,
tokio::task::JoinHandle<()>,
Arc<Metrics>,
Arc<ClientHandler>,
)> {
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store,
RATE_PER_MB,
16,
|deps| match tiers {
FaultTiers::LocalOnly => deps.local_populate = Some(Duration::from_secs(20)),
FaultTiers::LocalAndWindow => {
deps.local_populate = Some(Duration::from_secs(20));
deps.pull_through_origin =
Some(Arc::new(decdn_node::node_origin::NodeOrigin::new()));
}
FaultTiers::Buffered => deps.pull_through = Some(Duration::from_secs(20)),
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), Arc::clone(&handler));
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
Ok((
target,
server_eth.address(),
server_ep,
server_task,
metrics,
handler,
))
}
fn assert_reject_reason(
metrics: &Arc<Metrics>,
internal_error: u64,
cache_miss: u64,
) -> anyhow::Result<()> {
let encoded = metrics.encode()?;
for (name, want) in [
(
"decdn_serve_stream_rejected_internal_error_total",
internal_error,
),
("decdn_serve_stream_rejected_cache_miss_total", cache_miss),
] {
let line = format!("{name} {want}");
anyhow::ensure!(
metric_line_present(&encoded, &line),
"expected metric line `{line}`; counters were:\n{}",
encoded
.lines()
.filter(|l| l.starts_with("decdn_serve_stream_rejected"))
.collect::<Vec<_>>()
.join("\n")
);
}
Ok(())
}
fn assert_one_miss_line(
spans: &support::SpanCapture,
hash: decdn_cache::Hash,
path: &str,
reason: &str,
) -> anyhow::Result<std::collections::HashMap<String, String>> {
let lines = spans.events(
"serve-miss: refusing the request",
"hash",
&hash.to_string(),
);
let [line] = lines.as_slice() else {
anyhow::bail!("expected one serve-miss refusal line for the hash, got {lines:?}");
};
anyhow::ensure!(
line.get("path").map(String::as_str) == Some(path)
&& line.get("reason").map(String::as_str) == Some(reason),
"expected path={path} reason={reason}: {line:?}"
);
Ok(line.clone())
}
#[derive(Debug)]
struct FailingOrigin {
hits: Arc<std::sync::atomic::AtomicUsize>,
}
impl decdn_cache::Origin for FailingOrigin {
fn fetch(
&self,
_hash: decdn_cache::Hash,
_max_bytes: u64,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<decdn_cache::origin::OriginFetch, decdn_cache::OriginPullError>,
> + Send
+ '_,
>,
> {
self.hits.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Box::pin(async {
Err(decdn_cache::OriginPullError::Transient(anyhow::anyhow!(
"synthetic origin outage (HTTP 503)"
)))
})
}
fn kind(&self) -> decdn_cache::OriginKind {
decdn_cache::OriginKind::Http
}
}
async fn empty_cache_with_failing_origin(
payload: &[u8],
) -> anyhow::Result<(
CacheEngine,
decdn_cache::Hash,
Arc<std::sync::atomic::AtomicUsize>,
tempfile::TempDir,
)> {
let hash = decdn_cache::Hash::new(payload);
let cache_dir = tempfile::tempdir()?;
let hits = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let origin = Arc::new(FailingOrigin {
hits: Arc::clone(&hits),
});
let cache = CacheEngine::open_full(
cache_dir.path(),
vec![origin as Arc<dyn decdn_cache::Origin>],
16,
PinnedHashes::empty(),
RetryPolicy::default(),
CircuitBreakerPolicy::default(),
Some(Arc::new(CacheMetrics::default())),
Duration::ZERO,
)
.await?;
anyhow::ensure!(!cache.has(hash).await?, "cache store must start empty");
Ok((cache, hash, hits, cache_dir))
}
#[tokio::test(flavor = "multi_thread")]
async fn local_origin_hard_fault_is_internal_error_not_signed_not_found() -> anyhow::Result<()> {
let payload = vec![0x7Eu8; 64 * 1024];
let (cache, hash, origin_hits, _cache_tmp) = empty_cache_with_failing_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_fault_server(cache, Arc::clone(&store), FaultTiers::LocalOnly).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("a hard origin fault must not deliver"),
Err(e) => {
let msg = e.to_string();
anyhow::ensure!(
msg.contains("Declined"),
"a hard origin fault must surface as Declined (InternalError), got: {e}"
);
anyhow::ensure!(
!msg.contains("NotFound"),
"a hard origin fault must NOT sign an authoritative NotFound, got: {e}"
);
}
}
anyhow::ensure!(
origin_hits.load(std::sync::atomic::Ordering::SeqCst) > 0,
"the failing origin should have been consulted (proving the fault came from \
the reactive fill, not a short-circuit before it)"
);
assert_reject_reason(&metrics, 1, 0)?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn local_hard_fault_survives_fallthrough_to_the_window_tier() -> anyhow::Result<()> {
let spans = support::capture_spans();
let payload = vec![0x3Du8; 64 * 1024];
let (cache, hash, origin_hits, _cache_tmp) = empty_cache_with_failing_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_fault_server(cache, Arc::clone(&store), FaultTiers::LocalAndWindow).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("a hard origin fault must not deliver"),
Err(e) => {
let msg = e.to_string();
anyhow::ensure!(
msg.contains("Declined"),
"a local hard fault must still surface as InternalError after the \
node→node tier cleanly misses, got: {e}"
);
anyhow::ensure!(
!msg.contains("NotFound"),
"the window tier's clean miss must not overwrite the local fault with \
a signed NotFound, got: {e}"
);
}
}
anyhow::ensure!(
origin_hits.load(std::sync::atomic::Ordering::SeqCst) > 0,
"the failing local origin should have been consulted before the window tier"
);
assert_reject_reason(&metrics, 1, 0)?;
let line = assert_one_miss_line(&spans, hash, "pull_leg_miss", "internal_error")?;
anyhow::ensure!(
line.get("cause").map(String::as_str) == Some("clean_miss"),
"pull-leg miss cause: {line:?}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn buffered_pull_through_hard_fault_is_internal_error() -> anyhow::Result<()> {
let spans = support::capture_spans();
let payload = vec![0x9Cu8; 64 * 1024];
let (cache, hash, origin_hits, _cache_tmp) = empty_cache_with_failing_origin(&payload).await?;
let (store, signer, deposit) = seeded_store()?;
let (target, server_eth_addr, server_ep, server_task, metrics) =
spawn_fault_server(cache, Arc::clone(&store), FaultTiers::Buffered).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let own_node_id = B256::from(*client_ep.id().as_bytes());
let binding = sign_client_binding(&signer, own_node_id, &binding_domain())?;
let ctx = pool_context(&client_ep, Arc::clone(&signer), deposit).with_client_binding(binding);
match stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth_addr,
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
{
Ok(_) => anyhow::bail!("a hard origin fault must not deliver"),
Err(e) => {
let msg = e.to_string();
anyhow::ensure!(
msg.contains("Declined"),
"a buffered-tier hard fault must surface as InternalError, got: {e}"
);
anyhow::ensure!(
!msg.contains("NotFound"),
"a buffered-tier hard fault must not report the node as merely empty, got: {e}"
);
}
}
anyhow::ensure!(
origin_hits.load(std::sync::atomic::Ordering::SeqCst) > 0,
"the failing origin should have been consulted by the buffered tier"
);
assert_reject_reason(&metrics, 1, 0)?;
assert_one_miss_line(&spans, hash, "buffered_miss", "internal_error")?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn cache_miss_refused_under_concurrency_pressure() -> anyhow::Result<()> {
let (cache, _cache_tmp) = empty_cache().await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&fresh_lane(signer.address(), U256::from(10_000_000u64)))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let shed = decdn_node::load_shed::LoadShedController::from_config(
&decdn_common::config::ResolvedLoadShed {
policy: decdn_common::config::LoadShedPolicyKind::ResourcePressure,
egress_budget_mbps: 0,
max_concurrent_serves_high: 1,
max_concurrent_serves_low: 0,
per_client_serve_cap: 0,
},
);
let held = shed
.try_admit(
decdn_node::load_shed::RequestClass::CacheHit,
B256::repeat_byte(0xAA),
)
.map_err(|reason| anyhow::anyhow!("pre-occupy the sole concurrency slot: {reason:?}"))?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|deps| {
deps.shed = Arc::clone(&shed);
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let miss_hash = Hash::new(b"load-shed test: never cached");
let (refusal, refusal_ext) =
open_expecting_refusal(&conn, *miss_hash.as_bytes(), Some(&ext)).await?;
anyhow::ensure!(
!refusal.body.ok,
"a cache-miss request must be shed while the node is at its concurrency ceiling"
);
anyhow::ensure!(
matches!(
refusal_ext.error,
Some(decdn_protocol::client::StreamError::NotFound)
),
"expected the collapsed NotFound wire code, got {:?}",
refusal_ext.error
);
drop(held);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn cache_hit_refused_under_egress_saturation() -> anyhow::Result<()> {
let payload = b"load-shed test: a blob the node already has cached".to_vec();
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&fresh_lane(signer.address(), U256::from(10_000_000u64)))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let shed = decdn_node::load_shed::LoadShedController::from_config(
&decdn_common::config::ResolvedLoadShed {
policy: decdn_common::config::LoadShedPolicyKind::ResourcePressure,
egress_budget_mbps: 1, max_concurrent_serves_high: 10_000,
max_concurrent_serves_low: 9_000,
per_client_serve_cap: 0,
},
);
shed.record_egress(1_000_000);
let sampled = shed.sample_egress(1);
anyhow::ensure!(
sampled >= 125_000,
"instant egress rate must be at/above the 1 Mbps ceiling, got {sampled}"
);
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
|deps| {
deps.shed = Arc::clone(&shed);
},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let client_sk = fresh_key();
let client_node_id = B256::from(*client_sk.public().as_bytes());
let (client_ep, _) = local_endpoint(client_sk, vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let ext = binding_ext(&signer, client_node_id)?;
let (refusal, refusal_ext) =
open_expecting_refusal(&conn, *hash.as_bytes(), Some(&ext)).await?;
anyhow::ensure!(
!refusal.body.ok,
"a cache-hit request must be shed while measured egress is at the configured ceiling"
);
anyhow::ensure!(
matches!(
refusal_ext.error,
Some(decdn_protocol::client::StreamError::NotFound)
),
"expected the collapsed NotFound wire code, got {:?}",
refusal_ext.error
);
let encoded = metrics.encode()?;
anyhow::ensure!(
metric_line_present(&encoded, "decdn_serve_cache_hit_total 1"),
"a shed-refused cache hit must still count as a hit:\n{encoded}"
);
anyhow::ensure!(
metric_line_present(
&encoded,
"decdn_serve_stream_rejected_load_shed_hit_total 1"
),
"the shed refusal keeps its own reason counter:\n{encoded}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn send_signed_voucher(
send: &mut SendStream,
signer: &PrivateKeySigner,
bytes_delivered: u64,
amount: U256,
) -> anyhow::Result<()> {
let voucher = Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount,
bytes_delivered: U256::from(bytes_delivered),
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
}
.sign(signer, &payment_domain())
.map_err(|e| anyhow::anyhow!("sign voucher: {e}"))?;
write_client_msg(
send,
&ClientMessage::Voucher(signed_to_wire_voucher(&voucher)?),
)
.await
}
async fn send_unrecoverable_voucher(
send: &mut SendStream,
signer: &PrivateKeySigner,
bytes_delivered: u64,
) -> anyhow::Result<()> {
let amount = min_payment(bytes_delivered, RATE_PER_MB);
let voucher = Voucher {
pool_id: pool_id(),
signer: signer.address(),
provider: operator_addr(),
amount,
bytes_delivered: U256::from(bytes_delivered),
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
}
.sign(signer, &payment_domain())
.map_err(|e| anyhow::anyhow!("sign voucher: {e}"))?;
let mut wire = signed_to_wire_voucher(&voucher)?;
wire.signature = vec![0u8; wire.signature.len()];
write_client_msg(send, &ClientMessage::Voucher(wire)).await
}
async fn read_voucher_reject(recv: &mut RecvStream) -> anyhow::Result<(VoucherRejectReason, bool)> {
loop {
let msg = tokio::time::timeout(Duration::from_secs(10), read_client_msg(recv))
.await
.map_err(|_| anyhow::anyhow!("node never answered with a rejection"))??;
match msg {
ClientMessage::ChunkData(_) => {}
ClientMessage::StreamError(StreamError::VoucherRejected { reason, bundle }) => {
return Ok((reason, bundle.is_some()));
}
other => anyhow::bail!("expected a VoucherRejected, got {other:?}"),
}
}
}
async fn assert_stream_reset(recv: &mut RecvStream, code: u32) -> anyhow::Result<()> {
let expected = VarInt::from_u32(code);
match recv.read_to_end(4096).await {
Err(ReadToEndError::Read(ReadError::Reset(got))) if got == expected => Ok(()),
Err(ReadToEndError::Read(ReadError::ConnectionLost(
ConnectionError::ApplicationClosed(ApplicationClose { error_code, .. }),
))) if error_code == expected => Ok(()),
Ok(bytes) => anyhow::bail!(
"expected a reset carrying {code:#x}, but the server sent {} bytes (it signed a \
response instead of shedding)",
bytes.len()
),
other => anyhow::bail!("expected a reset carrying {code:#x}, got {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn underpaying_closing_voucher_fails_the_stream_and_credits_nothing() -> anyhow::Result<()> {
let payload = vec![0x7Eu8; 768 * 1024];
let fx = idle_fixture(&payload, Duration::from_secs(30)).await?;
let blob = fx.blob(0)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let mut stalled = stall_delivery_at_closing_voucher(
&conn,
*blob.hash.as_bytes(),
blob.wire_bytes,
Some(&ext),
)
.await?;
let owed = min_payment(blob.wire_bytes, RATE_PER_MB);
anyhow::ensure!(
owed > U256::from(1u64),
"fixture must owe more than one base unit for the underpay to be unambiguous"
);
let reply = stalled
.send_cumulative_voucher(&fx.client_signer, blob.wire_bytes, U256::from(1u64))
.await?;
anyhow::ensure!(
matches!(
reply,
ClientMessage::StreamError(StreamError::VoucherRejected {
reason: VoucherRejectReason::Underpaid,
bundle: None,
})
),
"an underpaying voucher on a fresh lane must draw a bundle-less Underpaid, got {reply:?}"
);
let persisted = fx.store.load_all()?;
if let Some(lane) = persisted.first() {
anyhow::ensure!(
lane.last_bytes_delivered() == U256::ZERO,
"an underpaid stream must not advance the lane watermark, got {}",
lane.last_bytes_delivered()
);
anyhow::ensure!(
lane.last_amount() == U256::ZERO,
"an underpaid stream must credit no amount, got {}",
lane.last_amount()
);
}
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
struct MidStreamAfterAccept {
send: SendStream,
recv: RecvStream,
signer: Arc<PrivateKeySigner>,
store: Arc<dyn PoolStateStore>,
metrics: Arc<Metrics>,
conn: Connection,
client_ep: Endpoint,
server_ep: Endpoint,
server_task: tokio::task::JoinHandle<()>,
}
async fn drive_to_second_interval_awaiting_voucher() -> anyhow::Result<MidStreamAfterAccept> {
const CREDIT_MAX: u64 = 64 * 1024 * 1024;
let payload = vec![0x9Cu8; 16 * 1024 * 1024];
let (cache, hash, cache_tmp) = cache_with_blob(&payload).await?;
std::mem::forget(cache_tmp); let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_pipelined_server_with_metrics(cache, Arc::clone(&store), CREDIT_MAX, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (mut send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_FLOOR_BYTES).await?;
pay_cumulative(&mut send, &signer, HARNESS_INTERVAL_BYTES).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
Ok(MidStreamAfterAccept {
send,
recv,
signer,
store,
metrics,
conn,
client_ep,
server_ep,
server_task,
})
}
#[tokio::test(flavor = "multi_thread")]
async fn bad_signature_after_an_accepted_voucher_is_rejected() -> anyhow::Result<()> {
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
send_unrecoverable_voucher(&mut fx.send, &fx.signer, 2 * HARNESS_INTERVAL_BYTES).await?;
let (reason, has_bundle) = read_voucher_reject(&mut fx.recv).await?;
anyhow::ensure!(
reason == VoucherRejectReason::BadSignature,
"expected BadSignature, got {reason:?}"
);
anyhow::ensure!(
!has_bundle,
"a BadSignature reject carries no watermark bundle: the fix is a valid signature, not a resync"
);
let persisted = fx.store.load_all()?;
let lane = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane after an accepted voucher"))?;
anyhow::ensure!(
lane.last_bytes_delivered() == U256::from(HARNESS_INTERVAL_BYTES),
"the accepted watermark must survive the reject, got {}",
lane.last_bytes_delivered()
);
support::assert_inbound_failures_attributed(&fx.metrics, 1).await?;
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_voucher_rejected_total")? == 1,
"a rejected voucher must be counted"
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_zero_byte_voucher_after_paying_counts_as_rejected() -> anyhow::Result<()> {
let spans = support::capture_spans();
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB) + U256::from(1u64);
send_signed_voucher(&mut fx.send, &fx.signer, HARNESS_INTERVAL_BYTES, amount).await?;
match tokio::time::timeout(Duration::from_secs(10), read_client_msg(&mut fx.recv)).await {
Err(_elapsed) => anyhow::bail!("the node kept the stream open after a zero-byte voucher"),
Ok(Ok(other)) => anyhow::bail!("expected the stream to drop, got {other:?}"),
Ok(Err(_)) => {}
}
support::assert_inbound_failures_attributed(&fx.metrics, 1).await?;
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_voucher_rejected_total")? == 1,
"a rate-check bail is a rejected voucher"
);
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_client_abandoned_total")? == 0,
"a rate-check bail after payment is not an abandon"
);
assert_one_reject_line(&spans, &fx.signer, "RateCheck", false)?;
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_client_that_leaves_after_paying_counts_as_abandoned() -> anyhow::Result<()> {
let fx = drive_to_second_interval_awaiting_voucher().await?;
fx.conn.close(0u32.into(), b"client-abandoned");
support::assert_inbound_failures_attributed(&fx.metrics, 1).await?;
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_client_abandoned_total")? == 1,
"a client that leaves after paying must count as abandoned"
);
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_client_declined_total")? == 0,
"a client that paid did not decline"
);
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_requester_that_leaves_before_paying_counts_as_declined() -> anyhow::Result<()> {
let payload = vec![0x3Du8; 4 * 1024 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_pipelined_server_with_metrics(cache, store, 64 * 1024 * 1024, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let (_send, mut recv) = open_paid_stream(&conn, *hash.as_bytes(), Some(&ext)).await?;
read_exact_chunks(&mut recv, HARNESS_INTERVAL_BYTES).await?;
conn.close(0u32.into(), b"upstream-pull-dropped");
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_client_declined_total")? == 1,
"a requester that leaves before paying must count as declined"
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_node_fault_total")? == 0,
"a requester that leaves is not a node fault"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn an_unreadable_request_is_reset_and_counted() -> anyhow::Result<()> {
let payload = vec![0x3Eu8; 64 * 1024];
let (cache, _hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, _signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task, metrics) =
spawn_pipelined_server_with_metrics(cache, store, 64 * 1024 * 1024, 2).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.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, &[0xFF; 16])
.await
.map_err(|e| anyhow::anyhow!("write garbage frame: {e}"))?;
anyhow::ensure!(
read_frame(&mut recv).await.is_err(),
"the node must not answer an undecodable request"
);
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_request_unreadable_total")? == 1,
"an undecodable request must be counted"
);
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_node_fault_total")? == 0,
"an undecodable request is the peer's fault"
);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn regressing_voucher_after_an_accepted_voucher_is_rejected() -> anyhow::Result<()> {
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let accepted_amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
send_signed_voucher(
&mut fx.send,
&fx.signer,
2 * HARNESS_INTERVAL_BYTES,
accepted_amount,
)
.await?;
let (reason, has_bundle) = read_voucher_reject(&mut fx.recv).await?;
anyhow::ensure!(
reason == VoucherRejectReason::BytesRegression,
"expected BytesRegression, got {reason:?}"
);
anyhow::ensure!(
!has_bundle,
"a divergent equal-amount voucher is a single-signer fault, not a resyncable watermark: no bundle"
);
let persisted = fx.store.load_all()?;
let lane = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane after an accepted voucher"))?;
anyhow::ensure!(
lane.last_bytes_delivered() == U256::from(HARNESS_INTERVAL_BYTES),
"the accepted watermark must not move on a regression reject, got {}",
lane.last_bytes_delivered()
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn underpaying_voucher_after_an_accepted_voucher_returns_the_watermark() -> anyhow::Result<()>
{
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let accepted_amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
send_signed_voucher(
&mut fx.send,
&fx.signer,
2 * HARNESS_INTERVAL_BYTES,
accepted_amount + U256::from(1u64),
)
.await?;
let bundle = loop {
let msg = tokio::time::timeout(Duration::from_secs(10), read_client_msg(&mut fx.recv))
.await
.map_err(|_| anyhow::anyhow!("node never answered with a rejection"))??;
match msg {
ClientMessage::ChunkData(_) => {}
ClientMessage::StreamError(StreamError::VoucherRejected {
reason: VoucherRejectReason::Underpaid,
bundle: Some(bundle),
}) => break bundle,
other => anyhow::bail!("expected Underpaid carrying the watermark, got {other:?}"),
}
};
anyhow::ensure!(
U256::from(bundle.amount) == accepted_amount
&& bundle.bytes_delivered == HARNESS_INTERVAL_BYTES,
"the bundle must report the accepted watermark, got ({}, {})",
bundle.amount,
bundle.bytes_delivered
);
let persisted = fx.store.load_all()?;
let lane = persisted
.first()
.ok_or_else(|| anyhow::anyhow!("no persisted lane after an accepted voucher"))?;
anyhow::ensure!(
lane.last_bytes_delivered() == U256::from(HARNESS_INTERVAL_BYTES),
"the accepted watermark must not move on an underpay, got {}",
lane.last_bytes_delivered()
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
const SLIVER_BYTES: u64 = 64 * 1024;
#[tokio::test(flavor = "multi_thread")]
async fn a_sliver_voucher_leaves_the_rest_of_the_chunk_owed() -> anyhow::Result<()> {
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let mut amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
amount += min_payment(SLIVER_BYTES, RATE_PER_MB);
send_signed_voucher(
&mut fx.send,
&fx.signer,
HARNESS_INTERVAL_BYTES + SLIVER_BYTES,
amount,
)
.await?;
assert_parked_awaiting_voucher(&mut fx.recv).await?;
amount += min_payment(HARNESS_INTERVAL_BYTES - SLIVER_BYTES, RATE_PER_MB);
send_signed_voucher(&mut fx.send, &fx.signer, 2 * HARNESS_INTERVAL_BYTES, amount).await?;
read_exact_chunks(&mut fx.recv, HARNESS_INTERVAL_BYTES).await?;
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[test]
fn a_sliver_voucher_does_not_starve_the_runtime() -> anyhow::Result<()> {
use std::sync::atomic::{AtomicU64, Ordering};
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()?;
let ticks = Arc::new(AtomicU64::new(0));
let (mut fx, heartbeat) = rt.block_on(async {
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let heartbeat = tokio::spawn({
let ticks = Arc::clone(&ticks);
async move {
loop {
tokio::time::sleep(Duration::from_millis(10)).await;
ticks.fetch_add(1, Ordering::Relaxed);
}
}
});
let mut amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
amount += min_payment(SLIVER_BYTES, RATE_PER_MB);
send_signed_voucher(
&mut fx.send,
&fx.signer,
HARNESS_INTERVAL_BYTES + SLIVER_BYTES,
amount,
)
.await?;
anyhow::Ok((fx, heartbeat))
})?;
let before = ticks.load(Ordering::Relaxed);
std::thread::sleep(Duration::from_secs(1));
let during = ticks.load(Ordering::Relaxed).saturating_sub(before);
if during < 10 {
std::mem::forget(fx);
std::mem::forget(rt);
anyhow::bail!(
"the runtime worker ticked {during} times in 1 s: a serve loop holds it without \
awaiting"
);
}
rt.block_on(async {
heartbeat.abort();
assert_parked_awaiting_voucher(&mut fx.recv).await?;
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
})
}
#[tokio::test(flavor = "multi_thread")]
async fn sliver_vouchers_exhaust_the_per_chunk_proof_budget() -> anyhow::Result<()> {
const PROOF_BUDGET: u64 = 8;
anyhow::ensure!(
PROOF_BUDGET * SLIVER_BYTES < HARNESS_INTERVAL_BYTES,
"the slivers must not add up to a whole interval"
);
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let mut amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
for n in 1..=PROOF_BUDGET {
amount += min_payment(SLIVER_BYTES, RATE_PER_MB);
let bytes = HARNESS_INTERVAL_BYTES + n * SLIVER_BYTES;
send_signed_voucher(&mut fx.send, &fx.signer, bytes, amount).await?;
}
match tokio::time::timeout(Duration::from_secs(10), read_client_msg(&mut fx.recv)).await {
Err(_elapsed) => anyhow::bail!("the node kept the stream open past the proof budget"),
Ok(Ok(ClientMessage::ChunkData(chunk))) => anyhow::bail!(
"the node delivered {} more wire bytes on a chunk that is still owed",
chunk.bytes().len()
),
Ok(Ok(other)) => anyhow::bail!("expected the stream to fail, got {other:?}"),
Ok(Err(_)) => {}
}
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_node_fault_total")? == 0,
"a spent proof budget is a client payment fault, not a node fault"
);
anyhow::ensure!(
counter(
&fx.metrics,
"decdn_serve_stream_proof_budget_exhausted_total"
)? == 1,
"a spent proof budget must be counted"
);
support::assert_inbound_failures_attributed(&fx.metrics, 1).await?;
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_chunk_settled_by_the_last_proof_of_its_budget_is_paid() -> anyhow::Result<()> {
const PROOF_BUDGET: u64 = 8;
let mut fx = drive_to_second_interval_awaiting_voucher().await?;
let mut amount = min_payment(HARNESS_INTERVAL_BYTES, RATE_PER_MB);
for n in 1..PROOF_BUDGET {
amount += min_payment(SLIVER_BYTES, RATE_PER_MB);
let bytes = HARNESS_INTERVAL_BYTES + n * SLIVER_BYTES;
send_signed_voucher(&mut fx.send, &fx.signer, bytes, amount).await?;
}
let slivers = (PROOF_BUDGET - 1) * SLIVER_BYTES;
amount += min_payment(HARNESS_INTERVAL_BYTES - slivers, RATE_PER_MB);
send_signed_voucher(&mut fx.send, &fx.signer, 2 * HARNESS_INTERVAL_BYTES, amount).await?;
read_exact_chunks(&mut fx.recv, HARNESS_INTERVAL_BYTES).await?;
anyhow::ensure!(
counter(
&fx.metrics,
"decdn_serve_stream_proof_budget_exhausted_total"
)? == 0,
"a chunk the last proof of its budget settles must not count as a spent budget"
);
fx.conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&fx.client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn over_cap_stream_is_reset_without_signing_a_response() -> anyhow::Result<()> {
let payload = vec![0x4Bu8; 64 * 1024];
let (cache, hash, _cache_tmp) = cache_with_blob(&payload).await?;
let wire_bytes = support::bao_wire_len_whole(payload.len() as u64);
let signer = Arc::new(PrivateKeySigner::random());
let store = Arc::new(MemoryPoolStateStore::new());
store.record(&fresh_lane(signer.address(), U256::from(10_000_000u64)))?;
let store_dyn: Arc<dyn PoolStateStore> = store.clone();
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = operator_signer();
let metrics = Arc::new(Metrics::new());
let limiter = permissive_limiter(&metrics);
let handler = build_handler_limited_configured(
server_id,
&server_eth,
&metrics,
limiter,
cache,
store_dyn,
RATE_PER_MB,
1,
|_deps| {},
)?;
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_server(server_ep.clone(), handler);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&signer, client_node_id)?;
let _held =
stall_delivery_at_closing_voucher(&conn, *hash.as_bytes(), wire_bytes, Some(&ext)).await?;
let (mut send2, mut recv2) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi (second stream): {e}"))?;
let req = StreamRequest {
hash: *hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0012_61a0,
};
write_frame(&mut send2, &encode_stream_request(&req, Some(&ext))?)
.await
.map_err(|e| anyhow::anyhow!("write second request: {e}"))?;
assert_stream_reset(&mut recv2, decdn_protocol::APP_ERR_RATE_LIMITED).await?;
support::assert_inbound_failures_attributed(&metrics, 1).await?;
anyhow::ensure!(
counter(
&metrics,
"decdn_serve_stream_rejected_stream_cap_full_total"
)? == 1,
"an over-cap stream must be counted"
);
conn.close(0u32.into(), b"done");
support::assert_inbound_failures_attributed(&metrics, 2).await?;
anyhow::ensure!(
counter(&metrics, "decdn_serve_stream_client_declined_total")? == 1,
"the parked stream ends unpaid"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
const LYING_FRAME: usize = 64 * 1024;
fn honest_bao_wire(payload: &[u8]) -> anyhow::Result<Vec<u8>> {
support::honest_bao_wire_range(payload, 0, 0)
}
#[derive(Clone, Copy)]
enum Lie {
OverSend,
WrongBytes,
OverSendStraddling,
InflatedGarbage {
frame: usize,
},
}
async fn serve_lying_stream(
conn: Connection,
eth: &Arc<PrivateKeySigner>,
slash: &Eip712Domain,
served_wire: &[u8],
advertised_total_bytes: u64,
lie: Lie,
) -> anyhow::Result<()> {
let (mut send, mut recv) = conn
.accept_bi()
.await
.map_err(|e| anyhow::anyhow!("accept_bi: {e}"))?;
let req = match read_client_msg(&mut recv).await? {
ClientMessage::StreamRequest(req) => req,
other => anyhow::bail!("lying upstream: expected a StreamRequest, got {other:?}"),
};
let body = StreamResponseBody {
hash: req.hash,
ok: true,
rate_per_mb: RATE_PER_MB,
total_bytes: advertised_total_bytes,
pool_id: req.pool_id,
timestamp_us: req.timestamp_us,
};
let slash_sig = StreamSlashData::from_response_body(&body)
.sign(eth.as_ref(), slash)
.map_err(|e| anyhow::anyhow!("slash sign: {e}"))?
.as_bytes()
.to_vec();
let resp = StreamResponse { body, slash_sig };
write_frame(
&mut send,
&encode_stream_response(&resp, Some(&StreamResponseExt { error: None }))?,
)
.await
.map_err(|e| anyhow::anyhow!("write response: {e}"))?;
let frames: Vec<&[u8]> = served_wire.chunks(LYING_FRAME).collect();
for (i, chunk) in frames.iter().enumerate() {
let mut payload = chunk.to_vec();
if matches!(lie, Lie::OverSendStraddling) && i + 1 == frames.len() {
payload.extend(std::iter::repeat_n(0u8, 4096));
}
write_client_msg(
&mut send,
&ClientMessage::ChunkData(ChunkData::new(payload)?),
)
.await?;
}
match lie {
Lie::OverSendStraddling => {
}
Lie::InflatedGarbage { frame } => {
let mut garbage = vec![0u8; frame];
let mut noise: u64 = 0x9e37_79b9_7f4a_7c15;
loop {
for b in &mut garbage {
noise ^= noise << 13;
noise ^= noise >> 7;
noise ^= noise << 17;
*b = noise.to_le_bytes().first().copied().unwrap_or(0);
}
let msg = ClientMessage::ChunkData(ChunkData::new(garbage.clone())?);
if write_client_msg(&mut send, &msg).await.is_err() {
break;
}
}
}
Lie::OverSend => {
write_client_msg(
&mut send,
&ClientMessage::ChunkData(ChunkData::new(vec![0u8; LYING_FRAME])?),
)
.await?;
}
Lie::WrongBytes => {
let _ = read_client_msg(&mut recv).await;
let _ = write_client_msg(&mut send, &ClientMessage::StreamEnd).await;
}
}
let _ = send.finish();
conn.closed().await;
Ok(())
}
fn spawn_lying_server(
ep: Endpoint,
eth: Arc<PrivateKeySigner>,
slash: Eip712Domain,
served_wire: Vec<u8>,
advertised_total_bytes: u64,
lie: Lie,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
if let Ok(conn) = support::accept_one(&ep).await {
let _ = serve_lying_stream(
conn,
ð,
&slash,
&served_wire,
advertised_total_bytes,
lie,
)
.await;
}
})
}
#[tokio::test(flavor = "multi_thread")]
async fn client_rejects_a_server_that_oversends_past_the_promised_length() -> anyhow::Result<()> {
let payload = vec![0x2Du8; 256 * 1024];
let hash = Hash::new(&payload);
let wire = honest_bao_wire(&payload)?;
let total_bytes = payload.len() as u64;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_lying_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
wire,
total_bytes,
Lie::OverSend,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("an over-sending server must fail the fetch"))?;
let msg = format!("{err:#}");
anyhow::ensure!(
msg.contains("more than") || msg.contains("after the promised total"),
"the fetch must fail on the over-delivery guard, got: {msg}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_rejects_a_frame_that_straddles_the_promised_length() -> anyhow::Result<()> {
let payload = vec![0x2Eu8; 256 * 1024];
let hash = Hash::new(&payload);
let wire = honest_bao_wire(&payload)?;
let total_bytes = payload.len() as u64;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_lying_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
wire,
total_bytes,
Lie::OverSendStraddling,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let mut progress = VoucherProgress::default();
let err = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c0_ffee,
PullDeadlines::whole_transfer(Duration::from_secs(20)),
0,
0,
&mut progress,
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a straddling frame must fail the fetch"))?;
let msg = format!("{err:#}");
anyhow::ensure!(
msg.contains("more than"),
"the fetch must fail on the mid-stream overrun guard, got: {msg}"
);
anyhow::ensure!(
progress.advanced().is_none(),
"the straddling frame is rejected before it is paid for: {:?}",
progress.advanced()
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_rejects_hash_mismatched_bytes() -> anyhow::Result<()> {
let wanted = vec![0x11u8; 256 * 1024];
let served = vec![0x22u8; 256 * 1024];
let wanted_hash = Hash::new(&wanted);
anyhow::ensure!(Hash::new(&served) != wanted_hash, "fixtures must differ");
let served_wire = honest_bao_wire(&served)?;
let total_bytes = wanted.len() as u64;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_lying_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
served_wire,
total_bytes,
Lie::WrongBytes,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*wanted_hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a hash-mismatched delivery must fail the fetch"))?;
anyhow::ensure!(
err.downcast_ref::<HashMismatch>().is_some(),
"the fetch must fail with the typed HashMismatch, got: {err:#}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_rejects_an_empty_claim_for_a_non_empty_hash() -> anyhow::Result<()> {
let wanted_hash = Hash::new(b"not the empty blob");
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_lying_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
Vec::new(),
0,
Lie::WrongBytes,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*wanted_hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("an empty claim for a non-empty hash must fail the fetch"))?;
anyhow::ensure!(
err.downcast_ref::<HashMismatch>().is_some(),
"the fetch must fail with the typed HashMismatch, got: {err:#}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn fetch_inflated_garbage(frame: usize) -> anyhow::Result<(anyhow::Error, VoucherProgress)> {
let hash = Hash::new(b"the blob the buyer actually asked for");
let claimed_total: u64 = 32 << 20;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_lying_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
Vec::new(),
claimed_total,
Lie::InflatedGarbage { frame },
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(1_000_000_000u64),
);
let mut progress = VoucherProgress::default();
let err = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c0_ffee,
PullDeadlines::whole_transfer(Duration::from_secs(20)),
0,
0,
&mut progress,
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a garbage delivery must fail the fetch"))?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok((err, progress))
}
#[tokio::test(flavor = "multi_thread")]
async fn client_pays_nothing_for_an_inflated_garbage_claim() -> anyhow::Result<()> {
let (err, progress) = fetch_inflated_garbage(LYING_FRAME).await?;
anyhow::ensure!(
err.downcast_ref::<HashMismatch>().is_some(),
"garbage must surface the typed HashMismatch, got: {err:#}"
);
anyhow::ensure!(
progress.advanced().is_none(),
"the first chunk group fails before a metering interval accrues, so nothing is \
paid: {:?}",
progress.advanced()
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_spend_on_a_garbage_stream_is_bounded_by_one_frame() -> anyhow::Result<()> {
let frame: usize = 3 * 1024 * 1024;
let (err, progress) = fetch_inflated_garbage(frame).await?;
anyhow::ensure!(
err.downcast_ref::<HashMismatch>().is_some(),
"garbage must surface the typed HashMismatch, got: {err:#}"
);
let paid_bytes = progress.advanced().map_or(U256::ZERO, |(bytes, _)| bytes);
anyhow::ensure!(
paid_bytes <= U256::from(frame),
"paid {paid_bytes} bytes, more than the single {frame}-byte frame that arrived"
);
anyhow::ensure!(
paid_bytes < U256::from(32u64 << 20),
"paid {paid_bytes} bytes against a 32 MiB claim: the claim must never be billed"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_pays_for_an_honest_prefix_and_stops_at_the_first_bad_group() -> anyhow::Result<()> {
let payload = vec![0x5Du8; 4 * 1024 * 1024];
let hash = Hash::new(&payload);
let total_bytes = payload.len() as u64;
let mut wire = honest_bao_wire(&payload)?;
let corrupt_from = 2_500_000usize;
for b in wire.iter_mut().skip(corrupt_from) {
*b ^= 0xff;
}
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_lying_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
wire,
total_bytes,
Lie::WrongBytes,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(1_000_000_000u64),
);
let mut progress = VoucherProgress::default();
let err = stream_fetch_tracked(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
decdn_protocol::client::NO_NAMESPACE,
0,
0x00c0_ffee,
PullDeadlines::whole_transfer(Duration::from_secs(20)),
0,
0,
&mut progress,
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a corrupt tail must fail the fetch"))?;
anyhow::ensure!(
err.downcast_ref::<HashMismatch>().is_some(),
"the corrupt tail must surface the typed HashMismatch, got: {err:#}"
);
let (paid, _) = progress
.advanced()
.ok_or_else(|| anyhow::anyhow!("the verified prefix spans two metering intervals"))?;
anyhow::ensure!(
paid >= U256::from(2u64 << 20),
"paid {paid} wire bytes for a ~2.5 MiB verified prefix"
);
let bound = U256::from(corrupt_from as u64);
anyhow::ensure!(
paid <= bound,
"paid {paid} wire bytes, past the first corrupt byte at {bound}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn request_read_timeout_resets_a_silent_opener() -> anyhow::Result<()> {
let payload = vec![0x61u8; 64 * 1024];
let (cache, _hash, _cache_tmp) = cache_with_blob(&payload).await?;
let (store, _signer, _deposit) = seeded_store()?;
let (target, _server_eth, server_ep, server_task) =
spawn_handler_server(cache, store, RATE_PER_MB, 16).await?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(target, ALPN_CLIENT)
.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}"))?;
send.write_all(&[64u8])
.await
.map_err(|e| anyhow::anyhow!("write frame len: {e}"))?;
let ended = tokio::time::timeout(Duration::from_secs(15), read_frame(&mut recv)).await;
match ended {
Ok(Err(_)) => {}
Ok(Ok(frame)) => {
anyhow::bail!(
"server answered a partial request with a {}-byte frame",
frame.len()
)
}
Err(_elapsed) => {
anyhow::bail!("server never reset a silent opener within the request-read timeout")
}
}
drop(send);
conn.close(0u32.into(), b"done");
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn voucher_read_timeout_ends_a_parked_delivery() -> anyhow::Result<()> {
let payload = vec![0x62u8; 64 * 1024];
let fx = idle_fixture(&payload, Duration::from_secs(45)).await?;
let blob = fx.blob(0)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let mut stalled = stall_delivery_at_closing_voucher(
&conn,
*blob.hash.as_bytes(),
blob.wire_bytes,
Some(&ext),
)
.await?;
let ended =
tokio::time::timeout(Duration::from_secs(20), read_client_msg(&mut stalled.recv)).await;
match ended {
Ok(Err(_)) => {}
Ok(Ok(ClientMessage::StreamEnd)) => {
anyhow::bail!("the delivery must not complete without a paid closing voucher")
}
Ok(Ok(other)) => anyhow::bail!("expected the stream to end unpaid, got {other:?}"),
Err(_elapsed) => {
anyhow::bail!("server never ended the parked delivery within its proof wait")
}
}
let persisted = fx.store.load_all()?;
if let Some(lane) = persisted.first() {
anyhow::ensure!(
lane.last_bytes_delivered() == U256::ZERO,
"a timed-out delivery must advance no watermark, got {}",
lane.last_bytes_delivered()
);
}
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_payer_that_stops_reading_faults_at_the_no_progress_timeout() -> anyhow::Result<()> {
let payload: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let fx = idle_fixture(&payload, Duration::from_secs(45)).await?;
let blob = fx.blob(0)?;
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let conn = client_ep
.connect(fx.target.clone(), ALPN_CLIENT)
.await
.map_err(|e| anyhow::anyhow!("connect: {e}"))?;
let client_node_id = B256::from(*client_ep.id().as_bytes());
let ext = binding_ext(&fx.client_signer, client_node_id)?;
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow::anyhow!("open_bi: {e}"))?;
let req = StreamRequest {
hash: *blob.hash.as_bytes(),
namespace_id: decdn_protocol::client::NO_NAMESPACE,
pool_id: pool_id().into(),
byte_offset: 0,
byte_len: 0,
timestamp_us: 0x0012_61a1,
};
let request = encode_stream_request(&req, Some(&ext))
.map_err(|e| anyhow::anyhow!("encode request: {e}"))?;
write_frame(&mut send, &request)
.await
.map_err(|e| anyhow::anyhow!("write request: {e}"))?;
let (resp, resp_ext) = read_stream_response(&mut recv).await?;
anyhow::ensure!(resp.body.ok, "delivery refused: {:?}", resp_ext.error);
let waiting_since = Instant::now();
let stopped = tokio::time::timeout(Duration::from_secs(25), send.stopped()).await;
let waited = waiting_since.elapsed();
match stopped {
Ok(Ok(_)) => {}
Ok(Err(e)) => anyhow::bail!("the stream ended without a stop: {e}"),
Err(_elapsed) => anyhow::bail!("the node held a silent payer past 25s"),
}
anyhow::ensure!(
waited >= Duration::from_secs(9),
"the node faulted the payer after {waited:?}, before the 10s no-progress timeout"
);
anyhow::ensure!(
waited < Duration::from_secs(15),
"the node held a payer with no transport progress for {waited:?}; it must fault \
at the 10s no-progress timeout, not at the 30s ceiling"
);
support::assert_inbound_failures_attributed(&fx.metrics, 1).await?;
anyhow::ensure!(
counter(&fx.metrics, "decdn_serve_stream_node_fault_total")? == 0,
"a stalled payer must not count as a node fault"
);
let persisted = fx.store.load_all()?;
if let Some(lane) = persisted.first() {
anyhow::ensure!(
lane.last_bytes_delivered() == U256::ZERO,
"an unpaid delivery must advance no watermark, got {}",
lane.last_bytes_delivered()
);
}
drop(recv);
drop(send);
conn.close(0u32.into(), b"done");
shutdown([fx.server_task], [&client_ep, &fx.server_ep]).await?;
Ok(())
}
#[derive(Clone, Copy)]
enum StopThenSignal {
End,
Reject(VoucherRejectReason),
}
#[derive(Clone, Copy)]
enum StopPoint {
BeforeData,
AfterFirstReveal,
}
async fn serve_stop_send_then_signal(
conn: Connection,
eth: &Arc<PrivateKeySigner>,
slash: &Eip712Domain,
payload: &[u8],
stop: StopPoint,
signal: StopThenSignal,
) -> anyhow::Result<()> {
let total_bytes = u64::try_from(payload.len()).unwrap_or(u64::MAX);
let (mut send, mut recv) = conn
.accept_bi()
.await
.map_err(|e| anyhow::anyhow!("accept_bi: {e}"))?;
let req = match read_client_msg(&mut recv).await? {
ClientMessage::StreamRequest(req) => req,
other => anyhow::bail!("stop-send upstream: expected a StreamRequest, got {other:?}"),
};
let body = StreamResponseBody {
hash: req.hash,
ok: true,
rate_per_mb: RATE_PER_MB,
total_bytes,
pool_id: req.pool_id,
timestamp_us: req.timestamp_us,
};
let slash_sig = StreamSlashData::from_response_body(&body)
.sign(eth.as_ref(), slash)
.map_err(|e| anyhow::anyhow!("slash sign: {e}"))?
.as_bytes()
.to_vec();
let resp = StreamResponse { body, slash_sig };
write_frame(
&mut send,
&encode_stream_response(&resp, Some(&StreamResponseExt { error: None }))?,
)
.await
.map_err(|e| anyhow::anyhow!("write response: {e}"))?;
let served_wire = support::honest_bao_wire_range(payload, req.byte_offset, req.byte_len)?;
let mut frames = served_wire.chunks(LYING_FRAME);
let floor_bytes = usize::try_from(HARNESS_FLOOR_BYTES)?;
if matches!(stop, StopPoint::AfterFirstReveal) && served_wire.len() >= floor_bytes {
let mut served = 0usize;
while served < floor_bytes {
let chunk = frames
.next()
.ok_or_else(|| anyhow::anyhow!("wire shorter than one chunk"))?;
served += chunk.len();
write_client_msg(
&mut send,
&ClientMessage::ChunkData(ChunkData::new(chunk.to_vec())?),
)
.await?;
}
loop {
match read_client_msg(&mut recv).await? {
ClientMessage::ChunkPreimage(_) => break,
ClientMessage::Voucher(_) => {}
other => anyhow::bail!("stop-send upstream: expected a proof, got {other:?}"),
}
}
}
recv.stop(VarInt::from_u32(0))
.map_err(|e| anyhow::anyhow!("stop recv: {e}"))?;
for chunk in frames {
write_client_msg(
&mut send,
&ClientMessage::ChunkData(ChunkData::new(chunk.to_vec())?),
)
.await?;
}
match signal {
StopThenSignal::End => {
write_client_msg(&mut send, &ClientMessage::StreamEnd).await?;
}
StopThenSignal::Reject(reason) => {
write_client_msg(
&mut send,
&ClientMessage::StreamError(StreamError::VoucherRejected {
reason,
bundle: None,
}),
)
.await?;
}
}
let _ = send.finish();
conn.closed().await;
Ok(())
}
fn spawn_stop_send_server(
ep: Endpoint,
eth: Arc<PrivateKeySigner>,
slash: Eip712Domain,
payload: Vec<u8>,
signal: StopThenSignal,
) -> tokio::task::JoinHandle<()> {
spawn_stop_send_server_counting(
ep,
eth,
slash,
payload,
StopPoint::BeforeData,
signal,
1,
Arc::default(),
)
}
#[allow(clippy::too_many_arguments)]
fn spawn_stop_send_server_counting(
ep: Endpoint,
eth: Arc<PrivateKeySigner>,
slash: Eip712Domain,
payload: Vec<u8>,
stop: StopPoint,
signal: StopThenSignal,
max_conns: usize,
accepted: Arc<std::sync::atomic::AtomicUsize>,
) -> tokio::task::JoinHandle<()> {
use std::sync::atomic::Ordering;
tokio::spawn(async move {
let payload = Arc::new(payload);
let mut serves = Vec::new();
for _ in 0..max_conns {
let Ok(conn) = support::accept_one(&ep).await else {
break;
};
accepted.fetch_add(1, Ordering::SeqCst);
let (eth, slash, payload) = (Arc::clone(ð), slash.clone(), Arc::clone(&payload));
serves.push(tokio::spawn(async move {
let _ =
serve_stop_send_then_signal(conn, ð, &slash, &payload, stop, signal).await;
}));
}
for serve in serves {
let _ = serve.await;
}
})
}
#[tokio::test(flavor = "multi_thread")]
async fn client_completes_when_its_send_is_stopped_before_the_closing_voucher() -> anyhow::Result<()>
{
let payload = vec![0x5Au8; 256 * 1024];
let hash = Hash::new(&payload);
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_stop_send_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
payload.clone(),
StopThenSignal::End,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let blob = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
blob.as_ref() == payload.as_slice(),
"a fully delivered blob must survive a closing-voucher peer-stop"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn client_surfaces_typed_rejection_when_its_send_is_stopped_before_the_closing_voucher()
-> anyhow::Result<()> {
let payload = vec![0x5Bu8; 256 * 1024];
let hash = Hash::new(&payload);
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_stop_send_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
payload.clone(),
StopThenSignal::Reject(VoucherRejectReason::SpendingCapExhausted),
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffee,
Duration::from_secs(20),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a rejected fetch must fail"))?;
anyhow::ensure!(
err.downcast_ref::<UpstreamVoucherRejected>().is_some(),
"the fetch must surface the typed UpstreamVoucherRejected, got: {err:#}"
);
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(())
}
async fn fetch_with_frames_in_flight(signal: StopThenSignal) -> anyhow::Result<anyhow::Error> {
let payload: Vec<u8> = (0..3 * 1024 * 1024u32).map(|i| (i % 239) as u8).collect();
let hash = Hash::new(&payload);
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let server_task = spawn_stop_send_server(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
payload,
signal,
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let err = stream_fetch(
&client_ep,
target,
&ctx,
&slash_domain(),
server_eth.address(),
*hash.as_bytes(),
0,
0x00c0_ffef,
Duration::from_secs(20),
)
.await
.err()
.ok_or_else(|| anyhow::anyhow!("a fetch whose proof write fails mid-blob must fail"))?;
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(err)
}
#[tokio::test(flavor = "multi_thread")]
async fn client_reads_past_in_flight_frames_to_a_typed_rejection() -> anyhow::Result<()> {
let err = fetch_with_frames_in_flight(StopThenSignal::Reject(
VoucherRejectReason::SpendingCapExhausted,
))
.await?;
anyhow::ensure!(
err.downcast_ref::<UpstreamVoucherRejected>().is_some(),
"the fetch must surface the typed UpstreamVoucherRejected, got: {err:#}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_stream_end_behind_skipped_frames_fails_the_leg() -> anyhow::Result<()> {
let err = fetch_with_frames_in_flight(StopThenSignal::End).await?;
anyhow::ensure!(
format!("{err:#}").contains("did not take in"),
"the fetch must fail on the skipped frames, got: {err:#}"
);
Ok(())
}
struct StopSendDrive {
result: anyhow::Result<()>,
ledger: Arc<PoolLedger>,
accepted: usize,
promoted: Option<Vec<u8>>,
logs: String,
}
#[derive(Clone)]
struct CapturedLogs(Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for CapturedLogs {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
const CONFIRMED_EVENT: &str = "confirmed the voucher";
fn capture_client_events() -> (
tracing::subscriber::DefaultGuard,
Arc<std::sync::Mutex<Vec<u8>>>,
) {
use tracing_subscriber::Layer as _;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let buf = Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = CapturedLogs(Arc::clone(&buf));
let guard = tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(move || sink.clone())
.with_ansi(false)
.with_filter(
tracing_subscriber::filter::Targets::new()
.with_target("decdn_client", tracing::Level::DEBUG),
),
)
.set_default();
(guard, buf)
}
async fn drive_against_stop_send_server(
payload: Vec<u8>,
stop: StopPoint,
signal: StopThenSignal,
) -> anyhow::Result<StopSendDrive> {
use decdn_client::driver::drive;
use decdn_client::{BudgetPacer, ClientRangedStore, PeerSource};
use std::sync::atomic::{AtomicUsize, Ordering};
let hash = Hash::new(&payload);
let total_bytes = u64::try_from(payload.len())?;
let server_sk = fresh_key();
let server_id = server_sk.public();
let server_eth = Arc::new(PrivateKeySigner::random());
let (server_ep, server_addr) = local_endpoint(server_sk, vec![ALPN_CLIENT.to_vec()]).await?;
let accepted = Arc::new(AtomicUsize::new(0));
let server_task = spawn_stop_send_server_counting(
server_ep.clone(),
Arc::clone(&server_eth),
slash_domain(),
payload,
stop,
signal,
3,
Arc::clone(&accepted),
);
let target = EndpointAddr::new(server_id).with_ip_addr(server_addr);
let (client_ep, _) = local_endpoint(fresh_key(), vec![]).await?;
let ctx = pool_context(
&client_ep,
Arc::new(PrivateKeySigner::random()),
U256::from(10_000_000u64),
);
let ledger = Arc::new(ctx.new_ledger());
let ctx = Arc::new(std::sync::Mutex::new(ctx));
let store_dir = tempfile::tempdir()?;
let store = ClientRangedStore::create(store_dir.path(), "blob", *hash.as_bytes(), total_bytes)?;
let slash = slash_domain();
let source = PeerSource::new(
&client_ep,
target,
Arc::clone(&ctx),
Arc::clone(&ledger),
&slash,
server_eth.address(),
decdn_protocol::client::NO_NAMESPACE,
0,
u64::MAX,
PullDeadlines::new(Duration::from_secs(10), Duration::from_secs(10), 0)?,
None,
);
let pacer = BudgetPacer::new();
let (log_guard, log_buf) = capture_client_events();
let result = drive(
&store,
&source,
&pacer,
&ctx,
&ledger,
*hash.as_bytes(),
0,
0,
None,
None,
None,
None,
)
.await;
drop(log_guard);
let logs = String::from_utf8_lossy(
&log_buf
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
.into_owned();
drop(source);
let promoted = std::fs::read(store_dir.path().join("blob")).ok();
shutdown([server_task], [&client_ep, &server_ep]).await?;
Ok(StopSendDrive {
result,
ledger,
accepted: accepted.load(Ordering::SeqCst),
promoted,
logs,
})
}
fn ensure_one_leg_fully_paid(d: &StopSendDrive, payload: &[u8]) -> anyhow::Result<()> {
let wire = honest_bao_wire(payload)?;
anyhow::ensure!(
d.logs.contains(CONFIRMED_EVENT),
"the closing-voucher write must fail and the recovery must confirm it; captured \
events:\n{}",
d.logs
);
anyhow::ensure!(
d.promoted.as_deref() == Some(payload),
"the drive must finalize the whole blob into the ranged store"
);
anyhow::ensure!(
d.accepted == 1,
"the fetch must complete on one connection, dialled {}",
d.accepted
);
let committed = d.ledger.committed();
anyhow::ensure!(
committed == d.ledger.settlement(),
"the closing voucher must be committed, not merely armed: committed {committed:?} vs \
settlement {:?}",
d.ledger.settlement()
);
anyhow::ensure!(
committed.bytes == U256::from(wire.len()),
"exactly the one honest wire is billed, got {} for a {}-byte wire",
committed.bytes,
wire.len()
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn drive_completes_in_one_leg_when_its_send_is_stopped_before_the_closing_voucher()
-> anyhow::Result<()> {
let payload = vec![0x5Au8; 256 * 1024];
let mut d =
drive_against_stop_send_server(payload.clone(), StopPoint::BeforeData, StopThenSignal::End)
.await?;
std::mem::replace(&mut d.result, Ok(()))?;
ensure_one_leg_fully_paid(&d, &payload)
}
#[tokio::test(flavor = "multi_thread")]
async fn drive_completes_in_one_leg_after_reveals_when_its_send_is_stopped_before_the_closing_voucher()
-> anyhow::Result<()> {
let payload = vec![0x5Bu8; 3 * usize::try_from(CHUNK_BYTES)? / 2];
let mut d = drive_against_stop_send_server(
payload.clone(),
StopPoint::AfterFirstReveal,
StopThenSignal::End,
)
.await?;
std::mem::replace(&mut d.result, Ok(()))?;
ensure_one_leg_fully_paid(&d, &payload)
}
#[tokio::test(flavor = "multi_thread")]
async fn drive_surfaces_typed_rejection_when_its_send_is_stopped_before_the_closing_voucher()
-> anyhow::Result<()> {
let payload = vec![0x5Au8; 256 * 1024];
let d = drive_against_stop_send_server(
payload,
StopPoint::BeforeData,
StopThenSignal::Reject(VoucherRejectReason::SpendingCapExhausted),
)
.await?;
let err = d
.result
.err()
.ok_or_else(|| anyhow::anyhow!("a rejected drive must fail"))?;
anyhow::ensure!(
err.downcast_ref::<UpstreamVoucherRejected>().is_some(),
"the drive must surface the typed UpstreamVoucherRejected, got: {err:#}"
);
anyhow::ensure!(
d.accepted == 1,
"a rejection must not re-leg, dialled {}",
d.accepted
);
anyhow::ensure!(
d.promoted.is_none(),
"a rejected drive must not promote the blob"
);
anyhow::ensure!(
d.ledger.committed() == Cumulative::default(),
"a rejection confirms nothing: committed {:?}",
d.ledger.committed()
);
anyhow::ensure!(
!d.logs.contains(CONFIRMED_EVENT),
"a rejection confirms nothing; captured events:\n{}",
d.logs
);
Ok(())
}