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