use crate::Head;
use alloc::{
collections::{BTreeMap, BTreeSet},
vec::Vec,
};
use alloy_primitives::{hex, BlockNumber, B256};
use alloy_rlp::{Error as RlpError, *};
#[cfg(any(test, feature = "arbitrary"))]
use arbitrary::Arbitrary;
use core::{
cmp::Ordering,
fmt,
ops::{Add, AddAssign},
};
use crc::*;
#[cfg(any(test, feature = "arbitrary"))]
use proptest_derive::Arbitrary as PropTestArbitrary;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const CRC_32_IEEE: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
const TIMESTAMP_BEFORE_ETHEREUM_MAINNET: u64 = 1_300_000_000;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(any(test, feature = "arbitrary"), derive(PropTestArbitrary, Arbitrary))]
#[derive(
Clone, Copy, PartialEq, Eq, Hash, RlpEncodableWrapper, RlpDecodableWrapper, RlpMaxEncodedLen,
)]
pub struct ForkHash(pub [u8; 4]);
impl fmt::Debug for ForkHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("ForkHash").field(&hex::encode(&self.0[..])).finish()
}
}
impl From<B256> for ForkHash {
fn from(genesis: B256) -> Self {
Self(CRC_32_IEEE.checksum(&genesis[..]).to_be_bytes())
}
}
impl<T> AddAssign<T> for ForkHash
where
T: Into<u64>,
{
fn add_assign(&mut self, v: T) {
let blob = v.into().to_be_bytes();
let digest = CRC_32_IEEE.digest_with_initial(u32::from_be_bytes(self.0));
let value = digest.finalize();
let mut digest = CRC_32_IEEE.digest_with_initial(value);
digest.update(&blob);
self.0 = digest.finalize().to_be_bytes();
}
}
impl<T> Add<T> for ForkHash
where
T: Into<u64>,
{
type Output = Self;
fn add(mut self, block: T) -> Self {
self += block;
self
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ForkFilterKey {
Block(BlockNumber),
Time(u64),
}
impl PartialOrd for ForkFilterKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ForkFilterKey {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(Self::Block(a), Self::Block(b)) | (Self::Time(a), Self::Time(b)) => a.cmp(b),
(Self::Block(_), Self::Time(_)) => Ordering::Less,
_ => Ordering::Greater,
}
}
}
impl From<ForkFilterKey> for u64 {
fn from(value: ForkFilterKey) -> Self {
match value {
ForkFilterKey::Block(block) => block,
ForkFilterKey::Time(time) => time,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(any(test, feature = "arbitrary"), derive(PropTestArbitrary, Arbitrary))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable, RlpMaxEncodedLen)]
pub struct ForkId {
pub hash: ForkHash,
pub next: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable)]
pub struct EnrForkIdEntry {
pub fork_id: ForkId,
}
impl Decodable for EnrForkIdEntry {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
let b = &mut &**buf;
let rlp_head = Header::decode(b)?;
if !rlp_head.list {
return Err(RlpError::UnexpectedString)
}
let started_len = b.len();
let this = Self { fork_id: Decodable::decode(b)? };
let consumed = started_len - b.len();
if consumed > rlp_head.payload_length {
return Err(RlpError::ListLengthMismatch {
expected: rlp_head.payload_length,
got: consumed,
})
}
let rem = rlp_head.payload_length - consumed;
b.advance(rem);
*buf = *b;
Ok(this)
}
}
impl From<ForkId> for EnrForkIdEntry {
fn from(fork_id: ForkId) -> Self {
Self { fork_id }
}
}
impl From<EnrForkIdEntry> for ForkId {
fn from(entry: EnrForkIdEntry) -> Self {
entry.fork_id
}
}
#[derive(Clone, Copy, Debug, thiserror_no_std::Error, PartialEq, Eq, Hash)]
pub enum ValidationError {
#[error(
"remote node is outdated and needs a software update: local={local:?}, remote={remote:?}"
)]
RemoteStale {
local: ForkId,
remote: ForkId,
},
#[error("local node is on an incompatible chain or needs a software update: local={local:?}, remote={remote:?}")]
LocalIncompatibleOrStale {
local: ForkId,
remote: ForkId,
},
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForkFilter {
forks: BTreeMap<ForkFilterKey, ForkHash>,
head: Head,
cache: Cache,
}
impl ForkFilter {
pub fn new<F>(head: Head, genesis_hash: B256, genesis_timestamp: u64, forks: F) -> Self
where
F: IntoIterator<Item = ForkFilterKey>,
{
let genesis_fork_hash = ForkHash::from(genesis_hash);
let mut forks = forks.into_iter().collect::<BTreeSet<_>>();
forks.remove(&ForkFilterKey::Time(0));
forks.remove(&ForkFilterKey::Block(0));
let forks = forks
.into_iter()
.filter(|key| match key {
ForkFilterKey::Block(_) => true,
ForkFilterKey::Time(time) => *time > genesis_timestamp,
})
.collect::<BTreeSet<_>>()
.into_iter()
.fold(
(BTreeMap::from([(ForkFilterKey::Block(0), genesis_fork_hash)]), genesis_fork_hash),
|(mut acc, base_hash), key| {
let fork_hash = base_hash + u64::from(key);
acc.insert(key, fork_hash);
(acc, fork_hash)
},
)
.0;
let cache = Cache::compute_cache(&forks, head);
Self { forks, head, cache }
}
fn set_head_priv(&mut self, head: Head) -> Option<ForkTransition> {
let head_in_past = match self.cache.epoch_start {
ForkFilterKey::Block(epoch_start_block) => head.number < epoch_start_block,
ForkFilterKey::Time(epoch_start_time) => head.timestamp < epoch_start_time,
};
let head_in_future = match self.cache.epoch_end {
Some(ForkFilterKey::Block(epoch_end_block)) => head.number >= epoch_end_block,
Some(ForkFilterKey::Time(epoch_end_time)) => head.timestamp >= epoch_end_time,
None => false,
};
self.head = head;
(head_in_past || head_in_future).then(|| {
let past = self.current();
self.cache = Cache::compute_cache(&self.forks, head);
ForkTransition { current: self.current(), past }
})
}
pub fn set_head(&mut self, head: Head) -> Option<ForkTransition> {
self.set_head_priv(head)
}
#[must_use]
pub const fn current(&self) -> ForkId {
self.cache.fork_id
}
pub fn set_current_fork_id(&mut self, fork_id: ForkId) {
self.cache.fork_id = fork_id;
}
pub fn validate(&self, fork_id: ForkId) -> Result<(), ValidationError> {
if self.current().hash == fork_id.hash {
if fork_id.next == 0 {
return Ok(())
}
let is_incompatible = if self.head.number < TIMESTAMP_BEFORE_ETHEREUM_MAINNET {
(fork_id.next > TIMESTAMP_BEFORE_ETHEREUM_MAINNET &&
self.head.timestamp >= fork_id.next) ||
(fork_id.next <= TIMESTAMP_BEFORE_ETHEREUM_MAINNET &&
self.head.number >= fork_id.next)
} else {
let head_block_or_time = match self.cache.epoch_start {
ForkFilterKey::Block(_) => self.head.number,
ForkFilterKey::Time(_) => self.head.timestamp,
};
head_block_or_time >= fork_id.next
};
return if is_incompatible {
Err(ValidationError::LocalIncompatibleOrStale {
local: self.current(),
remote: fork_id,
})
} else {
Ok(())
}
}
let mut it = self.cache.past.iter();
while let Some((_, hash)) = it.next() {
if *hash == fork_id.hash {
if let Some((actual_key, _)) = it.next() {
return if u64::from(*actual_key) == fork_id.next {
Ok(())
} else {
Err(ValidationError::RemoteStale { local: self.current(), remote: fork_id })
}
}
break
}
}
for future_fork_hash in &self.cache.future {
if *future_fork_hash == fork_id.hash {
return Ok(())
}
}
Err(ValidationError::LocalIncompatibleOrStale { local: self.current(), remote: fork_id })
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ForkTransition {
pub current: ForkId,
pub past: ForkId,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
struct Cache {
epoch_start: ForkFilterKey,
epoch_end: Option<ForkFilterKey>,
past: Vec<(ForkFilterKey, ForkHash)>,
future: Vec<ForkHash>,
fork_id: ForkId,
}
impl Cache {
fn compute_cache(forks: &BTreeMap<ForkFilterKey, ForkHash>, head: Head) -> Self {
let mut past = Vec::with_capacity(forks.len());
let mut future = Vec::with_capacity(forks.len());
let mut epoch_start = ForkFilterKey::Block(0);
let mut epoch_end = None;
for (key, hash) in forks {
let active = match key {
ForkFilterKey::Block(block) => *block <= head.number,
ForkFilterKey::Time(time) => *time <= head.timestamp,
};
if active {
epoch_start = *key;
past.push((*key, *hash));
} else {
if epoch_end.is_none() {
epoch_end = Some(*key);
}
future.push(*hash);
}
}
let fork_id = ForkId {
hash: past.last().expect("there is always at least one - genesis - fork hash").1,
next: epoch_end.unwrap_or(ForkFilterKey::Block(0)).into(),
};
Self { epoch_start, epoch_end, past, future, fork_id }
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::constants::MAINNET_GENESIS_HASH;
#[test]
fn forkhash() {
let mut fork_hash = ForkHash::from(MAINNET_GENESIS_HASH);
assert_eq!(fork_hash.0, hex!("fc64ec04"));
fork_hash += 1_150_000u64;
assert_eq!(fork_hash.0, hex!("97c2c34c"));
fork_hash += 1_920_000u64;
assert_eq!(fork_hash.0, hex!("91d1f948"));
}
#[test]
fn compatibility_check() {
let mut filter = ForkFilter::new(
Head { number: 0, ..Default::default() },
MAINNET_GENESIS_HASH,
0,
vec![
ForkFilterKey::Block(1_150_000),
ForkFilterKey::Block(1_920_000),
ForkFilterKey::Block(2_463_000),
ForkFilterKey::Block(2_675_000),
ForkFilterKey::Block(4_370_000),
ForkFilterKey::Block(7_280_000),
],
);
filter.set_head(Head { number: 7_987_396, ..Default::default() });
assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("668db0af")), next: 0 }), Ok(()));
filter.set_head(Head { number: 7_987_396, ..Default::default() });
assert_eq!(
filter.validate(ForkId { hash: ForkHash(hex!("668db0af")), next: BlockNumber::MAX }),
Ok(())
);
filter.set_head(Head { number: 7_279_999, ..Default::default() });
assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 0 }), Ok(()));
filter.set_head(Head { number: 7_279_999, ..Default::default() });
assert_eq!(
filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 7_280_000 }),
Ok(())
);
filter.set_head(Head { number: 7_279_999, ..Default::default() });
assert_eq!(
filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: BlockNumber::MAX }),
Ok(())
);
filter.set_head(Head { number: 7_987_396, ..Default::default() });
assert_eq!(
filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 7_280_000 }),
Ok(())
);
filter.set_head(Head { number: 7_987_396, ..Default::default() });
assert_eq!(
filter.validate(ForkId { hash: ForkHash(hex!("3edd5b10")), next: 4_370_000 }),
Ok(())
);
filter.set_head(Head { number: 7_279_999, ..Default::default() });
assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("668db0af")), next: 0 }), Ok(()));
filter.set_head(Head { number: 4_369_999, ..Default::default() });
assert_eq!(filter.validate(ForkId { hash: ForkHash(hex!("a00bc324")), next: 0 }), Ok(()));
filter.set_head(Head { number: 7_987_396, ..Default::default() });
let remote = ForkId { hash: ForkHash(hex!("a00bc324")), next: 0 };
assert_eq!(
filter.validate(remote),
Err(ValidationError::RemoteStale { local: filter.current(), remote })
);
filter.set_head(Head { number: 7_987_396, ..Default::default() });
let remote = ForkId { hash: ForkHash(hex!("5cddc0e1")), next: 0 };
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter.set_head(Head { number: 7_279_999, ..Default::default() });
let remote = ForkId { hash: ForkHash(hex!("5cddc0e1")), next: 0 };
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter.set_head(Head { number: 7_987_396, ..Default::default() });
let remote = ForkId { hash: ForkHash(hex!("afec6b27")), next: 0 };
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter.set_head(Head { number: 88_888_888, ..Default::default() });
let remote = ForkId { hash: ForkHash(hex!("668db0af")), next: 88_888_888 };
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter.set_head(Head { number: 7_279_999, ..Default::default() });
let remote = ForkId { hash: ForkHash(hex!("a00bc324")), next: 7_279_999 };
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter
.set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1, ..Default::default() });
let remote = ForkId {
hash: ForkHash(hex!("668db0af")),
next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1,
};
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter
.set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1, ..Default::default() });
let remote = ForkId {
hash: ForkHash(hex!("668db0af")),
next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 2,
};
assert_eq!(filter.validate(remote), Ok(()));
filter.set_head(Head {
number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 1,
timestamp: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 2,
..Default::default()
});
let remote = ForkId {
hash: ForkHash(hex!("668db0af")),
next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET + 1,
};
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter
.set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 1, ..Default::default() });
let remote = ForkId {
hash: ForkHash(hex!("668db0af")),
next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 2,
};
assert_eq!(
filter.validate(remote),
Err(ValidationError::LocalIncompatibleOrStale { local: filter.current(), remote })
);
filter
.set_head(Head { number: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 2, ..Default::default() });
let remote = ForkId {
hash: ForkHash(hex!("668db0af")),
next: TIMESTAMP_BEFORE_ETHEREUM_MAINNET - 1,
};
assert_eq!(filter.validate(remote), Ok(()));
}
#[test]
fn forkid_serialization() {
assert_eq!(
&*encode_fixed_size(&ForkId { hash: ForkHash(hex!("00000000")), next: 0 }),
hex!("c6840000000080")
);
assert_eq!(
&*encode_fixed_size(&ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADD_CAFE }),
hex!("ca84deadbeef84baddcafe")
);
assert_eq!(
&*encode_fixed_size(&ForkId { hash: ForkHash(hex!("ffffffff")), next: u64::MAX }),
hex!("ce84ffffffff88ffffffffffffffff")
);
assert_eq!(
ForkId::decode(&mut (&hex!("c6840000000080") as &[u8])).unwrap(),
ForkId { hash: ForkHash(hex!("00000000")), next: 0 }
);
assert_eq!(
ForkId::decode(&mut (&hex!("ca84deadbeef84baddcafe") as &[u8])).unwrap(),
ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADD_CAFE }
);
assert_eq!(
ForkId::decode(&mut (&hex!("ce84ffffffff88ffffffffffffffff") as &[u8])).unwrap(),
ForkId { hash: ForkHash(hex!("ffffffff")), next: u64::MAX }
);
}
#[test]
fn fork_id_rlp() {
let val = hex!("c6840000000080");
let id = ForkId::decode(&mut &val[..]).unwrap();
assert_eq!(id, ForkId { hash: ForkHash(hex!("00000000")), next: 0 });
assert_eq!(alloy_rlp::encode(id), &val[..]);
let val = hex!("ca84deadbeef84baddcafe");
let id = ForkId::decode(&mut &val[..]).unwrap();
assert_eq!(id, ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE });
assert_eq!(alloy_rlp::encode(id), &val[..]);
let val = hex!("ce84ffffffff88ffffffffffffffff");
let id = ForkId::decode(&mut &val[..]).unwrap();
assert_eq!(id, ForkId { hash: ForkHash(u32::MAX.to_be_bytes()), next: u64::MAX });
assert_eq!(alloy_rlp::encode(id), &val[..]);
}
#[test]
fn compute_cache() {
let b1 = 1_150_000;
let b2 = 1_920_000;
let h0 = ForkId { hash: ForkHash(hex!("fc64ec04")), next: b1 };
let h1 = ForkId { hash: ForkHash(hex!("97c2c34c")), next: b2 };
let h2 = ForkId { hash: ForkHash(hex!("91d1f948")), next: 0 };
let mut fork_filter = ForkFilter::new(
Head { number: 0, ..Default::default() },
MAINNET_GENESIS_HASH,
0,
vec![ForkFilterKey::Block(b1), ForkFilterKey::Block(b2)],
);
assert!(fork_filter.set_head_priv(Head { number: 0, ..Default::default() }).is_none());
assert_eq!(fork_filter.current(), h0);
assert!(fork_filter.set_head_priv(Head { number: 1, ..Default::default() }).is_none());
assert_eq!(fork_filter.current(), h0);
assert_eq!(
fork_filter.set_head_priv(Head { number: b1 + 1, ..Default::default() }).unwrap(),
ForkTransition { current: h1, past: h0 }
);
assert_eq!(fork_filter.current(), h1);
assert!(fork_filter.set_head_priv(Head { number: b1, ..Default::default() }).is_none());
assert_eq!(fork_filter.current(), h1);
assert_eq!(
fork_filter.set_head_priv(Head { number: b1 - 1, ..Default::default() }).unwrap(),
ForkTransition { current: h0, past: h1 }
);
assert_eq!(fork_filter.current(), h0);
assert!(fork_filter.set_head_priv(Head { number: b1, ..Default::default() }).is_some());
assert_eq!(fork_filter.current(), h1);
assert!(fork_filter.set_head_priv(Head { number: b2 - 1, ..Default::default() }).is_none());
assert_eq!(fork_filter.current(), h1);
assert!(fork_filter.set_head_priv(Head { number: b2, ..Default::default() }).is_some());
assert_eq!(fork_filter.current(), h2);
}
mod eip8 {
use super::*;
fn junk_enr_fork_id_entry() -> Vec<u8> {
let mut buf = Vec::new();
let fork_id = ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE };
let junk: u64 = 112233;
let payload_length = fork_id.length() + junk.length();
alloy_rlp::Header { list: true, payload_length }.encode(&mut buf);
fork_id.encode(&mut buf);
junk.encode(&mut buf);
buf
}
#[test]
fn eip8_decode_enr_fork_id_entry() {
let enr_fork_id_entry_with_junk = junk_enr_fork_id_entry();
let mut buf = enr_fork_id_entry_with_junk.as_slice();
let decoded = EnrForkIdEntry::decode(&mut buf).unwrap();
assert_eq!(
decoded.fork_id,
ForkId { hash: ForkHash(hex!("deadbeef")), next: 0xBADDCAFE }
);
}
}
}