Skip to main content

ant_node/payment/
pricing.rs

1//! Quadratic pricing with a baseline floor for ant-node.
2//!
3//! Formula: `price_per_chunk_ANT(n) = BASELINE + K × (n / D)²`
4//!
5//! The non-zero `BASELINE` makes empty nodes charge a meaningful spam-barrier
6//! price, and `K` is anchored so per-GB USD pricing matches real-world targets
7//! at the current ~$0.10/ANT token price. An earlier formula produced ~$25/GB
8//! at the lower stable boundary and ~$0/GB when nodes were empty — both
9//! unreasonable.
10//!
11//! ## Parameters
12//!
13//! | Constant  | Value         | Role                                            |
14//! |-----------|---------------|-------------------------------------------------|
15//! | BASELINE  | 0.00390625 ANT| Price at empty (bootstrap-phase spam barrier)   |
16//! | K         | 0.03515625 ANT| Quadratic coefficient                           |
17//! | D         | 6000          | Lower stable boundary (records stored)          |
18//!
19//! ## Design Rationale
20//!
21//! - **Empty / lightly loaded nodes** charge the `BASELINE` floor, preventing
22//!   free storage and acting as a bootstrap-phase spam barrier.
23//! - **Moderately loaded nodes** add a small quadratic contribution on top.
24//! - **Heavily loaded nodes** charge quadratically more, pushing clients
25//!   toward less-loaded nodes elsewhere in the network.
26
27use evmlib::common::Amount;
28
29/// Lower stable boundary of the quadratic curve, in records stored.
30const PRICING_DIVISOR: u128 = 6000;
31
32/// `PRICING_DIVISOR²`, precomputed to avoid repeated multiplication.
33const DIVISOR_SQUARED: u128 = PRICING_DIVISOR * PRICING_DIVISOR;
34
35/// Baseline price at empty / bootstrap-phase spam barrier.
36///
37/// `0.00390625 ANT × 10¹⁸ wei/ANT = 3_906_250_000_000_000 wei`.
38const PRICE_BASELINE_WEI: u128 = 3_906_250_000_000_000;
39
40/// Quadratic coefficient `K`.
41///
42/// `0.03515625 ANT × 10¹⁸ wei/ANT = 35_156_250_000_000_000 wei`.
43const PRICE_COEFFICIENT_WEI: u128 = 35_156_250_000_000_000;
44
45/// Price increment per squared record after simplifying `PRICE_COEFFICIENT_WEI / DIVISOR_SQUARED`.
46const PRICE_PER_RECORD_SQUARED_WEI: u128 = PRICE_COEFFICIENT_WEI / DIVISOR_SQUARED;
47
48/// Derive the quoted record count from a quote price.
49///
50/// This is the inverse of [`calculate_price`] and is used to validate quote
51/// freshness without relying on wall-clock timestamps. It intentionally floors
52/// to the nearest integer record count, matching the existing storage-delta
53/// tolerance behaviour.
54///
55/// Saturates to `u64::MAX` for any price that would otherwise overflow `u64`.
56/// This matters because the verifier calls this on untrusted deserialized
57/// `quote.price` values BEFORE signature verification: a panic here is a
58/// pre-auth crash vector. Saturating leaves the delta check to reject the
59/// quote as out-of-range without aborting the process.
60#[must_use]
61pub fn derive_records_stored_from_price(price: Amount) -> u64 {
62    let baseline = Amount::from(PRICE_BASELINE_WEI);
63    if price <= baseline {
64        return 0;
65    }
66
67    let excess = price - baseline;
68    let n_squared = excess / Amount::from(PRICE_PER_RECORD_SQUARED_WEI);
69    let root = n_squared.root(2);
70    // ruint's `Uint::to::<u64>()` panics on overflow. We MUST NOT panic here:
71    // freshness runs on untrusted deserialized `quote.price` before signature
72    // verification, so a hostile oversized price would otherwise be a pre-auth
73    // crash vector. Saturate to `u64::MAX` instead; the delta check rejects
74    // out-of-range quotes.
75    if root > Amount::from(u64::MAX) {
76        u64::MAX
77    } else {
78        root.to::<u64>()
79    }
80}
81
82/// Calculate storage price in wei from the number of close records stored.
83///
84/// Formula: `price_wei = BASELINE + n² × K / D²`
85///
86/// where `BASELINE = 0.00390625 ANT`, `K = 0.03515625 ANT`, and `D = 6000`.
87/// U256 arithmetic prevents overflow for large record counts.
88#[must_use]
89pub fn calculate_price(close_records_stored: usize) -> Amount {
90    let n = Amount::from(close_records_stored);
91    let n_squared = n.saturating_mul(n);
92    let quadratic_wei = n_squared.saturating_mul(Amount::from(PRICE_COEFFICIENT_WEI))
93        / Amount::from(DIVISOR_SQUARED);
94    Amount::from(PRICE_BASELINE_WEI).saturating_add(quadratic_wei)
95}
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used, clippy::expect_used)]
99mod tests {
100    use super::*;
101
102    /// 1 token = 10¹⁸ wei (used for test sanity-checks).
103    const WEI_PER_TOKEN: u128 = 1_000_000_000_000_000_000;
104
105    /// Helper: expected price matching the formula `BASELINE + n² × K / D²`.
106    fn expected_price(n: u64) -> Amount {
107        let n_amt = Amount::from(n);
108        let quad =
109            n_amt * n_amt * Amount::from(PRICE_COEFFICIENT_WEI) / Amount::from(DIVISOR_SQUARED);
110        Amount::from(PRICE_BASELINE_WEI) + quad
111    }
112
113    #[test]
114    fn test_zero_records_gets_baseline() {
115        // At n = 0 the quadratic term vanishes, leaving the baseline floor.
116        let price = calculate_price(0);
117        assert_eq!(price, Amount::from(PRICE_BASELINE_WEI));
118    }
119
120    #[test]
121    fn test_baseline_is_nonzero_spam_barrier() {
122        // The baseline ensures even empty nodes charge a meaningful price,
123        // making the legacy MIN_PRICE_WEI = 1 sentinel redundant.
124        assert!(calculate_price(0) > Amount::ZERO);
125        assert!(calculate_price(1) > calculate_price(0));
126    }
127
128    #[test]
129    fn test_one_record_above_baseline() {
130        let price = calculate_price(1);
131        assert_eq!(price, expected_price(1));
132        assert!(price > Amount::from(PRICE_BASELINE_WEI));
133    }
134
135    #[test]
136    fn test_at_divisor_is_baseline_plus_k() {
137        // At n = D the quadratic contribution equals K × 1² = K.
138        // price = BASELINE + K = 0.00390625 + 0.03515625 = 0.0390625 ANT
139        let price = calculate_price(6000);
140        let expected = Amount::from(PRICE_BASELINE_WEI + PRICE_COEFFICIENT_WEI);
141        assert_eq!(price, expected);
142    }
143
144    #[test]
145    fn test_double_divisor_is_baseline_plus_four_k() {
146        // At n = 2D the quadratic contribution is 4K.
147        let price = calculate_price(12000);
148        let expected = Amount::from(PRICE_BASELINE_WEI + 4 * PRICE_COEFFICIENT_WEI);
149        assert_eq!(price, expected);
150    }
151
152    #[test]
153    fn test_triple_divisor_is_baseline_plus_nine_k() {
154        // At n = 3D the quadratic contribution is 9K.
155        let price = calculate_price(18000);
156        let expected = Amount::from(PRICE_BASELINE_WEI + 9 * PRICE_COEFFICIENT_WEI);
157        assert_eq!(price, expected);
158    }
159
160    #[test]
161    fn test_smooth_pricing_no_staircase() {
162        // 11999 should give a strictly higher price than 6000 (no integer-division plateau).
163        let price_6k = calculate_price(6000);
164        let price_11k = calculate_price(11999);
165        assert!(
166            price_11k > price_6k,
167            "11999 records ({price_11k}) should cost more than 6000 ({price_6k})"
168        );
169    }
170
171    #[test]
172    fn test_price_increases_with_records() {
173        let price_low = calculate_price(6000);
174        let price_mid = calculate_price(12000);
175        let price_high = calculate_price(18000);
176        assert!(price_mid > price_low);
177        assert!(price_high > price_mid);
178    }
179
180    #[test]
181    fn test_price_increases_monotonically() {
182        let mut prev_price = Amount::ZERO;
183        for records in (0..60000).step_by(100) {
184            let price = calculate_price(records);
185            assert!(
186                price >= prev_price,
187                "Price at {records} records ({price}) should be >= previous ({prev_price})"
188            );
189            prev_price = price;
190        }
191    }
192
193    #[test]
194    fn test_large_value_no_overflow() {
195        let price = calculate_price(usize::MAX);
196        assert!(price > Amount::ZERO);
197    }
198
199    #[test]
200    fn test_price_deterministic() {
201        let price1 = calculate_price(12000);
202        let price2 = calculate_price(12000);
203        assert_eq!(price1, price2);
204    }
205
206    #[test]
207    fn test_quadratic_growth_excluding_baseline() {
208        // Subtracting the baseline, quadratic contribution should scale with n².
209        // At 2× records the quadratic portion is 4×; at 4× records it is 16×.
210        let base = Amount::from(PRICE_BASELINE_WEI);
211        let quad_6k = calculate_price(6000) - base;
212        let quad_12k = calculate_price(12000) - base;
213        let quad_24k = calculate_price(24000) - base;
214        assert_eq!(quad_12k, quad_6k * Amount::from(4u64));
215        assert_eq!(quad_24k, quad_6k * Amount::from(16u64));
216    }
217
218    #[test]
219    fn test_small_record_counts_near_baseline() {
220        // At small n, price is dominated by the baseline — quadratic term is tiny.
221        let price = calculate_price(100);
222        assert_eq!(price, expected_price(100));
223        assert!(price < Amount::from(WEI_PER_TOKEN)); // well below 1 ANT
224        assert!(price > Amount::from(PRICE_BASELINE_WEI)); // strictly above baseline
225    }
226
227    #[test]
228    fn test_derive_records_stored_from_price_round_trips() {
229        for records in [0usize, 1, 5, 100, 6_000, 12_000, 60_000] {
230            let price = calculate_price(records);
231            assert_eq!(derive_records_stored_from_price(price), records as u64);
232        }
233    }
234
235    #[test]
236    fn test_derive_records_stored_from_baseline_or_lower_is_zero() {
237        assert_eq!(derive_records_stored_from_price(Amount::ZERO), 0);
238        assert_eq!(
239            derive_records_stored_from_price(Amount::from(PRICE_BASELINE_WEI)),
240            0
241        );
242    }
243
244    #[test]
245    fn test_derive_records_stored_from_max_price_saturates_no_panic() {
246        // Hostile/malformed quotes may carry an oversized U256 price.
247        // The verifier calls this BEFORE signature verification, so we MUST
248        // NOT panic on overflow — saturate to u64::MAX and let the delta
249        // check reject the quote.
250        let v = derive_records_stored_from_price(Amount::MAX);
251        assert_eq!(v, u64::MAX);
252    }
253}