From 6a76bf163f78f09df4e5fe672d2a136ee0f2b601 Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Wed, 9 Sep 2026 13:46:05 +0000 Subject: [PATCH 1/3] docs(fee): document effective base-fee floor vs minBaseFee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin the empty-block truncation floor (kRate=1250 → 7 wei) with a unit test and record the max(minBaseFee, 10000/kRate) interaction in ADR-0004. Leaves ProtocolConfig requires alone so historical testnet genesis still regenerates. Addresses #367. --- crates/execution-config/src/gas_fee.rs | 23 +++++++++++++++++++++++ docs/adr/0004-base-fee-validation.md | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/crates/execution-config/src/gas_fee.rs b/crates/execution-config/src/gas_fee.rs index 37b944c8..811a2142 100644 --- a/crates/execution-config/src/gas_fee.rs +++ b/crates/execution-config/src/gas_fee.rs @@ -387,4 +387,27 @@ mod tests { ); } } + + /// Empty blocks stop lowering the fee once `base_fee * k_rate < 10_000` + /// (integer truncation on the decrease path). For early-testnet-like + /// `k_rate = 1250`, that resting value is 7 — matching the on-chain + /// observation in #367. + #[test] + fn empty_blocks_rest_at_truncation_floor() { + let k_rate = 1250; + let iem = 5000; // 50% target + let gas_limit = 30_000_000; + let mut base_fee = 1_000_000_000u64; // 1 gwei + for _ in 0..2_000 { + base_fee = arc_calc_next_block_base_fee(0, gas_limit, base_fee, k_rate, iem); + } + assert_eq!(base_fee, 7, "expected truncation floor for k_rate={k_rate}"); + // One more empty block must not move it. + assert_eq!( + arc_calc_next_block_base_fee(0, gas_limit, base_fee, k_rate, iem), + 7 + ); + // Truncation point intuition: floor stops below 10000/k_rate (= 8). + assert_eq!(10_000 / k_rate, 8); + } } diff --git a/docs/adr/0004-base-fee-validation.md b/docs/adr/0004-base-fee-validation.md index 3be52023..4818292e 100644 --- a/docs/adr/0004-base-fee-validation.md +++ b/docs/adr/0004-base-fee-validation.md @@ -132,6 +132,7 @@ next_base_fee = arc_calc_next_block_base_fee(smoothed_gas, gas_limit, base_fee, if fee_params is not None: next_base_fee = clamp(next_base_fee, fee_params.minBaseFee, fee_params.maxBaseFee) + # 4. Apply chainspec absolute bounds next_base_fee = clamp(next_base_fee, config.absolute_min_base_fee, config.absolute_max_base_fee) @@ -162,6 +163,18 @@ assert decode(header.extra_data) == expected This replaces the current `parent.extra_data → child.base_fee` check with a post-execution invariant: the proposer's `extra_data` must match the deterministic output of execution. +### Effective base-fee floor vs `minBaseFee` + +`arc_calc_next_block_base_fee` guarantees a minimum **increase** of 1 when utilization is above target, but the **decrease** path truncates to zero with no equivalent floor. For `k_rate > 0`, once `base_fee * k_rate < 10_000` an empty block no longer lowers the fee. The practical resting floor under sustained empty blocks is therefore: + +```text +effective_floor ≈ max(minBaseFee, 10000 / kRate) +``` + +Operators reading only `feeParams.minBaseFee` can miss this interaction. It does not affect mainnet as currently configured (`minBaseFee = 20 gwei`, `kRate = 200` ⇒ truncation point 50 wei), but it did pin early testnet near 7 wei for an extended period against a declared `minBaseFee` of 1. A future governance update that lowers `minBaseFee` below `10000/kRate` (or lowers `kRate` enough to raise that truncation point above `minBaseFee`) would recreate the gap. See #367. + +Also note: the fee parameters in `assets/testnet/config.json` are **historical genesis values**. Live testnet `ProtocolConfig.feeParams()` currently matches the mainnet parameter set. + ## Consequences ### Positive From 5d46d895de0b63e1a5fc9840a6d1bb777fbc1ba7 Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Wed, 9 Sep 2026 20:49:21 +0000 Subject: [PATCH 2/3] docs(fee): correct resting floor to ceil(10000/kRate)-1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tighten ADR-0004 wording and the unit test per review on #367: the truncation threshold is 10000/kRate, but the fee rests one wei below it. Pin the early-testnet (8→7, 7→7) boundary vectors explicitly. --- crates/execution-config/src/gas_fee.rs | 31 +++++++++++++++++--------- docs/adr/0004-base-fee-validation.md | 7 +++--- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/crates/execution-config/src/gas_fee.rs b/crates/execution-config/src/gas_fee.rs index 811a2142..bd9b32bc 100644 --- a/crates/execution-config/src/gas_fee.rs +++ b/crates/execution-config/src/gas_fee.rs @@ -389,25 +389,34 @@ mod tests { } /// Empty blocks stop lowering the fee once `base_fee * k_rate < 10_000` - /// (integer truncation on the decrease path). For early-testnet-like - /// `k_rate = 1250`, that resting value is 7 — matching the on-chain - /// observation in #367. + /// (integer truncation on the decrease path). At production-scale gas + /// limits the resting value is `ceil(10000/k_rate) - 1` — matching early + /// testnet (`k_rate = 1250` → 7 wei) from #367. #[test] fn empty_blocks_rest_at_truncation_floor() { let k_rate = 1250; let iem = 5000; // 50% target let gas_limit = 30_000_000; + let resting = 7u64; // ceil(10000/1250) - 1 + assert_eq!( + arc_calc_next_block_base_fee(0, gas_limit, 8, k_rate, iem), + 7, + "one step above resting must fall" + ); + assert_eq!( + arc_calc_next_block_base_fee(0, gas_limit, resting, k_rate, iem), + resting, + "at resting value must stay put" + ); + assert_eq!( + arc_calc_next_block_base_fee(0, gas_limit, resting - 1, k_rate, iem), + resting - 1, + "below resting value must stay put" + ); let mut base_fee = 1_000_000_000u64; // 1 gwei for _ in 0..2_000 { base_fee = arc_calc_next_block_base_fee(0, gas_limit, base_fee, k_rate, iem); } - assert_eq!(base_fee, 7, "expected truncation floor for k_rate={k_rate}"); - // One more empty block must not move it. - assert_eq!( - arc_calc_next_block_base_fee(0, gas_limit, base_fee, k_rate, iem), - 7 - ); - // Truncation point intuition: floor stops below 10000/k_rate (= 8). - assert_eq!(10_000 / k_rate, 8); + assert_eq!(base_fee, resting, "long empty-block run reaches resting value"); } } diff --git a/docs/adr/0004-base-fee-validation.md b/docs/adr/0004-base-fee-validation.md index 4818292e..aed43b0a 100644 --- a/docs/adr/0004-base-fee-validation.md +++ b/docs/adr/0004-base-fee-validation.md @@ -165,13 +165,14 @@ This replaces the current `parent.extra_data → child.base_fee` check with a po ### Effective base-fee floor vs `minBaseFee` -`arc_calc_next_block_base_fee` guarantees a minimum **increase** of 1 when utilization is above target, but the **decrease** path truncates to zero with no equivalent floor. For `k_rate > 0`, once `base_fee * k_rate < 10_000` an empty block no longer lowers the fee. The practical resting floor under sustained empty blocks is therefore: +`arc_calc_next_block_base_fee` guarantees a minimum **increase** of 1 when utilization is above target, but the **decrease** path truncates to zero with no equivalent floor. For `k_rate > 0`, an empty block stops lowering the fee once `base_fee * k_rate < 10_000`. Under sustained empty blocks at production-scale gas limits, the resting value is therefore: ```text -effective_floor ≈ max(minBaseFee, 10000 / kRate) +resting_value = ceil(10000 / kRate) - 1 +effective_floor ≈ max(minBaseFee, resting_value) ``` -Operators reading only `feeParams.minBaseFee` can miss this interaction. It does not affect mainnet as currently configured (`minBaseFee = 20 gwei`, `kRate = 200` ⇒ truncation point 50 wei), but it did pin early testnet near 7 wei for an extended period against a declared `minBaseFee` of 1. A future governance update that lowers `minBaseFee` below `10000/kRate` (or lowers `kRate` enough to raise that truncation point above `minBaseFee`) would recreate the gap. See #367. +`10000 / kRate` is the truncation *threshold*; the fee comes to rest one wei below it (e.g. `kRate = 1250` ⇒ threshold 8, rest 7; `kRate = 200` ⇒ threshold 50, rest 49). Operators reading only `feeParams.minBaseFee` can miss this interaction. It does not affect mainnet as currently configured (`minBaseFee = 20 gwei`, `kRate = 200`), but early testnet sat at 7 wei for millions of blocks against a declared `minBaseFee` of 1. A future governance update that lowers `minBaseFee` below that resting value (or lowers `kRate` enough to raise it above `minBaseFee`) would recreate the gap. See #367. Also note: the fee parameters in `assets/testnet/config.json` are **historical genesis values**. Live testnet `ProtocolConfig.feeParams()` currently matches the mainnet parameter set. From ebc3d35f4c00b026698898f6b9ceb3005d9e6161 Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Wed, 9 Sep 2026 22:26:41 +0000 Subject: [PATCH 3/3] style(execution-config): rustfmt empty-block fee floor test Fix cargo fmt --check failure on the assert_eq! line length. --- crates/execution-config/src/gas_fee.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/execution-config/src/gas_fee.rs b/crates/execution-config/src/gas_fee.rs index bd9b32bc..df488282 100644 --- a/crates/execution-config/src/gas_fee.rs +++ b/crates/execution-config/src/gas_fee.rs @@ -417,6 +417,9 @@ mod tests { for _ in 0..2_000 { base_fee = arc_calc_next_block_base_fee(0, gas_limit, base_fee, k_rate, iem); } - assert_eq!(base_fee, resting, "long empty-block run reaches resting value"); + assert_eq!( + base_fee, resting, + "long empty-block run reaches resting value" + ); } }