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;