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 = "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#[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 #[cfg_attr(
36 feature = "serde",
37 serde(with = "alloy_serde::quantity", rename = "mainBlockGeneralGasLimit")
38 )]
39 pub general_gas_limit: u64,
40
41 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
43 pub shared_gas_limit: u64,
44
45 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
47 pub timestamp_millis_part: u64,
48
49 #[cfg_attr(feature = "serde", serde(flatten))]
51 pub inner: Header,
52
53 #[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 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 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 let header = TempoHeader::default();
211 assert_eq!(header.timestamp_millis(), 0);
212
213 let header = TempoHeader {
215 timestamp_millis_part: 999,
216 ..Default::default()
217 };
218 assert_eq!(header.timestamp_millis(), 999);
219
220 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 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 let h1 = header.hash_slow();
307 let h2 = header.hash_slow();
308 assert_eq!(h1, h2);
309 assert_ne!(h1, B256::ZERO);
310
311 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}