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#[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#[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 #[cfg_attr(
34 feature = "serde",
35 serde(with = "alloy_serde::quantity", rename = "mainBlockGeneralGasLimit")
36 )]
37 pub general_gas_limit: u64,
38
39 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
41 pub shared_gas_limit: u64,
42
43 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
45 pub timestamp_millis_part: u64,
46
47 #[cfg_attr(feature = "serde", serde(flatten))]
49 pub inner: Header,
50
51 #[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 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 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 let header = TempoHeader::default();
209 assert_eq!(header.timestamp_millis(), 0);
210
211 let header = TempoHeader {
213 timestamp_millis_part: 999,
214 ..Default::default()
215 };
216 assert_eq!(header.timestamp_millis(), 999);
217
218 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 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 let h1 = header.hash_slow();
305 let h2 = header.hash_slow();
306 assert_eq!(h1, h2);
307 assert_ne!(h1, B256::ZERO);
308
309 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}