use super::{PayloadStatus, PayloadStatusEnum};
use crate::PayloadId;
use alloy_primitives::B256;
pub const INVALID_FORK_CHOICE_STATE_ERROR: i32 = -38002;
pub const INVALID_PAYLOAD_ATTRIBUTES_ERROR: i32 = -38003;
pub const INVALID_FORK_CHOICE_STATE_ERROR_MSG: &str = "Invalid forkchoice state";
pub const INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG: &str = "Invalid payload attributes";
pub type ForkChoiceUpdateResult = Result<ForkchoiceUpdated, ForkchoiceUpdateError>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ForkchoiceState {
pub head_block_hash: B256,
pub safe_block_hash: B256,
pub finalized_block_hash: B256,
}
impl ForkchoiceState {
pub const fn same_hash(hash: B256) -> Self {
Self { head_block_hash: hash, safe_block_hash: hash, finalized_block_hash: hash }
}
#[inline]
pub fn state_head_hash(&self) -> Option<B256> {
if self.head_block_hash.is_zero() {
None
} else {
Some(self.head_block_hash)
}
}
#[inline]
pub fn state_safe_hash(&self) -> Option<B256> {
if self.safe_block_hash.is_zero() {
None
} else {
Some(self.safe_block_hash)
}
}
#[inline]
pub fn state_finalized_hash(&self) -> Option<B256> {
if self.finalized_block_hash.is_zero() {
None
} else {
Some(self.finalized_block_hash)
}
}
pub fn contains(&self, hash: B256) -> bool {
self.head_block_hash == hash
|| self.safe_block_hash == hash
|| self.finalized_block_hash == hash
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, derive_more::Display)]
pub enum ForkchoiceUpdateError {
#[display("invalid payload attributes")]
UpdatedInvalidPayloadAttributes,
#[display("invalid forkchoice state")]
InvalidState,
#[display("final block not available in database")]
UnknownFinalBlock,
}
impl core::error::Error for ForkchoiceUpdateError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ForkchoiceUpdated {
pub payload_status: PayloadStatus,
pub payload_id: Option<PayloadId>,
}
impl ForkchoiceUpdated {
pub const fn new(payload_status: PayloadStatus) -> Self {
Self { payload_status, payload_id: None }
}
pub const fn from_status(status: PayloadStatusEnum) -> Self {
Self { payload_status: PayloadStatus::from_status(status), payload_id: None }
}
pub const fn with_latest_valid_hash(mut self, hash: B256) -> Self {
self.payload_status.latest_valid_hash = Some(hash);
self
}
pub const fn with_payload_id(mut self, id: PayloadId) -> Self {
self.payload_id = Some(id);
self
}
pub const fn is_syncing(&self) -> bool {
self.payload_status.is_syncing()
}
pub const fn is_valid(&self) -> bool {
self.payload_status.is_valid()
}
pub const fn is_invalid(&self) -> bool {
self.payload_status.is_invalid()
}
}
#[cfg(feature = "ssz")]
impl ssz::Encode for ForkchoiceUpdated {
fn is_ssz_fixed_len() -> bool {
false
}
fn ssz_append(&self, buf: &mut alloc::vec::Vec<u8>) {
let payload_id = self.payload_id.unwrap_or_default();
let offset = <PayloadStatus as ssz::Encode>::ssz_fixed_len()
+ <alloy_primitives::B64 as ssz::Encode>::ssz_fixed_len();
let mut encoder = ssz::SszEncoder::container(buf, offset);
encoder.append(&self.payload_status);
encoder.append(&payload_id.0);
encoder.finalize();
}
fn ssz_bytes_len(&self) -> usize {
<PayloadStatus as ssz::Encode>::ssz_fixed_len()
+ <alloy_primitives::B64 as ssz::Encode>::ssz_fixed_len()
+ self.payload_status.ssz_bytes_len()
}
}
#[cfg(feature = "ssz")]
impl ssz::Decode for ForkchoiceUpdated {
fn is_ssz_fixed_len() -> bool {
false
}
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
let mut builder = ssz::SszDecoderBuilder::new(bytes);
builder.register_type::<PayloadStatus>()?;
builder.register_type::<alloy_primitives::B64>()?;
let mut decoder = builder.build()?;
let payload_status = decoder.decode_next()?;
let payload_id: alloy_primitives::B64 = decoder.decode_next()?;
let payload_id = (!payload_id.is_zero()).then_some(PayloadId(payload_id));
Ok(Self { payload_status, payload_id })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "ssz")]
fn ssz_forkchoice_state_roundtrip() {
use ssz::{Decode, Encode};
let state = ForkchoiceState {
head_block_hash: B256::with_last_byte(1),
safe_block_hash: B256::with_last_byte(2),
finalized_block_hash: B256::with_last_byte(3),
};
let encoded = state.as_ssz_bytes();
let decoded = ForkchoiceState::from_ssz_bytes(&encoded).unwrap();
assert_eq!(decoded, state);
}
#[test]
#[cfg(feature = "ssz")]
fn ssz_forkchoice_updated_roundtrip() {
use ssz::{Decode, Encode};
let updated = ForkchoiceUpdated {
payload_status: PayloadStatus {
status: PayloadStatusEnum::Invalid {
validation_error: "invalid block number".to_string(),
},
latest_valid_hash: Some(B256::with_last_byte(4)),
},
payload_id: Some(PayloadId(alloy_primitives::B64::with_last_byte(5))),
};
let encoded = updated.as_ssz_bytes();
let decoded = ForkchoiceUpdated::from_ssz_bytes(&encoded).unwrap();
assert_eq!(decoded, updated);
}
#[test]
#[cfg(feature = "ssz")]
fn ssz_forkchoice_updated_zero_payload_id_is_none() {
use ssz::{Decode, Encode};
let updated = ForkchoiceUpdated::from_status(PayloadStatusEnum::Syncing);
let encoded = updated.as_ssz_bytes();
let decoded = ForkchoiceUpdated::from_ssz_bytes(&encoded).unwrap();
assert_eq!(decoded, updated);
}
}