|
| 1 | +use std::{ |
| 2 | + any::Any, |
| 3 | + panic::{self, AssertUnwindSafe}, |
| 4 | +}; |
| 5 | + |
| 6 | +use base_consensus_genesis::{BaseHardforkConfig, HardForkConfig}; |
| 7 | + |
| 8 | +type ForkSetter = fn(&mut HardForkConfig); |
| 9 | + |
| 10 | +/// All supported hardfork stages in canonical order. Each setter activates |
| 11 | +/// exactly one additional fork; [`ForkMatrix::all`] applies these in sequence |
| 12 | +/// to produce a cumulative snapshot after each step. |
| 13 | +/// |
| 14 | +/// To add a new fork: insert one entry here in the correct position. All |
| 15 | +/// matrix constructors update automatically. |
| 16 | +static FORK_PROGRESSION: &[(&str, ForkSetter)] = &[ |
| 17 | + ("regolith", |h| h.regolith_time = Some(0)), |
| 18 | + ("canyon", |h| h.canyon_time = Some(0)), |
| 19 | + ("delta", |h| h.delta_time = Some(0)), |
| 20 | + ("ecotone", |h| h.ecotone_time = Some(0)), |
| 21 | + ("fjord", |h| h.fjord_time = Some(0)), |
| 22 | + ("granite", |h| h.granite_time = Some(0)), |
| 23 | + ("holocene", |h| h.holocene_time = Some(0)), |
| 24 | + ("pectra-blob-schedule", |h| h.pectra_blob_schedule_time = Some(0)), |
| 25 | + ("isthmus", |h| h.isthmus_time = Some(0)), |
| 26 | + ("jovian", |h| h.jovian_time = Some(0)), |
| 27 | + ("base-v1", |h| h.base.get_or_insert_with(BaseHardforkConfig::default).v1 = Some(0)), |
| 28 | +]; |
| 29 | + |
| 30 | +/// Named hardfork schedules for parametrizing harness tests across protocol upgrades. |
| 31 | +#[derive(Debug, Clone, Default, PartialEq, Eq)] |
| 32 | +pub struct ForkMatrix { |
| 33 | + forks: Vec<(&'static str, HardForkConfig)>, |
| 34 | +} |
| 35 | + |
| 36 | +impl ForkMatrix { |
| 37 | + /// Returns every cumulative hardfork stage supported by the harness. |
| 38 | + /// |
| 39 | + /// Each entry activates one additional fork on top of all previous ones, |
| 40 | + /// derived automatically from [`FORK_PROGRESSION`]. |
| 41 | + pub fn all() -> Self { |
| 42 | + Self::build(FORK_PROGRESSION, HardForkConfig::default()) |
| 43 | + } |
| 44 | + |
| 45 | + /// Returns the cumulative forks from Granite through Holocene (pre-Isthmus). |
| 46 | + /// |
| 47 | + /// Includes the `pectra-blob-schedule` compatibility patch, which sits |
| 48 | + /// between Holocene and Isthmus in the progression. |
| 49 | + pub fn pre_isthmus() -> Self { |
| 50 | + Self::all().retain(|_, h| h.granite_time.is_some() && h.isthmus_time.is_none()) |
| 51 | + } |
| 52 | + |
| 53 | + /// Returns the cumulative OP hardforks from Isthmus onward. |
| 54 | + /// |
| 55 | + /// Base-specific forks (e.g. `base-v1`) are excluded. |
| 56 | + pub fn from_isthmus() -> Self { |
| 57 | + Self::all().retain(|_, h| h.isthmus_time.is_some() && h.base.is_none()) |
| 58 | + } |
| 59 | + |
| 60 | + /// Returns the canonical OP fault-proof fork progression from Granite onward. |
| 61 | + /// |
| 62 | + /// The `pectra-blob-schedule` compatibility patch (a Base Sepolia-only quirk) |
| 63 | + /// and Base-specific forks are excluded; this matrix covers only the upstream |
| 64 | + /// OP mainnet upgrade sequence. |
| 65 | + pub fn from_granite() -> Self { |
| 66 | + static PROGRESSION: &[(&str, ForkSetter)] = &[ |
| 67 | + ("granite", |h| h.granite_time = Some(0)), |
| 68 | + ("holocene", |h| h.holocene_time = Some(0)), |
| 69 | + ("isthmus", |h| h.isthmus_time = Some(0)), |
| 70 | + ("jovian", |h| h.jovian_time = Some(0)), |
| 71 | + ]; |
| 72 | + Self::build( |
| 73 | + PROGRESSION, |
| 74 | + HardForkConfig { |
| 75 | + regolith_time: Some(0), |
| 76 | + canyon_time: Some(0), |
| 77 | + delta_time: Some(0), |
| 78 | + ecotone_time: Some(0), |
| 79 | + fjord_time: Some(0), |
| 80 | + ..Default::default() |
| 81 | + }, |
| 82 | + ) |
| 83 | + } |
| 84 | + |
| 85 | + /// Iterates through the named fork schedules. |
| 86 | + pub fn iter(&self) -> impl Iterator<Item = (&'static str, HardForkConfig)> + '_ { |
| 87 | + self.forks.iter().copied() |
| 88 | + } |
| 89 | + |
| 90 | + /// Keeps only the fork schedules matching the predicate. |
| 91 | + pub fn retain<F>(mut self, mut f: F) -> Self |
| 92 | + where |
| 93 | + F: FnMut(&'static str, HardForkConfig) -> bool, |
| 94 | + { |
| 95 | + self.forks.retain(|(name, config)| f(name, *config)); |
| 96 | + self |
| 97 | + } |
| 98 | + |
| 99 | + /// Runs a test closure once per configured fork, annotating any panic with the fork name. |
| 100 | + pub fn run<F>(&self, mut test: F) |
| 101 | + where |
| 102 | + F: FnMut(&'static str, HardForkConfig), |
| 103 | + { |
| 104 | + for (name, config) in self.iter() { |
| 105 | + if let Err(e) = panic::catch_unwind(AssertUnwindSafe(|| test(name, config))) { |
| 106 | + Self::panic_with_fork_context(name, e); |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + fn build(progression: &[(&'static str, ForkSetter)], base: HardForkConfig) -> Self { |
| 112 | + let mut config = base; |
| 113 | + Self { |
| 114 | + forks: progression |
| 115 | + .iter() |
| 116 | + .map(|(name, apply)| { |
| 117 | + apply(&mut config); |
| 118 | + (*name, config) |
| 119 | + }) |
| 120 | + .collect(), |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + fn panic_with_fork_context(fork: &str, payload: Box<dyn Any + Send + 'static>) -> ! { |
| 125 | + let msg = payload |
| 126 | + .downcast_ref::<String>() |
| 127 | + .map(String::as_str) |
| 128 | + .or_else(|| payload.downcast_ref::<&str>().copied()) |
| 129 | + .unwrap_or("non-string panic payload"); |
| 130 | + panic!("fork matrix case `{fork}` failed: {msg}"); |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +#[cfg(test)] |
| 135 | +mod tests { |
| 136 | + use base_consensus_genesis::RollupConfig; |
| 137 | + |
| 138 | + use super::*; |
| 139 | + |
| 140 | + fn rollup_config(hardforks: HardForkConfig) -> RollupConfig { |
| 141 | + RollupConfig { block_time: 2, hardforks, ..Default::default() } |
| 142 | + } |
| 143 | + |
| 144 | + fn panic_message(payload: Box<dyn Any + Send>) -> String { |
| 145 | + payload |
| 146 | + .downcast_ref::<String>() |
| 147 | + .cloned() |
| 148 | + .or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_owned())) |
| 149 | + .unwrap_or_else(|| "non-string panic payload".to_owned()) |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn all_covers_the_supported_hardfork_progression() { |
| 154 | + let names: Vec<_> = ForkMatrix::all().iter().map(|(name, _)| name).collect(); |
| 155 | + assert_eq!( |
| 156 | + names, |
| 157 | + vec![ |
| 158 | + "regolith", |
| 159 | + "canyon", |
| 160 | + "delta", |
| 161 | + "ecotone", |
| 162 | + "fjord", |
| 163 | + "granite", |
| 164 | + "holocene", |
| 165 | + "pectra-blob-schedule", |
| 166 | + "isthmus", |
| 167 | + "jovian", |
| 168 | + "base-v1", |
| 169 | + ] |
| 170 | + ); |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn from_granite_matches_the_fault_proof_forks() { |
| 175 | + let names: Vec<_> = ForkMatrix::from_granite().iter().map(|(name, _)| name).collect(); |
| 176 | + assert_eq!(names, vec!["granite", "holocene", "isthmus", "jovian"]); |
| 177 | + } |
| 178 | + |
| 179 | + #[test] |
| 180 | + fn pre_isthmus_includes_pectra_and_excludes_isthmus_and_later() { |
| 181 | + let names: Vec<_> = ForkMatrix::pre_isthmus().iter().map(|(name, _)| name).collect(); |
| 182 | + assert_eq!(names, vec!["granite", "holocene", "pectra-blob-schedule"]); |
| 183 | + } |
| 184 | + |
| 185 | + #[test] |
| 186 | + fn from_isthmus_includes_only_op_forks_from_isthmus_onward() { |
| 187 | + let names: Vec<_> = ForkMatrix::from_isthmus().iter().map(|(name, _)| name).collect(); |
| 188 | + assert_eq!(names, vec!["isthmus", "jovian"]); |
| 189 | + } |
| 190 | + |
| 191 | + #[test] |
| 192 | + fn each_case_is_cumulative_without_enabling_the_next_fork() { |
| 193 | + for (fork_name, hardforks) in ForkMatrix::all().iter() { |
| 194 | + let cfg = rollup_config(hardforks); |
| 195 | + match fork_name { |
| 196 | + "regolith" => { |
| 197 | + assert!(cfg.is_regolith_active(0)); |
| 198 | + assert!(!cfg.is_canyon_active(0)); |
| 199 | + } |
| 200 | + "canyon" => { |
| 201 | + assert!(cfg.is_canyon_active(0)); |
| 202 | + assert!(!cfg.is_delta_active(0)); |
| 203 | + } |
| 204 | + "delta" => { |
| 205 | + assert!(cfg.is_delta_active(0)); |
| 206 | + assert!(!cfg.is_ecotone_active(0)); |
| 207 | + } |
| 208 | + "ecotone" => { |
| 209 | + assert!(cfg.is_ecotone_active(0)); |
| 210 | + assert!(!cfg.is_fjord_active(0)); |
| 211 | + } |
| 212 | + "fjord" => { |
| 213 | + assert!(cfg.is_fjord_active(0)); |
| 214 | + assert!(!cfg.is_granite_active(0)); |
| 215 | + } |
| 216 | + "granite" => { |
| 217 | + assert!(cfg.is_granite_active(0)); |
| 218 | + assert!(!cfg.is_holocene_active(0)); |
| 219 | + } |
| 220 | + "holocene" => { |
| 221 | + assert!(cfg.is_holocene_active(0)); |
| 222 | + assert!(!cfg.is_pectra_blob_schedule_active(0)); |
| 223 | + assert!(!cfg.is_isthmus_active(0)); |
| 224 | + } |
| 225 | + "pectra-blob-schedule" => { |
| 226 | + assert!(cfg.is_holocene_active(0)); |
| 227 | + assert!(cfg.is_pectra_blob_schedule_active(0)); |
| 228 | + assert!(!cfg.is_isthmus_active(0)); |
| 229 | + } |
| 230 | + "isthmus" => { |
| 231 | + assert!(cfg.is_isthmus_active(0)); |
| 232 | + assert!(!cfg.is_jovian_active(0)); |
| 233 | + } |
| 234 | + "jovian" => { |
| 235 | + assert!(cfg.is_jovian_active(0)); |
| 236 | + assert!(!cfg.is_base_v1_active(0)); |
| 237 | + } |
| 238 | + "base-v1" => { |
| 239 | + assert!(cfg.is_jovian_active(0)); |
| 240 | + assert!(cfg.is_base_v1_active(0)); |
| 241 | + } |
| 242 | + _ => unreachable!("unexpected fork {fork_name}"), |
| 243 | + } |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + #[test] |
| 248 | + fn run_includes_the_fork_name_in_panics() { |
| 249 | + let panic = std::panic::catch_unwind(|| { |
| 250 | + ForkMatrix::from_granite().run(|fork_name, _| { |
| 251 | + assert_ne!(fork_name, "granite", "boom"); |
| 252 | + }); |
| 253 | + }) |
| 254 | + .expect_err("granite case must panic"); |
| 255 | + |
| 256 | + let message = panic_message(panic); |
| 257 | + assert!(message.contains("granite")); |
| 258 | + assert!(message.contains("boom")); |
| 259 | + } |
| 260 | +} |
0 commit comments