alloy_consensus_any/block/
header.rs

1use alloy_consensus::{BlockHeader, Header};
2use alloy_primitives::{Address, BlockNumber, Bloom, Bytes, 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    #[cfg_attr(
62        feature = "serde",
63        serde(
64            default,
65            skip_serializing_if = "Option::is_none",
66            with = "alloy_serde::quantity::opt"
67        )
68    )]
69    pub base_fee_per_gas: Option<u64>,
70    /// Withdrawals root hash added by EIP-4895 and is ignored in legacy headers.
71    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
72    pub withdrawals_root: Option<B256>,
73    /// Blob gas used
74    #[cfg_attr(
75        feature = "serde",
76        serde(
77            default,
78            skip_serializing_if = "Option::is_none",
79            with = "alloy_serde::quantity::opt"
80        )
81    )]
82    pub blob_gas_used: Option<u64>,
83    /// Excess blob gas
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 excess_blob_gas: Option<u64>,
93    /// EIP-4788 parent beacon block root
94    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
95    pub parent_beacon_block_root: Option<B256>,
96    /// EIP-7685 requests hash.
97    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
98    pub requests_hash: Option<B256>,
99}
100
101impl AnyHeader {
102    /// Attempts to convert this header into a `Header`.
103    ///
104    /// This can fail if the header is missing required fields:
105    /// - nonce
106    /// - mix_hash
107    ///
108    /// If the conversion fails, the original [`AnyHeader`] is returned.
109    pub fn try_into_header(self) -> Result<Header, Self> {
110        if self.nonce.is_none() || self.mix_hash.is_none() {
111            return Err(self);
112        }
113
114        let Self {
115            parent_hash,
116            ommers_hash,
117            beneficiary,
118            state_root,
119            transactions_root,
120            receipts_root,
121            logs_bloom,
122            difficulty,
123            number,
124            gas_limit,
125            gas_used,
126            timestamp,
127            extra_data,
128            mix_hash,
129            nonce,
130            base_fee_per_gas,
131            withdrawals_root,
132            blob_gas_used,
133            excess_blob_gas,
134            parent_beacon_block_root,
135            requests_hash,
136        } = self;
137
138        Ok(Header {
139            parent_hash,
140            ommers_hash,
141            beneficiary,
142            state_root,
143            transactions_root,
144            receipts_root,
145            logs_bloom,
146            difficulty,
147            number,
148            gas_limit,
149            gas_used,
150            timestamp,
151            extra_data,
152            mix_hash: mix_hash.unwrap(),
153            nonce: nonce.unwrap(),
154            base_fee_per_gas,
155            withdrawals_root,
156            blob_gas_used,
157            excess_blob_gas,
158            parent_beacon_block_root,
159            requests_hash,
160        })
161    }
162
163    /// Converts this header into a [`Header`] with default values for missing mandatory fields:
164    /// - mix_hash
165    /// - nonce
166    pub fn into_header_with_defaults(self) -> Header {
167        let Self {
168            parent_hash,
169            ommers_hash,
170            beneficiary,
171            state_root,
172            transactions_root,
173            receipts_root,
174            logs_bloom,
175            difficulty,
176            number,
177            gas_limit,
178            gas_used,
179            timestamp,
180            extra_data,
181            mix_hash,
182            nonce,
183            base_fee_per_gas,
184            withdrawals_root,
185            blob_gas_used,
186            excess_blob_gas,
187            parent_beacon_block_root,
188            requests_hash,
189        } = self;
190
191        Header {
192            parent_hash,
193            ommers_hash,
194            beneficiary,
195            state_root,
196            transactions_root,
197            receipts_root,
198            logs_bloom,
199            difficulty,
200            number,
201            gas_limit,
202            gas_used,
203            timestamp,
204            extra_data,
205            mix_hash: mix_hash.unwrap_or_default(),
206            nonce: nonce.unwrap_or_default(),
207            base_fee_per_gas,
208            withdrawals_root,
209            blob_gas_used,
210            excess_blob_gas,
211            parent_beacon_block_root,
212            requests_hash,
213        }
214    }
215}
216
217impl BlockHeader for AnyHeader {
218    fn parent_hash(&self) -> B256 {
219        self.parent_hash
220    }
221
222    fn ommers_hash(&self) -> B256 {
223        self.ommers_hash
224    }
225
226    fn beneficiary(&self) -> Address {
227        self.beneficiary
228    }
229
230    fn state_root(&self) -> B256 {
231        self.state_root
232    }
233
234    fn transactions_root(&self) -> B256 {
235        self.transactions_root
236    }
237
238    fn receipts_root(&self) -> B256 {
239        self.receipts_root
240    }
241
242    fn withdrawals_root(&self) -> Option<B256> {
243        self.withdrawals_root
244    }
245
246    fn logs_bloom(&self) -> Bloom {
247        self.logs_bloom
248    }
249
250    fn difficulty(&self) -> U256 {
251        self.difficulty
252    }
253
254    fn number(&self) -> BlockNumber {
255        self.number
256    }
257
258    fn gas_limit(&self) -> u64 {
259        self.gas_limit
260    }
261
262    fn gas_used(&self) -> u64 {
263        self.gas_used
264    }
265
266    fn timestamp(&self) -> u64 {
267        self.timestamp
268    }
269
270    fn mix_hash(&self) -> Option<B256> {
271        self.mix_hash
272    }
273
274    fn nonce(&self) -> Option<B64> {
275        self.nonce
276    }
277
278    fn base_fee_per_gas(&self) -> Option<u64> {
279        self.base_fee_per_gas
280    }
281
282    fn blob_gas_used(&self) -> Option<u64> {
283        self.blob_gas_used
284    }
285
286    fn excess_blob_gas(&self) -> Option<u64> {
287        self.excess_blob_gas
288    }
289
290    fn parent_beacon_block_root(&self) -> Option<B256> {
291        self.parent_beacon_block_root
292    }
293
294    fn requests_hash(&self) -> Option<B256> {
295        self.requests_hash
296    }
297
298    fn extra_data(&self) -> &Bytes {
299        &self.extra_data
300    }
301}
302
303impl From<Header> for AnyHeader {
304    fn from(value: Header) -> Self {
305        let Header {
306            parent_hash,
307            ommers_hash,
308            beneficiary,
309            state_root,
310            transactions_root,
311            receipts_root,
312            logs_bloom,
313            difficulty,
314            number,
315            gas_limit,
316            gas_used,
317            timestamp,
318            extra_data,
319            mix_hash,
320            nonce,
321            base_fee_per_gas,
322            withdrawals_root,
323            blob_gas_used,
324            excess_blob_gas,
325            parent_beacon_block_root,
326            requests_hash,
327        } = value;
328
329        Self {
330            parent_hash,
331            ommers_hash,
332            beneficiary,
333            state_root,
334            transactions_root,
335            receipts_root,
336            logs_bloom,
337            difficulty,
338            number,
339            gas_limit,
340            gas_used,
341            timestamp,
342            extra_data,
343            mix_hash: Some(mix_hash),
344            nonce: Some(nonce),
345            base_fee_per_gas,
346            withdrawals_root,
347            blob_gas_used,
348            excess_blob_gas,
349            parent_beacon_block_root,
350            requests_hash,
351        }
352    }
353}
354
355impl TryFrom<AnyHeader> for Header {
356    type Error = AnyHeader;
357
358    fn try_from(value: AnyHeader) -> Result<Self, Self::Error> {
359        value.try_into_header()
360    }
361}
362
363/// Custom deserializer for `state_root` that treats `"0x"` or empty as `B256::ZERO`
364///
365/// This exists because some networks (like Tron) may serialize the state root as `"0x"`
366#[cfg(feature = "serde")]
367fn lenient_state_root<'de, D>(deserializer: D) -> Result<B256, D::Error>
368where
369    D: serde::de::Deserializer<'de>,
370{
371    use alloc::string::String;
372    use core::str::FromStr;
373    use serde::de::Error;
374
375    let s: String = serde::de::Deserialize::deserialize(deserializer)?;
376    let s = s.trim();
377
378    if s == "0x" || s.is_empty() {
379        return Ok(B256::ZERO);
380    }
381
382    B256::from_str(s).map_err(D::Error::custom)
383}
384
385#[cfg(test)]
386mod tests {
387
388    // <https://github.com/alloy-rs/alloy/issues/2494>
389    #[test]
390    #[cfg(feature = "serde")]
391    fn deserializes_tron_state_root_in_header() {
392        use super::*;
393        use alloy_primitives::B256;
394
395        let s = r#"{
396  "baseFeePerGas": "0x0",
397  "difficulty": "0x0",
398  "extraData": "0x",
399  "gasLimit": "0x160227b88",
400  "gasUsed": "0x360d92",
401  "hash": "0x00000000040a0687e0fc7194aabd024a4786ce94ad63855774f8d48896d8750b",
402  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
403  "miner": "0x9a96c8003a1e3a6866c08acff9f629e2a6ef062b",
404  "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
405  "nonce": "0x0000000000000000",
406  "number": "0x40a0687",
407  "parentHash": "0x00000000040a068652c581a982a0d17976201ad44aa28eb4e24881e82f99ee04",
408  "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
409  "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
410  "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
411  "size": "0xba05",
412  "stateRoot": "0x",
413  "timestamp": "0x6759f2f1",
414  "totalDifficulty": "0x0"
415}"#;
416
417        let header: AnyHeader = serde_json::from_str(s).unwrap();
418        assert_eq!(header.state_root, B256::ZERO);
419    }
420}