Skip to main content

foundry_primitives/network/
header.rs

1use alloy_consensus::{BlockHeader, Header};
2use alloy_primitives::{Address, B64, B256, Bloom, Bytes, Sealable, U256};
3use alloy_rlp::{BufMut, Decodable, Encodable, Result};
4use std::ops::Deref;
5use tempo_primitives::TempoHeader;
6
7/// Consensus header used by Foundry's multi-network tooling.
8///
9/// The variant order is significant for untagged serde deserialization. [`Self::Tempo`] must stay
10/// first because Ethereum headers ignore unknown fields and would otherwise silently deserialize
11/// Tempo state dumps as [`Self::Ethereum`].
12#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
13#[serde(untagged)]
14#[allow(clippy::large_enum_variant)]
15pub enum FoundryHeader {
16    /// Tempo consensus header.
17    Tempo(TempoHeader),
18    /// Ethereum consensus header.
19    Ethereum(Header),
20}
21
22impl Default for FoundryHeader {
23    fn default() -> Self {
24        Self::Ethereum(Header::default())
25    }
26}
27
28impl FoundryHeader {
29    /// Creates a header for the selected network.
30    pub const fn new(inner: Header, is_tempo: bool) -> Self {
31        if is_tempo { Self::tempo(inner) } else { Self::Ethereum(inner) }
32    }
33
34    /// Creates a Tempo header from its Ethereum-shaped fields.
35    pub const fn tempo(inner: Header) -> Self {
36        Self::Tempo(TempoHeader {
37            general_gas_limit: inner.gas_limit,
38            shared_gas_limit: 0,
39            timestamp_millis_part: 0,
40            inner,
41            consensus_context: None,
42        })
43    }
44
45    /// Returns the Tempo header when this is a Tempo block.
46    pub const fn as_tempo(&self) -> Option<&TempoHeader> {
47        match self {
48            Self::Tempo(header) => Some(header),
49            Self::Ethereum(_) => None,
50        }
51    }
52
53    /// Returns the inner Ethereum-shaped header.
54    pub const fn inner(&self) -> &Header {
55        match self {
56            Self::Tempo(header) => &header.inner,
57            Self::Ethereum(header) => header,
58        }
59    }
60
61    const fn inner_mut(&mut self) -> &mut Header {
62        match self {
63            Self::Tempo(header) => &mut header.inner,
64            Self::Ethereum(header) => header,
65        }
66    }
67
68    /// Sets the transactions root shared by Ethereum and Tempo headers.
69    pub const fn set_transactions_root(&mut self, transactions_root: B256) {
70        self.inner_mut().transactions_root = transactions_root;
71    }
72
73    /// Sets the ommers root shared by Ethereum and Tempo headers.
74    pub const fn set_ommers_hash(&mut self, ommers_hash: B256) {
75        self.inner_mut().ommers_hash = ommers_hash;
76    }
77
78    /// Consumes the wrapper and returns the inner Ethereum-shaped header.
79    pub fn into_inner(self) -> Header {
80        match self {
81            Self::Tempo(header) => header.inner,
82            Self::Ethereum(header) => header,
83        }
84    }
85
86    /// Computes the canonical network header hash.
87    pub fn hash_slow(&self) -> B256 {
88        match self {
89            Self::Tempo(header) => header.hash_slow(),
90            Self::Ethereum(header) => header.hash_slow(),
91        }
92    }
93}
94
95impl From<Header> for FoundryHeader {
96    fn from(value: Header) -> Self {
97        Self::Ethereum(value)
98    }
99}
100
101impl From<TempoHeader> for FoundryHeader {
102    fn from(value: TempoHeader) -> Self {
103        Self::Tempo(value)
104    }
105}
106
107impl AsRef<Self> for FoundryHeader {
108    fn as_ref(&self) -> &Self {
109        self
110    }
111}
112
113impl Deref for FoundryHeader {
114    type Target = Header;
115
116    fn deref(&self) -> &Self::Target {
117        self.inner()
118    }
119}
120
121impl Encodable for FoundryHeader {
122    fn encode(&self, out: &mut dyn BufMut) {
123        match self {
124            Self::Tempo(header) => header.encode(out),
125            Self::Ethereum(header) => header.encode(out),
126        }
127    }
128
129    fn length(&self) -> usize {
130        match self {
131            Self::Tempo(header) => header.length(),
132            Self::Ethereum(header) => header.length(),
133        }
134    }
135}
136
137impl Decodable for FoundryHeader {
138    fn decode(buf: &mut &[u8]) -> Result<Self> {
139        // Tempo headers start with scalar gas-limit fields, while Ethereum headers start with a
140        // 32-byte parent hash, so trying Tempo first cannot misclassify a valid Ethereum header.
141        let mut tempo_buf = *buf;
142        if let Ok(header) = TempoHeader::decode(&mut tempo_buf) {
143            *buf = tempo_buf;
144            return Ok(Self::Tempo(header));
145        }
146
147        Header::decode(buf).map(Self::Ethereum)
148    }
149}
150
151impl Sealable for FoundryHeader {
152    fn hash_slow(&self) -> B256 {
153        Self::hash_slow(self)
154    }
155}
156
157macro_rules! delegate_header_methods {
158    ($($method:ident -> $return_type:ty),+ $(,)?) => {
159        $(
160            fn $method(&self) -> $return_type {
161                self.inner().$method()
162            }
163        )+
164    };
165}
166
167impl BlockHeader for FoundryHeader {
168    delegate_header_methods! {
169        parent_hash -> B256,
170        ommers_hash -> B256,
171        beneficiary -> Address,
172        state_root -> B256,
173        transactions_root -> B256,
174        receipts_root -> B256,
175        withdrawals_root -> Option<B256>,
176        logs_bloom -> Bloom,
177        difficulty -> U256,
178        number -> u64,
179        gas_limit -> u64,
180        gas_used -> u64,
181        timestamp -> u64,
182        mix_hash -> Option<B256>,
183        nonce -> Option<B64>,
184        base_fee_per_gas -> Option<u64>,
185        blob_gas_used -> Option<u64>,
186        excess_blob_gas -> Option<u64>,
187        parent_beacon_block_root -> Option<B256>,
188        requests_hash -> Option<B256>,
189        block_access_list_hash -> Option<B256>,
190        slot_number -> Option<u64>,
191    }
192
193    fn extra_data(&self) -> &Bytes {
194        self.inner().extra_data()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn rlp_roundtrip_preserves_network_header() {
204        for header in [
205            Header { number: 1, ..Default::default() }.into(),
206            FoundryHeader::tempo(Header { number: 2, gas_limit: 30_000_000, ..Default::default() }),
207        ] {
208            let encoded = alloy_rlp::encode(&header);
209            let decoded = FoundryHeader::decode(&mut encoded.as_ref()).unwrap();
210
211            assert_eq!(decoded, header);
212            assert_eq!(decoded.hash_slow(), header.hash_slow());
213            if let Some(tempo) = header.as_tempo() {
214                assert_eq!(header.hash_slow(), tempo.hash_slow());
215            }
216        }
217    }
218
219    #[test]
220    fn serde_roundtrip_preserves_tempo_fields() {
221        let header =
222            FoundryHeader::tempo(Header { number: 1, gas_limit: 30_000_000, ..Default::default() });
223        let value = serde_json::to_value(&header).unwrap();
224
225        assert_eq!(value["mainBlockGeneralGasLimit"], "0x1c9c380");
226        assert_eq!(serde_json::from_value::<FoundryHeader>(value).unwrap(), header);
227    }
228}