diff --git a/crates/execution-config/src/gas_fee.rs b/crates/execution-config/src/gas_fee.rs index 37b944c..df48828 100644 --- a/crates/execution-config/src/gas_fee.rs +++ b/crates/execution-config/src/gas_fee.rs @@ -387,4 +387,39 @@ mod tests { ); } } + + /// Empty blocks stop lowering the fee once `base_fee * k_rate < 10_000` + /// (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, 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 3be5202..aed43b0 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,19 @@ 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`, 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 +resting_value = ceil(10000 / kRate) - 1 +effective_floor ≈ max(minBaseFee, resting_value) +``` + +`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. + ## Consequences ### Positive