alloy_op_hardforks/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(
3    html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
4    html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
5)]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8#![no_std]
9
10extern crate alloc;
11use alloc::vec::Vec;
12use alloy_chains::{Chain, NamedChain};
13use alloy_hardforks::{EthereumHardfork, hardfork};
14pub use alloy_hardforks::{EthereumHardforks, ForkCondition};
15use alloy_primitives::U256;
16use core::ops::Index;
17
18pub mod optimism;
19pub use optimism::{mainnet as op_mainnet, mainnet::*, sepolia as op_sepolia, sepolia::*};
20
21pub mod base;
22pub use base::{mainnet as base_mainnet, mainnet::*, sepolia as base_sepolia, sepolia::*};
23
24hardfork!(
25    /// The name of an optimism hardfork.
26    ///
27    /// When building a list of hardforks for a chain, it's still expected to zip with
28    /// [`EthereumHardfork`].
29    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30    #[derive(Default)]
31    OpHardfork {
32        /// Bedrock: <https://blog.oplabs.co/introducing-optimism-bedrock>.
33        Bedrock,
34        /// Regolith: <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/superchain-upgrades.md#regolith>.
35        Regolith,
36        /// <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/superchain-upgrades.md#canyon>.
37        Canyon,
38        /// Ecotone: <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/superchain-upgrades.md#ecotone>.
39        Ecotone,
40        /// Fjord: <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/superchain-upgrades.md#fjord>
41        Fjord,
42        /// Granite: <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/superchain-upgrades.md#granite>
43        Granite,
44        /// Holocene: <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/superchain-upgrades.md#holocene>
45        Holocene,
46        /// Isthmus: <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/isthmus/overview.md>
47        #[default]
48        Isthmus,
49        /// Jovian: <https://github.com/ethereum-optimism/specs/tree/main/specs/protocol/jovian>
50        Jovian,
51        /// TODO: add interop hardfork overview when available
52        Interop,
53    }
54);
55
56impl OpHardfork {
57    /// Reverse lookup to find the hardfork given a chain ID and block timestamp.
58    /// Returns the active hardfork at the given timestamp for the specified OP chain.
59    pub fn from_chain_and_timestamp(chain: Chain, timestamp: u64) -> Option<Self> {
60        let named = chain.named()?;
61
62        match named {
63            NamedChain::Optimism => Some(match timestamp {
64                _i if timestamp < OP_MAINNET_CANYON_TIMESTAMP => Self::Regolith,
65                _i if timestamp < OP_MAINNET_ECOTONE_TIMESTAMP => Self::Canyon,
66                _i if timestamp < OP_MAINNET_FJORD_TIMESTAMP => Self::Ecotone,
67                _i if timestamp < OP_MAINNET_GRANITE_TIMESTAMP => Self::Fjord,
68                _i if timestamp < OP_MAINNET_HOLOCENE_TIMESTAMP => Self::Granite,
69                _i if timestamp < OP_MAINNET_ISTHMUS_TIMESTAMP => Self::Holocene,
70                _i if timestamp < OP_MAINNET_JOVIAN_TIMESTAMP => Self::Isthmus,
71                _ => Self::Jovian,
72            }),
73            NamedChain::OptimismSepolia => Some(match timestamp {
74                _i if timestamp < OP_SEPOLIA_CANYON_TIMESTAMP => Self::Regolith,
75                _i if timestamp < OP_SEPOLIA_ECOTONE_TIMESTAMP => Self::Canyon,
76                _i if timestamp < OP_SEPOLIA_FJORD_TIMESTAMP => Self::Ecotone,
77                _i if timestamp < OP_SEPOLIA_GRANITE_TIMESTAMP => Self::Fjord,
78                _i if timestamp < OP_SEPOLIA_HOLOCENE_TIMESTAMP => Self::Granite,
79                _i if timestamp < OP_SEPOLIA_ISTHMUS_TIMESTAMP => Self::Holocene,
80                _i if timestamp < OP_SEPOLIA_JOVIAN_TIMESTAMP => Self::Isthmus,
81                _ => Self::Jovian,
82            }),
83            NamedChain::Base => Some(match timestamp {
84                _i if timestamp < BASE_MAINNET_CANYON_TIMESTAMP => Self::Regolith,
85                _i if timestamp < BASE_MAINNET_ECOTONE_TIMESTAMP => Self::Canyon,
86                _i if timestamp < BASE_MAINNET_FJORD_TIMESTAMP => Self::Ecotone,
87                _i if timestamp < BASE_MAINNET_GRANITE_TIMESTAMP => Self::Fjord,
88                _i if timestamp < BASE_MAINNET_HOLOCENE_TIMESTAMP => Self::Granite,
89                _i if timestamp < BASE_MAINNET_ISTHMUS_TIMESTAMP => Self::Holocene,
90                _i if timestamp < BASE_MAINNET_JOVIAN_TIMESTAMP => Self::Isthmus,
91                _ => Self::Jovian,
92            }),
93            NamedChain::BaseSepolia => Some(match timestamp {
94                _i if timestamp < BASE_SEPOLIA_CANYON_TIMESTAMP => Self::Regolith,
95                _i if timestamp < BASE_SEPOLIA_ECOTONE_TIMESTAMP => Self::Canyon,
96                _i if timestamp < BASE_SEPOLIA_FJORD_TIMESTAMP => Self::Ecotone,
97                _i if timestamp < BASE_SEPOLIA_GRANITE_TIMESTAMP => Self::Fjord,
98                _i if timestamp < BASE_SEPOLIA_HOLOCENE_TIMESTAMP => Self::Granite,
99                _i if timestamp < BASE_SEPOLIA_ISTHMUS_TIMESTAMP => Self::Holocene,
100                _i if timestamp < BASE_SEPOLIA_JOVIAN_TIMESTAMP => Self::Isthmus,
101                _ => Self::Jovian,
102            }),
103            _ => None,
104        }
105    }
106
107    /// Optimism mainnet list of hardforks.
108    pub const fn op_mainnet() -> [(Self, ForkCondition); 9] {
109        [
110            (Self::Bedrock, ForkCondition::Block(OP_MAINNET_BEDROCK_BLOCK)),
111            (Self::Regolith, ForkCondition::Timestamp(OP_MAINNET_REGOLITH_TIMESTAMP)),
112            (Self::Canyon, ForkCondition::Timestamp(OP_MAINNET_CANYON_TIMESTAMP)),
113            (Self::Ecotone, ForkCondition::Timestamp(OP_MAINNET_ECOTONE_TIMESTAMP)),
114            (Self::Fjord, ForkCondition::Timestamp(OP_MAINNET_FJORD_TIMESTAMP)),
115            (Self::Granite, ForkCondition::Timestamp(OP_MAINNET_GRANITE_TIMESTAMP)),
116            (Self::Holocene, ForkCondition::Timestamp(OP_MAINNET_HOLOCENE_TIMESTAMP)),
117            (Self::Isthmus, ForkCondition::Timestamp(OP_MAINNET_ISTHMUS_TIMESTAMP)),
118            (Self::Jovian, ForkCondition::Timestamp(OP_MAINNET_JOVIAN_TIMESTAMP)),
119        ]
120    }
121
122    /// Optimism Sepolia list of hardforks.
123    pub const fn op_sepolia() -> [(Self, ForkCondition); 9] {
124        [
125            (Self::Bedrock, ForkCondition::Block(OP_SEPOLIA_BEDROCK_BLOCK)),
126            (Self::Regolith, ForkCondition::Timestamp(OP_SEPOLIA_REGOLITH_TIMESTAMP)),
127            (Self::Canyon, ForkCondition::Timestamp(OP_SEPOLIA_CANYON_TIMESTAMP)),
128            (Self::Ecotone, ForkCondition::Timestamp(OP_SEPOLIA_ECOTONE_TIMESTAMP)),
129            (Self::Fjord, ForkCondition::Timestamp(OP_SEPOLIA_FJORD_TIMESTAMP)),
130            (Self::Granite, ForkCondition::Timestamp(OP_SEPOLIA_GRANITE_TIMESTAMP)),
131            (Self::Holocene, ForkCondition::Timestamp(OP_SEPOLIA_HOLOCENE_TIMESTAMP)),
132            (Self::Isthmus, ForkCondition::Timestamp(OP_SEPOLIA_ISTHMUS_TIMESTAMP)),
133            (Self::Jovian, ForkCondition::Timestamp(OP_SEPOLIA_JOVIAN_TIMESTAMP)),
134        ]
135    }
136
137    /// Base mainnet list of hardforks.
138    pub const fn base_mainnet() -> [(Self, ForkCondition); 9] {
139        [
140            (Self::Bedrock, ForkCondition::Block(BASE_MAINNET_BEDROCK_BLOCK)),
141            (Self::Regolith, ForkCondition::Timestamp(BASE_MAINNET_REGOLITH_TIMESTAMP)),
142            (Self::Canyon, ForkCondition::Timestamp(BASE_MAINNET_CANYON_TIMESTAMP)),
143            (Self::Ecotone, ForkCondition::Timestamp(BASE_MAINNET_ECOTONE_TIMESTAMP)),
144            (Self::Fjord, ForkCondition::Timestamp(BASE_MAINNET_FJORD_TIMESTAMP)),
145            (Self::Granite, ForkCondition::Timestamp(BASE_MAINNET_GRANITE_TIMESTAMP)),
146            (Self::Holocene, ForkCondition::Timestamp(BASE_MAINNET_HOLOCENE_TIMESTAMP)),
147            (Self::Isthmus, ForkCondition::Timestamp(BASE_MAINNET_ISTHMUS_TIMESTAMP)),
148            (Self::Jovian, ForkCondition::Timestamp(BASE_MAINNET_JOVIAN_TIMESTAMP)),
149        ]
150    }
151
152    /// Base Sepolia list of hardforks.
153    pub const fn base_sepolia() -> [(Self, ForkCondition); 9] {
154        [
155            (Self::Bedrock, ForkCondition::Block(BASE_SEPOLIA_BEDROCK_BLOCK)),
156            (Self::Regolith, ForkCondition::Timestamp(BASE_SEPOLIA_REGOLITH_TIMESTAMP)),
157            (Self::Canyon, ForkCondition::Timestamp(BASE_SEPOLIA_CANYON_TIMESTAMP)),
158            (Self::Ecotone, ForkCondition::Timestamp(BASE_SEPOLIA_ECOTONE_TIMESTAMP)),
159            (Self::Fjord, ForkCondition::Timestamp(BASE_SEPOLIA_FJORD_TIMESTAMP)),
160            (Self::Granite, ForkCondition::Timestamp(BASE_SEPOLIA_GRANITE_TIMESTAMP)),
161            (Self::Holocene, ForkCondition::Timestamp(BASE_SEPOLIA_HOLOCENE_TIMESTAMP)),
162            (Self::Isthmus, ForkCondition::Timestamp(BASE_SEPOLIA_ISTHMUS_TIMESTAMP)),
163            (Self::Jovian, ForkCondition::Timestamp(BASE_SEPOLIA_JOVIAN_TIMESTAMP)),
164        ]
165    }
166
167    /// Devnet list of hardforks.
168    pub const fn devnet() -> [(Self, ForkCondition); 9] {
169        [
170            (Self::Bedrock, ForkCondition::ZERO_BLOCK),
171            (Self::Regolith, ForkCondition::ZERO_TIMESTAMP),
172            (Self::Canyon, ForkCondition::ZERO_TIMESTAMP),
173            (Self::Ecotone, ForkCondition::ZERO_TIMESTAMP),
174            (Self::Fjord, ForkCondition::ZERO_TIMESTAMP),
175            (Self::Granite, ForkCondition::ZERO_TIMESTAMP),
176            (Self::Holocene, ForkCondition::ZERO_TIMESTAMP),
177            (Self::Isthmus, ForkCondition::ZERO_TIMESTAMP),
178            (Self::Jovian, ForkCondition::Timestamp(1762185600)),
179        ]
180    }
181
182    /// Returns index of `self` in sorted canonical array.
183    pub const fn idx(&self) -> usize {
184        *self as usize
185    }
186}
187
188/// Extends [`EthereumHardforks`] with optimism helper methods.
189#[auto_impl::auto_impl(&, Arc)]
190pub trait OpHardforks: EthereumHardforks {
191    /// Retrieves [`ForkCondition`] by an [`OpHardfork`]. If `fork` is not present, returns
192    /// [`ForkCondition::Never`].
193    fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition;
194
195    /// Convenience method to check if [`OpHardfork::Bedrock`] is active at a given block
196    /// number.
197    fn is_bedrock_active_at_block(&self, block_number: u64) -> bool {
198        self.op_fork_activation(OpHardfork::Bedrock).active_at_block(block_number)
199    }
200
201    /// Returns `true` if [`Regolith`](OpHardfork::Regolith) is active at given block
202    /// timestamp.
203    fn is_regolith_active_at_timestamp(&self, timestamp: u64) -> bool {
204        self.op_fork_activation(OpHardfork::Regolith).active_at_timestamp(timestamp)
205    }
206
207    /// Returns `true` if [`Canyon`](OpHardfork::Canyon) is active at given block timestamp.
208    fn is_canyon_active_at_timestamp(&self, timestamp: u64) -> bool {
209        self.op_fork_activation(OpHardfork::Canyon).active_at_timestamp(timestamp)
210    }
211
212    /// Returns `true` if [`Ecotone`](OpHardfork::Ecotone) is active at given block timestamp.
213    fn is_ecotone_active_at_timestamp(&self, timestamp: u64) -> bool {
214        self.op_fork_activation(OpHardfork::Ecotone).active_at_timestamp(timestamp)
215    }
216
217    /// Returns `true` if [`Fjord`](OpHardfork::Fjord) is active at given block timestamp.
218    fn is_fjord_active_at_timestamp(&self, timestamp: u64) -> bool {
219        self.op_fork_activation(OpHardfork::Fjord).active_at_timestamp(timestamp)
220    }
221
222    /// Returns `true` if [`Granite`](OpHardfork::Granite) is active at given block timestamp.
223    fn is_granite_active_at_timestamp(&self, timestamp: u64) -> bool {
224        self.op_fork_activation(OpHardfork::Granite).active_at_timestamp(timestamp)
225    }
226
227    /// Returns `true` if [`Holocene`](OpHardfork::Holocene) is active at given block
228    /// timestamp.
229    fn is_holocene_active_at_timestamp(&self, timestamp: u64) -> bool {
230        self.op_fork_activation(OpHardfork::Holocene).active_at_timestamp(timestamp)
231    }
232
233    /// Returns `true` if [`Isthmus`](OpHardfork::Isthmus) is active at given block
234    /// timestamp.
235    fn is_isthmus_active_at_timestamp(&self, timestamp: u64) -> bool {
236        self.op_fork_activation(OpHardfork::Isthmus).active_at_timestamp(timestamp)
237    }
238
239    /// Returns `true` if [`Jovian`](OpHardfork::Jovian) is active at given block
240    /// timestamp.
241    fn is_jovian_active_at_timestamp(&self, timestamp: u64) -> bool {
242        self.op_fork_activation(OpHardfork::Jovian).active_at_timestamp(timestamp)
243    }
244
245    /// Returns `true` if [`Interop`](OpHardfork::Interop) is active at given block
246    /// timestamp.
247    fn is_interop_active_at_timestamp(&self, timestamp: u64) -> bool {
248        self.op_fork_activation(OpHardfork::Interop).active_at_timestamp(timestamp)
249    }
250}
251
252/// A type allowing to configure activation [`ForkCondition`]s for a given list of
253/// [`OpHardfork`]s.
254///
255/// Zips together [`EthereumHardfork`]s and [`OpHardfork`]s. Optimism hard forks, at least,
256/// whenever Ethereum hard forks. When Ethereum hard forks, a new [`OpHardfork`] piggybacks on top
257/// of the new [`EthereumHardfork`] to include (or to noop) the L1 changes on L2.
258///
259/// Optimism can also hard fork independently of Ethereum. The relation between Ethereum and
260/// Optimism hard forks is described by predicate [`EthereumHardfork`] `=>` [`OpHardfork`], since
261/// an OP chain can undergo an [`OpHardfork`] without an [`EthereumHardfork`], but not the other
262/// way around.
263#[derive(Debug, Clone)]
264pub struct OpChainHardforks {
265    /// Ordered list of OP hardfork activations.
266    forks: Vec<(OpHardfork, ForkCondition)>,
267}
268
269impl OpChainHardforks {
270    /// Creates a new [`OpChainHardforks`] with the given list of forks. The input list is sorted
271    /// w.r.t. the hardcoded canonicity of [`OpHardfork`]s.
272    pub fn new(forks: impl IntoIterator<Item = (OpHardfork, ForkCondition)>) -> Self {
273        let mut forks = forks.into_iter().collect::<Vec<_>>();
274        forks.sort();
275        Self { forks }
276    }
277
278    /// Creates a new [`OpChainHardforks`] with OP mainnet configuration.
279    pub fn op_mainnet() -> Self {
280        Self::new(OpHardfork::op_mainnet())
281    }
282
283    /// Creates a new [`OpChainHardforks`] with OP Sepolia configuration.
284    pub fn op_sepolia() -> Self {
285        Self::new(OpHardfork::op_sepolia())
286    }
287
288    /// Creates a new [`OpChainHardforks`] with Base mainnet configuration.
289    pub fn base_mainnet() -> Self {
290        Self::new(OpHardfork::base_mainnet())
291    }
292
293    /// Creates a new [`OpChainHardforks`] with Base Sepolia configuration.
294    pub fn base_sepolia() -> Self {
295        Self::new(OpHardfork::base_sepolia())
296    }
297
298    /// Creates a new [`OpChainHardforks`] with devnet configuration.
299    pub fn devnet() -> Self {
300        Self::new(OpHardfork::devnet())
301    }
302
303    /// Returns `true` if this is an OP mainnet instance.
304    pub fn is_op_mainnet(&self) -> bool {
305        self[OpHardfork::Bedrock] == ForkCondition::Block(OP_MAINNET_BEDROCK_BLOCK)
306    }
307}
308
309impl EthereumHardforks for OpChainHardforks {
310    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
311        use EthereumHardfork::{Cancun, Prague, Shanghai};
312        use OpHardfork::{Canyon, Ecotone, Isthmus};
313
314        if self.forks.is_empty() {
315            return ForkCondition::Never;
316        }
317
318        let forks_len = self.forks.len();
319        // check index out of bounds
320        match fork {
321            Shanghai if forks_len <= Canyon.idx() => ForkCondition::Never,
322            Cancun if forks_len <= Ecotone.idx() => ForkCondition::Never,
323            Prague if forks_len <= Isthmus.idx() => ForkCondition::Never,
324            _ => self[fork],
325        }
326    }
327}
328
329impl OpHardforks for OpChainHardforks {
330    fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition {
331        // check index out of bounds
332        if self.forks.len() <= fork.idx() {
333            return ForkCondition::Never;
334        }
335        self[fork]
336    }
337}
338
339impl Index<OpHardfork> for OpChainHardforks {
340    type Output = ForkCondition;
341
342    fn index(&self, hf: OpHardfork) -> &Self::Output {
343        use OpHardfork::*;
344
345        match hf {
346            Bedrock => &self.forks[Bedrock.idx()].1,
347            Regolith => &self.forks[Regolith.idx()].1,
348            Canyon => &self.forks[Canyon.idx()].1,
349            Ecotone => &self.forks[Ecotone.idx()].1,
350            Fjord => &self.forks[Fjord.idx()].1,
351            Granite => &self.forks[Granite.idx()].1,
352            Holocene => &self.forks[Holocene.idx()].1,
353            Isthmus => &self.forks[Isthmus.idx()].1,
354            Jovian => &self.forks[Jovian.idx()].1,
355            Interop => &self.forks[Interop.idx()].1,
356        }
357    }
358}
359
360impl Index<EthereumHardfork> for OpChainHardforks {
361    type Output = ForkCondition;
362
363    fn index(&self, hf: EthereumHardfork) -> &Self::Output {
364        use EthereumHardfork::*;
365        use OpHardfork::*;
366
367        match hf {
368            Frontier | Homestead | Tangerine | SpuriousDragon | Byzantium | Constantinople
369            | Petersburg | Istanbul | MuirGlacier => &ForkCondition::ZERO_BLOCK,
370            // Dao Hardfork is not needed for OpChainHardforks
371            Dao => &ForkCondition::Never,
372            Berlin if self.is_op_mainnet() => &ForkCondition::Block(OP_MAINNET_BERLIN_BLOCK),
373            Berlin => &ForkCondition::ZERO_BLOCK,
374            London | ArrowGlacier | GrayGlacier => &self[Bedrock],
375            Paris if self.is_op_mainnet() => &ForkCondition::TTD {
376                activation_block_number: OP_MAINNET_BEDROCK_BLOCK,
377                fork_block: Some(OP_MAINNET_BEDROCK_BLOCK),
378                total_difficulty: U256::ZERO,
379            },
380            Paris => &ForkCondition::TTD {
381                activation_block_number: 0,
382                fork_block: Some(0),
383                total_difficulty: U256::ZERO,
384            },
385            Shanghai => &self[Canyon],
386            Cancun => &self[Ecotone],
387            Prague => &self[Isthmus],
388            // Not activated for now
389            Osaka | Bpo1 | Bpo2 | Bpo3 | Bpo4 | Bpo5 | Amsterdam => &ForkCondition::Never,
390            _ => unreachable!(),
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use core::str::FromStr;
399
400    extern crate alloc;
401
402    #[test]
403    fn check_op_hardfork_from_str() {
404        let hardfork_str = [
405            "beDrOck", "rEgOlITH", "cAnYoN", "eCoToNe", "FJorD", "GRaNiTe", "hOlOcEnE", "isthMUS",
406            "jOvIaN", "inTerOP",
407        ];
408        let expected_hardforks = [
409            OpHardfork::Bedrock,
410            OpHardfork::Regolith,
411            OpHardfork::Canyon,
412            OpHardfork::Ecotone,
413            OpHardfork::Fjord,
414            OpHardfork::Granite,
415            OpHardfork::Holocene,
416            OpHardfork::Isthmus,
417            OpHardfork::Jovian,
418            OpHardfork::Interop,
419        ];
420
421        let hardforks: alloc::vec::Vec<OpHardfork> =
422            hardfork_str.iter().map(|h| OpHardfork::from_str(h).unwrap()).collect();
423
424        assert_eq!(hardforks, expected_hardforks);
425    }
426
427    #[test]
428    fn check_nonexistent_hardfork_from_str() {
429        assert!(OpHardfork::from_str("not a hardfork").is_err());
430    }
431
432    #[test]
433    fn op_mainnet_fork_conditions() {
434        use OpHardfork::*;
435
436        let op_mainnet_forks = OpChainHardforks::op_mainnet();
437        assert_eq!(op_mainnet_forks[Bedrock], ForkCondition::Block(OP_MAINNET_BEDROCK_BLOCK));
438        assert_eq!(
439            op_mainnet_forks[Regolith],
440            ForkCondition::Timestamp(OP_MAINNET_REGOLITH_TIMESTAMP)
441        );
442        assert_eq!(op_mainnet_forks[Canyon], ForkCondition::Timestamp(OP_MAINNET_CANYON_TIMESTAMP));
443        assert_eq!(
444            op_mainnet_forks[Ecotone],
445            ForkCondition::Timestamp(OP_MAINNET_ECOTONE_TIMESTAMP)
446        );
447        assert_eq!(op_mainnet_forks[Fjord], ForkCondition::Timestamp(OP_MAINNET_FJORD_TIMESTAMP));
448        assert_eq!(
449            op_mainnet_forks[Granite],
450            ForkCondition::Timestamp(OP_MAINNET_GRANITE_TIMESTAMP)
451        );
452        assert_eq!(
453            op_mainnet_forks[Holocene],
454            ForkCondition::Timestamp(OP_MAINNET_HOLOCENE_TIMESTAMP)
455        );
456        assert_eq!(
457            op_mainnet_forks[Isthmus],
458            ForkCondition::Timestamp(OP_MAINNET_ISTHMUS_TIMESTAMP)
459        );
460        assert_eq!(op_mainnet_forks[Jovian], ForkCondition::Timestamp(OP_MAINNET_JOVIAN_TIMESTAMP));
461        assert_eq!(op_mainnet_forks.op_fork_activation(Interop), ForkCondition::Never);
462    }
463
464    #[test]
465    fn op_sepolia_fork_conditions() {
466        use OpHardfork::*;
467
468        let op_sepolia_forks = OpChainHardforks::op_sepolia();
469        assert_eq!(op_sepolia_forks[Bedrock], ForkCondition::Block(OP_SEPOLIA_BEDROCK_BLOCK));
470        assert_eq!(
471            op_sepolia_forks[Regolith],
472            ForkCondition::Timestamp(OP_SEPOLIA_REGOLITH_TIMESTAMP)
473        );
474        assert_eq!(op_sepolia_forks[Canyon], ForkCondition::Timestamp(OP_SEPOLIA_CANYON_TIMESTAMP));
475        assert_eq!(
476            op_sepolia_forks[Ecotone],
477            ForkCondition::Timestamp(OP_SEPOLIA_ECOTONE_TIMESTAMP)
478        );
479        assert_eq!(op_sepolia_forks[Fjord], ForkCondition::Timestamp(OP_SEPOLIA_FJORD_TIMESTAMP));
480        assert_eq!(
481            op_sepolia_forks[Granite],
482            ForkCondition::Timestamp(OP_SEPOLIA_GRANITE_TIMESTAMP)
483        );
484        assert_eq!(
485            op_sepolia_forks[Holocene],
486            ForkCondition::Timestamp(OP_SEPOLIA_HOLOCENE_TIMESTAMP)
487        );
488        assert_eq!(
489            op_sepolia_forks[Isthmus],
490            ForkCondition::Timestamp(OP_SEPOLIA_ISTHMUS_TIMESTAMP)
491        );
492        assert_eq!(op_sepolia_forks[Jovian], ForkCondition::Timestamp(OP_SEPOLIA_JOVIAN_TIMESTAMP));
493        assert_eq!(op_sepolia_forks.op_fork_activation(Interop), ForkCondition::Never);
494    }
495
496    #[test]
497    fn base_mainnet_fork_conditions() {
498        use OpHardfork::*;
499
500        let base_mainnet_forks = OpChainHardforks::base_mainnet();
501        assert_eq!(base_mainnet_forks[Bedrock], ForkCondition::Block(BASE_MAINNET_BEDROCK_BLOCK));
502        assert_eq!(
503            base_mainnet_forks[Regolith],
504            ForkCondition::Timestamp(BASE_MAINNET_REGOLITH_TIMESTAMP)
505        );
506        assert_eq!(
507            base_mainnet_forks[Canyon],
508            ForkCondition::Timestamp(BASE_MAINNET_CANYON_TIMESTAMP)
509        );
510        assert_eq!(
511            base_mainnet_forks[Ecotone],
512            ForkCondition::Timestamp(BASE_MAINNET_ECOTONE_TIMESTAMP)
513        );
514        assert_eq!(
515            base_mainnet_forks[Fjord],
516            ForkCondition::Timestamp(BASE_MAINNET_FJORD_TIMESTAMP)
517        );
518        assert_eq!(
519            base_mainnet_forks[Granite],
520            ForkCondition::Timestamp(BASE_MAINNET_GRANITE_TIMESTAMP)
521        );
522        assert_eq!(
523            base_mainnet_forks[Holocene],
524            ForkCondition::Timestamp(BASE_MAINNET_HOLOCENE_TIMESTAMP)
525        );
526        assert_eq!(
527            base_mainnet_forks[Isthmus],
528            ForkCondition::Timestamp(BASE_MAINNET_ISTHMUS_TIMESTAMP)
529        );
530        assert_eq!(
531            base_mainnet_forks[Jovian],
532            ForkCondition::Timestamp(BASE_MAINNET_JOVIAN_TIMESTAMP)
533        );
534        assert_eq!(
535            base_mainnet_forks[Jovian],
536            ForkCondition::Timestamp(OP_MAINNET_JOVIAN_TIMESTAMP)
537        );
538        assert_eq!(base_mainnet_forks.op_fork_activation(Interop), ForkCondition::Never);
539    }
540
541    #[test]
542    fn base_sepolia_fork_conditions() {
543        use OpHardfork::*;
544
545        let base_sepolia_forks = OpChainHardforks::base_sepolia();
546        assert_eq!(base_sepolia_forks[Bedrock], ForkCondition::Block(BASE_SEPOLIA_BEDROCK_BLOCK));
547        assert_eq!(
548            base_sepolia_forks[Regolith],
549            ForkCondition::Timestamp(BASE_SEPOLIA_REGOLITH_TIMESTAMP)
550        );
551        assert_eq!(
552            base_sepolia_forks[Canyon],
553            ForkCondition::Timestamp(BASE_SEPOLIA_CANYON_TIMESTAMP)
554        );
555        assert_eq!(
556            base_sepolia_forks[Ecotone],
557            ForkCondition::Timestamp(BASE_SEPOLIA_ECOTONE_TIMESTAMP)
558        );
559        assert_eq!(
560            base_sepolia_forks[Fjord],
561            ForkCondition::Timestamp(BASE_SEPOLIA_FJORD_TIMESTAMP)
562        );
563        assert_eq!(
564            base_sepolia_forks[Granite],
565            ForkCondition::Timestamp(BASE_SEPOLIA_GRANITE_TIMESTAMP)
566        );
567        assert_eq!(
568            base_sepolia_forks[Holocene],
569            ForkCondition::Timestamp(BASE_SEPOLIA_HOLOCENE_TIMESTAMP)
570        );
571        assert_eq!(
572            base_sepolia_forks[Isthmus],
573            ForkCondition::Timestamp(BASE_SEPOLIA_ISTHMUS_TIMESTAMP)
574        );
575        assert_eq!(
576            base_sepolia_forks.op_fork_activation(Jovian),
577            ForkCondition::Timestamp(BASE_SEPOLIA_JOVIAN_TIMESTAMP)
578        );
579        assert_eq!(
580            base_sepolia_forks[Jovian],
581            ForkCondition::Timestamp(OP_SEPOLIA_JOVIAN_TIMESTAMP)
582        );
583        assert_eq!(base_sepolia_forks.op_fork_activation(Interop), ForkCondition::Never);
584    }
585
586    #[test]
587    fn is_jovian_active_at_timestamp() {
588        let op_mainnet_forks = OpChainHardforks::op_mainnet();
589        assert!(op_mainnet_forks.is_jovian_active_at_timestamp(OP_MAINNET_JOVIAN_TIMESTAMP));
590        assert!(!op_mainnet_forks.is_jovian_active_at_timestamp(OP_MAINNET_JOVIAN_TIMESTAMP - 1));
591        assert!(op_mainnet_forks.is_jovian_active_at_timestamp(OP_MAINNET_JOVIAN_TIMESTAMP + 1000));
592
593        let op_sepolia_forks = OpChainHardforks::op_sepolia();
594        assert!(op_sepolia_forks.is_jovian_active_at_timestamp(OP_SEPOLIA_JOVIAN_TIMESTAMP));
595        assert!(!op_sepolia_forks.is_jovian_active_at_timestamp(OP_SEPOLIA_JOVIAN_TIMESTAMP - 1));
596        assert!(op_sepolia_forks.is_jovian_active_at_timestamp(OP_SEPOLIA_JOVIAN_TIMESTAMP + 1000));
597
598        let base_mainnet_forks = OpChainHardforks::base_mainnet();
599        assert!(base_mainnet_forks.is_jovian_active_at_timestamp(BASE_MAINNET_JOVIAN_TIMESTAMP));
600        assert!(
601            !base_mainnet_forks.is_jovian_active_at_timestamp(BASE_MAINNET_JOVIAN_TIMESTAMP - 1)
602        );
603        assert!(
604            base_mainnet_forks.is_jovian_active_at_timestamp(BASE_MAINNET_JOVIAN_TIMESTAMP + 1000)
605        );
606
607        let base_sepolia_forks = OpChainHardforks::base_sepolia();
608        assert!(base_sepolia_forks.is_jovian_active_at_timestamp(BASE_SEPOLIA_JOVIAN_TIMESTAMP));
609        assert!(
610            !base_sepolia_forks.is_jovian_active_at_timestamp(BASE_SEPOLIA_JOVIAN_TIMESTAMP - 1)
611        );
612        assert!(
613            base_sepolia_forks.is_jovian_active_at_timestamp(BASE_SEPOLIA_JOVIAN_TIMESTAMP + 1000)
614        );
615    }
616
617    #[test]
618    fn test_reverse_lookup_op_chains() {
619        // Test key hardforks across all OP stack chains
620        let test_cases = [
621            // (chain_id, timestamp, expected) - focusing on major transitions
622            // OP Mainnet
623            (Chain::optimism_mainnet(), OP_MAINNET_CANYON_TIMESTAMP, OpHardfork::Canyon),
624            (Chain::optimism_mainnet(), OP_MAINNET_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
625            (Chain::optimism_mainnet(), OP_MAINNET_GRANITE_TIMESTAMP, OpHardfork::Granite),
626            (Chain::optimism_mainnet(), OP_MAINNET_CANYON_TIMESTAMP - 1, OpHardfork::Regolith),
627            (Chain::optimism_mainnet(), OP_MAINNET_ISTHMUS_TIMESTAMP + 1000, OpHardfork::Isthmus),
628            (Chain::optimism_mainnet(), OP_MAINNET_JOVIAN_TIMESTAMP, OpHardfork::Jovian),
629            (Chain::optimism_mainnet(), OP_MAINNET_JOVIAN_TIMESTAMP - 1, OpHardfork::Isthmus),
630            (Chain::optimism_mainnet(), OP_MAINNET_JOVIAN_TIMESTAMP + 1000, OpHardfork::Jovian),
631            // OP Sepolia
632            (Chain::optimism_sepolia(), OP_SEPOLIA_CANYON_TIMESTAMP, OpHardfork::Canyon),
633            (Chain::optimism_sepolia(), OP_SEPOLIA_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
634            (Chain::optimism_sepolia(), OP_SEPOLIA_CANYON_TIMESTAMP - 1, OpHardfork::Regolith),
635            (Chain::optimism_sepolia(), OP_SEPOLIA_JOVIAN_TIMESTAMP, OpHardfork::Jovian),
636            (Chain::optimism_sepolia(), OP_SEPOLIA_JOVIAN_TIMESTAMP - 1, OpHardfork::Isthmus),
637            (Chain::optimism_sepolia(), OP_SEPOLIA_JOVIAN_TIMESTAMP + 1000, OpHardfork::Jovian),
638            // Base Mainnet
639            (Chain::base_mainnet(), BASE_MAINNET_CANYON_TIMESTAMP, OpHardfork::Canyon),
640            (Chain::base_mainnet(), BASE_MAINNET_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
641            (Chain::base_mainnet(), BASE_MAINNET_JOVIAN_TIMESTAMP, OpHardfork::Jovian),
642            // Base Sepolia
643            (Chain::base_sepolia(), BASE_SEPOLIA_CANYON_TIMESTAMP, OpHardfork::Canyon),
644            (Chain::base_sepolia(), BASE_SEPOLIA_ECOTONE_TIMESTAMP, OpHardfork::Ecotone),
645            (Chain::base_sepolia(), BASE_SEPOLIA_JOVIAN_TIMESTAMP, OpHardfork::Jovian),
646        ];
647
648        for (chain_id, timestamp, expected) in test_cases {
649            assert_eq!(
650                OpHardfork::from_chain_and_timestamp(chain_id, timestamp),
651                Some(expected),
652                "chain {chain_id} at timestamp {timestamp}"
653            );
654        }
655
656        // Edge cases
657        assert_eq!(OpHardfork::from_chain_and_timestamp(Chain::from_id(999999), 1000000), None);
658    }
659
660    // https://github.com/alloy-rs/hardforks/issues/63
661    #[test]
662    fn test_ethereum_fork_activation_consistency() {
663        let op_mainnet_forks = OpChainHardforks::op_mainnet();
664        for ethereum_hardfork in EthereumHardfork::VARIANTS {
665            let _ = op_mainnet_forks.ethereum_fork_activation(*ethereum_hardfork);
666        }
667        for op_hardfork in OpHardfork::VARIANTS {
668            let _ = op_mainnet_forks.op_fork_activation(*op_hardfork);
669        }
670    }
671}