Skip to main content

tempo_primitives/
header.rs

1use alloy_consensus::{BlockHeader, Header, Sealable};
2use alloy_primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256, keccak256};
3use alloy_rlp::{RlpDecodable, RlpEncodable};
4
5use crate::ed25519::PublicKey;
6
7/// Consensus context metadata for a Tempo block.
8///
9/// The `proposer` is validated as a valid Ed25519 public key during RLP
10/// decoding to reject malformed blocks at the network boundary.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)]
12#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
15#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
16#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact))]
17pub struct TempoConsensusContext {
18    pub epoch: u64,
19    pub view: u64,
20    pub parent_view: u64,
21    pub proposer: PublicKey,
22}
23
24/// Tempo block header.
25///
26/// RLP-encoded as `[general_gas_limit, shared_gas_limit, timestamp_millis_part, inner,
27/// consensus_context?]`. The `consensus_context` is trailing and omitted for pre-fork blocks.
28#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, RlpEncodable, RlpDecodable)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
31#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
32#[rlp(trailing)]
33pub struct TempoHeader {
34    /// Non-payment gas limit for the block.
35    #[cfg_attr(
36        feature = "serde",
37        serde(with = "alloy_serde::quantity", rename = "mainBlockGeneralGasLimit")
38    )]
39    pub general_gas_limit: u64,
40
41    /// Shared gas limit allocated for the subblocks section of the block.
42    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
43    pub shared_gas_limit: u64,
44
45    /// Sub-second (milliseconds) portion of the timestamp.
46    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
47    pub timestamp_millis_part: u64,
48
49    /// Inner Ethereum [`Header`].
50    #[cfg_attr(feature = "serde", serde(flatten))]
51    pub inner: Header,
52
53    /// Consensus metadata for the block. `None` for pre-fork blocks.
54    #[cfg_attr(
55        feature = "serde",
56        serde(default, skip_serializing_if = "Option::is_none")
57    )]
58    pub consensus_context: Option<TempoConsensusContext>,
59}
60
61impl TempoHeader {
62    /// Returns the timestamp in milliseconds.
63    pub fn timestamp_millis(&self) -> u64 {
64        self.inner
65            .timestamp()
66            .saturating_mul(1000)
67            .saturating_add(self.timestamp_millis_part)
68    }
69}
70
71impl AsRef<Self> for TempoHeader {
72    fn as_ref(&self) -> &Self {
73        self
74    }
75}
76
77impl BlockHeader for TempoHeader {
78    fn parent_hash(&self) -> B256 {
79        self.inner.parent_hash()
80    }
81
82    fn ommers_hash(&self) -> B256 {
83        self.inner.ommers_hash()
84    }
85
86    fn beneficiary(&self) -> Address {
87        self.inner.beneficiary()
88    }
89
90    fn state_root(&self) -> B256 {
91        self.inner.state_root()
92    }
93
94    fn transactions_root(&self) -> B256 {
95        self.inner.transactions_root()
96    }
97
98    fn receipts_root(&self) -> B256 {
99        self.inner.receipts_root()
100    }
101
102    fn withdrawals_root(&self) -> Option<B256> {
103        self.inner.withdrawals_root()
104    }
105
106    fn logs_bloom(&self) -> Bloom {
107        self.inner.logs_bloom()
108    }
109
110    fn difficulty(&self) -> U256 {
111        self.inner.difficulty()
112    }
113
114    fn number(&self) -> BlockNumber {
115        self.inner.number()
116    }
117
118    fn gas_limit(&self) -> u64 {
119        self.inner.gas_limit()
120    }
121
122    fn gas_used(&self) -> u64 {
123        self.inner.gas_used()
124    }
125
126    fn timestamp(&self) -> u64 {
127        self.inner.timestamp()
128    }
129
130    fn mix_hash(&self) -> Option<B256> {
131        self.inner.mix_hash()
132    }
133
134    fn nonce(&self) -> Option<B64> {
135        self.inner.nonce()
136    }
137
138    fn base_fee_per_gas(&self) -> Option<u64> {
139        self.inner.base_fee_per_gas()
140    }
141
142    fn blob_gas_used(&self) -> Option<u64> {
143        self.inner.blob_gas_used()
144    }
145
146    fn excess_blob_gas(&self) -> Option<u64> {
147        self.inner.excess_blob_gas()
148    }
149
150    fn parent_beacon_block_root(&self) -> Option<B256> {
151        self.inner.parent_beacon_block_root()
152    }
153
154    fn requests_hash(&self) -> Option<B256> {
155        self.inner.requests_hash()
156    }
157
158    fn block_access_list_hash(&self) -> Option<B256> {
159        self.inner.block_access_list_hash()
160    }
161
162    fn slot_number(&self) -> Option<u64> {
163        self.inner.slot_number()
164    }
165
166    fn extra_data(&self) -> &Bytes {
167        self.inner.extra_data()
168    }
169}
170
171impl Sealable for TempoHeader {
172    fn hash_slow(&self) -> B256 {
173        keccak256(alloy_rlp::encode(self))
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use alloy_rlp::Decodable as _;
181
182    #[test]
183    fn consensus_context_rlp_roundtrip() {
184        let ctx = TempoConsensusContext {
185            epoch: 1,
186            view: 5,
187            proposer: PublicKey::from_seed([0xab; 32]),
188            parent_view: 4,
189        };
190
191        let encoded = alloy_rlp::encode(ctx);
192        let decoded = TempoConsensusContext::decode(&mut encoded.as_slice()).unwrap();
193        assert_eq!(ctx, decoded);
194    }
195
196    #[test]
197    fn timestamp_millis_variations() {
198        // basic: 100s + 500ms = 100_500
199        let header = TempoHeader {
200            timestamp_millis_part: 500,
201            inner: Header {
202                timestamp: 100,
203                ..Default::default()
204            },
205            ..Default::default()
206        };
207        assert_eq!(header.timestamp_millis(), 100_500);
208
209        // zero timestamp
210        let header = TempoHeader::default();
211        assert_eq!(header.timestamp_millis(), 0);
212
213        // millis part only (timestamp=0)
214        let header = TempoHeader {
215            timestamp_millis_part: 999,
216            ..Default::default()
217        };
218        assert_eq!(header.timestamp_millis(), 999);
219
220        // large timestamp saturating_mul safety
221        let header = TempoHeader {
222            timestamp_millis_part: 999,
223            inner: Header {
224                timestamp: u64::MAX / 1000,
225                ..Default::default()
226            },
227            ..Default::default()
228        };
229        let result = header.timestamp_millis();
230        assert!(result > 0);
231    }
232
233    #[test]
234    fn header_block_header_delegation() {
235        let inner = Header {
236            number: 42,
237            gas_limit: 30_000_000,
238            gas_used: 21_000,
239            timestamp: 1_700_000_000,
240            base_fee_per_gas: Some(1_000_000_000),
241            ..Default::default()
242        };
243        let header = TempoHeader {
244            inner: inner.clone(),
245            ..Default::default()
246        };
247
248        assert_eq!(BlockHeader::number(&header), 42);
249        assert_eq!(BlockHeader::gas_limit(&header), 30_000_000);
250        assert_eq!(BlockHeader::gas_used(&header), 21_000);
251        assert_eq!(BlockHeader::timestamp(&header), 1_700_000_000);
252        assert_eq!(BlockHeader::base_fee_per_gas(&header), Some(1_000_000_000));
253        assert_eq!(BlockHeader::parent_hash(&header), inner.parent_hash());
254        assert_eq!(BlockHeader::state_root(&header), inner.state_root());
255        assert_eq!(BlockHeader::difficulty(&header), inner.difficulty());
256    }
257
258    #[test]
259    fn header_rlp_roundtrip() {
260        let header = TempoHeader {
261            general_gas_limit: 15_000_000,
262            shared_gas_limit: 5_000_000,
263            timestamp_millis_part: 123,
264            inner: Header {
265                number: 1,
266                timestamp: 100,
267                ..Default::default()
268            },
269            consensus_context: Some(TempoConsensusContext {
270                epoch: 1,
271                view: 2,
272                parent_view: 1,
273                proposer: PublicKey::from_seed([0x01; 32]),
274            }),
275        };
276
277        let encoded = alloy_rlp::encode(&header);
278        let decoded = TempoHeader::decode(&mut encoded.as_slice()).unwrap();
279        assert_eq!(header, decoded);
280
281        // without consensus_context
282        let header_no_ctx = TempoHeader {
283            general_gas_limit: 10_000_000,
284            shared_gas_limit: 3_000_000,
285            timestamp_millis_part: 0,
286            inner: Header::default(),
287            consensus_context: None,
288        };
289        let encoded = alloy_rlp::encode(&header_no_ctx);
290        let decoded = TempoHeader::decode(&mut encoded.as_slice()).unwrap();
291        assert_eq!(header_no_ctx, decoded);
292    }
293
294    #[test]
295    fn header_sealable_hash() {
296        let header = TempoHeader {
297            general_gas_limit: 1,
298            inner: Header {
299                number: 42,
300                ..Default::default()
301            },
302            ..Default::default()
303        };
304
305        // deterministic
306        let h1 = header.hash_slow();
307        let h2 = header.hash_slow();
308        assert_eq!(h1, h2);
309        assert_ne!(h1, B256::ZERO);
310
311        // different header → different hash
312        let header2 = TempoHeader {
313            general_gas_limit: 2,
314            inner: Header {
315                number: 42,
316                ..Default::default()
317            },
318            ..Default::default()
319        };
320        assert_ne!(header.hash_slow(), header2.hash_slow());
321    }
322}