Skip to main content

ant_protocol/payment/
pricing.rs

1//! Quadratic pricing with a baseline floor.
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//! This is the **single source of truth** both the node (when pricing a quote)
12//! and the client (when verifying ADR-0004's forced price before paying) must
13//! agree on. It lives here, in the shared wire/contract crate, so the two sides
14//! can never drift: `ant-node` re-exports [`calculate_price`] from this module
15//! rather than defining its own.
16//!
17//! ## Parameters
18//!
19//! | Constant  | Value         | Role                                            |
20//! |-----------|---------------|-------------------------------------------------|
21//! | BASELINE  | 0.00390625 ANT| Price at empty (bootstrap-phase spam barrier)   |
22//! | K         | 0.03515625 ANT| Quadratic coefficient                           |
23//! | D         | 6000          | Lower stable boundary (records stored)          |
24
25use evmlib::common::Amount;
26
27/// Lower stable boundary of the quadratic curve, in records stored.
28const PRICING_DIVISOR: u128 = 6000;
29
30/// `PRICING_DIVISOR²`, precomputed to avoid repeated multiplication.
31const DIVISOR_SQUARED: u128 = PRICING_DIVISOR * PRICING_DIVISOR;
32
33/// Baseline price at empty / bootstrap-phase spam barrier.
34///
35/// `0.00390625 ANT × 10¹⁸ wei/ANT = 3_906_250_000_000_000 wei`.
36const PRICE_BASELINE_WEI: u128 = 3_906_250_000_000_000;
37
38/// Quadratic coefficient `K`.
39///
40/// `0.03515625 ANT × 10¹⁸ wei/ANT = 35_156_250_000_000_000 wei`.
41const PRICE_COEFFICIENT_WEI: u128 = 35_156_250_000_000_000;
42
43/// Price increment per squared record after simplifying `PRICE_COEFFICIENT_WEI / DIVISOR_SQUARED`.
44const PRICE_PER_RECORD_SQUARED_WEI: u128 = PRICE_COEFFICIENT_WEI / DIVISOR_SQUARED;
45
46/// Derive the quoted record count from a quote price.
47///
48/// This is the inverse of [`calculate_price`]. It intentionally floors to the
49/// nearest integer record count.
50///
51/// Saturates to `u64::MAX` for any price that would otherwise overflow `u64`.
52/// This matters because the node verifier calls this on untrusted deserialized
53/// `quote.price` values BEFORE signature verification: a panic here is a
54/// pre-auth crash vector. Saturating leaves the delta check to reject the
55/// quote as out-of-range without aborting the process.
56#[must_use]
57pub fn derive_records_stored_from_price(price: Amount) -> u64 {
58    let baseline = Amount::from(PRICE_BASELINE_WEI);
59    if price <= baseline {
60        return 0;
61    }
62
63    let excess = price - baseline;
64    let n_squared = excess / Amount::from(PRICE_PER_RECORD_SQUARED_WEI);
65    let root = n_squared.root(2);
66    // ruint's `Uint::to::<u64>()` panics on overflow. We MUST NOT panic here:
67    // a hostile oversized price would otherwise be a pre-auth crash vector.
68    // Saturate to `u64::MAX` instead; the delta check rejects out-of-range quotes.
69    if root > Amount::from(u64::MAX) {
70        u64::MAX
71    } else {
72        root.to::<u64>()
73    }
74}
75
76/// Calculate storage price in wei from the number of close records stored.
77///
78/// Formula: `price_wei = BASELINE + n² × K / D²`
79///
80/// where `BASELINE = 0.00390625 ANT`, `K = 0.03515625 ANT`, and `D = 6000`.
81/// U256 arithmetic prevents overflow for large record counts.
82#[must_use]
83pub fn calculate_price(close_records_stored: usize) -> Amount {
84    let n = Amount::from(close_records_stored);
85    let n_squared = n.saturating_mul(n);
86    let quadratic_wei = n_squared.saturating_mul(Amount::from(PRICE_COEFFICIENT_WEI))
87        / Amount::from(DIVISOR_SQUARED);
88    Amount::from(PRICE_BASELINE_WEI).saturating_add(quadratic_wei)
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used, clippy::expect_used)]
93mod tests {
94    use super::*;
95
96    /// 1 token = 10¹⁸ wei (used for test sanity-checks).
97    const WEI_PER_TOKEN: u128 = 1_000_000_000_000_000_000;
98
99    /// Helper: expected price matching the formula `BASELINE + n² × K / D²`.
100    fn expected_price(n: u64) -> Amount {
101        let n_amt = Amount::from(n);
102        let quad =
103            n_amt * n_amt * Amount::from(PRICE_COEFFICIENT_WEI) / Amount::from(DIVISOR_SQUARED);
104        Amount::from(PRICE_BASELINE_WEI) + quad
105    }
106
107    #[test]
108    fn test_zero_records_gets_baseline() {
109        let price = calculate_price(0);
110        assert_eq!(price, Amount::from(PRICE_BASELINE_WEI));
111    }
112
113    #[test]
114    fn test_baseline_is_nonzero_spam_barrier() {
115        assert!(calculate_price(0) > Amount::ZERO);
116        assert!(calculate_price(1) > calculate_price(0));
117    }
118
119    #[test]
120    fn test_one_record_above_baseline() {
121        let price = calculate_price(1);
122        assert_eq!(price, expected_price(1));
123        assert!(price > Amount::from(PRICE_BASELINE_WEI));
124    }
125
126    #[test]
127    fn test_at_divisor_is_baseline_plus_k() {
128        let price = calculate_price(6000);
129        let expected = Amount::from(PRICE_BASELINE_WEI + PRICE_COEFFICIENT_WEI);
130        assert_eq!(price, expected);
131    }
132
133    #[test]
134    fn test_double_divisor_is_baseline_plus_four_k() {
135        let price = calculate_price(12000);
136        let expected = Amount::from(PRICE_BASELINE_WEI + 4 * PRICE_COEFFICIENT_WEI);
137        assert_eq!(price, expected);
138    }
139
140    #[test]
141    fn test_triple_divisor_is_baseline_plus_nine_k() {
142        let price = calculate_price(18000);
143        let expected = Amount::from(PRICE_BASELINE_WEI + 9 * PRICE_COEFFICIENT_WEI);
144        assert_eq!(price, expected);
145    }
146
147    #[test]
148    fn test_smooth_pricing_no_staircase() {
149        let price_6k = calculate_price(6000);
150        let price_11k = calculate_price(11999);
151        assert!(
152            price_11k > price_6k,
153            "11999 records ({price_11k}) should cost more than 6000 ({price_6k})"
154        );
155    }
156
157    #[test]
158    fn test_price_increases_with_records() {
159        let price_low = calculate_price(6000);
160        let price_mid = calculate_price(12000);
161        let price_high = calculate_price(18000);
162        assert!(price_mid > price_low);
163        assert!(price_high > price_mid);
164    }
165
166    #[test]
167    fn test_price_increases_monotonically() {
168        let mut prev_price = Amount::ZERO;
169        for records in (0..60000).step_by(100) {
170            let price = calculate_price(records);
171            assert!(
172                price >= prev_price,
173                "Price at {records} records ({price}) should be >= previous ({prev_price})"
174            );
175            prev_price = price;
176        }
177    }
178
179    #[test]
180    fn test_large_value_no_overflow() {
181        let price = calculate_price(usize::MAX);
182        assert!(price > Amount::ZERO);
183    }
184
185    #[test]
186    fn test_price_deterministic() {
187        let price1 = calculate_price(12000);
188        let price2 = calculate_price(12000);
189        assert_eq!(price1, price2);
190    }
191
192    #[test]
193    fn test_quadratic_growth_excluding_baseline() {
194        let base = Amount::from(PRICE_BASELINE_WEI);
195        let quad_6k = calculate_price(6000) - base;
196        let quad_12k = calculate_price(12000) - base;
197        let quad_24k = calculate_price(24000) - base;
198        assert_eq!(quad_12k, quad_6k * Amount::from(4u64));
199        assert_eq!(quad_24k, quad_6k * Amount::from(16u64));
200    }
201
202    #[test]
203    fn test_small_record_counts_near_baseline() {
204        let price = calculate_price(100);
205        assert_eq!(price, expected_price(100));
206        assert!(price < Amount::from(WEI_PER_TOKEN));
207        assert!(price > Amount::from(PRICE_BASELINE_WEI));
208    }
209
210    #[test]
211    fn test_derive_records_stored_from_price_round_trips() {
212        for records in [0usize, 1, 5, 100, 6_000, 12_000, 60_000] {
213            let price = calculate_price(records);
214            assert_eq!(derive_records_stored_from_price(price), records as u64);
215        }
216    }
217
218    #[test]
219    fn test_derive_records_stored_from_baseline_or_lower_is_zero() {
220        assert_eq!(derive_records_stored_from_price(Amount::ZERO), 0);
221        assert_eq!(
222            derive_records_stored_from_price(Amount::from(PRICE_BASELINE_WEI)),
223            0
224        );
225    }
226
227    #[test]
228    fn test_derive_records_stored_from_max_price_saturates_no_panic() {
229        let v = derive_records_stored_from_price(Amount::MAX);
230        assert_eq!(v, u64::MAX);
231    }
232}