Skip to main content

alloy_consensus_any/block/
header.rs

1use alloy_consensus::{error::ValueError, BlockHeader, Header};
2use alloy_primitives::{Address, BlockNumber, Bloom, Bytes, Sealed, B256, B64, U256};
3
4/// Block header representation with certain fields made optional to account for possible
5/// differences in network implementations.
6#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
7#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
10pub struct AnyHeader {
11    /// Hash of the parent
12    pub parent_hash: B256,
13    /// Hash of the uncles
14    #[cfg_attr(feature = "serde", serde(rename = "sha3Uncles"))]
15    pub ommers_hash: B256,
16    /// Alias of `author`
17    #[cfg_attr(feature = "serde", serde(rename = "miner"))]
18    pub beneficiary: Address,
19    /// State root hash
20    #[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_state_root"))]
21    pub state_root: B256,
22    /// Transactions root hash
23    pub transactions_root: B256,
24    /// Transactions receipts root hash
25    pub receipts_root: B256,
26    /// Logs bloom
27    pub logs_bloom: Bloom,
28    /// Difficulty
29    pub difficulty: U256,
30    /// Block number
31    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
32    pub number: u64,
33    /// Gas Limit
34    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
35    pub gas_limit: u64,
36    /// Gas Used
37    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
38    pub gas_used: u64,
39    /// Timestamp
40    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
41    pub timestamp: u64,
42    /// Extra data
43    pub extra_data: Bytes,
44    /// Mix Hash
45    ///
46    /// Before the merge this proves, combined with the nonce, that a sufficient amount of
47    /// computation has been carried out on this block: the Proof-of-Work (PoW).
48    ///
49    /// After the merge this is `prevRandao`: Randomness value for the generated payload.
50    ///
51    /// This is an Option because it is not always set by non-ethereum networks.
52    ///
53    /// See also <https://eips.ethereum.org/EIPS/eip-4399>
54    /// And <https://github.com/ethereum/execution-apis/issues/328>
55    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
56    pub mix_hash: Option<B256>,
57    /// Nonce
58    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
59    pub nonce: Option<B64>,
60    /// Base fee per unit of gas (if past London).
61    ///
62    /// The Ethereum execution spec defines `base_fee_per_gas` as an arbitrary-precision integer
63    /// (`Uint`), but alloy types it as `u64` for compatibility with Ethereum mainnet, which has
64    /// never observed a value above `u64::MAX`. Some EVM-compatible chains (e.g. Stable,
65    /// chain id 988) do return values above `u64::MAX` for historical blocks. To keep RPC
66    /// deserialization from failing on those chains, oversized inputs saturate to `u64::MAX`
67    /// rather than erroring. Consumers that need the full-precision value should construct a
68    /// custom header type with a wider field.
69    ///
70    /// See <https://github.com/alloy-rs/alloy/issues/3741>.
71    #[cfg_attr(
72        feature = "serde",
73        serde(
74            default,
75            skip_serializing_if = "Option::is_none",
76            with = "saturating_base_fee_per_gas"
77        )
78    )]
79    pub base_fee_per_gas: Option<u64>,
80    /// Withdrawals root hash added by EIP-4895 and is ignored in legacy headers.
81    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
82    pub withdrawals_root: Option<B256>,
83    /// Blob gas used
84    #[cfg_attr(
85        feature = "serde",
86        serde(
87            default,
88            skip_serializing_if = "Option::is_none",
89            with = "alloy_serde::quantity::opt"
90        )
91    )]
92    pub blob_gas_used: Option<u64>,
93    /// Excess blob gas
94    #[cfg_attr(
95        feature = "serde",
96        serde(
97            default,
98            skip_serializing_if = "Option::is_none",
99            with = "alloy_serde::quantity::opt"
100        )
101    )]
102    pub excess_blob_gas: Option<u64>,
103    /// EIP-4788 parent beacon block root
104    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
105    pub parent_beacon_block_root: Option<B256>,
106    /// EIP-7685 requests hash.
107    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
108    pub requests_hash: Option<B256>,
109    /// EIP-7928 block access list hash.
110    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
111    pub block_access_list_hash: Option<B256>,
112    /// EIP-7843 slot number.
113    #[cfg_attr(
114        feature = "serde",
115        serde(
116            default,
117            skip_serializing_if = "Option::is_none",
118            with = "alloy_serde::quantity::opt"
119        )
120    )]
121    pub slot_number: Option<u64>,
122}
123
124impl AnyHeader {
125    /// Seal the header with a known hash.
126    ///
127    /// WARNING: This method does not perform validation whether the hash is correct.
128    #[inline]
129    pub const fn seal(self, hash: B256) -> Sealed<Self> {
130        Sealed::new_unchecked(self, hash)
131    }
132
133    /// Attempts to convert this header into a `Header`.
134    ///
135    /// This can fail if the header is missing required fields:
136    /// - nonce
137    /// - mix_hash
138    ///
139    /// If the conversion fails, the original [`AnyHeader`] is returned.
140    pub fn try_into_header(self) -> Result<Header, ValueError<Self>> {
141        if self.nonce.is_none() {
142            return Err(ValueError::new(self, "missing nonce field"));
143        }
144        if self.mix_hash.is_none() {
145            return Err(ValueError::new(self, "missing mix hash field"));
146        }
147
148        let Self {
149            parent_hash,
150            ommers_hash,
151            beneficiary,
152            state_root,
153            transactions_root,
154            receipts_root,
155            logs_bloom,
156            difficulty,
157            number,
158            gas_limit,
159            gas_used,
160            timestamp,
161            extra_data,
162            mix_hash,
163            nonce,
164            base_fee_per_gas,
165            withdrawals_root,
166            blob_gas_used,
167            excess_blob_gas,
168            parent_beacon_block_root,
169            requests_hash,
170            block_access_list_hash,
171            slot_number,
172        } = self;
173
174        Ok(Header {
175            parent_hash,
176            ommers_hash,
177            beneficiary,
178            state_root,
179            transactions_root,
180            receipts_root,
181            logs_bloom,
182            difficulty,
183            number,
184            gas_limit,
185            gas_used,
186            timestamp,
187            extra_data,
188            mix_hash: mix_hash.unwrap(),
189            nonce: nonce.unwrap(),
190            base_fee_per_gas,
191            withdrawals_root,
192            blob_gas_used,
193            excess_blob_gas,
194            parent_beacon_block_root,
195            requests_hash,
196            block_access_list_hash,
197            slot_number,
198        })
199    }
200
201    /// Converts this header into a [`Header`] with default values for missing mandatory fields:
202    /// - mix_hash
203    /// - nonce
204    pub fn into_header_with_defaults(self) -> Header {
205        let Self {
206            parent_hash,
207            ommers_hash,
208            beneficiary,
209            state_root,
210            transactions_root,
211            receipts_root,
212            logs_bloom,
213            difficulty,
214            number,
215            gas_limit,
216            gas_used,
217            timestamp,
218            extra_data,
219            mix_hash,
220            nonce,
221            base_fee_per_gas,
222            withdrawals_root,
223            blob_gas_used,
224            excess_blob_gas,
225            parent_beacon_block_root,
226            requests_hash,
227            block_access_list_hash,
228            slot_number,
229        } = self;
230
231        Header {
232            parent_hash,
233            ommers_hash,
234            beneficiary,
235            state_root,
236            transactions_root,
237            receipts_root,
238            logs_bloom,
239            difficulty,
240            number,
241            gas_limit,
242            gas_used,
243            timestamp,
244            extra_data,
245            mix_hash: mix_hash.unwrap_or_default(),
246            nonce: nonce.unwrap_or_default(),
247            base_fee_per_gas,
248            withdrawals_root,
249            blob_gas_used,
250            excess_blob_gas,
251            parent_beacon_block_root,
252            requests_hash,
253            block_access_list_hash,
254            slot_number,
255        }
256    }
257}
258
259impl BlockHeader for AnyHeader {
260    fn parent_hash(&self) -> B256 {
261        self.parent_hash
262    }
263
264    fn ommers_hash(&self) -> B256 {
265        self.ommers_hash
266    }
267
268    fn beneficiary(&self) -> Address {
269        self.beneficiary
270    }
271
272    fn state_root(&self) -> B256 {
273        self.state_root
274    }
275
276    fn transactions_root(&self) -> B256 {
277        self.transactions_root
278    }
279
280    fn receipts_root(&self) -> B256 {
281        self.receipts_root
282    }
283
284    fn withdrawals_root(&self) -> Option<B256> {
285        self.withdrawals_root
286    }
287
288    fn logs_bloom(&self) -> Bloom {
289        self.logs_bloom
290    }
291
292    fn difficulty(&self) -> U256 {
293        self.difficulty
294    }
295
296    fn number(&self) -> BlockNumber {
297        self.number
298    }
299
300    fn gas_limit(&self) -> u64 {
301        self.gas_limit
302    }
303
304    fn gas_used(&self) -> u64 {
305        self.gas_used
306    }
307
308    fn timestamp(&self) -> u64 {
309        self.timestamp
310    }
311
312    fn mix_hash(&self) -> Option<B256> {
313        self.mix_hash
314    }
315
316    fn nonce(&self) -> Option<B64> {
317        self.nonce
318    }
319
320    fn base_fee_per_gas(&self) -> Option<u64> {
321        self.base_fee_per_gas
322    }
323
324    fn blob_gas_used(&self) -> Option<u64> {
325        self.blob_gas_used
326    }
327
328    fn excess_blob_gas(&self) -> Option<u64> {
329        self.excess_blob_gas
330    }
331
332    fn parent_beacon_block_root(&self) -> Option<B256> {
333        self.parent_beacon_block_root
334    }
335
336    fn requests_hash(&self) -> Option<B256> {
337        self.requests_hash
338    }
339
340    fn block_access_list_hash(&self) -> Option<B256> {
341        self.block_access_list_hash
342    }
343
344    fn slot_number(&self) -> Option<u64> {
345        self.slot_number
346    }
347
348    fn extra_data(&self) -> &Bytes {
349        &self.extra_data
350    }
351}
352
353impl From<Header> for AnyHeader {
354    fn from(value: Header) -> Self {
355        let Header {
356            parent_hash,
357            ommers_hash,
358            beneficiary,
359            state_root,
360            transactions_root,
361            receipts_root,
362            logs_bloom,
363            difficulty,
364            number,
365            gas_limit,
366            gas_used,
367            timestamp,
368            extra_data,
369            mix_hash,
370            nonce,
371            base_fee_per_gas,
372            withdrawals_root,
373            blob_gas_used,
374            excess_blob_gas,
375            parent_beacon_block_root,
376            requests_hash,
377            block_access_list_hash,
378            slot_number,
379        } = value;
380
381        Self {
382            parent_hash,
383            ommers_hash,
384            beneficiary,
385            state_root,
386            transactions_root,
387            receipts_root,
388            logs_bloom,
389            difficulty,
390            number,
391            gas_limit,
392            gas_used,
393            timestamp,
394            extra_data,
395            mix_hash: Some(mix_hash),
396            nonce: Some(nonce),
397            base_fee_per_gas,
398            withdrawals_root,
399            blob_gas_used,
400            excess_blob_gas,
401            parent_beacon_block_root,
402            requests_hash,
403            block_access_list_hash,
404            slot_number,
405        }
406    }
407}
408
409impl TryFrom<AnyHeader> for Header {
410    type Error = ValueError<AnyHeader>;
411
412    fn try_from(value: AnyHeader) -> Result<Self, Self::Error> {
413        value.try_into_header()
414    }
415}
416
417/// Saturating serde adapter for `base_fee_per_gas`.
418///
419/// Serializes identically to [`alloy_serde::quantity::opt`]. On deserialize, accepts any input
420/// that fits in `U256`; values above `u64::MAX` are clamped to `u64::MAX` instead of erroring.
421/// See [the field-level comment](AnyHeader::base_fee_per_gas) and issue #3741 for context.
422#[cfg(feature = "serde")]
423mod saturating_base_fee_per_gas {
424    use alloy_primitives::U256;
425    use serde::{Deserialize, Deserializer, Serializer};
426
427    pub(super) fn serialize<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
428    where
429        S: Serializer,
430    {
431        alloy_serde::quantity::opt::serialize(value, serializer)
432    }
433
434    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
435    where
436        D: Deserializer<'de>,
437    {
438        if deserializer.is_human_readable() {
439            let opt: Option<U256> = Option::deserialize(deserializer)?;
440            Ok(opt.map(|v| v.try_into().unwrap_or(u64::MAX)))
441        } else {
442            alloy_serde::quantity::opt::deserialize(deserializer)
443        }
444    }
445}
446
447/// Custom deserializer for `state_root` that treats `"0x"` or empty as `B256::ZERO`
448///
449/// This exists because some networks (like Tron) may serialize the state root as `"0x"`
450#[cfg(feature = "serde")]
451fn lenient_state_root<'de, D>(deserializer: D) -> Result<B256, D::Error>
452where
453    D: serde::de::Deserializer<'de>,
454{
455    use alloc::string::String;
456    use core::str::FromStr;
457    use serde::de::Error;
458
459    let s: String = serde::de::Deserialize::deserialize(deserializer)?;
460    let s = s.trim();
461
462    if s == "0x" || s.is_empty() {
463        return Ok(B256::ZERO);
464    }
465
466    B256::from_str(s).map_err(D::Error::custom)
467}
468
469#[cfg(test)]
470mod tests {
471
472    // <https://github.com/alloy-rs/alloy/issues/2494>
473    #[test]
474    #[cfg(feature = "serde")]
475    fn deserializes_tron_state_root_in_header() {
476        use super::*;
477        use alloy_primitives::B256;
478
479        let s = r#"{
480  "baseFeePerGas": "0x0",
481  "difficulty": "0x0",
482  "extraData": "0x",
483  "gasLimit": "0x160227b88",
484  "gasUsed": "0x360d92",
485  "hash": "0x00000000040a0687e0fc7194aabd024a4786ce94ad63855774f8d48896d8750b",
486  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
487  "miner": "0x9a96c8003a1e3a6866c08acff9f629e2a6ef062b",
488  "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
489  "nonce": "0x0000000000000000",
490  "number": "0x40a0687",
491  "parentHash": "0x00000000040a068652c581a982a0d17976201ad44aa28eb4e24881e82f99ee04",
492  "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
493  "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
494  "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
495  "size": "0xba05",
496  "stateRoot": "0x",
497  "timestamp": "0x6759f2f1",
498  "totalDifficulty": "0x0"
499}"#;
500
501        let header: AnyHeader = serde_json::from_str(s).unwrap();
502        assert_eq!(header.state_root, B256::ZERO);
503    }
504
505    // <https://github.com/alloy-rs/alloy/issues/3741>
506    #[test]
507    #[cfg(feature = "serde")]
508    fn deserializes_base_fee_within_u64() {
509        use super::*;
510
511        // Normal in-range hex baseFeePerGas — common case.
512        let s = r#"{
513  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
514  "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
515  "miner": "0x0000000000000000000000000000000000000000",
516  "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
517  "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
518  "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
519  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
520  "difficulty": "0x0",
521  "number": "0x1",
522  "gasLimit": "0x1c9c380",
523  "gasUsed": "0x5208",
524  "timestamp": "0x65",
525  "extraData": "0x",
526  "baseFeePerGas": "0x3b9aca00"
527}"#;
528        let header: AnyHeader = serde_json::from_str(s).unwrap();
529        assert_eq!(header.base_fee_per_gas, Some(1_000_000_000));
530    }
531
532    // Stable chain (chainId 988) returns baseFeePerGas = 10^21 for a range of
533    // historical blocks. With the prior `Option<u64>` deserializer this errored
534    // with "invalid type: integer 1000000000000000000000, expected u64".
535    // Saturate to u64::MAX instead.
536    // <https://github.com/alloy-rs/alloy/issues/3741>
537    #[test]
538    #[cfg(feature = "serde")]
539    fn deserializes_base_fee_saturates_above_u64() {
540        use super::*;
541
542        // baseFeePerGas = 0x3635c9adc5dea00000 = 10^21, well above u64::MAX.
543        let s = r#"{
544  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
545  "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
546  "miner": "0x0000000000000000000000000000000000000000",
547  "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
548  "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
549  "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
550  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
551  "difficulty": "0x0",
552  "number": "0x1",
553  "gasLimit": "0x1c9c380",
554  "gasUsed": "0x5208",
555  "timestamp": "0x65",
556  "extraData": "0x",
557  "baseFeePerGas": "0x3635c9adc5dea00000"
558}"#;
559        let header: AnyHeader = serde_json::from_str(s).unwrap();
560        assert_eq!(header.base_fee_per_gas, Some(u64::MAX));
561    }
562
563    // Exactly u64::MAX should round-trip without saturation.
564    #[test]
565    #[cfg(feature = "serde")]
566    fn deserializes_base_fee_exact_u64_max() {
567        use super::*;
568
569        // u64::MAX = 0xffffffffffffffff
570        let s = r#"{
571  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
572  "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
573  "miner": "0x0000000000000000000000000000000000000000",
574  "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
575  "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
576  "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
577  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
578  "difficulty": "0x0",
579  "number": "0x1",
580  "gasLimit": "0x1c9c380",
581  "gasUsed": "0x5208",
582  "timestamp": "0x65",
583  "extraData": "0x",
584  "baseFeePerGas": "0xffffffffffffffff"
585}"#;
586        let header: AnyHeader = serde_json::from_str(s).unwrap();
587        assert_eq!(header.base_fee_per_gas, Some(u64::MAX));
588    }
589
590    // The saturating JSON path must not change the binary serde shape inherited
591    // from `alloy_serde::quantity::opt`.
592    #[test]
593    #[cfg(feature = "serde")]
594    fn binary_roundtrip_preserves_base_fee() {
595        #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
596        struct BaseFee {
597            #[serde(with = "super::saturating_base_fee_per_gas")]
598            base_fee_per_gas: Option<u64>,
599        }
600
601        let header = BaseFee { base_fee_per_gas: Some(1_000_000_000) };
602        let encoded = bincode::serde::encode_to_vec(&header, bincode::config::legacy()).unwrap();
603        let (decoded, _) =
604            bincode::serde::decode_from_slice::<BaseFee, _>(&encoded, bincode::config::legacy())
605                .unwrap();
606
607        assert_eq!(decoded, header);
608    }
609
610    // Absent baseFeePerGas (pre-London) must still deserialize as None.
611    #[test]
612    #[cfg(feature = "serde")]
613    fn deserializes_base_fee_absent() {
614        use super::*;
615
616        let s = r#"{
617  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
618  "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
619  "miner": "0x0000000000000000000000000000000000000000",
620  "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
621  "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
622  "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
623  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
624  "difficulty": "0x0",
625  "number": "0x1",
626  "gasLimit": "0x1c9c380",
627  "gasUsed": "0x5208",
628  "timestamp": "0x65",
629  "extraData": "0x"
630}"#;
631        let header: AnyHeader = serde_json::from_str(s).unwrap();
632        assert_eq!(header.base_fee_per_gas, None);
633    }
634}