use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::Address;
use alloy::signers::local::PrivateKeySigner;
use decdn_client::discovery::NodeCandidate;
use decdn_client::source::{SourceFuture, SourceStream};
use decdn_client::{
CredentialSlot, Holder, LaneLease, LaneLedgers, NoAffordableSource, PeerHealth, PeerSource,
PoolContext, RecoveryGate, SourceProvider, SourceSet, StopPolicy, StreamCandidate, first_open,
};
use decdn_common::cli;
use decdn_incentive::buyer_pool::BuyerPoolStore as _;
use decdn_incentive::{CapabilityGrant, PoolId};
use iroh::RelayUrl;
use super::bundle_pull::LaneStreamCap;
use super::fetch::{
self, CliFunder, DriveFetchDeps, FaceLaneHandle, LateProbes, ProbeOutcome, ProxyWarmingParams,
ResolvedTargets,
};
pub(crate) struct FirstClaim {
pub(crate) total_bytes: u64,
pub(crate) holders: Vec<Holder>,
}
async fn first_claim_or_open<F, Fut>(
hint: Option<u64>,
holders: Vec<Holder>,
open: F,
) -> anyhow::Result<FirstClaim>
where
F: FnOnce(Vec<Holder>) -> Fut,
Fut: std::future::Future<Output = anyhow::Result<FirstClaim>>,
{
match hint {
Some(total_bytes) => Ok(FirstClaim {
total_bytes,
holders,
}),
None => open(holders).await,
}
}
pub(crate) struct CliSources<'a, P> {
deps: &'a DriveFetchDeps<'a, P>,
common: &'a cli::ClientFetchArgs,
relays: &'a [RelayUrl],
grant: Option<&'a CapabilityGrant>,
signer: &'a Arc<PrivateKeySigner>,
voucher_dom: &'a Eip712Domain,
open_lock: Option<&'a tokio::sync::Mutex<()>>,
ledgers: Option<&'a LaneLedgers>,
lane_cap: Option<&'a LaneStreamCap>,
recovery: &'a Arc<RecoveryGate>,
nodes: Mutex<HashMap<Address, NodeCandidate>>,
peer_store: decdn_client::PeerStore,
handles: Mutex<Vec<FaceLaneHandle>>,
parked: Mutex<HashMap<Address, StreamCandidate<PeerSource<'a>>>>,
size_hint: Mutex<Option<u64>>,
late: Mutex<Option<LateProbes>>,
}
impl<'a, P> CliSources<'a, P>
where
P: alloy::providers::Provider + Clone,
{
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
deps: &'a DriveFetchDeps<'a, P>,
common: &'a cli::ClientFetchArgs,
relays: &'a [RelayUrl],
grant: Option<&'a CapabilityGrant>,
signer: &'a Arc<PrivateKeySigner>,
voucher_dom: &'a Eip712Domain,
open_lock: Option<&'a tokio::sync::Mutex<()>>,
ledgers: Option<&'a LaneLedgers>,
lane_cap: Option<&'a LaneStreamCap>,
recovery: &'a Arc<RecoveryGate>,
) -> Self {
Self {
deps,
common,
relays,
grant,
signer,
voucher_dom,
open_lock,
ledgers,
lane_cap,
recovery,
nodes: Mutex::new(HashMap::new()),
peer_store: decdn_client::PeerStore::open(&deps.chain.data_dir),
handles: Mutex::new(Vec::new()),
parked: Mutex::new(HashMap::new()),
size_hint: Mutex::new(None),
late: Mutex::new(None),
}
}
pub(crate) fn holders_from(&self, targets: &ResolvedTargets) -> Vec<Holder> {
if let Some(hint) = targets.size_hint {
*self
.size_hint
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(hint);
}
if let Some(late) = targets.late.take() {
*self.late.lock().unwrap_or_else(PoisonError::into_inner) = Some(late);
}
holders_from(targets, &mut self.lock_nodes())
}
fn register_late(&self, join: LateJoin) -> Option<Holder> {
let holder = admit_late(&mut self.lock_nodes(), join)?;
if let Some(timings) = self.deps.timings {
timings.holder_joined();
}
Some(holder)
}
pub(crate) fn funder(&self) -> CliFunder<'_, P> {
CliFunder {
contract: self.deps.contract,
rpc: self.deps.rpc,
store: self.deps.store,
owner: self.deps.self_address,
signer: self.signer,
deployment: self.deps.chain.deployment(),
token: self.deps.token,
payment_pool_addr: self.deps.chain.payment_pool,
working_deposit: self.deps.chain.working_deposit,
max_approve: self.deps.chain.max_approve,
funding: self.deps.funding,
}
}
pub(crate) fn credentials(&self) -> Option<CredentialSlot> {
let grant = self.grant?;
let capability = grant.to_signed_capability().ok()?;
let slot = CredentialSlot::new(Arc::clone(self.signer), capability)
.ok()?
.with_swap_wait(std::time::Duration::ZERO);
let signer = self.signer.address();
let row = self
.deps
.store
.get_by_pool_id(grant.pool_id)
.unwrap_or_else(|error| {
tracing::warn!(
pool_id = %grant.pool_id,
%error,
"could not read the buyer store; this key's earlier spend counts as zero"
);
None
});
let recorded = row.map_or(alloy::primitives::U256::ZERO, |row| {
row.lanes()
.filter(|(lane, _)| lane.signer == signer)
.fold(alloy::primitives::U256::ZERO, |acc, (_, progress)| {
acc.saturating_add(progress.last_amount)
})
});
slot.record_spend(signer, recorded);
Some(slot)
}
pub(crate) const fn recovery(&self) -> &Arc<RecoveryGate> {
self.recovery
}
pub(crate) async fn first_claim(
&self,
hash: [u8; 32],
holders: Vec<Holder>,
health: &Arc<PeerHealth>,
stop: &StopPolicy,
) -> anyhow::Result<FirstClaim> {
let hint = *self
.size_hint
.lock()
.unwrap_or_else(PoisonError::into_inner);
first_claim_or_open(hint, holders, |holders| {
self.open_for_header(hash, holders, health, stop)
})
.await
}
async fn open_for_header(
&self,
hash: [u8; 32],
holders: Vec<Holder>,
health: &Arc<PeerHealth>,
stop: &StopPolicy,
) -> anyhow::Result<FirstClaim> {
let mut set = SourceSet::new(self, hash, Arc::clone(health), holders);
let funder = self.funder();
let credentials = self.credentials();
let opened = first_open(
&mut set,
stop,
&funder,
self.recovery,
credentials.as_ref(),
|lane| async move {
let (header, _whole) = lane.source.open_whole(hash).await?;
let provider = lane
.ctx
.lock()
.unwrap_or_else(PoisonError::into_inner)
.provider;
self.record_open(provider, header.rate_per_mb);
Ok(header.total_bytes)
},
)
.await;
let answered = opened.as_ref().ok().map(|&(provider, _)| provider);
if let Some((provider, lane)) = keep_answering(set.take_lanes(), answered) {
self.parked
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(provider, lane);
}
let holders = set.holders().to_vec();
opened.map(|(_signer, total_bytes)| FirstClaim {
total_bytes,
holders,
})
}
pub(crate) async fn persist_watermarks(&self) {
if self.queue_watermarks().await.is_ok() {
return;
}
let (pools, providers): (Vec<_>, Vec<_>) = self
.handles
.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.map(|h| (h.pool_id.to_string(), h.provider.to_string()))
.unzip();
tracing::warn!(
lanes = providers.len(),
pools = %pools.join(","),
providers = %providers.join(","),
"the voucher watermark write for {} lane(s) did not finish: it panicked or the \
runtime cancelled it; the next reuse may re-sign a stale watermark, which its \
provider rejects",
providers.len()
);
}
pub(crate) fn persist_watermarks_detached(&self) {
drop(self.queue_watermarks());
}
fn queue_watermarks(&self) -> tokio::sync::oneshot::Receiver<()> {
let handles = self.handles.lock().unwrap_or_else(PoisonError::into_inner);
fetch::queue_face_watermarks(
self.deps.writes,
self.deps.store,
self.deps.self_address,
&handles,
)
}
pub(crate) fn annotate(&self, err: anyhow::Error) -> anyhow::Error {
let first_ctx = self
.handles
.lock()
.unwrap_or_else(PoisonError::into_inner)
.first()
.map(|h| Arc::clone(&h.ctx));
let err = match first_ctx {
Some(ctx) => annotate_with(err, &ctx),
None => err,
};
if self.grant.is_some() {
return fetch::annotate_delegated_exhaustion(err);
}
with_top_up_hint(err, self.deps.funding.pool_id())
}
fn record_open(&self, provider: Address, rate_per_mb: u64) {
let Some(node_id) = self.lock_nodes().get(&provider).map(|n| n.node_id) else {
return;
};
self.file(PeerEvent::Open {
node_id,
rate_per_mb,
});
}
fn file(&self, event: PeerEvent) {
let store = self.peer_store.clone();
self.deps.writes.queue(move || event.write(&store));
}
fn lock_nodes(&self) -> std::sync::MutexGuard<'_, HashMap<Address, NodeCandidate>> {
self.nodes.lock().unwrap_or_else(PoisonError::into_inner)
}
async fn build(&self, holder: &Holder) -> anyhow::Result<StreamCandidate<PeerSource<'a>>> {
let parked = self
.parked
.lock()
.unwrap_or_else(PoisonError::into_inner)
.remove(&holder.provider);
let mut lane = if let Some(lane) = parked {
lane
} else {
let lease = lane_lease(self.lane_cap, holder.provider).await?;
let mut lane = self.build_fresh(holder).await?;
lane.lease = lease;
lane
};
if lane.widen.is_none()
&& let Some(cap) = self.lane_cap
{
lane.widen = Some(cap.widen(holder.provider).await);
}
Ok(lane)
}
async fn build_fresh(
&self,
holder: &Holder,
) -> anyhow::Result<StreamCandidate<PeerSource<'a>>> {
let candidate = self
.lock_nodes()
.get(&holder.provider)
.cloned()
.ok_or_else(|| anyhow::anyhow!("no known node for provider {}", holder.provider))?;
let target = fetch::lane_target(&candidate, self.common.addr, self.relays);
let lane = fetch::build_multi_lane(
self.deps,
self.grant,
self.signer,
self.voucher_dom,
holder.provider,
target,
self.open_lock,
self.ledgers,
)
.await?;
self.deps.funding.note_pool(lane.pool_id);
self.handles
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(FaceLaneHandle {
pool_id: lane.pool_id,
provider: lane.provider,
prior_amount: lane.prior_amount,
ledger: Arc::clone(&lane.ledger),
ctx: Arc::clone(&lane.ctx),
});
let mut candidate = StreamCandidate::new(lane.source, lane.ctx, lane.ledger);
candidate.coverage.clone_from(&holder.coverage);
Ok(candidate)
}
}
async fn lane_lease(cap: Option<&LaneStreamCap>, provider: Address) -> anyhow::Result<LaneLease> {
let Some(cap) = cap else {
return Ok(LaneLease::default());
};
cap.try_permit(provider)
.await
.map(LaneLease::new)
.ok_or_else(|| {
anyhow::anyhow!(
"every stream permit for provider {provider} is taken; the lane builds once \
one frees"
)
})
}
fn keep_answering<L>(
lanes: Vec<(Address, Arc<L>)>,
answered: Option<Address>,
) -> Option<(Address, L)> {
let answered = answered?;
lanes
.into_iter()
.find(|(provider, _)| *provider == answered)
.and_then(|(provider, lane)| Arc::try_unwrap(lane).ok().map(|lane| (provider, lane)))
}
fn with_top_up_hint(err: anyhow::Error, pool_id: Option<PoolId>) -> anyhow::Error {
if err.downcast_ref::<NoAffordableSource>().is_none() {
return err;
}
let pool = pool_id.map_or_else(|| "<pool id>".to_string(), |id| id.to_string());
err.context(format!(
"top up with: decdn pool top-up --pool {pool} --amount-micro-usdc <MICRO_USDC>"
))
}
fn annotate_with(err: anyhow::Error, ctx: &Mutex<PoolContext>) -> anyhow::Error {
match ctx.lock() {
Ok(guard) => fetch::annotate_unbound_cache_miss(err, &guard),
Err(_) => err,
}
}
impl<'a, P> SourceProvider for CliSources<'a, P>
where
P: alloy::providers::Provider + Clone,
{
type Source = PeerSource<'a>;
fn discover(&self, hash: [u8; 32]) -> SourceFuture<'_, Vec<Holder>> {
Box::pin(async move {
let mut args = self.common.clone();
args.rediscover = true;
let probe = fetch::ProbeOpts {
timings: None,
round: fetch::ProbeRound::Settle,
};
let targets = fetch::resolve_target_node(
&args,
self.deps.chain,
self.deps.endpoint,
self.relays,
hash,
probe,
)
.await?;
Ok(self.holders_from(&targets))
})
}
fn arrivals(&self) -> Option<SourceStream<'_, Holder>> {
use futures_util::StreamExt as _;
let LateProbes {
tail,
best_holder_rtt_ms,
warming,
} = self
.late
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()?;
let dir = self.deps.chain.data_dir.clone();
Some(Box::pin(futures_util::stream::unfold(
(tail, best_holder_rtt_ms, Vec::new()),
move |(mut tail, mut best, mut samples)| {
let dir = dir.clone();
async move {
while let Some(outcome) = tail.next().await {
if let Some(sample) = outcome.sample() {
samples.push(sample);
}
if let Some(holder) = late_join(outcome, &mut best, warming)
.and_then(|join| self.register_late(join))
{
return Some((holder, (tail, best, samples)));
}
}
if !samples.is_empty() {
drop(fetch::spawn_harvest(&dir, Vec::new(), samples));
}
None
}
},
)))
}
fn connect<'b>(
&'b self,
holder: &'b Holder,
) -> SourceFuture<'b, StreamCandidate<Self::Source>> {
Box::pin(self.build(holder))
}
fn on_source_fault(&self, holder: &Holder) {
let Some(node_id) = self.lock_nodes().get(&holder.provider).map(|n| n.node_id) else {
return;
};
self.file(PeerEvent::Failure {
node_id,
at_secs: fetch::now_secs_cli(),
});
}
}
#[derive(Debug, Clone, Copy)]
enum PeerEvent {
Open {
node_id: iroh::PublicKey,
rate_per_mb: u64,
},
Failure {
node_id: iroh::PublicKey,
at_secs: u64,
},
}
impl PeerEvent {
fn write(self, store: &decdn_client::PeerStore) {
match self {
Self::Open {
node_id,
rate_per_mb,
} => {
if let Err(e) = store.record_open(&node_id, rate_per_mb) {
tracing::debug!(
%node_id,
"could not record a first open in the peer store: {e:#}"
);
}
}
Self::Failure { node_id, at_secs } => {
if let Err(e) = store.record_failure(&node_id, at_secs) {
tracing::debug!(
%node_id,
"could not record a source fault in the peer store: {e:#}"
);
}
}
}
}
}
pub(crate) enum LateJoin {
Holder(decdn_client::discovery::Probed),
Proxy(decdn_client::discovery::WarmingCandidate),
}
pub(crate) fn late_join(
outcome: ProbeOutcome,
best_holder_rtt_ms: &mut f64,
warming: ProxyWarmingParams,
) -> Option<LateJoin> {
match outcome {
ProbeOutcome::Holder(holder, _) => {
*best_holder_rtt_ms = best_holder_rtt_ms.min(holder.rtt_ms);
Some(LateJoin::Holder(holder))
}
ProbeOutcome::NonHolder(candidate, _) => {
let picked = warming.enabled
&& !decdn_client::discovery::proxy_warming_order(
*best_holder_rtt_ms,
warming.rtt_threshold_ms,
warming.margin_ms,
std::slice::from_ref(&candidate),
)
.is_empty();
picked.then_some(LateJoin::Proxy(candidate))
}
ProbeOutcome::Unreachable
| ProbeOutcome::RateLimited
| ProbeOutcome::Unverifiable
| ProbeOutcome::Unusable => None,
}
}
pub(crate) fn admit_late(
nodes: &mut HashMap<Address, NodeCandidate>,
join: LateJoin,
) -> Option<Holder> {
let (candidate, holder) = match join {
LateJoin::Holder(probed) => {
let holder = Holder {
provider: probed.candidate.eth_address,
coverage: Some(probed.coverage),
rtt_ms: probed.rtt_ms,
probed_holder: true,
};
(probed.candidate, holder)
}
LateJoin::Proxy(proxy) => {
let holder = Holder {
provider: proxy.eth_address,
coverage: None,
rtt_ms: proxy.rtt_ms,
probed_holder: false,
};
let candidate = NodeCandidate {
node_id: proxy.node_id,
eth_address: proxy.eth_address,
region_hint: None,
multiaddrs: proxy.multiaddrs,
};
(candidate, holder)
}
};
if nodes.contains_key(&candidate.eth_address) {
return None;
}
nodes.insert(candidate.eth_address, candidate);
Some(holder)
}
pub(crate) fn holders_from(
targets: &ResolvedTargets,
nodes: &mut HashMap<Address, NodeCandidate>,
) -> Vec<Holder> {
let rtt: HashMap<_, f64> = targets
.probed_samples
.iter()
.map(|&(node_id, rtt_ms, _)| (node_id, rtt_ms))
.collect();
let mut holders: Vec<Holder> = Vec::with_capacity(targets.candidates.len());
for (rank, candidate) in targets.candidates.iter().enumerate() {
if holders.iter().any(|h| h.provider == candidate.eth_address) {
continue;
}
let rtt_ms = rtt
.get(&candidate.node_id)
.copied()
.unwrap_or_else(|| f64::from(u32::try_from(rank).unwrap_or(u32::MAX)));
let coverage = targets.coverage_by_node.get(&candidate.node_id).cloned();
holders.push(Holder {
provider: candidate.eth_address,
probed_holder: targets.pinned || coverage.is_some(),
coverage,
rtt_ms,
});
nodes.insert(candidate.eth_address, candidate.clone());
}
holders
}
#[cfg(test)]
mod tests;