Skip to main content

eth_valkyoth_protocol/
fork.rs

1use core::fmt;
2
3use eth_valkyoth_primitives::{BlockNumber, ChainId, UnixTimestamp};
4
5/// Execution-layer hardfork identity used by explicit chain specs.
6#[non_exhaustive]
7#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
8pub enum Hardfork {
9    /// Frontier genesis rules.
10    Frontier,
11    /// Homestead rules.
12    Homestead,
13    /// Byzantium rules.
14    Byzantium,
15    /// London rules.
16    London,
17    /// Shanghai rules.
18    Shanghai,
19    /// Cancun rules.
20    Cancun,
21    /// Prague/Pectra rules.
22    Prague,
23    /// Amsterdam rules.
24    Amsterdam,
25}
26
27/// Fork selection or activation failure.
28#[non_exhaustive]
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum ForkError {
31    /// The selected fork is absent from the supplied chain spec.
32    Unsupported,
33    /// The selected fork is not active for the supplied validation context.
34    Inactive,
35    /// Fork activation data is incomplete.
36    MissingActivation,
37    /// A fork spec was supplied for a different chain.
38    ChainMismatch,
39    /// The chain spec contains the same hardfork more than once.
40    DuplicateFork,
41    /// The chain spec hardfork entries are not in chronological order.
42    NonMonotonicForkOrder,
43}
44
45impl ForkError {
46    /// Stable machine-readable error code.
47    #[must_use]
48    pub const fn code(self) -> &'static str {
49        match self {
50            Self::Unsupported => "ETH_FORK_UNSUPPORTED",
51            Self::Inactive => "ETH_FORK_INACTIVE",
52            Self::MissingActivation => "ETH_FORK_MISSING_ACTIVATION",
53            Self::ChainMismatch => "ETH_FORK_CHAIN_MISMATCH",
54            Self::DuplicateFork => "ETH_FORK_DUPLICATE",
55            Self::NonMonotonicForkOrder => "ETH_FORK_NON_MONOTONIC_ORDER",
56        }
57    }
58
59    /// Stable human-readable error message.
60    #[must_use]
61    pub const fn message(self) -> &'static str {
62        match self {
63            Self::Unsupported => "fork is not supported by this crate version",
64            Self::Inactive => "fork is not active for the validation context",
65            Self::MissingActivation => "fork activation data is incomplete",
66            Self::ChainMismatch => "fork chain id does not match the selected chain spec",
67            Self::DuplicateFork => "chain spec contains a duplicate hardfork entry",
68            Self::NonMonotonicForkOrder => "chain spec fork entries are not monotonic",
69        }
70    }
71}
72
73impl fmt::Display for ForkError {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        formatter.write_str(self.message())
76    }
77}
78
79#[cfg(feature = "std")]
80impl std::error::Error for ForkError {}
81
82/// Unambiguous fork activation rule.
83#[non_exhaustive]
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum ForkActivation {
86    /// Block number alone determines activation.
87    BlockOnly {
88        /// Activation block for this fork view.
89        activation_block: BlockNumber,
90    },
91    /// Both block number and timestamp must be satisfied.
92    BlockAndTimestamp {
93        /// Activation block for this fork view.
94        activation_block: BlockNumber,
95        /// Activation timestamp for timestamp-based forks.
96        activation_timestamp: UnixTimestamp,
97    },
98}
99
100/// Ethereum fork rules selected for a validation operation.
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub struct ForkSpec {
103    /// Chain being validated.
104    pub chain_id: ChainId,
105    /// Hardfork these activation rules identify.
106    pub hardfork: Hardfork,
107    /// Activation rule for this fork view.
108    pub activation: ForkActivation,
109}
110
111impl ForkSpec {
112    /// Returns whether this fork is active at the supplied block and timestamp.
113    #[must_use]
114    pub fn is_active_at(self, block_number: BlockNumber, timestamp: UnixTimestamp) -> bool {
115        match self.activation {
116            ForkActivation::BlockOnly { activation_block } => block_number >= activation_block,
117            ForkActivation::BlockAndTimestamp {
118                activation_block,
119                activation_timestamp,
120            } => block_number >= activation_block && timestamp >= activation_timestamp,
121        }
122    }
123}
124
125/// Explicit chain rules used to select fork validation context.
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct ChainSpec<'a> {
128    chain_id: ChainId,
129    forks: &'a [ForkSpec],
130}
131
132impl<'a> ChainSpec<'a> {
133    /// Creates a chain spec from caller-reviewed static fork entries.
134    ///
135    /// This constructor is intentionally `const` for hand-audited static
136    /// tables. Use [`Self::try_new`] when entries come from dynamic config,
137    /// generated data, or any source that is not reviewed in code. Selection
138    /// methods still reject duplicate, unordered, or wrong-chain entries before
139    /// returning fork context.
140    #[must_use]
141    pub const fn new(chain_id: ChainId, forks: &'a [ForkSpec]) -> Self {
142        Self { chain_id, forks }
143    }
144
145    /// Creates a chain spec after validating ordering, uniqueness, and chain ID.
146    pub fn try_new(chain_id: ChainId, forks: &'a [ForkSpec]) -> Result<Self, ForkError> {
147        let spec = Self { chain_id, forks };
148        spec.validate()?;
149        Ok(spec)
150    }
151
152    /// Chain ID this spec validates.
153    #[must_use]
154    pub const fn chain_id(self) -> ChainId {
155        self.chain_id
156    }
157
158    /// Fork entries in caller-reviewed activation order.
159    #[must_use]
160    pub const fn forks(self) -> &'a [ForkSpec] {
161        self.forks
162    }
163
164    /// Returns the spec for a hardfork admitted by this chain spec.
165    pub fn fork_spec(self, hardfork: Hardfork) -> Result<ForkSpec, ForkError> {
166        self.validate()?;
167        for fork in self.forks {
168            if fork.hardfork == hardfork {
169                return Ok(*fork);
170            }
171        }
172        Err(ForkError::Unsupported)
173    }
174
175    /// Builds an explicit validation context for an active hardfork.
176    pub fn validation_context(
177        self,
178        hardfork: Hardfork,
179        block_number: BlockNumber,
180        timestamp: UnixTimestamp,
181    ) -> Result<ValidationContext, ForkError> {
182        let fork = self.fork_spec(hardfork)?;
183        let context = ValidationContext {
184            fork,
185            block_number,
186            timestamp,
187        };
188        if context.fork_is_active() {
189            Ok(context)
190        } else {
191            Err(ForkError::Inactive)
192        }
193    }
194
195    /// Returns the highest active hardfork in this chain spec.
196    pub fn active_fork(
197        self,
198        block_number: BlockNumber,
199        timestamp: UnixTimestamp,
200    ) -> Result<ForkSpec, ForkError> {
201        let mut active: Option<ForkSpec> = None;
202        self.validate()?;
203        for fork in self.forks {
204            if fork.is_active_at(block_number, timestamp) {
205                active = match active {
206                    Some(previous) if previous.hardfork > fork.hardfork => Some(previous),
207                    _ => Some(*fork),
208                };
209            }
210        }
211        active.ok_or(ForkError::Inactive)
212    }
213
214    fn validate(self) -> Result<(), ForkError> {
215        let mut previous: Option<ForkSpec> = None;
216        for fork in self.forks {
217            if fork.chain_id != self.chain_id {
218                return Err(ForkError::ChainMismatch);
219            }
220            if let Some(previous_fork) = previous {
221                if fork.hardfork == previous_fork.hardfork {
222                    return Err(ForkError::DuplicateFork);
223                }
224                if fork.hardfork < previous_fork.hardfork
225                    || !fork_activation_is_monotonic(previous_fork.activation, fork.activation)
226                {
227                    return Err(ForkError::NonMonotonicForkOrder);
228                }
229            }
230            previous = Some(*fork);
231        }
232        Ok(())
233    }
234}
235
236fn fork_activation_is_monotonic(previous: ForkActivation, next: ForkActivation) -> bool {
237    let previous_block = fork_activation_block(previous);
238    let next_block = fork_activation_block(next);
239    if next_block < previous_block {
240        return false;
241    }
242
243    // Chain specs are chain-agnostic: a timestamp-based fork may be followed by
244    // a block-only fork on non-mainnet chains. Timestamp ordering is enforced
245    // only when both adjacent activation rules carry timestamps.
246    match (previous, next) {
247        (
248            ForkActivation::BlockAndTimestamp {
249                activation_timestamp: previous_timestamp,
250                ..
251            },
252            ForkActivation::BlockAndTimestamp {
253                activation_timestamp: next_timestamp,
254                ..
255            },
256        ) => next_timestamp >= previous_timestamp,
257        _ => true,
258    }
259}
260
261fn fork_activation_block(activation: ForkActivation) -> BlockNumber {
262    match activation {
263        ForkActivation::BlockOnly { activation_block }
264        | ForkActivation::BlockAndTimestamp {
265            activation_block, ..
266        } => activation_block,
267    }
268}
269
270/// Validation context that must be explicit for consensus-sensitive operations.
271#[derive(Clone, Copy, Debug, Eq, PartialEq)]
272pub struct ValidationContext {
273    /// Fork rules.
274    pub fork: ForkSpec,
275    /// Current block number.
276    pub block_number: BlockNumber,
277    /// Current block timestamp.
278    pub timestamp: UnixTimestamp,
279}
280
281impl ValidationContext {
282    /// Returns whether the configured fork is active for this context.
283    #[must_use]
284    pub fn fork_is_active(self) -> bool {
285        self.fork.is_active_at(self.block_number, self.timestamp)
286    }
287}
288
289#[cfg(test)]
290#[path = "fork_tests.rs"]
291mod tests;