1use alloy_consensus::BlockHeader;
4use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams};
5use alloy_primitives::Bytes;
6use foundry_evm::hardfork::OpHardfork;
7use op_alloy_consensus::{
8 decode_holocene_extra_data, decode_jovian_extra_data, encode_holocene_extra_data,
9 encode_jovian_extra_data,
10};
11
12#[derive(Clone, Copy, Debug)]
14pub(super) enum OptimismBaseFeeRules {
15 Holocene { params: BaseFeeParams },
16 Jovian { params: BaseFeeParams, min_base_fee: u64 },
17}
18
19impl OptimismBaseFeeRules {
20 pub(super) fn for_hardfork(hardfork: OpHardfork, params: BaseFeeParams) -> Option<Self> {
21 if hardfork >= OpHardfork::Jovian {
22 Some(Self::Jovian { params, min_base_fee: 0 })
23 } else if hardfork >= OpHardfork::Holocene {
24 Some(Self::Holocene { params })
25 } else {
26 None
27 }
28 }
29
30 pub(super) fn decode(extra_data: &[u8]) -> Option<Self> {
31 if let Ok((elasticity, denominator, min_base_fee)) = decode_jovian_extra_data(extra_data) {
32 return Some(Self::Jovian {
33 params: BaseFeeParams::new(denominator as u128, elasticity as u128),
34 min_base_fee,
35 });
36 }
37 if let Ok((elasticity, denominator)) = decode_holocene_extra_data(extra_data) {
38 return Some(Self::Holocene {
39 params: BaseFeeParams::new(denominator as u128, elasticity as u128),
40 });
41 }
42 None
43 }
44
45 pub(super) const fn params(self) -> BaseFeeParams {
46 match self {
47 Self::Holocene { params, .. } | Self::Jovian { params, .. } => params,
48 }
49 }
50
51 pub(super) fn extra_data(self) -> Bytes {
52 match self {
53 Self::Holocene { params } => encode_holocene_extra_data([0; 8].into(), params),
54 Self::Jovian { params, min_base_fee } => {
55 encode_jovian_extra_data([0; 8].into(), params, min_base_fee)
56 }
57 }
58 .expect("optimism fee parameters fit canonical extra data")
59 }
60
61 pub(super) const fn is_jovian(self) -> bool {
62 matches!(self, Self::Jovian { .. })
63 }
64
65 pub(super) fn next_block_base_fee<H: BlockHeader>(self, header: &H) -> u64 {
66 let gas_used = match self {
67 Self::Holocene { .. } => header.gas_used(),
68 Self::Jovian { .. } => {
69 header.gas_used().max(header.blob_gas_used().unwrap_or_default())
70 }
71 };
72 let next_base_fee = calc_next_block_base_fee(
73 gas_used,
74 header.gas_limit(),
75 header.base_fee_per_gas().unwrap_or_default(),
76 self.params(),
77 );
78 match self {
79 Self::Jovian { min_base_fee, .. } => next_base_fee.max(min_base_fee),
80 Self::Holocene { .. } => next_base_fee,
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use alloy_consensus::Header;
88
89 use super::*;
90
91 #[test]
92 fn header_base_fee_rules_match_upstream_blocks() {
93 struct Case {
94 extra_data: &'static [u8],
95 gas_limit: u64,
96 gas_used: u64,
97 blob_gas_used: u64,
98 base_fee: u64,
99 expected: u64,
100 }
101
102 let cases = [
104 Case {
105 extra_data: &[1, 0, 0, 0, 100, 0, 0, 0, 5, 0, 0, 0, 0, 0, 76, 75, 64],
106 gas_limit: 0x17d7_8400,
107 gas_used: 0x132_0096,
108 blob_gas_used: 0x37_5b00,
109 base_fee: 0x4c_4b40,
110 expected: 0x4c_4b40,
111 },
112 Case {
113 extra_data: &[1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0],
114 gas_limit: 0x262_5a00,
115 gas_used: 0xa5_646d,
116 blob_gas_used: 0x22_cd60,
117 base_fee: 0x196,
118 expected: 0x196,
119 },
120 Case {
121 extra_data: &[0, 0, 0, 0, 50, 0, 0, 0, 2],
122 gas_limit: 0x858_3b00,
123 gas_used: 0x311_2ab9,
124 blob_gas_used: 0,
125 base_fee: 0xc_bdf6,
126 expected: 0xc_acae,
127 },
128 Case {
129 extra_data: &[0, 0, 0, 0, 250, 0, 0, 0, 2],
130 gas_limit: 0x262_5a00,
131 gas_used: 0xe6_2170,
132 blob_gas_used: 0,
133 base_fee: 0xd87,
134 expected: 0xd84,
135 },
136 ];
137
138 for case in cases {
139 let rules = OptimismBaseFeeRules::decode(case.extra_data).unwrap();
140 let header = Header {
141 gas_limit: case.gas_limit,
142 gas_used: case.gas_used,
143 blob_gas_used: Some(case.blob_gas_used),
144 base_fee_per_gas: Some(case.base_fee),
145 ..Default::default()
146 };
147
148 assert_eq!(rules.next_block_base_fee(&header), case.expected);
149 assert_eq!(rules.extra_data().as_ref(), case.extra_data);
150 }
151 }
152
153 #[test]
154 fn jovian_base_fee_uses_blob_gas_when_greater() {
155 let extra_data = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0];
156 let rules = OptimismBaseFeeRules::decode(&extra_data).unwrap();
157 let header = Header {
158 gas_limit: 10_000_000_000,
159 gas_used: 1_000_000_000,
160 blob_gas_used: Some(5_000_000_000),
161 base_fee_per_gas: Some(super::super::INITIAL_BASE_FEE),
162 ..Default::default()
163 };
164
165 let expected = calc_next_block_base_fee(
166 header.blob_gas_used.unwrap(),
167 header.gas_limit,
168 header.base_fee_per_gas.unwrap(),
169 BaseFeeParams::new(250, 2),
170 );
171 assert_eq!(rules.next_block_base_fee(&header), expected);
172 }
173
174 #[test]
175 fn supplied_header_overrides_cached_optimism_rules() {
176 let holocene = [0, 0, 0, 0, 50, 0, 0, 0, 2];
177 let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
178 let fallback = BaseFeeParams::ethereum();
179
180 for (cached, supplied) in
181 [(jovian.as_slice(), holocene.as_slice()), (holocene.as_slice(), jovian.as_slice())]
182 {
183 let supplied_rules = OptimismBaseFeeRules::decode(supplied).unwrap();
184 let header = Header {
185 gas_limit: 10_000_000_000,
186 gas_used: 1_000_000_000,
187 blob_gas_used: Some(5_000_000_000),
188 base_fee_per_gas: Some(super::super::INITIAL_BASE_FEE),
189 extra_data: Bytes::copy_from_slice(supplied),
190 ..Default::default()
191 };
192 let rules = super::super::BaseFeeRules::Optimism {
193 inherited: Some(OptimismBaseFeeRules::decode(cached).unwrap()),
194 fallback,
195 };
196 let parent_fees = rules.parent_header_fees(&header);
197
198 assert_eq!(parent_fees.base_fee, supplied_rules.next_block_base_fee(&header));
199 assert_eq!(parent_fees.extra_data.as_ref(), supplied);
200 assert_eq!(parent_fees.optimism_jovian, Some(supplied_rules.is_jovian()));
201 }
202 }
203}