alloy-rpc-types-engine 2.0.5

Types for the `engine` Ethereum JSON-RPC namespace
Documentation
use super::{PayloadStatus, PayloadStatusEnum};
use crate::PayloadId;
use alloy_primitives::B256;

/// invalid forkchoice state error code.
pub const INVALID_FORK_CHOICE_STATE_ERROR: i32 = -38002;

/// invalid payload attributes error code.
pub const INVALID_PAYLOAD_ATTRIBUTES_ERROR: i32 = -38003;

/// invalid forkchoice state error message.
pub const INVALID_FORK_CHOICE_STATE_ERROR_MSG: &str = "Invalid forkchoice state";

/// invalid payload attributes error message.
pub const INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG: &str = "Invalid payload attributes";

/// Represents possible variants of a processed forkchoice update.
pub type ForkChoiceUpdateResult = Result<ForkchoiceUpdated, ForkchoiceUpdateError>;

/// This structure encapsulates the fork choice state
#[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 {
    /// Hash of the head block.
    pub head_block_hash: B256,
    /// Hash of the safe block.
    pub safe_block_hash: B256,
    /// Hash of finalized block.
    pub finalized_block_hash: B256,
}

impl ForkchoiceState {
    /// Creates a new [ForkchoiceState] with head, safe, and finalized block hashes set to the given
    /// hash.
    pub const fn same_hash(hash: B256) -> Self {
        Self { head_block_hash: hash, safe_block_hash: hash, finalized_block_hash: hash }
    }

    /// Returns the `head_block_hash`, only if it is not [`B256::ZERO`], otherwise this returns
    /// [`None`].
    #[inline]
    pub fn state_head_hash(&self) -> Option<B256> {
        if self.head_block_hash.is_zero() {
            None
        } else {
            Some(self.head_block_hash)
        }
    }

    /// Returns the `safe_block_hash`, only if it is not [`B256::ZERO`], otherwise this returns
    /// [`None`].
    #[inline]
    pub fn state_safe_hash(&self) -> Option<B256> {
        if self.safe_block_hash.is_zero() {
            None
        } else {
            Some(self.safe_block_hash)
        }
    }

    /// Returns the `finalized_block_hash`, only if it is not [`B256::ZERO`], otherwise this
    /// returns [`None`].
    #[inline]
    pub fn state_finalized_hash(&self) -> Option<B256> {
        if self.finalized_block_hash.is_zero() {
            None
        } else {
            Some(self.finalized_block_hash)
        }
    }

    /// Returns true if any of the hashes in this [`ForkchoiceState`] match the given hash
    pub fn contains(&self, hash: B256) -> bool {
        self.head_block_hash == hash
            || self.safe_block_hash == hash
            || self.finalized_block_hash == hash
    }
}

/// A standalone forkchoice update errors for RPC.
///
/// These are considered hard RPC errors and are _not_ returned as [PayloadStatus] or
/// [PayloadStatusEnum::Invalid].
#[derive(Clone, Copy, Debug, PartialEq, Eq, derive_more::Display)]
pub enum ForkchoiceUpdateError {
    /// The forkchoice update has been processed, but the requested contained invalid
    /// [PayloadAttributes](crate::PayloadAttributes).
    ///
    /// This is returned as an error because the payload attributes are invalid and the payload is not valid, See <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/paris.md#engine_forkchoiceupdatedv1>
    #[display("invalid payload attributes")]
    UpdatedInvalidPayloadAttributes,
    /// The given [ForkchoiceState] is invalid or inconsistent.
    #[display("invalid forkchoice state")]
    InvalidState,
    /// Thrown when a forkchoice final block does not exist in the database.
    #[display("final block not available in database")]
    UnknownFinalBlock,
}

impl core::error::Error for ForkchoiceUpdateError {}

/// Represents a successfully _processed_ forkchoice state update.
///
/// Note: this can still be INVALID if the provided payload was invalid.
#[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 {
    /// Represents the outcome of the validation of the payload, independently of the payload being
    /// valid or not.
    pub payload_status: PayloadStatus,
    /// The identifier of the payload build process that was successfully initiated.
    pub payload_id: Option<PayloadId>,
}

impl ForkchoiceUpdated {
    /// Creates a new [ForkchoiceUpdated] with the given [PayloadStatus].
    pub const fn new(payload_status: PayloadStatus) -> Self {
        Self { payload_status, payload_id: None }
    }

    /// Creates a new [ForkchoiceUpdated] with the given [PayloadStatusEnum].
    pub const fn from_status(status: PayloadStatusEnum) -> Self {
        Self { payload_status: PayloadStatus::from_status(status), payload_id: None }
    }

    /// Sets the latest valid hash of the payload status.
    pub const fn with_latest_valid_hash(mut self, hash: B256) -> Self {
        self.payload_status.latest_valid_hash = Some(hash);
        self
    }

    /// Sets the payload id of the created payload job.
    pub const fn with_payload_id(mut self, id: PayloadId) -> Self {
        self.payload_id = Some(id);
        self
    }

    /// Returns true if the payload status is syncing.
    pub const fn is_syncing(&self) -> bool {
        self.payload_status.is_syncing()
    }

    /// Returns true if the payload status is valid.
    pub const fn is_valid(&self) -> bool {
        self.payload_status.is_valid()
    }

    /// Returns true if the payload status is invalid.
    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);
    }
}