Skip to main content

decdn_node/
serve_economics.rs

1//! Buy-side profitability gate for cache-miss relay legs (ADR 041).
2//!
3//! Node-local, config-selectable. Sibling to the ADR-040 cache policies, not part
4//! of them: cache policy decides what a node keeps; this decides what a node buys
5//! to serve. The policy returns a per-MB buy ceiling; the `node_origin` buy loop does
6//! the candidate scan and the refusal.
7
8use std::sync::Arc;
9
10use decdn_common::config::resolved::ResolvedServeEconomics;
11
12const BPS_DENOMINATOR: u64 = 10_000;
13
14/// Inputs to the buy ceiling, gathered per cache-miss.
15#[derive(Debug, Clone, Copy)]
16pub struct ServeEconomicsCtx {
17    /// The node's own sell rate, per MB, already raised to the on-chain floor.
18    pub sell_rate_per_mb: u64,
19    /// Operator basis points from `FeeRouter.getShares()[0]`; `(1 - f)` numerator.
20    pub operator_bps: u16,
21    /// ADR-040 frequency estimate for the hash; 0 = cold.
22    pub heat_estimate: u32,
23    /// The source node still has warming allowance: buy at market to warm.
24    /// False once the source's allowance is spent: buy only at the amortized floor.
25    pub warming_available: bool,
26}
27
28/// A node-local decision on the most the node pays upstream, per MB.
29pub trait ServeEconomicsPolicy: Send + Sync + std::fmt::Debug {
30    /// `None` = no economic ceiling (only the static ceiling applies).
31    /// `Some(v)` = a real ceiling; `v` may be 0, refusing every paid candidate.
32    fn max_buy_per_mb(&self, ctx: &ServeEconomicsCtx) -> Option<u64>;
33}
34
35/// Disables the economic gate; only `cache.max_rate_per_mb` bounds the buy.
36#[derive(Debug)]
37pub struct OffPolicy;
38
39impl ServeEconomicsPolicy for OffPolicy {
40    fn max_buy_per_mb(&self, _ctx: &ServeEconomicsCtx) -> Option<u64> {
41        None
42    }
43}
44
45/// Two-regime ceiling. `amortized = (operator_bps/10_000) · n_hat · sell` with
46/// `n_hat = clamp(round(discount · heat), 1, n_max)`. While the source has warming
47/// allowance, buy at the market price `max(sell, amortized)` (warming); once spent,
48/// buy only at `amortized` (grief-proof). See ADR 041 § The buy ceiling.
49#[derive(Debug)]
50pub struct MarginPolicy {
51    discount_bps: u32,
52    n_max: u32,
53}
54
55impl MarginPolicy {
56    /// `discount_bps` scales heat into the expected re-serve count `n_hat`,
57    /// which `n_max` caps.
58    #[must_use]
59    pub const fn new(discount_bps: u32, n_max: u32) -> Self {
60        Self {
61            discount_bps,
62            n_max,
63        }
64    }
65
66    fn n_hat(&self, heat_estimate: u32) -> u64 {
67        // round(discount · heat) with integer math: (discount_bps·heat + 5000) / 10_000
68        let scaled = u64::from(self.discount_bps)
69            .saturating_mul(u64::from(heat_estimate))
70            .saturating_add(BPS_DENOMINATOR / 2)
71            / BPS_DENOMINATOR;
72        scaled.clamp(1, u64::from(self.n_max))
73    }
74}
75
76impl ServeEconomicsPolicy for MarginPolicy {
77    fn max_buy_per_mb(&self, ctx: &ServeEconomicsCtx) -> Option<u64> {
78        let n_hat = self.n_hat(ctx.heat_estimate);
79        let amortized = u64::from(ctx.operator_bps)
80            .saturating_mul(n_hat)
81            .saturating_mul(ctx.sell_rate_per_mb)
82            / BPS_DENOMINATOR;
83        // Warm at market while the source has allowance; else the amortized floor.
84        Some(if ctx.warming_available {
85            ctx.sell_rate_per_mb.max(amortized)
86        } else {
87            amortized
88        })
89    }
90}
91
92/// Build the configured policy. Unknown names are impossible: config validation
93/// restricts `policy` to `"off"` | `"margin"` at load; anything else here
94/// falls back to `OffPolicy` (safe: only the static ceiling applies).
95#[must_use]
96pub fn select_policy(cfg: &ResolvedServeEconomics) -> Arc<dyn ServeEconomicsPolicy> {
97    match cfg.policy.as_str() {
98        "margin" => Arc::new(MarginPolicy::new(cfg.discount_bps, cfg.n_max)),
99        _ => Arc::new(OffPolicy),
100    }
101}
102
103#[cfg(test)]
104mod tests;