use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::{Address, B256, U256};
use alloy::signers::local::PrivateKeySigner;
use dashmap::DashMap;
use decdn_cache::{CHUNK_GROUP_BYTES, CacheEngine, CacheError, Hash};
use decdn_incentive::rate::{DEFAULT_TOLERANCE_BPS, RateError, min_payment, verify_rate};
use decdn_incentive::store::{PoolStateStore, StoreError};
use decdn_incentive::{
Capability, LaneKey, LaneState, RetrySignal, SignedCapability, SignedVoucher, StreamSlashData,
verify_binding, voucher_reject_reason, wire_voucher_to_signed,
};
use decdn_protocol::client::{
ClientMessage, StreamError, StreamRequest, StreamRequestExt, StreamResponse,
StreamResponseBody, StreamResponseExt, VoucherRejectReason, WatermarkBundle,
};
use decdn_protocol::{
ALPN_CLIENT, APP_ERR_RATE_LIMITED, CHUNK_BYTES, FrameError, decode_message, encode_message,
is_unknown_variant, read_frame, write_frame,
};
use iroh::PublicKey;
use iroh::endpoint::{Connection, RecvStream, SendStream, VarInt};
use iroh::protocol::{AcceptError, ProtocolHandler};
use tokio::io::AsyncReadExt;
use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc};
use crate::dispatch::{ConnectionLimiter, RejectReason};
use crate::metrics::Metrics;
use crate::node_origin::NodeOrigin;
use crate::receipt_log::{RawReceipt, ReceiptSink};
use crate::warn_throttle::WarnThrottle;
mod delivery;
mod dispatch;
mod fill;
mod outcome;
mod proof_wait;
mod ramp;
mod serve_encoder;
mod serve_leg;
mod serve_waits;
mod voucher;
mod window;
mod wire;
pub(crate) use proof_wait::PROOF_WAIT_CEILING;
pub const MAX_CLIENT_STREAMS: usize = 100;
const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(5);
const VOUCHER_READ_TIMEOUT: Duration = Duration::from_secs(10);
const REJECTION_CLOSE_TIMEOUT: Duration = Duration::from_millis(250);
const POST_END_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
const WINDOW_PULL_FALLBACK_DEADLINE: Duration = Duration::from_mins(1);
const FIRST_FRAME_BYTES: u64 = 4 * decdn_bao_range::CHUNK_GROUP_BYTES;
const APP_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
const APP_ERR_NO_ERROR: u32 = 0x00;
const APP_ERR_UNSUPPORTED_MESSAGE: u32 = 0x01;
const APP_ERR_MALFORMED_MESSAGE: u32 = 0x03;
#[derive(Debug)]
struct LaneDeliveryState {
state: LaneState,
bytes_delivered_cumulative: U256,
paid_credited: U256,
last_voucher_at: AtomicU64,
ramp_pool: Arc<ramp::RampPool>,
}
impl LaneDeliveryState {
fn hydrated(state: LaneState) -> Self {
let owed_bytes = state.owed_bytes();
Self {
state,
bytes_delivered_cumulative: owed_bytes,
paid_credited: owed_bytes,
last_voucher_at: AtomicU64::new(0),
ramp_pool: Arc::default(),
}
}
}
#[derive(Clone)]
pub struct LaneActivityClock {
lanes: Arc<DashMap<LaneKey, Arc<Mutex<LaneDeliveryState>>>>,
}
impl std::fmt::Debug for LaneActivityClock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LaneActivityClock").finish_non_exhaustive()
}
}
impl LaneActivityClock {
#[must_use]
pub fn empty() -> Self {
Self {
lanes: Arc::new(DashMap::new()),
}
}
pub async fn ages(&self) -> HashMap<LaneKey, u64> {
let now_ms = unix_millis();
let handles: Vec<(LaneKey, Arc<Mutex<LaneDeliveryState>>)> = self
.lanes
.iter()
.map(|e| (*e.key(), Arc::clone(e.value())))
.collect();
let mut out = HashMap::with_capacity(handles.len());
for (key, lane) in handles {
let stamped = lane.lock().await.last_voucher_at.load(Ordering::Relaxed);
if stamped != 0 {
out.insert(key, now_ms.saturating_sub(stamped) / 1000);
}
}
out
}
#[cfg(test)]
pub(crate) fn with_stamped_lanes(lanes: &[LaneKey]) -> Self {
let now = unix_millis();
let map = DashMap::new();
for &key in lanes {
let state = LaneState::hydrate(
key.pool_id,
key.signer,
key.provider,
U256::MAX,
0,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
);
map.insert(
key,
Arc::new(Mutex::new(LaneDeliveryState {
state,
bytes_delivered_cumulative: U256::ZERO,
paid_credited: U256::ZERO,
last_voucher_at: AtomicU64::new(now),
ramp_pool: Arc::default(),
})),
);
}
Self {
lanes: Arc::new(map),
}
}
}
pub(super) fn unix_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}
impl ClientHandler {
#[must_use]
pub fn lane_activity_clock(&self) -> LaneActivityClock {
LaneActivityClock {
lanes: Arc::clone(&self.lanes),
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct SignerFloorState {
live_reservation: U256,
}
impl SignerFloorState {
fn is_empty(self) -> bool {
self.live_reservation.is_zero()
}
}
#[derive(Debug, Clone)]
pub(super) struct PoolFloorState {
live_reservation: U256,
signers: HashMap<Address, SignerFloorState>,
epoch: u64,
}
static POOL_FLOOR_EPOCH: AtomicU64 = AtomicU64::new(0);
impl Default for PoolFloorState {
fn default() -> Self {
Self {
live_reservation: U256::ZERO,
signers: HashMap::new(),
epoch: POOL_FLOOR_EPOCH.fetch_add(1, Ordering::Relaxed),
}
}
}
impl PoolFloorState {
const fn live_committed(&self) -> U256 {
self.live_reservation
}
fn prune_spent(&mut self, signer: Address) {
if self
.signers
.get(&signer)
.is_some_and(|lane| lane.is_empty())
{
self.signers.remove(&signer);
}
}
}
#[must_use = "dropping the guard at once releases its live reservation"]
pub(super) struct FloorReservation {
map: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
pool_id: B256,
epoch: u64,
signer: Address,
reserved: U256,
repaid: AtomicBool,
}
impl FloorReservation {
#[cfg(test)]
fn reserve(
map: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
pool_id: B256,
signer: Address,
reserved: U256,
) -> Self {
let epoch = {
let mut guard = map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let entry = guard.entry(pool_id).or_default();
entry.live_reservation = entry.live_reservation.saturating_add(reserved);
let lane = entry.signers.entry(signer).or_default();
lane.live_reservation = lane.live_reservation.saturating_add(reserved);
entry.epoch
};
Self::new_charged(map, pool_id, signer, reserved, epoch)
}
const fn new_charged(
map: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
pool_id: B256,
signer: Address,
reserved: U256,
epoch: u64,
) -> Self {
Self {
map,
pool_id,
epoch,
signer,
reserved,
repaid: AtomicBool::new(false),
}
}
fn release_live_repaid(&self) {
if self.repaid.swap(true, Ordering::Relaxed) {
return; }
let mut guard = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(entry) = guard
.get_mut(&self.pool_id)
.filter(|entry| entry.epoch == self.epoch)
{
entry.live_reservation = entry.live_reservation.saturating_sub(self.reserved);
if let Some(lane) = entry.signers.get_mut(&self.signer) {
lane.live_reservation = lane.live_reservation.saturating_sub(self.reserved);
}
entry.prune_spent(self.signer);
}
}
fn release_if_repaid(&self, paid_micro: U256) {
if paid_micro >= self.reserved {
self.release_live_repaid();
}
}
fn release_unspent(&self) {
self.release_live_repaid();
}
}
impl Drop for FloorReservation {
fn drop(&mut self) {
self.release_live_repaid();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum FloorRefusal {
PoolExhausted,
SignerAtCap { signer_cap: U256 },
}
#[derive(Debug, Clone, Copy)]
pub(super) struct FloorRefusalSite {
pub pool_id: B256,
pub signer: Address,
pub hash: Hash,
pub remaining: U256,
pub ceiling: U256,
}
impl From<FloorRefusal> for ServeRejectReason {
fn from(refusal: FloorRefusal) -> Self {
match refusal {
FloorRefusal::PoolExhausted => Self::InsufficientDeposit,
FloorRefusal::SignerAtCap { .. } => Self::SignerFloorAtCap,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ServeRejectReason {
EvictedSinceProbe,
CacheMiss,
InternalError,
UnknownLane,
OwnerMismatch,
InsufficientDeposit,
PoolUnconfirmed,
PoolClosing {
proven: bool,
},
SignerCapExhausted,
SignerFloorAtCap,
PullLoopGuard,
LoadShedHit,
LoadShedMiss,
RangeNotSatisfiable,
BlobTooLarge,
HashDenied,
ChainHashDenied,
OriginDenied,
ForeignNamespaceDeclined,
ChainStale,
}
impl ServeRejectReason {
const fn wire_error(self) -> StreamError {
match self {
Self::InsufficientDeposit
| Self::SignerCapExhausted
| Self::PoolClosing { proven: true } => StreamError::Unfunded,
Self::CacheMiss
| Self::UnknownLane
| Self::OwnerMismatch
| Self::PoolUnconfirmed
| Self::PoolClosing { proven: false }
| Self::SignerFloorAtCap
| Self::PullLoopGuard
| Self::LoadShedHit
| Self::LoadShedMiss
| Self::ChainStale => StreamError::NotFound,
Self::EvictedSinceProbe
| Self::InternalError
| Self::RangeNotSatisfiable
| Self::BlobTooLarge
| Self::HashDenied
| Self::ChainHashDenied
| Self::OriginDenied
| Self::ForeignNamespaceDeclined => StreamError::Declined,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FillOutcome {
Filled,
CleanMiss,
HardFault,
}
impl FillOutcome {
const fn as_str(self) -> &'static str {
match self {
Self::Filled => "filled",
Self::CleanMiss => "clean_miss",
Self::HardFault => "hard_fault",
}
}
}
impl FillOutcome {
const fn miss_reason(fault_seen: bool) -> ServeRejectReason {
if fault_seen {
ServeRejectReason::InternalError
} else {
ServeRejectReason::CacheMiss
}
}
const fn is_fault(self) -> bool {
matches!(self, Self::HardFault)
}
const fn is_filled(self) -> bool {
matches!(self, Self::Filled)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MissPath {
CoverageGate(CoverageShortfall),
PullLegMiss(crate::node_origin::PullMiss),
PullLegTimeout,
BufferedMiss,
NoPullThrough,
NoLane,
}
impl MissPath {
const fn as_str(self) -> &'static str {
match self {
Self::CoverageGate(_) => "coverage_gate",
Self::PullLegMiss(_) => "pull_leg_miss",
Self::PullLegTimeout => "pull_leg_timeout",
Self::BufferedMiss => "buffered_miss",
Self::NoPullThrough => "no_pull_through",
Self::NoLane => "no_lane",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CoverageShortfall {
pull_offset: u64,
pull_len: u64,
candidates: usize,
first_uncovered_chunk: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct MissRefusal {
reason: ServeRejectReason,
path: MissPath,
}
impl MissRefusal {
const fn new(fault_seen: bool, path: MissPath) -> Self {
Self {
reason: FillOutcome::miss_reason(fault_seen),
path,
}
}
}
pub struct ClientHandlerDeps {
pub node_id: PublicKey,
pub metrics: Arc<Metrics>,
pub limiter: Arc<ConnectionLimiter>,
pub shed: Arc<crate::load_shed::LoadShedController>,
pub cache: CacheEngine,
pub eth_signer: Arc<PrivateKeySigner>,
pub slash_domain: Eip712Domain,
pub voucher_domain: Eip712Domain,
pub bind_domain: Eip712Domain,
pub pool_state_store: Arc<dyn PoolStateStore>,
pub receipt_sink: Arc<dyn ReceiptSink>,
pub pool_view: Option<Arc<dyn crate::pool_view::PoolView>>,
pub pool_min_remaining_deposit: U256,
pub rate_per_mb: u64,
pub max_concurrent_streams: usize,
pub content_deny: Arc<crate::content_deny::ContentDenylist>,
pub chain_freshness: Option<crate::chain_freshness::ChainFreshness>,
pub redeem_hint: Option<mpsc::Sender<LaneKey>>,
pub pull_through: Option<Duration>,
pub local_populate: Option<Duration>,
pub pull_through_origin: Option<Arc<NodeOrigin>>,
pub credit_max: u64,
pub credit_ramp_divisor: u64,
pub frame_target_bytes: u64,
pub idle_timeout: Option<Duration>,
pub pool_recheck_interval: Option<Duration>,
pub pool_floor_signer_live_windows: u64,
pub warming_credit: Arc<dyn crate::warming_allowance::WarmingCreditSink>,
pub operator_shares: crate::fee_shares::OperatorShares,
pub relay_foreign_namespaces: bool,
pub coarse_clock: Option<Arc<crate::coarse_clock::CoarseClock>>,
pub redeem_interval_secs: u64,
}
impl std::fmt::Debug for ClientHandlerDeps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientHandlerDeps")
.field("node_id", &self.node_id)
.field("max_concurrent_streams", &self.max_concurrent_streams)
.finish_non_exhaustive()
}
}
impl ClientHandlerDeps {
#[allow(clippy::too_many_arguments)] pub fn new(
node_id: PublicKey,
metrics: Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
cache: CacheEngine,
eth_signer: Arc<PrivateKeySigner>,
slash_domain: Eip712Domain,
voucher_domain: Eip712Domain,
bind_domain: Eip712Domain,
pool_state_store: Arc<dyn PoolStateStore>,
receipt_sink: Arc<dyn ReceiptSink>,
rate_per_mb: u64,
max_concurrent_streams: usize,
content_deny: Arc<crate::content_deny::ContentDenylist>,
pool_min_remaining_deposit: U256,
shed: Arc<crate::load_shed::LoadShedController>,
) -> Self {
Self {
node_id,
metrics,
limiter,
shed,
cache,
eth_signer,
slash_domain,
voucher_domain,
bind_domain,
pool_state_store,
receipt_sink,
pool_view: None,
pool_min_remaining_deposit,
rate_per_mb,
max_concurrent_streams,
content_deny,
chain_freshness: None,
redeem_hint: None,
pull_through: None,
local_populate: None,
pull_through_origin: None,
credit_max: decdn_common::config::DEFAULT_CREDIT_MAX,
credit_ramp_divisor: decdn_common::config::DEFAULT_CREDIT_RAMP_DIVISOR,
frame_target_bytes: decdn_common::config::DEFAULT_FRAME_TARGET_BYTES,
idle_timeout: None,
pool_recheck_interval: None,
pool_floor_signer_live_windows: u64::MAX,
warming_credit: Arc::new(crate::warming_allowance::NoopWarmingCreditSink),
operator_shares: crate::fee_shares::OperatorShares::new(0),
relay_foreign_namespaces: decdn_common::config::DEFAULT_RELAY_FOREIGN_NAMESPACES,
coarse_clock: None,
redeem_interval_secs: decdn_common::config::DEFAULT_REDEEM_INTERVAL_SECS,
}
}
}
const CAPABILITY_VERIFY_CACHE_CAPACITY: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CapabilityVerifyOutcome {
Owner(Address),
Invalid,
}
#[derive(Debug)]
struct CapabilityVerifyCache {
entries: HashMap<(B256, [u8; 65]), (CapabilityVerifyOutcome, u64)>,
tick: u64,
cap: usize,
}
impl Default for CapabilityVerifyCache {
fn default() -> Self {
Self::with_capacity(CAPABILITY_VERIFY_CACHE_CAPACITY)
}
}
impl CapabilityVerifyCache {
fn with_capacity(cap: usize) -> Self {
Self {
entries: HashMap::with_capacity(cap),
tick: 0,
cap: cap.max(1),
}
}
fn get(&mut self, signing_hash: B256, signature: [u8; 65]) -> Option<CapabilityVerifyOutcome> {
self.tick = self.tick.wrapping_add(1);
let tick = self.tick;
let (outcome, last_used) = self.entries.get_mut(&(signing_hash, signature))?;
*last_used = tick;
Some(*outcome)
}
fn insert(
&mut self,
signing_hash: B256,
signature: [u8; 65],
outcome: CapabilityVerifyOutcome,
) {
self.tick = self.tick.wrapping_add(1);
let tick = self.tick;
let key = (signing_hash, signature);
if self.entries.insert(key, (outcome, tick)).is_some() {
return;
}
while self.entries.len() > self.cap {
let Some(least_recent) = self
.entries
.iter()
.min_by_key(|(_, (_, last_used))| *last_used)
.map(|(k, _)| *k)
else {
break;
};
self.entries.remove(&least_recent);
}
}
#[cfg(test)]
fn len(&self) -> usize {
self.entries.len()
}
}
pub struct ClientHandler {
node_id: PublicKey,
metrics: Arc<Metrics>,
limiter: Arc<ConnectionLimiter>,
shed: Arc<crate::load_shed::LoadShedController>,
cache: CacheEngine,
eth_signer: Arc<PrivateKeySigner>,
slash_domain: Eip712Domain,
voucher_domain: Eip712Domain,
bind_domain: Eip712Domain,
pool_state_store: Arc<dyn PoolStateStore>,
pool_min_remaining_deposit: U256,
receipt_sink: Arc<dyn ReceiptSink>,
pool_view: Option<Arc<dyn crate::pool_view::PoolView>>,
chain_freshness: Option<crate::chain_freshness::ChainFreshness>,
lanes: Arc<DashMap<LaneKey, Arc<Mutex<LaneDeliveryState>>>>,
lane_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
lane_after_resolution: std::sync::Mutex<Option<LaneState>>,
lane_gauge_publish: std::sync::Mutex<()>,
capability_verify_cache: std::sync::Mutex<CapabilityVerifyCache>,
pool_floor: Arc<std::sync::Mutex<HashMap<B256, PoolFloorState>>>,
pool_floor_signer_live_windows: u64,
redeem_hint: Option<mpsc::Sender<LaneKey>>,
pull_through: Option<Duration>,
local_populate: Option<Duration>,
pull_through_origin: Option<Arc<NodeOrigin>>,
credit_max: u64,
credit_ramp_divisor: u64,
frame_target_bytes: u64,
pub(crate) content_deny: Arc<crate::content_deny::ContentDenylist>,
rate_per_mb: u64,
max_concurrent_streams: usize,
deposit_refusal_warn: WarnThrottle,
signer_cap_refusal_warn: WarnThrottle,
binding_warn: WarnThrottle,
unbound_request_warn: WarnThrottle,
unknown_lane_warn: WarnThrottle,
no_lane_miss_log: WarnThrottle,
idle_timeout: Option<Duration>,
pool_recheck_interval: Option<Duration>,
warming_credit: Arc<dyn crate::warming_allowance::WarmingCreditSink>,
operator_shares: crate::fee_shares::OperatorShares,
relay_foreign_namespaces: bool,
coarse_clock: Arc<crate::coarse_clock::CoarseClock>,
capability_expiry_margin_secs: u64,
}
impl std::fmt::Debug for ClientHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientHandler")
.field("node_id", &self.node_id)
.field("max_concurrent_streams", &self.max_concurrent_streams)
.finish_non_exhaustive()
}
}
impl ClientHandler {
pub const ALPN: &'static [u8] = ALPN_CLIENT;
pub fn new(deps: ClientHandlerDeps) -> anyhow::Result<Self> {
let map: DashMap<LaneKey, Arc<Mutex<LaneDeliveryState>>> = DashMap::new();
for state in deps.pool_state_store.load_all()? {
map.insert(
state.key(),
Arc::new(Mutex::new(LaneDeliveryState::hydrated(state))),
);
}
let open_lanes = map.len();
let lane_count = AtomicUsize::new(open_lanes);
deps.metrics.set_lanes_open(open_lanes);
let pool_floor: HashMap<B256, PoolFloorState> = HashMap::new();
Ok(Self {
node_id: deps.node_id,
metrics: deps.metrics,
limiter: deps.limiter,
shed: deps.shed,
cache: deps.cache,
eth_signer: deps.eth_signer,
slash_domain: deps.slash_domain,
voucher_domain: deps.voucher_domain,
bind_domain: deps.bind_domain,
pool_state_store: deps.pool_state_store,
pool_min_remaining_deposit: deps.pool_min_remaining_deposit,
receipt_sink: deps.receipt_sink,
pool_view: deps.pool_view,
chain_freshness: deps.chain_freshness,
lanes: Arc::new(map),
lane_count,
#[cfg(any(test, feature = "test-support"))]
lane_after_resolution: std::sync::Mutex::new(None),
lane_gauge_publish: std::sync::Mutex::new(()),
capability_verify_cache: std::sync::Mutex::new(CapabilityVerifyCache::default()),
pool_floor: Arc::new(std::sync::Mutex::new(pool_floor)),
pool_floor_signer_live_windows: deps.pool_floor_signer_live_windows,
redeem_hint: deps.redeem_hint,
pull_through: deps.pull_through,
local_populate: deps.local_populate,
pull_through_origin: deps.pull_through_origin,
credit_max: deps.credit_max,
credit_ramp_divisor: deps.credit_ramp_divisor,
frame_target_bytes: deps.frame_target_bytes,
content_deny: deps.content_deny,
rate_per_mb: deps.rate_per_mb,
max_concurrent_streams: deps.max_concurrent_streams,
deposit_refusal_warn: WarnThrottle::new(Self::DEPOSIT_REFUSAL_WARN_INTERVAL),
signer_cap_refusal_warn: WarnThrottle::new(Self::DEPOSIT_REFUSAL_WARN_INTERVAL),
binding_warn: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
unbound_request_warn: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
unknown_lane_warn: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
no_lane_miss_log: WarnThrottle::new(Self::PEER_FAULT_WARN_INTERVAL),
idle_timeout: deps.idle_timeout,
pool_recheck_interval: deps.pool_recheck_interval,
warming_credit: deps.warming_credit,
operator_shares: deps.operator_shares,
relay_foreign_namespaces: deps.relay_foreign_namespaces,
coarse_clock: deps
.coarse_clock
.unwrap_or_else(|| Arc::new(crate::coarse_clock::CoarseClock::new())),
capability_expiry_margin_secs: decdn_common::config::capability_expiry_margin_secs(
deps.redeem_interval_secs,
),
})
}
fn credit_warming_serve(&self, hash: Hash, served_bytes: u64) {
let sell_rate = self.rate_per_mb;
let margin_per_mb = sell_rate
.saturating_mul(u64::from(self.operator_shares.bps()))
.saturating_div(10_000);
let mb = served_bytes.div_ceil(decdn_protocol::MB_BYTES);
self.warming_credit
.credit(hash, mb.saturating_mul(margin_per_mb));
}
pub(super) fn pool_recheck_interval(&self) -> Duration {
self.pool_recheck_interval
.unwrap_or(crate::pool_view::POOL_RECHECK_INTERVAL)
}
pub(super) async fn pool_view_status(
&self,
pool_id: B256,
) -> Option<crate::pool_view::PoolStatus> {
self.pool_view.as_ref()?.status(pool_id).await
}
pub(super) async fn pool_view_status_cached(
&self,
pool_id: B256,
) -> Option<crate::pool_view::PoolStatus> {
self.pool_view.as_ref()?.cached_status(pool_id).await
}
pub(super) async fn signer_spent_cached(&self, pool_id: B256, signer: Address) -> Option<u64> {
self.pool_view
.as_ref()?
.signer_spent_cached(pool_id, signer)
.await
}
pub(super) async fn signer_cap_drained_midstream(
&self,
pool_id: B256,
signer: Address,
held_cap: U256,
rate_per_mb: u64,
) -> bool {
let Some(spent) = self.signer_spent_cached(pool_id, signer).await else {
return false;
};
let headroom = held_cap.saturating_sub(U256::from(spent));
let floor_micro = U256::from(decdn_incentive::min_payment(
self.credit_window(CHUNK_BYTES, 0),
rate_per_mb,
));
headroom < floor_micro
}
pub(super) async fn pool_funder(&self, pool_id: B256) -> Option<Address> {
self.pool_view_status(pool_id).await.map(|s| s.owner)
}
fn recover_capability_owner(&self, grant: &SignedCapability) -> CapabilityVerifyOutcome {
let domain = &self.voucher_domain;
let signing_hash = grant.capability.signing_hash(domain);
let signature = grant.signature.as_bytes();
let recovered = {
let mut cache = self
.capability_verify_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
cache.get(signing_hash, signature)
};
if let Some(outcome) = recovered {
return outcome;
}
let outcome = match grant.recover_owner(domain) {
Ok(owner) => CapabilityVerifyOutcome::Owner(owner),
Err(_) => CapabilityVerifyOutcome::Invalid,
};
self.capability_verify_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(signing_hash, signature, outcome);
outcome
}
#[allow(clippy::cognitive_complexity)] fn intake_capability(
&self,
pool_id: B256,
signer: Address,
pool_owner: Address,
capability: &decdn_protocol::client::WireCapability,
) {
let spending_cap = capability.spending_cap;
let expiry = capability.expiry;
let Ok(sig_bytes) = <[u8; 65]>::try_from(capability.owner_signature.as_slice()) else {
tracing::debug!(%pool_id, %signer, "dropping capability: owner signature is not a recoverable EOA signature");
return;
};
let Ok(signature) = alloy::primitives::Signature::from_raw(&sig_bytes) else {
tracing::debug!(%pool_id, %signer, "dropping capability: malformed owner signature");
return;
};
let grant = SignedCapability {
capability: Capability {
signer,
spending_cap,
pool_id,
expiry,
},
signature,
};
match self.recover_capability_owner(&grant) {
CapabilityVerifyOutcome::Owner(owner) if owner == pool_owner => {}
CapabilityVerifyOutcome::Owner(recovered) => {
tracing::warn!(
%pool_id,
%signer,
error = %decdn_incentive::capability::CapabilityError::WrongOwner {
expected: pool_owner,
recovered,
},
"dropping capability: owner verification failed"
);
return;
}
CapabilityVerifyOutcome::Invalid => {
tracing::debug!(
%pool_id,
%signer,
error = %decdn_incentive::capability::CapabilityError::InvalidSignature,
"dropping capability: owner verification failed"
);
return;
}
}
let mut lane = LaneState::hydrate(
pool_id,
signer,
self.eth_signer.address(),
U256::from(spending_cap),
expiry,
U256::ZERO,
U256::ZERO,
None,
decdn_incentive::LaneChain::NONE,
);
lane.owner_sig = Some(sig_bytes);
if let Err(e) = self.register_lane(lane) {
tracing::warn!(%pool_id, %signer, error = %e, "lane registration failed; the request refuses as an unknown lane and the client retries");
}
}
pub fn register_lane(&self, state: LaneState) -> Result<(), StoreError> {
let key = state.key();
if self.lanes.contains_key(&key) {
return Ok(());
}
self.pool_state_store.record(&state)?;
if let dashmap::mapref::entry::Entry::Vacant(entry) = self.lanes.entry(key) {
entry.insert(Arc::new(Mutex::new(LaneDeliveryState::hydrated(state))));
self.tune_lane_gauge(1);
}
Ok(())
}
#[cfg(any(test, feature = "test-support"))]
pub fn register_lane_after_next_resolution(&self, state: LaneState) {
if let Ok(mut armed) = self.lane_after_resolution.lock() {
*armed = Some(state);
}
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn fire_lane_after_resolution(&self) {
let armed = self
.lane_after_resolution
.lock()
.ok()
.and_then(|mut armed| armed.take());
if let Some(state) = armed
&& let Err(e) = self.register_lane(state)
{
tracing::warn!(error = %e, "test seam: armed lane registration failed");
}
}
async fn clamp_lane_to_registration(
&self,
lane: &Arc<Mutex<LaneDeliveryState>>,
cap: u64,
expiry: u64,
) {
let mut guard = lane.lock().await;
if !guard.state.clamp_to_registration(cap, expiry) {
return;
}
tracing::debug!(
pool_id = %guard.state.pool_id,
signer = %guard.state.signer,
held_cap = %guard.state.cap,
held_expiry = guard.state.expiry,
"lane clamped to the signer's on-chain registration"
);
if let Err(error) = self.pool_state_store.record(&guard.state) {
tracing::warn!(
pool_id = %guard.state.pool_id,
signer = %guard.state.signer,
%error,
"failed to record a lane clamped to its signer's registration"
);
}
}
pub async fn forget_lane(&self, key: LaneKey) -> Result<(), StoreError> {
if self.lanes.remove(&key).is_some() {
self.tune_lane_gauge(-1);
}
let store = Arc::clone(&self.pool_state_store);
tokio::task::spawn_blocking(move || store.forget(key))
.await
.map_err(|e| StoreError::Backend(format!("forget_lane join: {e}")))?
}
pub(super) const DEPOSIT_REFUSAL_WARN_INTERVAL: Duration = Duration::from_mins(5);
pub(super) const PEER_FAULT_WARN_INTERVAL: Duration = Duration::from_mins(1);
fn log_floor_refusal(&self, refusal: FloorRefusal, at: FloorRefusalSite) {
let FloorRefusalSite {
pool_id,
signer,
hash,
remaining,
ceiling,
} = at;
let headroom = remaining.saturating_sub(self.pool_min_remaining_deposit);
match refusal {
FloorRefusal::PoolExhausted => {
self.log_deposit_refusal(pool_id, hash, headroom, ceiling);
}
FloorRefusal::SignerAtCap { signer_cap } => {
self.log_signer_at_cap(pool_id, signer, hash, signer_cap, ceiling, headroom);
}
}
}
fn log_signer_at_cap(
&self,
pool_id: B256,
signer: Address,
hash: Hash,
signer_cap: U256,
ceiling: U256,
headroom: U256,
) {
tracing::debug!(
%pool_id, %signer, %hash, %signer_cap, %ceiling, %headroom,
"refusing delivery: this capability signer already holds its live concurrency cap \
of un-vouchered floor reservation; the pool itself can still pay"
);
if let Some(suppressed) = self.signer_cap_refusal_warn.admit() {
tracing::warn!(
%pool_id, %signer, %signer_cap, %headroom, suppressed,
interval = ?self.signer_cap_refusal_warn.interval(),
"refusing a paying client: one capability signer is running its whole live \
concurrency cap of un-vouchered streams at once while the pool is solvent. This \
clears as those streams pay; a sustained rate means that signer runs more \
concurrent un-vouchered streams than its cap covers. Rotate the session key or \
widen pool_floor_signer_live_windows — see docs/runbook.md, which covers why \
widening it needs a restart"
);
}
}
pub(super) fn log_deposit_refusal(
&self,
pool_id: B256,
hash: Hash,
headroom: U256,
ceiling: U256,
) {
tracing::debug!(
%pool_id, %hash, %headroom, %ceiling,
"refusing delivery: pool's refundable remaining deposit cannot cover the reserved cost"
);
if let Some(suppressed) = self.deposit_refusal_warn.admit() {
tracing::warn!(
%pool_id, %headroom, %ceiling, suppressed,
interval = ?self.deposit_refusal_warn.interval(),
"refusing paying clients: pool's refundable remaining deposit below the reserved cost. \
A sustained rate here is either a client running dry (no action) or this \
node's chain watcher lagging behind an on-chain top-up (check RPC health) \
— see docs/runbook.md"
);
}
}
pub(super) fn frame_target(&self, unvouchered: u64, interval_bytes: u64, room: u64) -> usize {
debug_assert!(
unvouchered < interval_bytes,
"unvouchered {unvouchered} must stay below the {interval_bytes}-byte interval"
);
let to_boundary = interval_bytes
.checked_sub(unvouchered)
.filter(|remaining| *remaining > 0)
.unwrap_or(interval_bytes);
let want = self
.frame_target_bytes
.min(to_boundary)
.min(room.max(decdn_bao_range::CHUNK_GROUP_BYTES));
usize::try_from(want)
.unwrap_or_else(|_| usize::try_from(decdn_bao_range::CHUNK_GROUP_BYTES).unwrap_or(1))
}
pub(super) fn first_frame_target(&self, interval_bytes: u64, opening_window: u64) -> usize {
let cap = usize::try_from(FIRST_FRAME_BYTES).unwrap_or(usize::MAX);
self.frame_target(0, interval_bytes, opening_window)
.min(cap)
}
pub(super) fn credit_window(&self, interval_bytes: u64, paid: u64) -> u64 {
decdn_incentive::ramped_credit_window(
self.credit_ramp_divisor,
interval_bytes.saturating_add(decdn_client::VERIFY_LAG_BYTES),
self.credit_max,
paid,
)
}
const fn ramp_credit_cap(&self) -> u64 {
self.credit_max.saturating_mul(self.credit_ramp_divisor)
}
async fn take_ramp_carry(
&self,
lane: Option<&Arc<Mutex<LaneDeliveryState>>>,
) -> ramp::RampCarry {
let Some(lane) = lane else {
return ramp::RampCarry::detached();
};
let pool = Arc::clone(&lane.lock().await.ramp_pool);
ramp::RampCarry::take(pool, self.ramp_credit_cap())
}
pub(super) fn pool_remaining_covers_window(
&self,
remaining: U256,
reserved_bytes: u64,
rate_per_mb: u64,
) -> bool {
let refundable_headroom = remaining.saturating_sub(self.pool_min_remaining_deposit);
refundable_headroom >= min_payment(reserved_bytes, rate_per_mb)
}
#[cfg(test)]
pub(super) fn reserve_floor(
&self,
pool_id: B256,
signer: Address,
reserved: U256,
) -> FloorReservation {
FloorReservation::reserve(Arc::clone(&self.pool_floor), pool_id, signer, reserved)
}
pub(super) fn one_window(&self, rate_per_mb: u64) -> U256 {
min_payment(self.credit_window(CHUNK_BYTES, 0), rate_per_mb)
}
pub(super) fn signer_floor_cap(&self, rate_per_mb: u64) -> U256 {
let one_window = self.one_window(rate_per_mb);
let k = self.pool_floor_signer_live_windows.max(1);
one_window.saturating_mul(U256::from(k))
}
pub(super) fn pool_budget_covers_reserve(
&self,
pool_id: B256,
remaining: U256,
new_reserve: U256,
) -> bool {
let committed = {
let guard = self
.pool_floor
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard
.get(&pool_id)
.map_or(U256::ZERO, PoolFloorState::live_committed)
};
decdn_incentive::pool_budget_covers(
remaining,
self.pool_min_remaining_deposit,
committed,
new_reserve,
)
}
pub(super) fn try_reserve_floor(
&self,
pool_id: B256,
signer: Address,
remaining: U256,
rate_per_mb: u64,
reserved: U256,
) -> Result<FloorReservation, FloorRefusal> {
let signer_cap = self.signer_floor_cap(rate_per_mb);
let epoch;
{
let mut guard = self
.pool_floor
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (pool_live, signer_live) = match guard.get(&pool_id) {
Some(entry) => {
let signer_live = entry
.signers
.get(&signer)
.map_or(U256::ZERO, |lane| lane.live_reservation);
(entry.live_reservation, signer_live)
}
None => (U256::ZERO, U256::ZERO),
};
if !decdn_incentive::pool_budget_covers(
remaining,
self.pool_min_remaining_deposit,
pool_live,
reserved,
) {
return Err(FloorRefusal::PoolExhausted);
}
if signer_live.saturating_add(reserved) > signer_cap {
return Err(FloorRefusal::SignerAtCap { signer_cap });
}
let entry = guard.entry(pool_id).or_default();
entry.live_reservation = entry.live_reservation.saturating_add(reserved);
let lane = entry.signers.entry(signer).or_default();
lane.live_reservation = lane.live_reservation.saturating_add(reserved);
epoch = entry.epoch;
}
Ok(FloorReservation::new_charged(
Arc::clone(&self.pool_floor),
pool_id,
signer,
reserved,
epoch,
))
}
pub(crate) fn forget_pool_floor(&self, pool_id: B256) {
let mut guard = self
.pool_floor
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.remove(&pool_id);
}
pub(super) fn takedown_landed(&self, hash: Hash, funder: Option<Address>) -> bool {
self.cache.refuses(hash)
|| funder.is_some_and(|addr| self.content_deny.is_origin_denied(&addr))
}
pub(super) fn terminate_for_takedown(send: &mut SendStream, recv: &mut RecvStream, hash: Hash) {
tracing::warn!(
%hash,
"terminating an in-flight delivery: a takedown landed after the stream opened"
);
reset_stream(send, recv, APP_ERR_NO_ERROR);
}
fn tune_lane_gauge(&self, delta: isize) {
let _publish = self
.lane_gauge_publish
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let open = if delta >= 0 {
self.lane_count
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1)
} else {
self.lane_count
.fetch_sub(1, Ordering::Relaxed)
.saturating_sub(1)
};
self.metrics.set_lanes_open(open);
}
}
enum VoucherStop {
Continue { credited_bytes: u64 },
Rejected,
}
#[derive(Clone, Debug)]
pub struct ClientProtocol(Arc<ClientHandler>);
impl ClientProtocol {
pub const ALPN: &'static [u8] = ALPN_CLIENT;
#[must_use]
pub const fn new(handler: Arc<ClientHandler>) -> Self {
Self(handler)
}
}
impl ProtocolHandler for ClientProtocol {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
Arc::clone(&self.0)
.serve(connection)
.await
.map_err(|e| AcceptError::from_boxed(e.into()))
}
}
struct StreamReadError {
err: anyhow::Error,
app_code: u32,
}
const fn frame_err_code(e: &FrameError) -> u32 {
match e {
FrameError::TooLarge(_) => 0x02,
FrameError::Io(_) => APP_ERR_NO_ERROR,
FrameError::Varint | FrameError::Decode(_) => APP_ERR_MALFORMED_MESSAGE,
}
}
fn reset_stream(send: &mut SendStream, recv: &mut RecvStream, code: u32) {
let v = VarInt::from_u32(code);
let _ = send.reset(v);
let _ = recv.stop(v);
}
pub(super) async fn drain_recv_to_fin(recv: &mut RecvStream) {
let _ = tokio::time::timeout(POST_END_DRAIN_TIMEOUT, recv.read_to_end(64 * 1024)).await;
}
enum FirstMessage {
Delivery(StreamRequest, StreamRequestExt),
}
async fn read_first_message(recv: &mut RecvStream) -> Result<FirstMessage, StreamReadError> {
let frame = match tokio::time::timeout(REQUEST_READ_TIMEOUT, read_frame(recv)).await {
Err(_) => {
return Err(StreamReadError {
err: anyhow::anyhow!("stream request timed out after {REQUEST_READ_TIMEOUT:?}"),
app_code: APP_ERR_NO_ERROR,
});
}
Ok(Err(e)) => {
let app_code = frame_err_code(&e);
return Err(StreamReadError {
err: anyhow::anyhow!("stream request frame read failed: {e}"),
app_code,
});
}
Ok(Ok(frame)) => frame,
};
match decode_message::<ClientMessage>(&frame) {
Ok((ClientMessage::StreamRequest(req), remainder)) => {
let ext = decdn_protocol::parse_stream_request_ext(remainder).map_err(|e| {
StreamReadError {
err: anyhow::anyhow!("stream request ext decode failed: {e}"),
app_code: APP_ERR_MALFORMED_MESSAGE,
}
})?;
ext.validate().map_err(|e| StreamReadError {
err: anyhow::anyhow!("stream request ext rejected: {e}"),
app_code: APP_ERR_MALFORMED_MESSAGE,
})?;
Ok(FirstMessage::Delivery(req, ext))
}
Ok((_, _)) => Err(StreamReadError {
err: anyhow::anyhow!("expected StreamRequest"),
app_code: APP_ERR_UNSUPPORTED_MESSAGE,
}),
Err(e) => {
let app_code = if is_unknown_variant::<ClientMessage>(&frame) {
APP_ERR_UNSUPPORTED_MESSAGE
} else {
APP_ERR_MALFORMED_MESSAGE
};
Err(StreamReadError {
err: anyhow::anyhow!("stream request decode failed: {e}"),
app_code,
})
}
}
}
const MAX_PROOFS_PER_CHUNK: u32 = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Proof {
Voucher(decdn_protocol::client::Voucher),
Preimage(decdn_protocol::client::ChunkPreimage),
}
#[derive(Default)]
pub(super) struct BufferedProofReader {
buf: Vec<u8>,
}
impl BufferedProofReader {
pub(super) async fn read(&mut self, recv: &mut RecvStream) -> anyhow::Result<Proof> {
loop {
if let Some(result) = self.take_buffered()? {
return result;
}
let mut scratch = [0u8; 4096];
let n = AsyncReadExt::read(recv, &mut scratch).await.map_err(|e| {
anyhow::Error::new(wire::PeerFault)
.context(format!("proof stream read failed: {e}"))
})?;
if n == 0 {
return Err(
anyhow::Error::new(wire::PeerFault).context("proof stream closed mid-frame")
);
}
let chunk = scratch
.get(..n)
.ok_or_else(|| anyhow::anyhow!("short read length out of range"))?;
self.buf.extend_from_slice(chunk);
}
}
fn take_buffered(&mut self) -> anyhow::Result<Option<anyhow::Result<Proof>>> {
let Some((header_len, payload_len)) = decdn_protocol::framing::parse_frame(&self.buf)
.map_err(|e| {
anyhow::Error::new(wire::PeerFault)
.context(format!("proof frame parse failed: {e}"))
})?
else {
return Ok(None);
};
let total = header_len.saturating_add(payload_len);
let payload = self
.buf
.get(header_len..total)
.ok_or_else(|| anyhow::anyhow!("proof frame bounds out of range"))?;
let decoded = decode_message::<ClientMessage>(payload);
let result =
match decoded {
Ok((ClientMessage::Voucher(v), _)) => Ok(Proof::Voucher(v)),
Ok((ClientMessage::ChunkPreimage(p), _)) => Ok(Proof::Preimage(p)),
Ok((_, _)) => Err(anyhow::Error::new(wire::PeerFault)
.context("expected ClientMessage::Voucher or ClientMessage::ChunkPreimage")),
Err(e) => Err(anyhow::Error::new(wire::PeerFault)
.context(format!("proof decode failed: {e}"))),
};
self.buf.drain(..total);
Ok(Some(result))
}
}
#[cfg(test)]
fn always_admit_shed() -> Arc<crate::load_shed::LoadShedController> {
crate::load_shed::LoadShedController::from_config(&decdn_common::config::ResolvedLoadShed {
policy: decdn_common::config::LoadShedPolicyKind::AlwaysAdmit,
..Default::default()
})
}
#[cfg(test)]
#[allow(clippy::expect_used)]
pub(super) async fn handler_over_store(
metrics: &Arc<Metrics>,
store: Arc<dyn PoolStateStore>,
) -> (Arc<ClientHandler>, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("tempdir");
let cache = CacheEngine::open(dir.path(), Vec::new(), 16)
.await
.expect("cache");
let domain = alloy::sol_types::eip712_domain! { name: "t", version: "1", };
let deps = ClientHandlerDeps::new(
iroh::SecretKey::generate().public(),
Arc::clone(metrics),
Arc::new(ConnectionLimiter::new(
&decdn_common::config::ResolvedSecurity {
max_concurrent_handlers: u32::MAX,
per_source_rate_per_sec: 1e9,
per_source_burst: u32::MAX,
max_tracked_sources: 16,
},
Arc::clone(metrics),
)),
cache,
Arc::new(alloy::signers::local::PrivateKeySigner::random()),
domain.clone(),
domain.clone(),
domain,
store,
Arc::new(crate::receipt_log::DirectReceiptSink::new(Arc::new(
crate::receipt_log::NoopReceiptLog,
))) as Arc<dyn ReceiptSink>,
1,
16,
Arc::new(crate::content_deny::ContentDenylist::empty()),
U256::ZERO,
always_admit_shed(),
);
let handler = ClientHandler::new(deps).expect("handler");
(Arc::new(handler), dir)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests;