Skip to main content

eth_valkyoth_protocol/
header.rs

1use eth_valkyoth_codec::{
2    DecodeError, DecodeLimits, RlpInteger, RlpItem, RlpScalar, decode_rlp_list,
3};
4use eth_valkyoth_hash::{Keccak256, hash_one};
5use eth_valkyoth_primitives::{Address, B256, BlockNumber, Gas, UnixTimestamp, Wei};
6
7mod error;
8pub use error::{BlockHeaderDecodeError, BlockHeaderDecodeErrorCategory, BlockHeaderField};
9
10const ADDRESS_BYTES: usize = 20;
11const B256_BYTES: usize = 32;
12const BLOOM_BYTES: usize = 256;
13const NONCE_BYTES: usize = 8;
14
15/// Pre-London execution header field count.
16pub const LEGACY_HEADER_FIELD_COUNT: usize = 15;
17/// London execution header field count, including `base_fee_per_gas`.
18pub const LONDON_HEADER_FIELD_COUNT: usize = 16;
19/// Shanghai execution header field count, including `withdrawals_root`.
20pub const SHANGHAI_HEADER_FIELD_COUNT: usize = 17;
21/// Cancun execution header field count, including blob gas fields and parent
22/// beacon block root.
23pub const CANCUN_HEADER_FIELD_COUNT: usize = 20;
24/// Prague execution header field count, including `requests_hash`.
25pub const PRAGUE_HEADER_FIELD_COUNT: usize = 21;
26
27/// Fork-specific execution header field set.
28#[non_exhaustive]
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum HeaderFieldSet {
31    /// Header layout before EIP-1559.
32    Legacy,
33    /// Header layout with `base_fee_per_gas`.
34    London,
35    /// Header layout with `withdrawals_root`.
36    Shanghai,
37    /// Header layout with blob gas fields and `parent_beacon_block_root`.
38    Cancun,
39    /// Header layout with `requests_hash`.
40    Prague,
41}
42
43impl HeaderFieldSet {
44    /// Returns the exact RLP field count expected for this field set.
45    #[must_use]
46    pub const fn field_count(self) -> usize {
47        match self {
48            Self::Legacy => LEGACY_HEADER_FIELD_COUNT,
49            Self::London => LONDON_HEADER_FIELD_COUNT,
50            Self::Shanghai => SHANGHAI_HEADER_FIELD_COUNT,
51            Self::Cancun => CANCUN_HEADER_FIELD_COUNT,
52            Self::Prague => PRAGUE_HEADER_FIELD_COUNT,
53        }
54    }
55
56    const fn has_base_fee(self) -> bool {
57        !matches!(self, Self::Legacy)
58    }
59
60    const fn has_withdrawals_root(self) -> bool {
61        matches!(self, Self::Shanghai | Self::Cancun | Self::Prague)
62    }
63
64    const fn has_cancun_fields(self) -> bool {
65        matches!(self, Self::Cancun | Self::Prague)
66    }
67
68    const fn has_requests_hash(self) -> bool {
69        matches!(self, Self::Prague)
70    }
71}
72
73/// Domain-separated execution block hash.
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct BlockHash(B256);
76
77impl BlockHash {
78    /// Creates a block-hash domain value from raw bytes.
79    #[must_use]
80    pub const fn from_b256(value: B256) -> Self {
81        Self(value)
82    }
83
84    /// Returns the raw 32-byte hash.
85    #[must_use]
86    pub const fn to_b256(self) -> B256 {
87        self.0
88    }
89}
90
91impl From<BlockHash> for B256 {
92    fn from(value: BlockHash) -> Self {
93        value.to_b256()
94    }
95}
96
97/// Borrowed execution-layer header decoded only into field domains.
98///
99/// This type is intentionally unvalidated. It does not prove ancestry, ommers
100/// hash, state root, transaction root, receipt root, bloom correctness, gas
101/// accounting, base-fee calculation, withdrawals root, blob-gas accounting,
102/// parent beacon root, requests hash, proof roots, or fork activation.
103/// The `extra_data` field is also not checked against any network's
104/// consensus-specific byte limit.
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub struct UnvalidatedBlockHeader<'a> {
107    encoded_rlp: &'a [u8],
108    /// Field layout used to decode the header.
109    pub field_set: HeaderFieldSet,
110    /// Parent block hash.
111    pub parent_hash: B256,
112    /// Ommers/uncles hash.
113    pub ommers_hash: B256,
114    /// Block beneficiary/coinbase.
115    pub beneficiary: Address,
116    /// Post-state root.
117    pub state_root: B256,
118    /// Transactions trie root.
119    pub transactions_root: B256,
120    /// Receipts trie root.
121    pub receipts_root: B256,
122    /// Logs bloom filter.
123    pub logs_bloom: LogsBloom,
124    /// Raw canonical U256 difficulty field.
125    pub difficulty: [u8; 32],
126    /// Block number.
127    pub number: BlockNumber,
128    /// Gas limit.
129    pub gas_limit: Gas,
130    /// Gas used.
131    pub gas_used: Gas,
132    /// Header timestamp.
133    pub timestamp: UnixTimestamp,
134    /// Borrowed extra data.
135    ///
136    /// This is not checked against the network's consensus limit, such as the
137    /// 32-byte mainnet cap. Callers must validate that limit for their chain.
138    pub extra_data: &'a [u8],
139    /// Mix hash or post-merge prev_randao.
140    pub mix_hash: B256,
141    /// Header nonce bytes.
142    pub nonce: [u8; 8],
143    /// London base fee per gas.
144    pub base_fee_per_gas: Option<Wei>,
145    /// Shanghai withdrawals root.
146    pub withdrawals_root: Option<B256>,
147    /// Cancun blob gas used.
148    pub blob_gas_used: Option<Gas>,
149    /// Cancun excess blob gas.
150    pub excess_blob_gas: Option<Gas>,
151    /// Cancun parent beacon block root.
152    pub parent_beacon_block_root: Option<B256>,
153    /// Prague requests hash.
154    pub requests_hash: Option<B256>,
155}
156
157impl UnvalidatedBlockHeader<'_> {
158    /// Returns the exact canonical RLP bytes that were decoded.
159    #[must_use]
160    pub const fn encoded_rlp(&self) -> &[u8] {
161        self.encoded_rlp
162    }
163
164    /// Hashes the exact canonical header RLP with the caller-provided Keccak
165    /// implementation and returns a block-hash domain value.
166    #[must_use]
167    pub fn hash_with<H>(&self, hasher: H) -> BlockHash
168    where
169        H: Keccak256,
170    {
171        BlockHash::from_b256(hash_one(hasher, self.encoded_rlp))
172    }
173}
174
175/// Ethereum logs bloom filter bytes.
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub struct LogsBloom([u8; BLOOM_BYTES]);
178
179impl LogsBloom {
180    /// Creates a logs bloom from raw bytes.
181    #[must_use]
182    pub const fn from_bytes(bytes: [u8; BLOOM_BYTES]) -> Self {
183        Self(bytes)
184    }
185
186    /// Returns the raw logs-bloom bytes.
187    #[must_use]
188    pub const fn to_bytes(self) -> [u8; BLOOM_BYTES] {
189        self.0
190    }
191}
192
193/// Decodes one canonical execution block header under explicit limits.
194///
195/// The selected [`HeaderFieldSet`] controls which optional fork fields must be
196/// present. This function is syntactic: successfully decoding a header does not
197/// prove block validity, fork activation, root correctness, or consensus-layer
198/// commitments.
199pub fn decode_block_header<'a>(
200    input: &'a [u8],
201    field_set: HeaderFieldSet,
202    limits: DecodeLimits,
203) -> Result<UnvalidatedBlockHeader<'a>, BlockHeaderDecodeError> {
204    let list = decode_rlp_list(input, limits).map_err(BlockHeaderDecodeError::Decode)?;
205    let expected = field_set.field_count();
206    let found = list.item_count();
207    if found != expected {
208        return Err(BlockHeaderDecodeError::WrongFieldCount { expected, found });
209    }
210
211    let mut fields = list.items();
212    let header = UnvalidatedBlockHeader {
213        encoded_rlp: input,
214        field_set,
215        parent_hash: decode_b256(&mut fields, BlockHeaderField::ParentHash)?,
216        ommers_hash: decode_b256(&mut fields, BlockHeaderField::OmmersHash)?,
217        beneficiary: decode_address(&mut fields, BlockHeaderField::Beneficiary)?,
218        state_root: decode_b256(&mut fields, BlockHeaderField::StateRoot)?,
219        transactions_root: decode_b256(&mut fields, BlockHeaderField::TransactionsRoot)?,
220        receipts_root: decode_b256(&mut fields, BlockHeaderField::ReceiptsRoot)?,
221        logs_bloom: decode_logs_bloom(&mut fields)?,
222        difficulty: decode_u256(&mut fields, BlockHeaderField::Difficulty)?,
223        number: BlockNumber::new(decode_u64(&mut fields, BlockHeaderField::Number)?),
224        gas_limit: Gas::new(decode_u64(&mut fields, BlockHeaderField::GasLimit)?),
225        gas_used: Gas::new(decode_u64(&mut fields, BlockHeaderField::GasUsed)?),
226        timestamp: UnixTimestamp::new(decode_u64(&mut fields, BlockHeaderField::Timestamp)?),
227        extra_data: next_scalar(&mut fields, BlockHeaderField::ExtraData)?.payload(),
228        mix_hash: decode_b256(&mut fields, BlockHeaderField::MixHash)?,
229        nonce: decode_fixed::<NONCE_BYTES>(&mut fields, BlockHeaderField::Nonce)?,
230        base_fee_per_gas: decode_optional_wei(
231            &mut fields,
232            field_set.has_base_fee(),
233            BlockHeaderField::BaseFeePerGas,
234        )?,
235        withdrawals_root: decode_optional_b256(
236            &mut fields,
237            field_set.has_withdrawals_root(),
238            BlockHeaderField::WithdrawalsRoot,
239        )?,
240        blob_gas_used: decode_optional_gas(
241            &mut fields,
242            field_set.has_cancun_fields(),
243            BlockHeaderField::BlobGasUsed,
244        )?,
245        excess_blob_gas: decode_optional_gas(
246            &mut fields,
247            field_set.has_cancun_fields(),
248            BlockHeaderField::ExcessBlobGas,
249        )?,
250        parent_beacon_block_root: decode_optional_b256(
251            &mut fields,
252            field_set.has_cancun_fields(),
253            BlockHeaderField::ParentBeaconBlockRoot,
254        )?,
255        requests_hash: decode_optional_b256(
256            &mut fields,
257            field_set.has_requests_hash(),
258            BlockHeaderField::RequestsHash,
259        )?,
260    };
261    Ok(header)
262}
263
264fn next_scalar<'a>(
265    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
266    field: BlockHeaderField,
267) -> Result<RlpScalar<'a>, BlockHeaderDecodeError> {
268    let item = fields
269        .next()
270        .ok_or(BlockHeaderDecodeError::FieldDecode {
271            field,
272            source: DecodeError::Malformed,
273        })?
274        .map_err(|source| BlockHeaderDecodeError::FieldDecode { field, source })?;
275    match item {
276        RlpItem::Scalar(scalar) => Ok(scalar),
277        RlpItem::List(_) => Err(BlockHeaderDecodeError::FieldDecode {
278            field,
279            source: DecodeError::UnexpectedList,
280        }),
281    }
282}
283
284fn decode_u64<'a>(
285    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
286    field: BlockHeaderField,
287) -> Result<u64, BlockHeaderDecodeError> {
288    RlpInteger::try_from_scalar(next_scalar(fields, field)?)
289        .and_then(RlpInteger::to_u64)
290        .map_err(|source| BlockHeaderDecodeError::FieldDecode { field, source })
291}
292
293fn decode_u256<'a>(
294    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
295    field: BlockHeaderField,
296) -> Result<[u8; 32], BlockHeaderDecodeError> {
297    RlpInteger::try_from_scalar(next_scalar(fields, field)?)
298        .and_then(RlpInteger::to_be_bytes32)
299        .map_err(|source| BlockHeaderDecodeError::FieldDecode { field, source })
300}
301
302fn decode_wei<'a>(
303    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
304    field: BlockHeaderField,
305) -> Result<Wei, BlockHeaderDecodeError> {
306    decode_u256(fields, field).map(Wei::from_be_bytes)
307}
308
309fn decode_fixed<'a, const N: usize>(
310    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
311    field: BlockHeaderField,
312) -> Result<[u8; N], BlockHeaderDecodeError> {
313    let scalar = next_scalar(fields, field)?;
314    scalar
315        .payload()
316        .try_into()
317        .map_err(|_| BlockHeaderDecodeError::InvalidFieldLength {
318            field,
319            expected: N,
320            found: scalar.payload().len(),
321        })
322}
323
324fn decode_b256<'a>(
325    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
326    field: BlockHeaderField,
327) -> Result<B256, BlockHeaderDecodeError> {
328    decode_fixed::<B256_BYTES>(fields, field).map(B256::from_bytes)
329}
330
331fn decode_address<'a>(
332    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
333    field: BlockHeaderField,
334) -> Result<Address, BlockHeaderDecodeError> {
335    decode_fixed::<ADDRESS_BYTES>(fields, field).map(Address::from_bytes)
336}
337
338fn decode_logs_bloom<'a>(
339    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
340) -> Result<LogsBloom, BlockHeaderDecodeError> {
341    decode_fixed::<BLOOM_BYTES>(fields, BlockHeaderField::LogsBloom).map(LogsBloom::from_bytes)
342}
343
344fn decode_optional_b256<'a>(
345    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
346    present: bool,
347    field: BlockHeaderField,
348) -> Result<Option<B256>, BlockHeaderDecodeError> {
349    if present {
350        decode_b256(fields, field).map(Some)
351    } else {
352        Ok(None)
353    }
354}
355
356fn decode_optional_gas<'a>(
357    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
358    present: bool,
359    field: BlockHeaderField,
360) -> Result<Option<Gas>, BlockHeaderDecodeError> {
361    if present {
362        decode_u64(fields, field).map(Gas::new).map(Some)
363    } else {
364        Ok(None)
365    }
366}
367
368fn decode_optional_wei<'a>(
369    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
370    present: bool,
371    field: BlockHeaderField,
372) -> Result<Option<Wei>, BlockHeaderDecodeError> {
373    if present {
374        decode_wei(fields, field).map(Some)
375    } else {
376        Ok(None)
377    }
378}
379
380#[cfg(test)]
381#[path = "header_tests.rs"]
382mod tests;