reth_optimism_chainspec/
basefee.rs

1//! Base fee related utilities for Optimism chains.
2
3use core::cmp::max;
4
5use alloy_consensus::BlockHeader;
6use alloy_eips::calc_next_block_base_fee;
7use op_alloy_consensus::{decode_holocene_extra_data, decode_jovian_extra_data, EIP1559ParamError};
8use reth_chainspec::{BaseFeeParams, EthChainSpec};
9use reth_optimism_forks::OpHardforks;
10
11/// Extracts the Holocene 1599 parameters from the encoded extra data from the parent header.
12///
13/// Caution: Caller must ensure that holocene is active in the parent header.
14///
15/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#base-fee-computation)
16pub fn decode_holocene_base_fee<H>(
17    chain_spec: impl EthChainSpec + OpHardforks,
18    parent: &H,
19    timestamp: u64,
20) -> Result<u64, EIP1559ParamError>
21where
22    H: BlockHeader,
23{
24    let (elasticity, denominator) = decode_holocene_extra_data(parent.extra_data())?;
25
26    let base_fee_params = if elasticity == 0 && denominator == 0 {
27        chain_spec.base_fee_params_at_timestamp(timestamp)
28    } else {
29        BaseFeeParams::new(denominator as u128, elasticity as u128)
30    };
31
32    Ok(parent.next_block_base_fee(base_fee_params).unwrap_or_default())
33}
34
35/// Extracts the Jovian 1599 parameters from the encoded extra data from the parent header.
36/// Additionally to [`decode_holocene_base_fee`], checks if the next block base fee is less than the
37/// minimum base fee, then the minimum base fee is returned.
38///
39/// Caution: Caller must ensure that jovian is active in the parent header.
40///
41/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/exec-engine.md#base-fee-computation)
42/// and [Minimum base fee in block header](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/exec-engine.md#minimum-base-fee-in-block-header)
43pub fn compute_jovian_base_fee<H>(
44    chain_spec: impl EthChainSpec + OpHardforks,
45    parent: &H,
46    timestamp: u64,
47) -> Result<u64, EIP1559ParamError>
48where
49    H: BlockHeader,
50{
51    let (elasticity, denominator, min_base_fee) = decode_jovian_extra_data(parent.extra_data())?;
52
53    let base_fee_params = if elasticity == 0 && denominator == 0 {
54        chain_spec.base_fee_params_at_timestamp(timestamp)
55    } else {
56        BaseFeeParams::new(denominator as u128, elasticity as u128)
57    };
58
59    // Starting from Jovian, we use the maximum of the gas used and the blob gas used to calculate
60    // the next base fee.
61    let gas_used = max(parent.gas_used(), parent.blob_gas_used().unwrap_or_default());
62
63    let next_base_fee = calc_next_block_base_fee(
64        gas_used,
65        parent.gas_limit(),
66        parent.base_fee_per_gas().unwrap_or_default(),
67        base_fee_params,
68    );
69
70    if next_base_fee < min_base_fee {
71        return Ok(min_base_fee);
72    }
73
74    Ok(next_base_fee)
75}
76
77#[cfg(test)]
78mod tests {
79    use alloc::sync::Arc;
80
81    use op_alloy_consensus::encode_jovian_extra_data;
82    use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
83    use reth_optimism_forks::OpHardfork;
84
85    use crate::{OpChainSpec, BASE_SEPOLIA};
86
87    use super::*;
88
89    const JOVIAN_TIMESTAMP: u64 = 1900000000;
90
91    fn get_chainspec() -> Arc<OpChainSpec> {
92        let mut base_sepolia_spec = BASE_SEPOLIA.inner.clone();
93        base_sepolia_spec
94            .hardforks
95            .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP));
96        Arc::new(OpChainSpec {
97            inner: ChainSpec {
98                chain: base_sepolia_spec.chain,
99                genesis: base_sepolia_spec.genesis,
100                genesis_header: base_sepolia_spec.genesis_header,
101                ..Default::default()
102            },
103        })
104    }
105
106    #[test]
107    fn test_next_base_fee_jovian_blob_gas_used_greater_than_gas_used() {
108        let chain_spec = get_chainspec();
109        let mut parent = chain_spec.genesis_header().clone();
110        let timestamp = JOVIAN_TIMESTAMP;
111
112        const GAS_LIMIT: u64 = 10_000_000_000;
113        const BLOB_GAS_USED: u64 = 5_000_000_000;
114        const GAS_USED: u64 = 1_000_000_000;
115        const MIN_BASE_FEE: u64 = 100_000_000;
116
117        parent.extra_data =
118            encode_jovian_extra_data([0; 8].into(), BaseFeeParams::base_sepolia(), MIN_BASE_FEE)
119                .unwrap();
120        parent.blob_gas_used = Some(BLOB_GAS_USED);
121        parent.gas_used = GAS_USED;
122        parent.gas_limit = GAS_LIMIT;
123
124        let expected_base_fee = calc_next_block_base_fee(
125            BLOB_GAS_USED,
126            parent.gas_limit(),
127            parent.base_fee_per_gas().unwrap_or_default(),
128            BaseFeeParams::base_sepolia(),
129        );
130        assert_eq!(
131            expected_base_fee,
132            compute_jovian_base_fee(chain_spec, &parent, timestamp).unwrap()
133        );
134        assert_ne!(
135            expected_base_fee,
136            calc_next_block_base_fee(
137                GAS_USED,
138                parent.gas_limit(),
139                parent.base_fee_per_gas().unwrap_or_default(),
140                BaseFeeParams::base_sepolia(),
141            )
142        )
143    }
144
145    #[test]
146    fn test_next_base_fee_jovian_blob_gas_used_less_than_gas_used() {
147        let chain_spec = get_chainspec();
148        let mut parent = chain_spec.genesis_header().clone();
149        let timestamp = JOVIAN_TIMESTAMP;
150
151        const GAS_LIMIT: u64 = 10_000_000_000;
152        const BLOB_GAS_USED: u64 = 100_000_000;
153        const GAS_USED: u64 = 1_000_000_000;
154        const MIN_BASE_FEE: u64 = 100_000_000;
155
156        parent.extra_data =
157            encode_jovian_extra_data([0; 8].into(), BaseFeeParams::base_sepolia(), MIN_BASE_FEE)
158                .unwrap();
159        parent.blob_gas_used = Some(BLOB_GAS_USED);
160        parent.gas_used = GAS_USED;
161        parent.gas_limit = GAS_LIMIT;
162
163        let expected_base_fee = calc_next_block_base_fee(
164            GAS_USED,
165            parent.gas_limit(),
166            parent.base_fee_per_gas().unwrap_or_default(),
167            BaseFeeParams::base_sepolia(),
168        );
169        assert_eq!(
170            expected_base_fee,
171            compute_jovian_base_fee(chain_spec, &parent, timestamp).unwrap()
172        );
173    }
174
175    #[test]
176    fn test_next_base_fee_jovian_min_base_fee() {
177        let chain_spec = get_chainspec();
178        let mut parent = chain_spec.genesis_header().clone();
179        let timestamp = JOVIAN_TIMESTAMP;
180
181        const GAS_LIMIT: u64 = 10_000_000_000;
182        const BLOB_GAS_USED: u64 = 100_000_000;
183        const GAS_USED: u64 = 1_000_000_000;
184        const MIN_BASE_FEE: u64 = 5_000_000_000;
185
186        parent.extra_data =
187            encode_jovian_extra_data([0; 8].into(), BaseFeeParams::base_sepolia(), MIN_BASE_FEE)
188                .unwrap();
189        parent.blob_gas_used = Some(BLOB_GAS_USED);
190        parent.gas_used = GAS_USED;
191        parent.gas_limit = GAS_LIMIT;
192
193        let expected_base_fee = MIN_BASE_FEE;
194        assert_eq!(
195            expected_base_fee,
196            compute_jovian_base_fee(chain_spec, &parent, timestamp).unwrap()
197        );
198    }
199}