Skip to content

Commit 91f6802

Browse files
committed
wip
1 parent dc79171 commit 91f6802

5 files changed

Lines changed: 154 additions & 51 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sequencer-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ serde = { version = "1", features = ["derive"] }
1616
thiserror = "1"
1717
ssz = { package = "ethereum_ssz", version = "0.10" }
1818
ssz_derive = { package = "ethereum_ssz_derive", version = "0.10" }
19+
20+
[build-dependencies]
21+
ruint = "1.17"

sequencer-core/build.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
//! Build script that generates the precomputed fee table for log-space fee
5+
//! encoding with base 129/128.
6+
//!
7+
//! The table contains `(129/128)^(2^i)` for i in 0..15, represented as
8+
//! fixed-point values with 64 fractional bits stored in 4×u64 limbs
9+
//! (little-endian, same layout as `alloy_primitives::U256`).
10+
//!
11+
//! All arithmetic is exact integer math — no floats, no approximation.
12+
13+
use std::env;
14+
use std::fs;
15+
use std::path::Path;
16+
17+
use ruint::Uint;
18+
19+
type U256 = Uint<256, 4>;
20+
type U512 = Uint<512, 8>;
21+
22+
const FRAC_BITS: usize = 64;
23+
24+
/// Truncate a U512 to U256 by taking the lower 4 limbs.
25+
/// Returns `None` if the upper 4 limbs are nonzero (value doesn't fit).
26+
fn try_narrow(wide: U512) -> Option<U256> {
27+
let limbs = wide.into_limbs();
28+
let upper_nonzero = limbs[4] != 0 || limbs[5] != 0 || limbs[6] != 0 || limbs[7] != 0;
29+
if upper_nonzero {
30+
return None;
31+
}
32+
Some(U256::from_limbs([limbs[0], limbs[1], limbs[2], limbs[3]]))
33+
}
34+
35+
/// Fixed-point multiplication: floor((a × b) >> 64).
36+
fn fixed_mul(a: U256, b: U256) -> U256 {
37+
let product: U512 = a.widening_mul(b);
38+
let shifted = product >> FRAC_BITS;
39+
try_narrow(shifted).expect("fixed_mul result overflows U256")
40+
}
41+
42+
/// Compute the maximum safe exponent for the fixed-point representation.
43+
///
44+
/// Returns the largest n such that `(129/128)^n × 2^64` fits in U256, i.e.,
45+
/// the full fixed-point value does not overflow 256 bits.
46+
///
47+
/// Uses a greedy bit-by-bit approach from the most significant bit downward:
48+
/// tentatively set each bit and check if the result still fits.
49+
fn compute_max_exponent(table: &[U256; 15]) -> u16 {
50+
let fixed_one = U256::from(1u64) << FRAC_BITS;
51+
let mut result_exp: u16 = 0;
52+
let mut result_val = fixed_one;
53+
54+
for i in (0..15).rev() {
55+
let product: U512 = result_val.widening_mul(table[i]);
56+
let shifted = product >> FRAC_BITS;
57+
if let Some(narrowed) = try_narrow(shifted) {
58+
result_exp |= 1 << i;
59+
result_val = narrowed;
60+
}
61+
}
62+
63+
result_exp
64+
}
65+
66+
fn main() {
67+
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
68+
let dest = Path::new(&out_dir).join("fee_table.rs");
69+
70+
// (129/128) in fixed-point: 129 × 2^64 / 128 = 129 × 2^57.
71+
let base_fixed = U256::from(129u64) << 57;
72+
73+
// Build the table by repeated squaring.
74+
let mut table = [U256::ZERO; 15];
75+
table[0] = base_fixed; // (129/128)^1
76+
for i in 1..15 {
77+
table[i] = fixed_mul(table[i - 1], table[i - 1]);
78+
}
79+
80+
let max_exp = compute_max_exponent(&table);
81+
82+
// Generate Rust source.
83+
let mut code = String::new();
84+
code.push_str("// Auto-generated by build.rs — do not edit.\n");
85+
code.push_str("// Precomputed table for log-space fee encoding (base 129/128).\n");
86+
code.push_str("// All values are exact integer computations, no floating-point.\n\n");
87+
code.push_str("use alloy_primitives::U256;\n\n");
88+
89+
code.push_str(&format!(
90+
"/// Maximum exponent whose fixed-point representation fits in U256.\n\
91+
/// Computed at build time: the largest n where `(129/128)^n × 2^64 < 2^256`.\n\
92+
pub const MAX_EXPONENT: u16 = {max_exp};\n\n"
93+
));
94+
95+
code.push_str(
96+
"/// Precomputed table: `TABLE[i]` = `(129/128)^(2^i)` in fixed-point with 64\n\
97+
/// fractional bits. 15 entries (bits 0..14) cover exponents 0..32767.\n\
98+
/// In practice exponents are capped at [`MAX_EXPONENT`].\n\
99+
///\n\
100+
/// Generated by `build.rs` using exact integer arithmetic.\n\
101+
pub const TABLE: [U256; 15] = [\n",
102+
);
103+
104+
for (i, entry) in table.iter().enumerate() {
105+
let limbs = entry.into_limbs();
106+
code.push_str(&format!(
107+
" // (129/128)^{}\n U256::from_limbs([{:#018x}, {:#018x}, {:#018x}, {:#018x}]),\n",
108+
1u32 << i,
109+
limbs[0],
110+
limbs[1],
111+
limbs[2],
112+
limbs[3]
113+
));
114+
}
115+
code.push_str("];\n");
116+
117+
fs::write(&dest, code).expect("failed to write fee_table.rs");
118+
println!("cargo::rerun-if-changed=build.rs");
119+
}

sequencer-core/src/fee.rs

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,20 @@
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
2328
use 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.
2637
pub 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.
3849
type 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
}

sequencer/tests/e2e_sequencer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ fn bootstrap_open_frame_with_deposits(db_path: &str, deposits: &[(Address, U256)
598598

599599
let safe_input_count = deposits.len() as u64;
600600
let leading_range = SafeInputRange::new(0, safe_input_count);
601-
// Default log_gas_price=0 → log_recommended_fee = 0+16+327+486 = 1060.
601+
// Default log_gas_price=0 → log_recommended_fee = 0+20+419+621 = 1060.
602602
let head = storage
603603
.initialize_open_state(1, leading_range)
604604
.expect("initialize open state");

0 commit comments

Comments
 (0)