Skip to main content

decdn_incentive/
rate.rs

1//! Rate enforcement for incremental voucher payments.
2//!
3//! Each voucher carries cumulative `amount` and `bytes_delivered`. Between
4//! two vouchers on the same lane, the increments must satisfy
5//! `amount_delta / bytes_delta >= rate_per_mb` — i.e. the client paid at
6//! least the advertised rate for the bytes delivered. Underpayment beyond a
7//! tolerance threshold (rounding error, edge-MB padding) indicates a faulty
8//! or hostile client.
9//!
10//! Rate units, all matching ADR 005 / ADR 003:
11//! - `amount` is in token base units (`µUSDC` = 10⁻⁶ `USDC`)
12//! - `bytes_delivered` is in bytes
13//! - `rate_per_mb` is in token base units per **megabyte**, where 1 `MB` =
14//!   1,048,576 bytes (binary `MB`, ADR 005)
15//!
16//! Tolerance is expressed in basis points (bps); 100 bps = 1%. The default
17//! [`DEFAULT_TOLERANCE_BPS`] of 100 leaves headroom for the rounding the
18//! client does when computing `amount = ceil(bytes / 1_048_576) * rate`.
19
20use alloy::primitives::U256;
21use decdn_protocol::client::CHUNK_BYTES;
22
23/// 1 `MB` in bytes per ADR 005 §Probe-Triggered Eviction Hold's `MB`
24/// definition.
25pub const BYTES_PER_MB: u64 = 1_048_576;
26
27/// Basis-points scale: 100% = `10_000` `bps`.
28pub const BPS_SCALE: u64 = 10_000;
29
30/// Default underpayment tolerance — 1%. Calibrated against the client-side
31/// rounding `amount = ceil(bytes / 1_048_576) * rate_per_mb`: at 1 MB
32/// granularity the worst-case underpayment is < 1 byte's-worth of rate, so
33/// 100 bps leaves >>10× headroom.
34pub const DEFAULT_TOLERANCE_BPS: u64 = 100;
35
36/// Verify that paying `amount_delta` for `bytes_delta` bytes meets
37/// `rate_per_mb` within `tolerance_bps`.
38///
39/// # Errors
40///
41/// - [`RateError::ZeroBytes`] — `bytes_delta == 0`. A voucher that adds no
42///   bytes cannot be priced and is silently invalid.
43/// - [`RateError::Underpayment`] — effective rate is below `rate_per_mb` by
44///   more than `tolerance_bps` basis points.
45/// - [`RateError::Overflow`] — `amount_delta * BYTES_PER_MB * BPS_SCALE`
46///   would exceed `U256::MAX`. Will not occur in practice (would require
47///   ~10⁶³ `µUSDC`, far more than total `USDC` supply).
48pub fn verify_rate(
49    amount_delta: U256,
50    bytes_delta: U256,
51    rate_per_mb: u64,
52    tolerance_bps: u64,
53) -> Result<(), RateError> {
54    if bytes_delta.is_zero() {
55        return Err(RateError::ZeroBytes);
56    }
57
58    // Required micro-USDC for `bytes_delta` at exactly `rate_per_mb`:
59    //     required = bytes_delta * rate_per_mb / BYTES_PER_MB
60    // Allowed slack:
61    //     min_paid = required * (BPS_SCALE - tolerance_bps) / BPS_SCALE
62    //
63    // We avoid the float divide by cross-multiplying:
64    //     amount_delta * BYTES_PER_MB * BPS_SCALE
65    //         >= bytes_delta * rate_per_mb * (BPS_SCALE - tolerance_bps)
66    //
67    // Both sides fit comfortably in U256 for any plausible network values.
68    let tolerance = tolerance_bps.min(BPS_SCALE);
69    let allowed_bps = BPS_SCALE - tolerance;
70
71    let lhs = amount_delta
72        .checked_mul(U256::from(BYTES_PER_MB))
73        .and_then(|v| v.checked_mul(U256::from(BPS_SCALE)))
74        .ok_or(RateError::Overflow)?;
75    let rhs = bytes_delta
76        .checked_mul(U256::from(rate_per_mb))
77        .and_then(|v| v.checked_mul(U256::from(allowed_bps)))
78        .ok_or(RateError::Overflow)?;
79
80    if lhs >= rhs {
81        Ok(())
82    } else {
83        Err(RateError::Underpayment {
84            paid: amount_delta,
85            bytes: bytes_delta,
86            rate_per_mb,
87            tolerance_bps: tolerance,
88        })
89    }
90}
91
92/// Minimum payment (in token base units) required to cover `bytes` delivered
93/// at `rate_per_mb`, i.e. `ceil(bytes * rate_per_mb / BYTES_PER_MB)`.
94///
95/// This is the exact lower bound [`verify_rate`] enforces at zero tolerance and
96/// the same arithmetic the buyer's voucher signer uses to price an interval
97/// (`bytes * rate / MB`, rounded up). It is intended for pre-flight cost
98/// estimation — e.g. gating a speculative pull on the requesting lane's
99/// remaining deposit covering the worst-case blob cost (#856).
100///
101/// Saturating on the (practically unreachable) multiply overflow: a saturated
102/// `U256::MAX` ceiling makes any finite deposit look insufficient, which is the
103/// safe, conservative direction for a guard.
104#[must_use]
105pub fn min_payment(bytes: u64, rate_per_mb: u64) -> U256 {
106    U256::from(bytes)
107        .saturating_mul(U256::from(rate_per_mb))
108        .div_ceil(U256::from(BYTES_PER_MB))
109}
110
111/// One-chunk floor priced in `µUSDC` — the un-self-funded credit a fresh lane
112/// draws before its first proof (ADR 003 §Credit window / §Pool solvency).
113///
114/// The credit window is floored at one chunk so a stream can always make
115/// progress: deliver a full chunk, then recoup it. Because `CHUNK_BYTES ==
116/// BYTES_PER_MB`, this is exactly `rate_per_mb` — one chunk costs one MB of
117/// price, by identity (ADR 003 §Chunk Cadence).
118#[must_use]
119pub fn floor_micro(rate_per_mb: u64) -> U256 {
120    min_payment(CHUNK_BYTES, rate_per_mb)
121}
122
123/// Stateful-B pool solvency: the pool's remaining deposit minus the refundable
124/// floor `M` must cover its already-committed concurrent floor credit plus this
125/// stream's new reservation. Saturating (a `remaining` below `M` yields no
126/// headroom rather than underflowing).
127#[must_use]
128pub fn pool_budget_covers(remaining: U256, m: U256, committed: U256, new_reserve: U256) -> bool {
129    remaining.saturating_sub(m) >= committed.saturating_add(new_reserve)
130}
131
132/// Failure modes for [`verify_rate`].
133#[derive(Debug, thiserror::Error, PartialEq, Eq)]
134pub enum RateError {
135    /// Voucher delta included zero bytes — cannot be priced. The node should
136    /// reject it before it advances lane state.
137    #[error("voucher carries zero bytes_delta — cannot enforce rate")]
138    ZeroBytes,
139    /// Effective rate is below the advertised rate by more than the
140    /// configured tolerance. The node should pause the stream and (per
141    /// ADR 003 §Voucher withholding) refuse further delivery.
142    #[error(
143        "underpayment: paid {paid} for {bytes} bytes against rate {rate_per_mb}/MB \
144         with {tolerance_bps} bps tolerance"
145    )]
146    Underpayment {
147        /// The voucher's amount delta.
148        paid: U256,
149        /// The voucher's bytes delta, priced against that amount.
150        bytes: U256,
151        /// The advertised per-MB rate the payment is judged against.
152        rate_per_mb: u64,
153        /// Allowed shortfall, in basis points of the expected amount.
154        tolerance_bps: u64,
155    },
156    /// Arithmetic would overflow U256. Will not occur for realistic values
157    /// but surfaced as an error rather than panicking, per the workspace
158    /// anti-panic policy.
159    #[error("rate enforcement arithmetic overflowed U256")]
160    Overflow,
161}
162
163#[cfg(test)]
164mod tests;