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