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