Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions crates/execution-config/src/gas_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
}
14 changes: 14 additions & 0 deletions docs/adr/0004-base-fee-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down