use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::{Address, U256};
use alloy::providers::Provider;
use alloy::signers::local::PrivateKeySigner;
use anyhow::{Context, Result};
use decdn_client::buyer_pool::{
ProgressWrite, WatermarkRow, ensure_allowance, open_pool, self_owned_lane_ctx,
};
use decdn_client::discovery::{self, NodeCandidate};
use decdn_client::{
Funder, PeerSource, PoolContext, PoolLedger, PullDeadlines, Recovery, SourceFuture,
StreamCandidate, VoucherProgress, effective_rate_ceiling, endpoint, probe, provider,
};
use decdn_incentive::buyer_pool_redb::RedbBuyerPoolStore;
use decdn_incentive::eth_identity::load_signer;
use decdn_incentive::payment_pool::PaymentPool;
use decdn_incentive::{
BuyerPoolState, BuyerPoolStore, Deployment, LaneKey, bind_node_id_domain, slash_judge_domain,
};
use decdn_protocol::Coverage;
use iroh::{Endpoint, EndpointAddr};
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
const PROBE_SAMPLE: usize = 16;
pub(crate) struct Holder {
candidate: NodeCandidate,
coverage: Coverage,
rate_per_mb: u64,
}
pub(crate) struct Env {
rpc_url: String,
chain_id: u64,
payment_pool: Address,
slash_judge: Address,
capacity_bond: Address,
keystore: PathBuf,
keystore_password: String,
data_dir: PathBuf,
deposit: U256,
}
impl Env {
pub(crate) fn from_env() -> Result<Self> {
fn var(name: &str) -> Result<String> {
std::env::var(name).with_context(|| format!("set {name}"))
}
Ok(Self {
rpc_url: var("DECDN_RPC_URL")?,
chain_id: var("DECDN_CHAIN_ID")?.parse()?,
payment_pool: var("DECDN_PAYMENT_POOL")?.parse()?,
slash_judge: var("DECDN_SLASH_JUDGE")?.parse()?,
capacity_bond: var("DECDN_CAPACITY_BOND")?.parse()?,
keystore: var("DECDN_KEYSTORE")?.into(),
keystore_password: var("DECDN_KEYSTORE_PASSWORD")?,
data_dir: var("DECDN_DATA_DIR")?.into(),
deposit: var("DECDN_DEPOSIT")?.parse()?,
})
}
}
pub(crate) struct Buyer {
env: Env,
signer: Arc<PrivateKeySigner>,
store: RedbBuyerPoolStore,
pool: BuyerPoolState,
voucher_domain: Eip712Domain,
slash_domain: Eip712Domain,
bind_domain: Eip712Domain,
endpoint: Endpoint,
}
impl Buyer {
pub(crate) async fn connect(env: Env) -> Result<Self> {
let signer = Arc::new(load_signer(&env.keystore, &env.keystore_password)?);
let owner = signer.address();
let rpc = provider::build_provider(&env.rpc_url, &signer)?;
let contract = PaymentPool::new(env.payment_pool, rpc.clone());
let deployment = Deployment {
chain_id: env.chain_id,
payment_pool: env.payment_pool,
};
let voucher_domain = deployment.voucher_domain();
let slash_domain = slash_judge_domain(env.chain_id, env.slash_judge);
let bind_domain = bind_node_id_domain(env.chain_id, env.capacity_bond);
let store = RedbBuyerPoolStore::open(&env.data_dir)?;
let reusable = store
.get_by_owner(owner)?
.filter(|pool| pool.is_on(deployment));
let pool = if let Some(pool) = reusable {
pool
} else {
let token = contract.usdc().call().await?;
ensure_allowance(&rpc, token, owner, env.payment_pool, Some(env.deposit)).await?;
let opened = open_pool(
&contract,
Arc::clone(&signer),
deployment,
token,
owner,
env.deposit,
)
.await?;
store
.record(&opened.state)
.with_context(|| format!("record the pool opened in {}", opened.tx))?;
opened.state
};
let relays = endpoint::resolve_relays(None, None)?;
let discovery = endpoint::client_discovery(None)?;
let endpoint = endpoint::client_endpoint(&relays, &discovery).await?;
Ok(Self {
env,
signer,
store,
pool,
voucher_domain,
slash_domain,
bind_domain,
endpoint,
})
}
pub(crate) async fn holders(&self, hash: [u8; 32]) -> Result<(Vec<Holder>, u64)> {
let bootstrap = discovery::bootstrap_nodes(
&self.env.rpc_url,
self.env.capacity_bond,
&self.env.data_dir,
PROBE_TIMEOUT,
)
.await?;
if let Some(warning) = bootstrap.warning() {
tracing::warn!("{warning}");
}
let sample = discovery::select_candidates(bootstrap.into_peers(), None, PROBE_SAMPLE);
let mut holders = Vec::new();
let mut total_bytes = None;
for candidate in sample {
let timestamp_us = now_us()?;
let Ok((resp, ext, _rtt_ms)) = probe::probe_once(
&self.endpoint,
dial_target(&candidate),
hash,
timestamp_us,
PROBE_TIMEOUT,
)
.await
else {
continue;
};
let verified = probe::verify_probe_response(
&resp,
candidate.eth_address,
&self.slash_domain,
hash,
timestamp_us,
);
if verified.is_err() || !ext.consistent_with(resp.body.has_blob) || !resp.body.has_blob
{
continue;
}
total_bytes = total_bytes.or(ext.total_bytes);
holders.push(Holder {
candidate,
coverage: ext.coverage,
rate_per_mb: resp.body.rate_per_mb,
});
}
let total_bytes = total_bytes.context("no verified holder reported the blob's size")?;
Ok((holders, total_bytes))
}
pub(crate) async fn lanes(
&self,
holders: Vec<Holder>,
) -> Result<(Vec<StreamCandidate<PeerSource<'_>>>, Vec<Lane>)> {
let deadlines = PullDeadlines::new(Duration::from_secs(30), Duration::from_secs(30), 4096)?;
let contract = PaymentPool::new(
self.env.payment_pool,
provider::build_provider(&self.env.rpc_url, &self.signer)?,
);
let mut candidates = Vec::with_capacity(holders.len());
let mut lanes = Vec::with_capacity(holders.len());
for holder in holders {
let provider = holder.candidate.eth_address;
let lane = LaneKey {
pool_id: self.pool.pool_id,
signer: self.signer.address(),
provider,
};
let (prior_bytes, prior_amount) = prior_payment(&self.pool, &contract, lane).await?;
let ctx = self_owned_lane_ctx(
&self.pool,
&self.signer,
&self.voucher_domain,
provider,
prior_bytes,
prior_amount,
)?
.bind_endpoint(&self.endpoint, &self.bind_domain)?;
let ledger = Arc::new(ctx.new_ledger());
let ctx: Arc<Mutex<PoolContext>> = Arc::new(Mutex::new(ctx));
let source = PeerSource::new(
&self.endpoint,
dial_target(&holder.candidate),
Arc::clone(&ctx),
Arc::clone(&ledger),
&self.slash_domain,
provider,
decdn_protocol::client::NO_NAMESPACE,
0,
effective_rate_ceiling(holder.rate_per_mb, 0),
deadlines,
None,
);
lanes.push(Lane {
key: lane,
prior_amount,
ledger: Arc::clone(&ledger),
});
let mut candidate = StreamCandidate::new(source, ctx, ledger);
candidate.coverage = Some(holder.coverage);
candidates.push(candidate);
}
Ok((candidates, lanes))
}
pub(crate) fn record_payments(&self, lanes: &[Lane]) {
for lane in lanes {
let anchor = lane.ledger.take_unsaved_rebase();
let progress =
VoucherProgress::from_cumulative(lane.ledger.settlement(), lane.prior_amount)
.with_rebase_anchor(anchor);
let Some(write) = ProgressWrite::of(&progress) else {
continue;
};
let owner = self.signer.address();
let row = WatermarkRow::Owned(owner);
if let Err(err) = write.apply(&self.store, row, lane.key) {
tracing::warn!(provider = %lane.key.provider, error = %err, "lane not recorded");
}
}
}
}
pub(crate) struct Lane {
key: LaneKey,
prior_amount: U256,
ledger: Arc<PoolLedger>,
}
pub(crate) struct NoFunding;
impl Funder for NoFunding {
fn recover(&self, _remaining: U256) -> SourceFuture<'_, Recovery> {
Box::pin(async { Ok(Recovery::Unavailable) })
}
}
async fn prior_payment<P: Provider + Clone>(
pool: &BuyerPoolState,
contract: &PaymentPool::PaymentPoolInstance<P>,
lane: LaneKey,
) -> Result<(U256, U256)> {
if let Some(progress) = pool.lane_progress(lane) {
return Ok((progress.last_bytes, progress.last_amount));
}
let onchain = contract
.getWatermark(lane.pool_id, lane.signer, lane.provider)
.call()
.await
.context("read the lane's on-chain watermark")?;
Ok((
U256::from(onchain.bytesDelivered),
U256::from(onchain.amount),
))
}
fn dial_target(candidate: &NodeCandidate) -> EndpointAddr {
discovery::with_dial_addrs(EndpointAddr::new(candidate.node_id), candidate)
}
fn now_us() -> Result<u64> {
Ok(u64::try_from(
SystemTime::now().duration_since(UNIX_EPOCH)?.as_micros(),
)?)
}