use core::fmt;
use eth_valkyoth_primitives::{BlockNumber, ChainId, UnixTimestamp};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Hardfork {
Frontier,
Homestead,
Byzantium,
London,
Shanghai,
Cancun,
Prague,
Amsterdam,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ForkError {
Unsupported,
Inactive,
MissingActivation,
ChainMismatch,
DuplicateFork,
NonMonotonicForkOrder,
}
impl ForkError {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Unsupported => "ETH_FORK_UNSUPPORTED",
Self::Inactive => "ETH_FORK_INACTIVE",
Self::MissingActivation => "ETH_FORK_MISSING_ACTIVATION",
Self::ChainMismatch => "ETH_FORK_CHAIN_MISMATCH",
Self::DuplicateFork => "ETH_FORK_DUPLICATE",
Self::NonMonotonicForkOrder => "ETH_FORK_NON_MONOTONIC_ORDER",
}
}
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::Unsupported => "fork is not supported by this crate version",
Self::Inactive => "fork is not active for the validation context",
Self::MissingActivation => "fork activation data is incomplete",
Self::ChainMismatch => "fork chain id does not match the selected chain spec",
Self::DuplicateFork => "chain spec contains a duplicate hardfork entry",
Self::NonMonotonicForkOrder => "chain spec fork entries are not monotonic",
}
}
}
impl fmt::Display for ForkError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.message())
}
}
#[cfg(feature = "std")]
impl std::error::Error for ForkError {}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ForkActivation {
BlockOnly {
activation_block: BlockNumber,
},
BlockAndTimestamp {
activation_block: BlockNumber,
activation_timestamp: UnixTimestamp,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ForkSpec {
pub chain_id: ChainId,
pub hardfork: Hardfork,
pub activation: ForkActivation,
}
impl ForkSpec {
#[must_use]
pub fn is_active_at(self, block_number: BlockNumber, timestamp: UnixTimestamp) -> bool {
match self.activation {
ForkActivation::BlockOnly { activation_block } => block_number >= activation_block,
ForkActivation::BlockAndTimestamp {
activation_block,
activation_timestamp,
} => block_number >= activation_block && timestamp >= activation_timestamp,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ChainSpec<'a> {
chain_id: ChainId,
forks: &'a [ForkSpec],
}
impl<'a> ChainSpec<'a> {
#[must_use]
pub const fn new(chain_id: ChainId, forks: &'a [ForkSpec]) -> Self {
Self { chain_id, forks }
}
pub fn try_new(chain_id: ChainId, forks: &'a [ForkSpec]) -> Result<Self, ForkError> {
let spec = Self { chain_id, forks };
spec.validate()?;
Ok(spec)
}
#[must_use]
pub const fn chain_id(self) -> ChainId {
self.chain_id
}
#[must_use]
pub const fn forks(self) -> &'a [ForkSpec] {
self.forks
}
pub fn fork_spec(self, hardfork: Hardfork) -> Result<ForkSpec, ForkError> {
self.validate()?;
for fork in self.forks {
if fork.hardfork == hardfork {
return Ok(*fork);
}
}
Err(ForkError::Unsupported)
}
pub fn validation_context(
self,
hardfork: Hardfork,
block_number: BlockNumber,
timestamp: UnixTimestamp,
) -> Result<ValidationContext, ForkError> {
let fork = self.fork_spec(hardfork)?;
let context = ValidationContext {
fork,
block_number,
timestamp,
};
if context.fork_is_active() {
Ok(context)
} else {
Err(ForkError::Inactive)
}
}
pub fn active_fork(
self,
block_number: BlockNumber,
timestamp: UnixTimestamp,
) -> Result<ForkSpec, ForkError> {
let mut active: Option<ForkSpec> = None;
self.validate()?;
for fork in self.forks {
if fork.is_active_at(block_number, timestamp) {
active = match active {
Some(previous) if previous.hardfork > fork.hardfork => Some(previous),
_ => Some(*fork),
};
}
}
active.ok_or(ForkError::Inactive)
}
fn validate(self) -> Result<(), ForkError> {
let mut previous: Option<ForkSpec> = None;
for fork in self.forks {
if fork.chain_id != self.chain_id {
return Err(ForkError::ChainMismatch);
}
if let Some(previous_fork) = previous {
if fork.hardfork == previous_fork.hardfork {
return Err(ForkError::DuplicateFork);
}
if fork.hardfork < previous_fork.hardfork
|| !fork_activation_is_monotonic(previous_fork.activation, fork.activation)
{
return Err(ForkError::NonMonotonicForkOrder);
}
}
previous = Some(*fork);
}
Ok(())
}
}
fn fork_activation_is_monotonic(previous: ForkActivation, next: ForkActivation) -> bool {
let previous_block = fork_activation_block(previous);
let next_block = fork_activation_block(next);
if next_block < previous_block {
return false;
}
match (previous, next) {
(
ForkActivation::BlockAndTimestamp {
activation_timestamp: previous_timestamp,
..
},
ForkActivation::BlockAndTimestamp {
activation_timestamp: next_timestamp,
..
},
) => next_timestamp >= previous_timestamp,
_ => true,
}
}
fn fork_activation_block(activation: ForkActivation) -> BlockNumber {
match activation {
ForkActivation::BlockOnly { activation_block }
| ForkActivation::BlockAndTimestamp {
activation_block, ..
} => activation_block,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidationContext {
pub fork: ForkSpec,
pub block_number: BlockNumber,
pub timestamp: UnixTimestamp,
}
impl ValidationContext {
#[must_use]
pub fn fork_is_active(self) -> bool {
self.fork.is_active_at(self.block_number, self.timestamp)
}
}
#[cfg(test)]
#[path = "fork_tests.rs"]
mod tests;