1919//! The key trick: multiplying by 129/128 in integer math is `x + (x >> 7)`.
2020//! Exponentiation uses a precomputed table of 15 entries with binary
2121//! exponentiation (at most 15 fixed-point multiplications).
22+ //!
23+ //! The precomputed table and [`MAX_EXPONENT`] are generated at build time by
24+ //! `build.rs` using exact integer arithmetic (iterated fixed-point squaring).
25+ //! Any reimplementation (e.g. in C++) must use the same table values to
26+ //! guarantee bit-identical fee calculations. See `build.rs` for the algorithm.
2227
2328use alloy_primitives:: U256 ;
2429
30+ // Import the build-time generated constants: TABLE and MAX_EXPONENT.
31+ mod generated {
32+ include ! ( concat!( env!( "OUT_DIR" ) , "/fee_table.rs" ) ) ;
33+ }
34+ use generated:: { MAX_EXPONENT , TABLE } ;
35+
2536/// Fee base numerator.
2637pub const FEE_BASE_NUM : u64 = 129 ;
2738
@@ -37,46 +48,6 @@ const FIXED_ONE: U256 = U256::from_limbs([0, 1, 0, 0]);
3748/// Type alias for the 512-bit intermediate used in fixed-point multiplication.
3849type U512 = alloy_primitives:: Uint < 512 , 8 > ;
3950
40- /// Maximum exponent that produces a result fitting in U256.
41- /// `(129/128)^22801 ≈ 1.15 × 10⁷⁷ < 2²⁵⁶`.
42- const MAX_EXPONENT : u16 = 22801 ;
43-
44- /// Precomputed table: `TABLE[i]` = `(129/128)^(2^i)` in fixed-point with 64
45- /// fractional bits. 15 entries (bits 0..14) cover exponents 0..32767. In
46- /// practice exponents are capped at [`MAX_EXPONENT`].
47- const TABLE : [ U256 ; 15 ] = [
48- // (129/128)^1
49- U256 :: from_limbs ( [ 0x0200000000000000 , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
50- // (129/128)^2
51- U256 :: from_limbs ( [ 0x0404000000000000 , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
52- // (129/128)^4
53- U256 :: from_limbs ( [ 0x0818201000000000 , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
54- // (129/128)^8
55- U256 :: from_limbs ( [ 0x1071C46707040100 , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
56- // (129/128)^16
57- U256 :: from_limbs ( [ 0x21F1F3E9E88A9FDE , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
58- // (129/128)^32
59- U256 :: from_limbs ( [ 0x48642D6345D6B724 , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
60- // (129/128)^64
61- U256 :: from_limbs ( [ 0xA540DB81E090D217 , 0x0000000000000001 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
62- // (129/128)^128
63- U256 :: from_limbs ( [ 0xB52E6267A9C41385 , 0x0000000000000002 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
64- // (129/128)^256
65- U256 :: from_limbs ( [ 0x54F4292CC0341C1C , 0x0000000000000007 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
66- // (129/128)^512
67- U256 :: from_limbs ( [ 0xC18B645664E97CB2 , 0x0000000000000035 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
68- // (129/128)^1024
69- U256 :: from_limbs ( [ 0xB60B04F629FAE140 , 0x0000000000000B49 , 0x0000000000000000 , 0x0000000000000000 ] ) ,
70- // (129/128)^2048
71- U256 :: from_limbs ( [ 0x4629A786F160E3C4 , 0x00000000007F6ADE , 0x0000000000000000 , 0x0000000000000000 ] ) ,
72- // (129/128)^4096
73- U256 :: from_limbs ( [ 0x7A189A75BFA7286D , 0x00003F6B3526706C , 0x0000000000000000 , 0x0000000000000000 ] ) ,
74- // (129/128)^8192
75- U256 :: from_limbs ( [ 0xDE16922E6F29BFD7 , 0x64804F18A8B89AD1 , 0x000000000FB5F10E , 0x0000000000000000 ] ) ,
76- // (129/128)^16384
77- U256 :: from_limbs ( [ 0xFAE86D02AC675940 , 0xB9209769AD99BAC7 , 0x770DC9A80163F037 , 0x00F6D38E711D40BD ] ) ,
78- ] ;
79-
8051/// Convert a log-space fee exponent to a linear [`U256`] value.
8152///
8253/// `fee_to_linear(n)` = `floor((129/128)^n)`.
@@ -100,9 +71,9 @@ fn fee_to_linear_fixed(log_fee: u16) -> U256 {
10071 "fee exponent {log_fee} exceeds MAX_EXPONENT ({MAX_EXPONENT})"
10172 ) ;
10273 let mut result = FIXED_ONE ; // 1.0 in fixed-point
103- for i in 0 .. 15 {
74+ for ( i , entry ) in TABLE . iter ( ) . enumerate ( ) {
10475 if log_fee & ( 1 << i) != 0 {
105- result = fixed_mul ( result, TABLE [ i ] ) ;
76+ result = fixed_mul ( result, * entry ) ;
10677 }
10778 }
10879 result
@@ -116,12 +87,20 @@ fn fee_to_linear_fixed(log_fee: u16) -> U256 {
11687///
11788/// Uses binary search over `fee_to_linear` — 15 iterations, each doing at
11889/// most 15 multiplies = 225 multiplies total. Fine for the rare inverse path.
119- pub fn fee_from_linear ( value : u64 ) -> u16 {
120- if value <= 1 {
90+ ///
91+ /// Accepts [`U256`] for symmetry with [`fee_to_linear`], which returns [`U256`].
92+ pub fn fee_from_linear ( value : U256 ) -> u16 {
93+ if value <= U256 :: from ( 1u64 ) {
12194 return 0 ;
12295 }
96+ // Values at or above the maximum representable fee saturate to MAX_EXPONENT.
97+ // This also prevents overflow in the left-shift below (which requires the
98+ // upper 64 bits of `value` to be zero).
99+ if value >= fee_to_linear ( MAX_EXPONENT ) {
100+ return MAX_EXPONENT ;
101+ }
123102 // Compare in fixed-point for full precision.
124- let target = U256 :: from ( value) << FRAC_BITS ;
103+ let target = value << FRAC_BITS ;
125104 // Binary search: find smallest n where fee_to_linear_fixed(n) >= target.
126105 let mut lo: u32 = 0 ;
127106 let mut hi: u32 = MAX_EXPONENT as u32 + 1 ;
@@ -237,11 +216,12 @@ mod tests {
237216 #[ test]
238217 fn round_trip ( ) {
239218 for original in [ 1u64 , 10 , 100 , 1_000 , 1_000_000 , 1_000_000_000 ] {
240- let exp = fee_from_linear ( original) ;
219+ let original_u256 = U256 :: from ( original) ;
220+ let exp = fee_from_linear ( original_u256) ;
241221 let recovered = fee_to_linear ( exp) ;
242222 // Within ~1% of original (0.78% per step, rounding can add half a step).
243- let lo = U256 :: from ( original ) * U256 :: from ( 99u64 ) / U256 :: from ( 100u64 ) ;
244- let hi = U256 :: from ( original ) * U256 :: from ( 101u64 ) / U256 :: from ( 100u64 ) ;
223+ let lo = original_u256 * U256 :: from ( 99u64 ) / U256 :: from ( 100u64 ) ;
224+ let hi = original_u256 * U256 :: from ( 101u64 ) / U256 :: from ( 100u64 ) ;
245225 assert ! (
246226 recovered >= lo && recovered <= hi,
247227 "round-trip failed for {original}: exp={exp}, recovered={recovered}"
@@ -275,7 +255,7 @@ mod tests {
275255
276256 #[ test]
277257 fn fee_from_linear_zero_and_one ( ) {
278- assert_eq ! ( fee_from_linear( 0 ) , 0 ) ;
279- assert_eq ! ( fee_from_linear( 1 ) , 0 ) ;
258+ assert_eq ! ( fee_from_linear( U256 :: ZERO ) , 0 ) ;
259+ assert_eq ! ( fee_from_linear( U256 :: from ( 1u64 ) ) , 0 ) ;
280260 }
281261}
0 commit comments