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