diff --git a/Cargo.lock b/Cargo.lock index ffecc04e5bb2..d00466a05c5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7723,14 +7723,20 @@ name = "reth-primitives-traits" version = "1.0.0-rc.1" dependencies = [ "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=00d81d7)", + "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=00d81d7)", "alloy-genesis 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=00d81d7)", "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=00d81d7)", "arbitrary", "bytes", + "derive_more", "modular-bitfield", "proptest", "proptest-derive", + "rand 0.8.5", "reth-codecs", + "revm-primitives", "serde", "test-fuzz", ] diff --git a/Cargo.toml b/Cargo.toml index a6b79d2ce15c..0b58652a637b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -357,9 +357,10 @@ alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", "eth", ] } alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } -alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } -alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } +alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } +alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } +alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false } alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "00d81d7", default-features = false, features = [ diff --git a/crates/primitives-traits/Cargo.toml b/crates/primitives-traits/Cargo.toml index 50c5f3c24970..11f714779fbe 100644 --- a/crates/primitives-traits/Cargo.toml +++ b/crates/primitives-traits/Cargo.toml @@ -14,9 +14,15 @@ workspace = true [dependencies] reth-codecs.workspace = true +alloy-consensus.workspace = true +alloy-eips.workspace = true alloy-genesis.workspace = true alloy-primitives.workspace = true -alloy-consensus.workspace = true +alloy-rlp.workspace = true +alloy-rpc-types-eth = { workspace = true, optional = true } + +derive_more.workspace = true +revm-primitives.workspace = true # required by reth-codecs modular-bitfield.workspace = true @@ -33,11 +39,13 @@ arbitrary = { workspace = true, features = ["derive"] } proptest.workspace = true proptest-derive.workspace = true test-fuzz.workspace = true +rand.workspace = true [features] +test-utils = ["arbitrary"] arbitrary = [ "dep:arbitrary", "dep:proptest", "dep:proptest-derive" ] - +alloy-compat = ["alloy-rpc-types-eth"] \ No newline at end of file diff --git a/crates/primitives-traits/src/alloy_compat.rs b/crates/primitives-traits/src/alloy_compat.rs new file mode 100644 index 000000000000..4bf80e1f7c35 --- /dev/null +++ b/crates/primitives-traits/src/alloy_compat.rs @@ -0,0 +1,48 @@ +use super::Header; +use alloy_rpc_types_eth::{ConversionError, Header as RpcHeader}; + +impl TryFrom for Header { + type Error = ConversionError; + + fn try_from(header: RpcHeader) -> Result { + Ok(Self { + base_fee_per_gas: header + .base_fee_per_gas + .map(|base_fee_per_gas| { + base_fee_per_gas.try_into().map_err(ConversionError::BaseFeePerGasConversion) + }) + .transpose()?, + beneficiary: header.miner, + blob_gas_used: header + .blob_gas_used + .map(|blob_gas_used| { + blob_gas_used.try_into().map_err(ConversionError::BlobGasUsedConversion) + }) + .transpose()?, + difficulty: header.difficulty, + excess_blob_gas: header + .excess_blob_gas + .map(|excess_blob_gas| { + excess_blob_gas.try_into().map_err(ConversionError::ExcessBlobGasConversion) + }) + .transpose()?, + extra_data: header.extra_data, + gas_limit: header.gas_limit.try_into().map_err(ConversionError::GasLimitConversion)?, + gas_used: header.gas_used.try_into().map_err(ConversionError::GasUsedConversion)?, + logs_bloom: header.logs_bloom, + mix_hash: header.mix_hash.unwrap_or_default(), + nonce: u64::from_be_bytes(header.nonce.unwrap_or_default().0), + number: header.number.ok_or(ConversionError::MissingBlockNumber)?, + ommers_hash: header.uncles_hash, + parent_beacon_block_root: header.parent_beacon_block_root, + parent_hash: header.parent_hash, + receipts_root: header.receipts_root, + state_root: header.state_root, + timestamp: header.timestamp, + transactions_root: header.transactions_root, + withdrawals_root: header.withdrawals_root, + // TODO: requests_root: header.requests_root, + requests_root: None, + }) + } +} diff --git a/crates/primitives-traits/src/header/error.rs b/crates/primitives-traits/src/header/error.rs new file mode 100644 index 000000000000..6161afc1a5cb --- /dev/null +++ b/crates/primitives-traits/src/header/error.rs @@ -0,0 +1,8 @@ +/// Errors that can occur during header sanity checks. +#[derive(Debug, PartialEq, Eq)] +pub enum HeaderError { + /// Represents an error when the block difficulty is too large. + LargeDifficulty, + /// Represents an error when the block extradata is too large. + LargeExtraData, +} diff --git a/crates/primitives-traits/src/header/mod.rs b/crates/primitives-traits/src/header/mod.rs new file mode 100644 index 000000000000..5ec41d41450c --- /dev/null +++ b/crates/primitives-traits/src/header/mod.rs @@ -0,0 +1,509 @@ +mod sealed; +pub use sealed::SealedHeader; + +mod error; +pub use error::HeaderError; + +#[cfg(any(test, feature = "test-utils", feature = "arbitrary"))] +pub mod test_utils; + +use alloy_consensus::constants::{EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; +use alloy_eips::{ + calc_next_block_base_fee, eip1559::BaseFeeParams, merge::ALLOWED_FUTURE_BLOCK_TIME_SECONDS, + BlockNumHash, +}; +use alloy_primitives::{keccak256, Address, BlockNumber, Bloom, Bytes, B256, B64, U256}; +use alloy_rlp::{length_of_length, Decodable, Encodable}; +use bytes::BufMut; +use reth_codecs::{main_codec, Compact}; +use revm_primitives::{calc_blob_gasprice, calc_excess_blob_gas}; +use std::mem; + +/// Block header +#[main_codec] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Header { + /// The Keccak 256-bit hash of the parent + /// block’s header, in its entirety; formally Hp. + pub parent_hash: B256, + /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. + pub ommers_hash: B256, + /// The 160-bit address to which all fees collected from the successful mining of this block + /// be transferred; formally Hc. + pub beneficiary: Address, + /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are + /// executed and finalisations applied; formally Hr. + pub state_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// transaction in the transactions list portion of the block; formally Ht. + pub transactions_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. + pub receipts_root: B256, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + /// See [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895). + pub withdrawals_root: Option, + /// The Bloom filter composed from indexable information (logger address and log topics) + /// contained in each log entry from the receipt of each transaction in the transactions list; + /// formally Hb. + pub logs_bloom: Bloom, + /// A scalar value corresponding to the difficulty level of this block. This can be calculated + /// from the previous block’s difficulty level and the timestamp; formally Hd. + pub difficulty: U256, + /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of + /// zero; formally Hi. + pub number: BlockNumber, + /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. + pub gas_limit: u64, + /// A scalar value equal to the total gas used in transactions in this block; formally Hg. + pub gas_used: u64, + /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; + /// formally Hs. + pub timestamp: u64, + /// A 256-bit hash which, combined with the + /// nonce, proves that a sufficient amount of computation has been carried out on this block; + /// formally Hm. + pub mix_hash: B256, + /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of + /// computation has been carried out on this block; formally Hn. + pub nonce: u64, + /// A scalar representing EIP1559 base fee which can move up or down each block according + /// to a formula which is a function of gas used in parent block and gas target + /// (block gas limit divided by elasticity multiplier) of parent block. + /// The algorithm results in the base fee per gas increasing when blocks are + /// above the gas target, and decreasing when blocks are below the gas target. The base fee per + /// gas is burned. + pub base_fee_per_gas: Option, + /// The total amount of blob gas consumed by the transactions within the block, added in + /// EIP-4844. + pub blob_gas_used: Option, + /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks + /// with above-target blob gas consumption increase this value, blocks with below-target blob + /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. + pub excess_blob_gas: Option, + /// The hash of the parent beacon block's root is included in execution blocks, as proposed by + /// EIP-4788. + /// + /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, + /// and more. + /// + /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. + pub parent_beacon_block_root: Option, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// [EIP-7685] request in the block body. + /// + /// [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + pub requests_root: Option, + /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or + /// fewer; formally Hx. + pub extra_data: Bytes, +} + +impl AsRef for Header { + fn as_ref(&self) -> &Self { + self + } +} + +impl Default for Header { + fn default() -> Self { + Self { + parent_hash: Default::default(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: Default::default(), + state_root: EMPTY_ROOT_HASH, + transactions_root: EMPTY_ROOT_HASH, + receipts_root: EMPTY_ROOT_HASH, + logs_bloom: Default::default(), + difficulty: Default::default(), + number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: Default::default(), + mix_hash: Default::default(), + nonce: 0, + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_root: None, + } + } +} + +impl Header { + /// Checks if the block's difficulty is set to zero, indicating a Proof-of-Stake header. + /// + /// This function is linked to EIP-3675, proposing the consensus upgrade to Proof-of-Stake: + /// [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#replacing-difficulty-with-0) + /// + /// Verifies whether, as per the EIP, the block's difficulty is updated to zero, + /// signifying the transition to a Proof-of-Stake mechanism. + /// + /// Returns `true` if the block's difficulty matches the constant zero set by the EIP. + pub fn is_zero_difficulty(&self) -> bool { + self.difficulty.is_zero() + } + + /// Performs a sanity check on the extradata field of the header. + /// + /// # Errors + /// + /// Returns an error if the extradata size is larger than 100 KB. + pub fn ensure_extradata_valid(&self) -> Result<(), HeaderError> { + if self.extra_data.len() > 100 * 1024 { + return Err(HeaderError::LargeExtraData) + } + Ok(()) + } + + /// Performs a sanity check on the block difficulty field of the header. + /// + /// # Errors + /// + /// Returns an error if the block difficulty exceeds 80 bits. + pub fn ensure_difficulty_valid(&self) -> Result<(), HeaderError> { + if self.difficulty.bit_len() > 80 { + return Err(HeaderError::LargeDifficulty) + } + Ok(()) + } + + /// Performs combined sanity checks on multiple header fields. + /// + /// This method combines checks for block difficulty and extradata sizes. + /// + /// # Errors + /// + /// Returns an error if either the block difficulty exceeds 80 bits + /// or if the extradata size is larger than 100 KB. + pub fn ensure_well_formed(&self) -> Result<(), HeaderError> { + self.ensure_difficulty_valid()?; + self.ensure_extradata_valid()?; + Ok(()) + } + + /// Checks if the block's timestamp is in the past compared to the parent block's timestamp. + /// + /// Note: This check is relevant only pre-merge. + pub const fn is_timestamp_in_past(&self, parent_timestamp: u64) -> bool { + self.timestamp <= parent_timestamp + } + + /// Checks if the block's timestamp is in the future based on the present timestamp. + /// + /// Clock can drift but this can be consensus issue. + /// + /// Note: This check is relevant only pre-merge. + pub const fn exceeds_allowed_future_timestamp(&self, present_timestamp: u64) -> bool { + self.timestamp > present_timestamp + ALLOWED_FUTURE_BLOCK_TIME_SECONDS + } + + /// Returns the parent block's number and hash + pub const fn parent_num_hash(&self) -> BlockNumHash { + BlockNumHash { number: self.number.saturating_sub(1), hash: self.parent_hash } + } + + /// Heavy function that will calculate hash of data and will *not* save the change to metadata. + /// Use [`Header::seal`], [`SealedHeader`] and unlock if you need hash to be persistent. + pub fn hash_slow(&self) -> B256 { + keccak256(alloy_rlp::encode(self)) + } + + /// Checks if the header is empty - has no transactions and no ommers + pub fn is_empty(&self) -> bool { + self.transaction_root_is_empty() && + self.ommers_hash_is_empty() && + self.withdrawals_root.map_or(true, |root| root == EMPTY_ROOT_HASH) + } + + /// Check if the ommers hash equals to empty hash list. + pub fn ommers_hash_is_empty(&self) -> bool { + self.ommers_hash == EMPTY_OMMER_ROOT_HASH + } + + /// Check if the transaction root equals to empty root. + pub fn transaction_root_is_empty(&self) -> bool { + self.transactions_root == EMPTY_ROOT_HASH + } + + /// Returns the blob fee for _this_ block according to the EIP-4844 spec. + /// + /// Returns `None` if `excess_blob_gas` is None + pub fn blob_fee(&self) -> Option { + self.excess_blob_gas.map(calc_blob_gasprice) + } + + /// Returns the blob fee for the next block according to the EIP-4844 spec. + /// + /// Returns `None` if `excess_blob_gas` is None. + /// + /// See also [`Self::next_block_excess_blob_gas`] + pub fn next_block_blob_fee(&self) -> Option { + self.next_block_excess_blob_gas().map(calc_blob_gasprice) + } + + /// Calculate base fee for next block according to the EIP-1559 spec. + /// + /// Returns a `None` if no base fee is set, no EIP-1559 support + pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option { + Some(calc_next_block_base_fee( + self.gas_used as u128, + self.gas_limit as u128, + self.base_fee_per_gas? as u128, + base_fee_params, + ) as u64) + } + + /// Calculate excess blob gas for the next block according to the EIP-4844 spec. + /// + /// Returns a `None` if no excess blob gas is set, no EIP-4844 support + pub fn next_block_excess_blob_gas(&self) -> Option { + Some(calc_excess_blob_gas(self.excess_blob_gas?, self.blob_gas_used?)) + } + + /// Seal the header with a known hash. + /// + /// WARNING: This method does not perform validation whether the hash is correct. + #[inline] + pub const fn seal(self, hash: B256) -> SealedHeader { + SealedHeader::new(self, hash) + } + + /// Calculate hash and seal the Header so that it can't be changed. + #[inline] + pub fn seal_slow(self) -> SealedHeader { + let hash = self.hash_slow(); + self.seal(hash) + } + + /// Calculate a heuristic for the in-memory size of the [Header]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // parent hash + mem::size_of::() + // ommers hash + mem::size_of::
() + // beneficiary + mem::size_of::() + // state root + mem::size_of::() + // transactions root + mem::size_of::() + // receipts root + mem::size_of::>() + // withdrawals root + mem::size_of::() + // logs bloom + mem::size_of::() + // difficulty + mem::size_of::() + // number + mem::size_of::() + // gas limit + mem::size_of::() + // gas used + mem::size_of::() + // timestamp + mem::size_of::() + // mix hash + mem::size_of::() + // nonce + mem::size_of::>() + // base fee per gas + mem::size_of::>() + // blob gas used + mem::size_of::>() + // excess blob gas + mem::size_of::>() + // parent beacon block root + self.extra_data.len() // extra data + } + + fn header_payload_length(&self) -> usize { + let mut length = 0; + length += self.parent_hash.length(); // Hash of the previous block. + length += self.ommers_hash.length(); // Hash of uncle blocks. + length += self.beneficiary.length(); // Address that receives rewards. + length += self.state_root.length(); // Root hash of the state object. + length += self.transactions_root.length(); // Root hash of transactions in the block. + length += self.receipts_root.length(); // Hash of transaction receipts. + length += self.logs_bloom.length(); // Data structure containing event logs. + length += self.difficulty.length(); // Difficulty value of the block. + length += U256::from(self.number).length(); // Block number. + length += U256::from(self.gas_limit).length(); // Maximum gas allowed. + length += U256::from(self.gas_used).length(); // Actual gas used. + length += self.timestamp.length(); // Block timestamp. + length += self.extra_data.length(); // Additional arbitrary data. + length += self.mix_hash.length(); // Hash used for mining. + length += B64::new(self.nonce.to_be_bytes()).length(); // Nonce for mining. + + if let Some(base_fee) = self.base_fee_per_gas { + // Adding base fee length if it exists. + length += U256::from(base_fee).length(); + } + + if let Some(root) = self.withdrawals_root { + // Adding withdrawals_root length if it exists. + length += root.length(); + } + + if let Some(blob_gas_used) = self.blob_gas_used { + // Adding blob_gas_used length if it exists. + length += U256::from(blob_gas_used).length(); + } + + if let Some(excess_blob_gas) = self.excess_blob_gas { + // Adding excess_blob_gas length if it exists. + length += U256::from(excess_blob_gas).length(); + } + + if let Some(parent_beacon_block_root) = self.parent_beacon_block_root { + length += parent_beacon_block_root.length(); + } + + if let Some(requests_root) = self.requests_root { + length += requests_root.length(); + } + + length + } +} + +impl Encodable for Header { + fn encode(&self, out: &mut dyn BufMut) { + // Create a header indicating the encoded content is a list with the payload length computed + // from the header's payload calculation function. + let list_header = + alloy_rlp::Header { list: true, payload_length: self.header_payload_length() }; + list_header.encode(out); + + // Encode each header field sequentially + self.parent_hash.encode(out); // Encode parent hash. + self.ommers_hash.encode(out); // Encode ommer's hash. + self.beneficiary.encode(out); // Encode beneficiary. + self.state_root.encode(out); // Encode state root. + self.transactions_root.encode(out); // Encode transactions root. + self.receipts_root.encode(out); // Encode receipts root. + self.logs_bloom.encode(out); // Encode logs bloom. + self.difficulty.encode(out); // Encode difficulty. + U256::from(self.number).encode(out); // Encode block number. + U256::from(self.gas_limit).encode(out); // Encode gas limit. + U256::from(self.gas_used).encode(out); // Encode gas used. + self.timestamp.encode(out); // Encode timestamp. + self.extra_data.encode(out); // Encode extra data. + self.mix_hash.encode(out); // Encode mix hash. + B64::new(self.nonce.to_be_bytes()).encode(out); // Encode nonce. + + // Encode base fee. Put empty list if base fee is missing, + // but withdrawals root is present. + if let Some(ref base_fee) = self.base_fee_per_gas { + U256::from(*base_fee).encode(out); + } + + // Encode withdrawals root. Put empty string if withdrawals root is missing, + // but blob gas used is present. + if let Some(ref root) = self.withdrawals_root { + root.encode(out); + } + + // Encode blob gas used. Put empty list if blob gas used is missing, + // but excess blob gas is present. + if let Some(ref blob_gas_used) = self.blob_gas_used { + U256::from(*blob_gas_used).encode(out); + } + + // Encode excess blob gas. Put empty list if excess blob gas is missing, + // but parent beacon block root is present. + if let Some(ref excess_blob_gas) = self.excess_blob_gas { + U256::from(*excess_blob_gas).encode(out); + } + + // Encode parent beacon block root. + if let Some(ref parent_beacon_block_root) = self.parent_beacon_block_root { + parent_beacon_block_root.encode(out); + } + + // Encode EIP-7685 requests root + // + // If new fields are added, the above pattern will need to + // be repeated and placeholders added. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if let Some(ref requests_root) = self.requests_root { + requests_root.encode(out); + } + } + + fn length(&self) -> usize { + let mut length = 0; + length += self.header_payload_length(); + length += length_of_length(length); + length + } +} + +impl Decodable for Header { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let rlp_head = alloy_rlp::Header::decode(buf)?; + if !rlp_head.list { + return Err(alloy_rlp::Error::UnexpectedString) + } + let started_len = buf.len(); + let mut this = Self { + parent_hash: Decodable::decode(buf)?, + ommers_hash: Decodable::decode(buf)?, + beneficiary: Decodable::decode(buf)?, + state_root: Decodable::decode(buf)?, + transactions_root: Decodable::decode(buf)?, + receipts_root: Decodable::decode(buf)?, + logs_bloom: Decodable::decode(buf)?, + difficulty: Decodable::decode(buf)?, + number: u64::decode(buf)?, + gas_limit: u64::decode(buf)?, + gas_used: u64::decode(buf)?, + timestamp: Decodable::decode(buf)?, + extra_data: Decodable::decode(buf)?, + mix_hash: Decodable::decode(buf)?, + nonce: u64::from_be_bytes(B64::decode(buf)?.0), + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_root: None, + }; + if started_len - buf.len() < rlp_head.payload_length { + this.base_fee_per_gas = Some(u64::decode(buf)?); + } + + // Withdrawals root for post-shanghai headers + if started_len - buf.len() < rlp_head.payload_length { + this.withdrawals_root = Some(Decodable::decode(buf)?); + } + + // Blob gas used and excess blob gas for post-cancun headers + if started_len - buf.len() < rlp_head.payload_length { + this.blob_gas_used = Some(u64::decode(buf)?); + } + + if started_len - buf.len() < rlp_head.payload_length { + this.excess_blob_gas = Some(u64::decode(buf)?); + } + + // Decode parent beacon block root. + if started_len - buf.len() < rlp_head.payload_length { + this.parent_beacon_block_root = Some(B256::decode(buf)?); + } + + // Decode requests root. + // + // If new fields are added, the above pattern will need to + // be repeated and placeholders decoded. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if started_len - buf.len() < rlp_head.payload_length { + this.requests_root = Some(B256::decode(buf)?); + } + + let consumed = started_len - buf.len(); + if consumed != rlp_head.payload_length { + return Err(alloy_rlp::Error::ListLengthMismatch { + expected: rlp_head.payload_length, + got: consumed, + }) + } + Ok(this) + } +} diff --git a/crates/primitives-traits/src/header/sealed.rs b/crates/primitives-traits/src/header/sealed.rs new file mode 100644 index 000000000000..91918b6877dc --- /dev/null +++ b/crates/primitives-traits/src/header/sealed.rs @@ -0,0 +1,156 @@ +use super::Header; +use alloy_eips::BlockNumHash; +use alloy_primitives::{keccak256, BlockHash}; +#[cfg(any(test, feature = "test-utils"))] +use alloy_primitives::{BlockNumber, B256, U256}; +use alloy_rlp::{Decodable, Encodable}; +use bytes::BufMut; +use derive_more::{AsRef, Deref}; +#[cfg(any(test, feature = "arbitrary"))] +use proptest::prelude::*; +use reth_codecs::{add_arbitrary_tests, main_codec, Compact}; +use std::mem; + +/// A [`Header`] that is sealed at a precalculated hash, use [`SealedHeader::unseal()`] if you want +/// to modify header. +#[main_codec(no_arbitrary)] +#[add_arbitrary_tests(rlp, compact)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref)] +pub struct SealedHeader { + /// Locked Header hash. + hash: BlockHash, + /// Locked Header fields. + #[as_ref] + #[deref] + header: Header, +} + +impl SealedHeader { + /// Creates the sealed header with the corresponding block hash. + #[inline] + pub const fn new(header: Header, hash: BlockHash) -> Self { + Self { header, hash } + } + + /// Returns the sealed Header fields. + #[inline] + pub const fn header(&self) -> &Header { + &self.header + } + + /// Returns header/block hash. + #[inline] + pub const fn hash(&self) -> BlockHash { + self.hash + } + + /// Extract raw header that can be modified. + pub fn unseal(self) -> Header { + self.header + } + + /// This is the inverse of [`Header::seal_slow`] which returns the raw header and hash. + pub fn split(self) -> (Header, BlockHash) { + (self.header, self.hash) + } + + /// Return the number hash tuple. + pub fn num_hash(&self) -> BlockNumHash { + BlockNumHash::new(self.number, self.hash) + } + + /// Calculates a heuristic for the in-memory size of the [`SealedHeader`]. + #[inline] + pub fn size(&self) -> usize { + self.header.size() + mem::size_of::() + } +} + +impl Default for SealedHeader { + fn default() -> Self { + Header::default().seal_slow() + } +} + +impl Encodable for SealedHeader { + fn encode(&self, out: &mut dyn BufMut) { + self.header.encode(out); + } +} + +impl Decodable for SealedHeader { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let b = &mut &**buf; + let started_len = buf.len(); + + // decode the header from temp buffer + let header = Header::decode(b)?; + + // hash the consumed bytes, the rlp encoded header + let consumed = started_len - b.len(); + let hash = keccak256(&buf[..consumed]); + + // update original buffer + *buf = *b; + + Ok(Self { header, hash }) + } +} + +#[cfg(any(test, feature = "test-utils"))] +impl SealedHeader { + /// Updates the block header. + pub fn set_header(&mut self, header: Header) { + self.header = header + } + + /// Updates the block hash. + pub fn set_hash(&mut self, hash: BlockHash) { + self.hash = hash + } + + /// Updates the parent block hash. + pub fn set_parent_hash(&mut self, hash: BlockHash) { + self.header.parent_hash = hash + } + + /// Updates the block number. + pub fn set_block_number(&mut self, number: BlockNumber) { + self.header.number = number; + } + + /// Updates the block state root. + pub fn set_state_root(&mut self, state_root: B256) { + self.header.state_root = state_root; + } + + /// Updates the block difficulty. + pub fn set_difficulty(&mut self, difficulty: U256) { + self.header.difficulty = difficulty; + } +} + +#[cfg(any(test, feature = "arbitrary"))] +impl proptest::arbitrary::Arbitrary for SealedHeader { + type Parameters = (); + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + // map valid header strategy by sealing + crate::test_utils::valid_header_strategy().prop_map(|header| header.seal_slow()).boxed() + } + type Strategy = proptest::strategy::BoxedStrategy; +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for SealedHeader { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let sealed_header = crate::test_utils::generate_valid_header( + u.arbitrary()?, + u.arbitrary()?, + u.arbitrary()?, + u.arbitrary()?, + u.arbitrary()?, + ) + .seal_slow(); + Ok(sealed_header) + } +} diff --git a/crates/primitives-traits/src/header/test_utils.rs b/crates/primitives-traits/src/header/test_utils.rs new file mode 100644 index 000000000000..07d00f03bba2 --- /dev/null +++ b/crates/primitives-traits/src/header/test_utils.rs @@ -0,0 +1,66 @@ +//! Test utilities to generate random valid headers. + +use crate::Header; +use alloy_primitives::B256; +use proptest::{arbitrary::any, prop_compose}; + +/// Generates a header which is valid __with respect to past and future forks__. This means, for +/// example, that if the withdrawals root is present, the base fee per gas is also present. +/// +/// If blob gas used were present, then the excess blob gas and parent beacon block root are also +/// present. In this example, the withdrawals root would also be present. +/// +/// This __does not, and should not guarantee__ that the header is valid with respect to __anything +/// else__. +pub const fn generate_valid_header( + mut header: Header, + eip_4844_active: bool, + blob_gas_used: u64, + excess_blob_gas: u64, + parent_beacon_block_root: B256, +) -> Header { + // EIP-1559 logic + if header.base_fee_per_gas.is_none() { + // If EIP-1559 is not active, clear related fields + header.withdrawals_root = None; + header.blob_gas_used = None; + header.excess_blob_gas = None; + header.parent_beacon_block_root = None; + } else if header.withdrawals_root.is_none() { + // If EIP-4895 is not active, clear related fields + header.blob_gas_used = None; + header.excess_blob_gas = None; + header.parent_beacon_block_root = None; + } else if eip_4844_active { + // Set fields based on EIP-4844 being active + header.blob_gas_used = Some(blob_gas_used); + header.excess_blob_gas = Some(excess_blob_gas); + header.parent_beacon_block_root = Some(parent_beacon_block_root); + } else { + // If EIP-4844 is not active, clear related fields + header.blob_gas_used = None; + header.excess_blob_gas = None; + header.parent_beacon_block_root = None; + } + + // todo(onbjerg): adjust this for eip-7589 + header.requests_root = None; + + header +} + +prop_compose! { + /// Generates a proptest strategy for constructing an instance of a header which is valid __with + /// respect to past and future forks__. + /// + /// See docs for [generate_valid_header] for more information. + pub fn valid_header_strategy()( + header in any::
(), + eip_4844_active in any::(), + blob_gas_used in any::(), + excess_blob_gas in any::(), + parent_beacon_block_root in any::() + ) -> Header { + generate_valid_header(header, eip_4844_active, blob_gas_used, excess_blob_gas, parent_beacon_block_root) + } +} diff --git a/crates/primitives-traits/src/lib.rs b/crates/primitives-traits/src/lib.rs index af8918de1977..146cf86eaa08 100644 --- a/crates/primitives-traits/src/lib.rs +++ b/crates/primitives-traits/src/lib.rs @@ -10,6 +10,15 @@ #![allow(unknown_lints, non_local_definitions)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[cfg(feature = "alloy-compat")] +mod alloy_compat; + /// Minimal account pub mod account; pub use account::Account; + +/// Common header types +pub mod header; +#[cfg(any(test, feature = "arbitrary", feature = "test-utils"))] +pub use header::test_utils; +pub use header::{Header, HeaderError, SealedHeader}; diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 7dda61ddd7d1..5d3cc821ea61 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -117,8 +117,11 @@ optimism = [ "reth-ethereum-forks/optimism", "revm/optimism", ] -alloy-compat = ["alloy-rpc-types"] -test-utils = [] +alloy-compat = [ + "reth-primitives-traits/alloy-compat", + "alloy-rpc-types", +] +test-utils = ["reth-primitives-traits/test-utils"] [[bench]] name = "recover_ecdsa_crit" diff --git a/crates/primitives/src/alloy_compat.rs b/crates/primitives/src/alloy_compat.rs index 492bb1533748..60618f587191 100644 --- a/crates/primitives/src/alloy_compat.rs +++ b/crates/primitives/src/alloy_compat.rs @@ -1,9 +1,9 @@ //! Common conversions from alloy types. use crate::{ - constants::EMPTY_TRANSACTIONS, transaction::extract_chain_id, Block, Header, Signature, - Transaction, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxEip4844, - TxLegacy, TxType, + constants::EMPTY_TRANSACTIONS, transaction::extract_chain_id, Block, Signature, Transaction, + TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxEip4844, TxLegacy, + TxType, }; use alloy_primitives::TxKind; use alloy_rlp::Error as RlpError; @@ -61,54 +61,6 @@ impl TryFrom for Block { } } -impl TryFrom for Header { - type Error = alloy_rpc_types::ConversionError; - - fn try_from(header: alloy_rpc_types::Header) -> Result { - use alloy_rpc_types::ConversionError; - - Ok(Self { - base_fee_per_gas: header - .base_fee_per_gas - .map(|base_fee_per_gas| { - base_fee_per_gas.try_into().map_err(ConversionError::BaseFeePerGasConversion) - }) - .transpose()?, - beneficiary: header.miner, - blob_gas_used: header - .blob_gas_used - .map(|blob_gas_used| { - blob_gas_used.try_into().map_err(ConversionError::BlobGasUsedConversion) - }) - .transpose()?, - difficulty: header.difficulty, - excess_blob_gas: header - .excess_blob_gas - .map(|excess_blob_gas| { - excess_blob_gas.try_into().map_err(ConversionError::ExcessBlobGasConversion) - }) - .transpose()?, - extra_data: header.extra_data, - gas_limit: header.gas_limit.try_into().map_err(ConversionError::GasLimitConversion)?, - gas_used: header.gas_used.try_into().map_err(ConversionError::GasUsedConversion)?, - logs_bloom: header.logs_bloom, - mix_hash: header.mix_hash.unwrap_or_default(), - nonce: u64::from_be_bytes(header.nonce.unwrap_or_default().0), - number: header.number.ok_or(ConversionError::MissingBlockNumber)?, - ommers_hash: header.uncles_hash, - parent_beacon_block_root: header.parent_beacon_block_root, - parent_hash: header.parent_hash, - receipts_root: header.receipts_root, - state_root: header.state_root, - timestamp: header.timestamp, - transactions_root: header.transactions_root, - withdrawals_root: header.withdrawals_root, - // TODO: requests_root: header.requests_root, - requests_root: None, - }) - } -} - impl TryFrom for Transaction { type Error = alloy_rpc_types::ConversionError; diff --git a/crates/primitives/src/block.rs b/crates/primitives/src/block.rs index 77664ed1cc2f..68665277800c 100644 --- a/crates/primitives/src/block.rs +++ b/crates/primitives/src/block.rs @@ -2,17 +2,18 @@ use crate::{ Address, Bytes, GotExpected, Header, Requests, SealedHeader, TransactionSigned, TransactionSignedEcRecovered, Withdrawals, B256, }; +pub use alloy_eips::eip1898::{ + BlockHashOrNumber, BlockId, BlockNumHash, BlockNumberOrTag, ForkBlock, RpcBlockHash, +}; use alloy_rlp::{RlpDecodable, RlpEncodable}; use derive_more::{Deref, DerefMut}; #[cfg(any(test, feature = "arbitrary"))] -use proptest::prelude::{any, prop_compose}; +use proptest::prelude::prop_compose; use reth_codecs::derive_arbitrary; +#[cfg(any(test, feature = "arbitrary"))] +pub use reth_primitives_traits::test_utils::{generate_valid_header, valid_header_strategy}; use serde::{Deserialize, Serialize}; -pub use alloy_eips::eip1898::{ - BlockHashOrNumber, BlockId, BlockNumHash, BlockNumberOrTag, ForkBlock, RpcBlockHash, -}; - // HACK(onbjerg): we need this to always set `requests` to `None` since we might otherwise generate // a block with `None` withdrawals and `Some` requests, in which case we end up trying to decode the // requests as withdrawals @@ -592,69 +593,6 @@ impl From for BlockBody { } } -/// Generates a header which is valid __with respect to past and future forks__. This means, for -/// example, that if the withdrawals root is present, the base fee per gas is also present. -/// -/// If blob gas used were present, then the excess blob gas and parent beacon block root are also -/// present. In this example, the withdrawals root would also be present. -/// -/// This __does not, and should not guarantee__ that the header is valid with respect to __anything -/// else__. -#[cfg(any(test, feature = "arbitrary"))] -pub const fn generate_valid_header( - mut header: Header, - eip_4844_active: bool, - blob_gas_used: u64, - excess_blob_gas: u64, - parent_beacon_block_root: B256, -) -> Header { - // EIP-1559 logic - if header.base_fee_per_gas.is_none() { - // If EIP-1559 is not active, clear related fields - header.withdrawals_root = None; - header.blob_gas_used = None; - header.excess_blob_gas = None; - header.parent_beacon_block_root = None; - } else if header.withdrawals_root.is_none() { - // If EIP-4895 is not active, clear related fields - header.blob_gas_used = None; - header.excess_blob_gas = None; - header.parent_beacon_block_root = None; - } else if eip_4844_active { - // Set fields based on EIP-4844 being active - header.blob_gas_used = Some(blob_gas_used); - header.excess_blob_gas = Some(excess_blob_gas); - header.parent_beacon_block_root = Some(parent_beacon_block_root); - } else { - // If EIP-4844 is not active, clear related fields - header.blob_gas_used = None; - header.excess_blob_gas = None; - header.parent_beacon_block_root = None; - } - - // todo(onbjerg): adjust this for eip-7589 - header.requests_root = None; - - header -} - -#[cfg(any(test, feature = "arbitrary"))] -prop_compose! { - /// Generates a proptest strategy for constructing an instance of a header which is valid __with - /// respect to past and future forks__. - /// - /// See docs for [generate_valid_header] for more information. - pub fn valid_header_strategy()( - header in any::
(), - eip_4844_active in any::(), - blob_gas_used in any::(), - excess_blob_gas in any::(), - parent_beacon_block_root in any::() - ) -> Header { - generate_valid_header(header, eip_4844_active, blob_gas_used, excess_blob_gas, parent_beacon_block_root) - } -} - #[cfg(test)] mod tests { use super::{BlockNumberOrTag::*, *}; diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs index d0d8c3de4e9a..ea80328e7528 100644 --- a/crates/primitives/src/header.rs +++ b/crates/primitives/src/header.rs @@ -1,665 +1,11 @@ -#[cfg(any(test, feature = "arbitrary"))] -use crate::block::{generate_valid_header, valid_header_strategy}; -use crate::{ - basefee::calc_next_block_base_fee, - constants::{ALLOWED_FUTURE_BLOCK_TIME_SECONDS, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}, - eip4844::{calc_blob_gasprice, calculate_excess_blob_gas}, - keccak256, Address, BaseFeeParams, BlockHash, BlockNumHash, BlockNumber, Bloom, Bytes, B256, - B64, U256, -}; -use alloy_rlp::{length_of_length, Decodable, Encodable}; +//! Header types. + +use alloy_rlp::{Decodable, Encodable}; use bytes::BufMut; -use derive_more::{AsRef, Deref}; -#[cfg(any(test, feature = "arbitrary"))] -use proptest::prelude::*; -use reth_codecs::{add_arbitrary_tests, derive_arbitrary, main_codec, Compact}; +use reth_codecs::derive_arbitrary; use serde::{Deserialize, Serialize}; -use std::mem; - -/// Errors that can occur during header sanity checks. -#[derive(Debug, PartialEq, Eq)] -pub enum HeaderError { - /// Represents an error when the block difficulty is too large. - LargeDifficulty, - /// Represents an error when the block extradata is too large. - LargeExtraData, -} - -/// Block header -#[main_codec] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Header { - /// The Keccak 256-bit hash of the parent - /// block’s header, in its entirety; formally Hp. - pub parent_hash: B256, - /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. - pub ommers_hash: B256, - /// The 160-bit address to which all fees collected from the successful mining of this block - /// be transferred; formally Hc. - pub beneficiary: Address, - /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are - /// executed and finalisations applied; formally Hr. - pub state_root: B256, - /// The Keccak 256-bit hash of the root node of the trie structure populated with each - /// transaction in the transactions list portion of the block; formally Ht. - pub transactions_root: B256, - /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts - /// of each transaction in the transactions list portion of the block; formally He. - pub receipts_root: B256, - /// The Keccak 256-bit hash of the withdrawals list portion of this block. - /// - /// See [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895). - pub withdrawals_root: Option, - /// The Bloom filter composed from indexable information (logger address and log topics) - /// contained in each log entry from the receipt of each transaction in the transactions list; - /// formally Hb. - pub logs_bloom: Bloom, - /// A scalar value corresponding to the difficulty level of this block. This can be calculated - /// from the previous block’s difficulty level and the timestamp; formally Hd. - pub difficulty: U256, - /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of - /// zero; formally Hi. - pub number: BlockNumber, - /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. - pub gas_limit: u64, - /// A scalar value equal to the total gas used in transactions in this block; formally Hg. - pub gas_used: u64, - /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; - /// formally Hs. - pub timestamp: u64, - /// A 256-bit hash which, combined with the - /// nonce, proves that a sufficient amount of computation has been carried out on this block; - /// formally Hm. - pub mix_hash: B256, - /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of - /// computation has been carried out on this block; formally Hn. - pub nonce: u64, - /// A scalar representing EIP1559 base fee which can move up or down each block according - /// to a formula which is a function of gas used in parent block and gas target - /// (block gas limit divided by elasticity multiplier) of parent block. - /// The algorithm results in the base fee per gas increasing when blocks are - /// above the gas target, and decreasing when blocks are below the gas target. The base fee per - /// gas is burned. - pub base_fee_per_gas: Option, - /// The total amount of blob gas consumed by the transactions within the block, added in - /// EIP-4844. - pub blob_gas_used: Option, - /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks - /// with above-target blob gas consumption increase this value, blocks with below-target blob - /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. - pub excess_blob_gas: Option, - /// The hash of the parent beacon block's root is included in execution blocks, as proposed by - /// EIP-4788. - /// - /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, - /// and more. - /// - /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. - pub parent_beacon_block_root: Option, - /// The Keccak 256-bit hash of the root node of the trie structure populated with each - /// [EIP-7685] request in the block body. - /// - /// [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 - pub requests_root: Option, - /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or - /// fewer; formally Hx. - pub extra_data: Bytes, -} - -impl AsRef for Header { - fn as_ref(&self) -> &Self { - self - } -} - -impl Default for Header { - fn default() -> Self { - Self { - parent_hash: Default::default(), - ommers_hash: EMPTY_OMMER_ROOT_HASH, - beneficiary: Default::default(), - state_root: EMPTY_ROOT_HASH, - transactions_root: EMPTY_ROOT_HASH, - receipts_root: EMPTY_ROOT_HASH, - logs_bloom: Default::default(), - difficulty: Default::default(), - number: 0, - gas_limit: 0, - gas_used: 0, - timestamp: 0, - extra_data: Default::default(), - mix_hash: Default::default(), - nonce: 0, - base_fee_per_gas: None, - withdrawals_root: None, - blob_gas_used: None, - excess_blob_gas: None, - parent_beacon_block_root: None, - requests_root: None, - } - } -} - -impl Header { - /// Checks if the block's difficulty is set to zero, indicating a Proof-of-Stake header. - /// - /// This function is linked to EIP-3675, proposing the consensus upgrade to Proof-of-Stake: - /// [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#replacing-difficulty-with-0) - /// - /// Verifies whether, as per the EIP, the block's difficulty is updated to zero, - /// signifying the transition to a Proof-of-Stake mechanism. - /// - /// Returns `true` if the block's difficulty matches the constant zero set by the EIP. - pub fn is_zero_difficulty(&self) -> bool { - self.difficulty.is_zero() - } - - /// Performs a sanity check on the extradata field of the header. - /// - /// # Errors - /// - /// Returns an error if the extradata size is larger than 100 KB. - pub fn ensure_extradata_valid(&self) -> Result<(), HeaderError> { - if self.extra_data.len() > 100 * 1024 { - return Err(HeaderError::LargeExtraData) - } - Ok(()) - } - - /// Performs a sanity check on the block difficulty field of the header. - /// - /// # Errors - /// - /// Returns an error if the block difficulty exceeds 80 bits. - pub fn ensure_difficulty_valid(&self) -> Result<(), HeaderError> { - if self.difficulty.bit_len() > 80 { - return Err(HeaderError::LargeDifficulty) - } - Ok(()) - } - - /// Performs combined sanity checks on multiple header fields. - /// - /// This method combines checks for block difficulty and extradata sizes. - /// - /// # Errors - /// - /// Returns an error if either the block difficulty exceeds 80 bits - /// or if the extradata size is larger than 100 KB. - pub fn ensure_well_formed(&self) -> Result<(), HeaderError> { - self.ensure_difficulty_valid()?; - self.ensure_extradata_valid()?; - Ok(()) - } - - /// Checks if the block's timestamp is in the past compared to the parent block's timestamp. - /// - /// Note: This check is relevant only pre-merge. - pub const fn is_timestamp_in_past(&self, parent_timestamp: u64) -> bool { - self.timestamp <= parent_timestamp - } - - /// Checks if the block's timestamp is in the future based on the present timestamp. - /// - /// Clock can drift but this can be consensus issue. - /// - /// Note: This check is relevant only pre-merge. - pub const fn exceeds_allowed_future_timestamp(&self, present_timestamp: u64) -> bool { - self.timestamp > present_timestamp + ALLOWED_FUTURE_BLOCK_TIME_SECONDS - } - - /// Returns the parent block's number and hash - pub const fn parent_num_hash(&self) -> BlockNumHash { - BlockNumHash { number: self.number.saturating_sub(1), hash: self.parent_hash } - } - - /// Heavy function that will calculate hash of data and will *not* save the change to metadata. - /// Use [`Header::seal`], [`SealedHeader`] and unlock if you need hash to be persistent. - pub fn hash_slow(&self) -> B256 { - keccak256(alloy_rlp::encode(self)) - } - - /// Checks if the header is empty - has no transactions and no ommers - pub fn is_empty(&self) -> bool { - self.transaction_root_is_empty() && - self.ommers_hash_is_empty() && - self.withdrawals_root.map_or(true, |root| root == EMPTY_ROOT_HASH) - } - - /// Check if the ommers hash equals to empty hash list. - pub fn ommers_hash_is_empty(&self) -> bool { - self.ommers_hash == EMPTY_OMMER_ROOT_HASH - } - - /// Check if the transaction root equals to empty root. - pub fn transaction_root_is_empty(&self) -> bool { - self.transactions_root == EMPTY_ROOT_HASH - } - - /// Returns the blob fee for _this_ block according to the EIP-4844 spec. - /// - /// Returns `None` if `excess_blob_gas` is None - pub fn blob_fee(&self) -> Option { - self.excess_blob_gas.map(calc_blob_gasprice) - } - - /// Returns the blob fee for the next block according to the EIP-4844 spec. - /// - /// Returns `None` if `excess_blob_gas` is None. - /// - /// See also [`Self::next_block_excess_blob_gas`] - pub fn next_block_blob_fee(&self) -> Option { - self.next_block_excess_blob_gas().map(calc_blob_gasprice) - } - - /// Calculate base fee for next block according to the EIP-1559 spec. - /// - /// Returns a `None` if no base fee is set, no EIP-1559 support - pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option { - Some(calc_next_block_base_fee( - self.gas_used as u128, - self.gas_limit as u128, - self.base_fee_per_gas? as u128, - base_fee_params, - ) as u64) - } - - /// Calculate excess blob gas for the next block according to the EIP-4844 spec. - /// - /// Returns a `None` if no excess blob gas is set, no EIP-4844 support - pub fn next_block_excess_blob_gas(&self) -> Option { - Some(calculate_excess_blob_gas(self.excess_blob_gas?, self.blob_gas_used?)) - } - - /// Seal the header with a known hash. - /// - /// WARNING: This method does not perform validation whether the hash is correct. - #[inline] - pub const fn seal(self, hash: B256) -> SealedHeader { - SealedHeader { header: self, hash } - } - - /// Calculate hash and seal the Header so that it can't be changed. - #[inline] - pub fn seal_slow(self) -> SealedHeader { - let hash = self.hash_slow(); - self.seal(hash) - } - - /// Calculate a heuristic for the in-memory size of the [Header]. - #[inline] - pub fn size(&self) -> usize { - mem::size_of::() + // parent hash - mem::size_of::() + // ommers hash - mem::size_of::
() + // beneficiary - mem::size_of::() + // state root - mem::size_of::() + // transactions root - mem::size_of::() + // receipts root - mem::size_of::>() + // withdrawals root - mem::size_of::() + // logs bloom - mem::size_of::() + // difficulty - mem::size_of::() + // number - mem::size_of::() + // gas limit - mem::size_of::() + // gas used - mem::size_of::() + // timestamp - mem::size_of::() + // mix hash - mem::size_of::() + // nonce - mem::size_of::>() + // base fee per gas - mem::size_of::>() + // blob gas used - mem::size_of::>() + // excess blob gas - mem::size_of::>() + // parent beacon block root - self.extra_data.len() // extra data - } - - fn header_payload_length(&self) -> usize { - let mut length = 0; - length += self.parent_hash.length(); // Hash of the previous block. - length += self.ommers_hash.length(); // Hash of uncle blocks. - length += self.beneficiary.length(); // Address that receives rewards. - length += self.state_root.length(); // Root hash of the state object. - length += self.transactions_root.length(); // Root hash of transactions in the block. - length += self.receipts_root.length(); // Hash of transaction receipts. - length += self.logs_bloom.length(); // Data structure containing event logs. - length += self.difficulty.length(); // Difficulty value of the block. - length += U256::from(self.number).length(); // Block number. - length += U256::from(self.gas_limit).length(); // Maximum gas allowed. - length += U256::from(self.gas_used).length(); // Actual gas used. - length += self.timestamp.length(); // Block timestamp. - length += self.extra_data.length(); // Additional arbitrary data. - length += self.mix_hash.length(); // Hash used for mining. - length += B64::new(self.nonce.to_be_bytes()).length(); // Nonce for mining. - - if let Some(base_fee) = self.base_fee_per_gas { - // Adding base fee length if it exists. - length += U256::from(base_fee).length(); - } - - if let Some(root) = self.withdrawals_root { - // Adding withdrawals_root length if it exists. - length += root.length(); - } - - if let Some(blob_gas_used) = self.blob_gas_used { - // Adding blob_gas_used length if it exists. - length += U256::from(blob_gas_used).length(); - } - - if let Some(excess_blob_gas) = self.excess_blob_gas { - // Adding excess_blob_gas length if it exists. - length += U256::from(excess_blob_gas).length(); - } - - if let Some(parent_beacon_block_root) = self.parent_beacon_block_root { - length += parent_beacon_block_root.length(); - } - - if let Some(requests_root) = self.requests_root { - length += requests_root.length(); - } - - length - } -} - -impl Encodable for Header { - fn encode(&self, out: &mut dyn BufMut) { - // Create a header indicating the encoded content is a list with the payload length computed - // from the header's payload calculation function. - let list_header = - alloy_rlp::Header { list: true, payload_length: self.header_payload_length() }; - list_header.encode(out); - - // Encode each header field sequentially - self.parent_hash.encode(out); // Encode parent hash. - self.ommers_hash.encode(out); // Encode ommer's hash. - self.beneficiary.encode(out); // Encode beneficiary. - self.state_root.encode(out); // Encode state root. - self.transactions_root.encode(out); // Encode transactions root. - self.receipts_root.encode(out); // Encode receipts root. - self.logs_bloom.encode(out); // Encode logs bloom. - self.difficulty.encode(out); // Encode difficulty. - U256::from(self.number).encode(out); // Encode block number. - U256::from(self.gas_limit).encode(out); // Encode gas limit. - U256::from(self.gas_used).encode(out); // Encode gas used. - self.timestamp.encode(out); // Encode timestamp. - self.extra_data.encode(out); // Encode extra data. - self.mix_hash.encode(out); // Encode mix hash. - B64::new(self.nonce.to_be_bytes()).encode(out); // Encode nonce. - - // Encode base fee. Put empty list if base fee is missing, - // but withdrawals root is present. - if let Some(ref base_fee) = self.base_fee_per_gas { - U256::from(*base_fee).encode(out); - } - - // Encode withdrawals root. Put empty string if withdrawals root is missing, - // but blob gas used is present. - if let Some(ref root) = self.withdrawals_root { - root.encode(out); - } - - // Encode blob gas used. Put empty list if blob gas used is missing, - // but excess blob gas is present. - if let Some(ref blob_gas_used) = self.blob_gas_used { - U256::from(*blob_gas_used).encode(out); - } - - // Encode excess blob gas. Put empty list if excess blob gas is missing, - // but parent beacon block root is present. - if let Some(ref excess_blob_gas) = self.excess_blob_gas { - U256::from(*excess_blob_gas).encode(out); - } - - // Encode parent beacon block root. - if let Some(ref parent_beacon_block_root) = self.parent_beacon_block_root { - parent_beacon_block_root.encode(out); - } - - // Encode EIP-7685 requests root - // - // If new fields are added, the above pattern will need to - // be repeated and placeholders added. Otherwise, it's impossible to tell _which_ - // fields are missing. This is mainly relevant for contrived cases where a header is - // created at random, for example: - // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are - // post-London, so this is technically not valid. However, a tool like proptest would - // generate a block like this. - if let Some(ref requests_root) = self.requests_root { - requests_root.encode(out); - } - } - - fn length(&self) -> usize { - let mut length = 0; - length += self.header_payload_length(); - length += length_of_length(length); - length - } -} - -impl Decodable for Header { - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - let rlp_head = alloy_rlp::Header::decode(buf)?; - if !rlp_head.list { - return Err(alloy_rlp::Error::UnexpectedString) - } - let started_len = buf.len(); - let mut this = Self { - parent_hash: Decodable::decode(buf)?, - ommers_hash: Decodable::decode(buf)?, - beneficiary: Decodable::decode(buf)?, - state_root: Decodable::decode(buf)?, - transactions_root: Decodable::decode(buf)?, - receipts_root: Decodable::decode(buf)?, - logs_bloom: Decodable::decode(buf)?, - difficulty: Decodable::decode(buf)?, - number: u64::decode(buf)?, - gas_limit: u64::decode(buf)?, - gas_used: u64::decode(buf)?, - timestamp: Decodable::decode(buf)?, - extra_data: Decodable::decode(buf)?, - mix_hash: Decodable::decode(buf)?, - nonce: u64::from_be_bytes(B64::decode(buf)?.0), - base_fee_per_gas: None, - withdrawals_root: None, - blob_gas_used: None, - excess_blob_gas: None, - parent_beacon_block_root: None, - requests_root: None, - }; - if started_len - buf.len() < rlp_head.payload_length { - this.base_fee_per_gas = Some(u64::decode(buf)?); - } - - // Withdrawals root for post-shanghai headers - if started_len - buf.len() < rlp_head.payload_length { - this.withdrawals_root = Some(Decodable::decode(buf)?); - } - - // Blob gas used and excess blob gas for post-cancun headers - if started_len - buf.len() < rlp_head.payload_length { - this.blob_gas_used = Some(u64::decode(buf)?); - } - - if started_len - buf.len() < rlp_head.payload_length { - this.excess_blob_gas = Some(u64::decode(buf)?); - } - - // Decode parent beacon block root. - if started_len - buf.len() < rlp_head.payload_length { - this.parent_beacon_block_root = Some(B256::decode(buf)?); - } - - // Decode requests root. - // - // If new fields are added, the above pattern will need to - // be repeated and placeholders decoded. Otherwise, it's impossible to tell _which_ - // fields are missing. This is mainly relevant for contrived cases where a header is - // created at random, for example: - // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are - // post-London, so this is technically not valid. However, a tool like proptest would - // generate a block like this. - if started_len - buf.len() < rlp_head.payload_length { - this.requests_root = Some(B256::decode(buf)?); - } - - let consumed = started_len - buf.len(); - if consumed != rlp_head.payload_length { - return Err(alloy_rlp::Error::ListLengthMismatch { - expected: rlp_head.payload_length, - got: consumed, - }) - } - Ok(this) - } -} - -/// A [`Header`] that is sealed at a precalculated hash, use [`SealedHeader::unseal()`] if you want -/// to modify header. -#[main_codec(no_arbitrary)] -#[add_arbitrary_tests(rlp, compact)] -#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref)] -pub struct SealedHeader { - /// Locked Header hash. - hash: BlockHash, - /// Locked Header fields. - #[as_ref] - #[deref] - header: Header, -} -impl SealedHeader { - /// Creates the sealed header with the corresponding block hash. - #[inline] - pub const fn new(header: Header, hash: BlockHash) -> Self { - Self { header, hash } - } - - /// Returns the sealed Header fields. - #[inline] - pub const fn header(&self) -> &Header { - &self.header - } - - /// Returns header/block hash. - #[inline] - pub const fn hash(&self) -> BlockHash { - self.hash - } - - /// Updates the block header. - #[cfg(any(test, feature = "test-utils"))] - pub fn set_header(&mut self, header: Header) { - self.header = header - } - - /// Updates the block hash. - #[cfg(any(test, feature = "test-utils"))] - pub fn set_hash(&mut self, hash: BlockHash) { - self.hash = hash - } - - /// Updates the parent block hash. - #[cfg(any(test, feature = "test-utils"))] - pub fn set_parent_hash(&mut self, hash: BlockHash) { - self.header.parent_hash = hash - } - - /// Updates the block number. - #[cfg(any(test, feature = "test-utils"))] - pub fn set_block_number(&mut self, number: BlockNumber) { - self.header.number = number; - } - - /// Updates the block state root. - #[cfg(any(test, feature = "test-utils"))] - pub fn set_state_root(&mut self, state_root: B256) { - self.header.state_root = state_root; - } - - /// Updates the block difficulty. - #[cfg(any(test, feature = "test-utils"))] - pub fn set_difficulty(&mut self, difficulty: U256) { - self.header.difficulty = difficulty; - } - - /// Extract raw header that can be modified. - pub fn unseal(self) -> Header { - self.header - } - - /// This is the inverse of [`Header::seal_slow`] which returns the raw header and hash. - pub fn split(self) -> (Header, BlockHash) { - (self.header, self.hash) - } - - /// Return the number hash tuple. - pub fn num_hash(&self) -> BlockNumHash { - BlockNumHash::new(self.number, self.hash) - } - - /// Calculates a heuristic for the in-memory size of the [`SealedHeader`]. - #[inline] - pub fn size(&self) -> usize { - self.header.size() + mem::size_of::() - } -} - -#[cfg(any(test, feature = "arbitrary"))] -impl proptest::arbitrary::Arbitrary for SealedHeader { - type Parameters = (); - fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { - // map valid header strategy by sealing - valid_header_strategy().prop_map(|header| header.seal_slow()).boxed() - } - type Strategy = proptest::strategy::BoxedStrategy; -} - -#[cfg(any(test, feature = "arbitrary"))] -impl<'a> arbitrary::Arbitrary<'a> for SealedHeader { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let sealed_header = generate_valid_header( - u.arbitrary()?, - u.arbitrary()?, - u.arbitrary()?, - u.arbitrary()?, - u.arbitrary()?, - ) - .seal_slow(); - Ok(sealed_header) - } -} - -impl Default for SealedHeader { - fn default() -> Self { - Header::default().seal_slow() - } -} - -impl Encodable for SealedHeader { - fn encode(&self, out: &mut dyn BufMut) { - self.header.encode(out); - } -} - -impl Decodable for SealedHeader { - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - let b = &mut &**buf; - let started_len = buf.len(); - - // decode the header from temp buffer - let header = Header::decode(b)?; - - // hash the consumed bytes, the rlp encoded header - let consumed = started_len - b.len(); - let hash = keccak256(&buf[..consumed]); - - // update original buffer - *buf = *b; - - Ok(Self { header, hash }) - } -} +pub use reth_primitives_traits::{Header, HeaderError, SealedHeader}; /// Represents the direction for a headers request depending on the `reverse` field of the request. /// > The response must contain a number of block headers, of rising number when reverse is 0, @@ -741,8 +87,10 @@ impl From for bool { #[cfg(test)] mod tests { - use super::{Bytes, Decodable, Encodable, Header, B256}; - use crate::{address, b256, bloom, bytes, hex, Address, HeadersDirection, U256}; + use crate::{ + address, b256, bloom, bytes, hex, Address, Bytes, Header, HeadersDirection, B256, U256, + }; + use alloy_rlp::{Decodable, Encodable}; use std::str::FromStr; // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 02e86c45987d..e2f695935ab3 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -30,7 +30,7 @@ pub mod constants; pub mod eip4844; mod error; pub mod genesis; -mod header; +pub mod header; mod integer_list; mod log; mod net;