1use alloy_primitives::{Address, B256, Bytes, Keccak256};
2use alloy_rpc_types_engine::PayloadId;
3use alloy_rpc_types_eth::Withdrawal;
4use reth_ethereum_engine_primitives::EthPayloadAttributes;
5use reth_node_api::PayloadAttributes;
6use serde::{Deserialize, Serialize};
7use std::sync::{Arc, atomic, atomic::Ordering};
8use tempo_primitives::{RecoveredSubBlock, TempoConsensusContext};
9
10#[derive(Debug, Clone, Default)]
14pub struct InterruptHandle(Arc<atomic::AtomicBool>);
15
16impl InterruptHandle {
17 pub fn interrupt(&self) {
19 self.0.store(true, Ordering::Relaxed);
20 }
21
22 pub fn is_interrupted(&self) -> bool {
24 self.0.load(Ordering::Relaxed)
25 }
26}
27
28#[derive(
34 derive_more::Debug, Clone, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut,
35)]
36#[serde(rename_all = "camelCase")]
37pub struct TempoPayloadAttributes {
38 #[deref]
40 #[deref_mut]
41 #[serde(flatten)]
42 inner: EthPayloadAttributes,
43 #[serde(skip)]
45 interrupt: InterruptHandle,
46 timestamp_millis_part: u64,
48 extra_data: Bytes,
53 proposer_public_key: Option<B256>,
57 consensus_context: Option<TempoConsensusContext>,
59 #[debug(skip)]
61 #[serde(skip, default = "default_subblocks")]
62 subblocks: Arc<dyn Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static>,
63}
64
65impl Default for TempoPayloadAttributes {
66 fn default() -> Self {
67 Self::from(EthPayloadAttributes::default())
68 }
69}
70
71impl TempoPayloadAttributes {
72 pub fn new(
78 proposer_public_key: Option<B256>,
79 timestamp: u64,
80 timestamp_millis_part: u64,
81 extra_data: Bytes,
82 consensus_context: Option<TempoConsensusContext>,
83 subblocks: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
84 ) -> Self {
85 Self {
86 inner: EthPayloadAttributes {
87 timestamp,
88 suggested_fee_recipient: Address::ZERO,
89 prev_randao: B256::ZERO,
90 withdrawals: Some(Default::default()),
91 parent_beacon_block_root: Some(B256::ZERO),
92 slot_number: None,
93 },
94 interrupt: InterruptHandle::default(),
95 timestamp_millis_part,
96 extra_data,
97 proposer_public_key,
98 consensus_context,
99 subblocks: Arc::new(subblocks),
100 }
101 }
102
103 pub fn extra_data(&self) -> &Bytes {
105 &self.extra_data
106 }
107
108 pub fn proposer_public_key(&self) -> Option<&B256> {
110 self.proposer_public_key.as_ref()
111 }
112
113 pub fn is_interrupted(&self) -> bool {
116 self.interrupt.0.load(Ordering::Relaxed)
117 }
118
119 pub fn interrupt_handle(&self) -> &InterruptHandle {
121 &self.interrupt
122 }
123
124 pub fn timestamp_millis_part(&self) -> u64 {
126 self.timestamp_millis_part
127 }
128
129 pub fn timestamp_millis(&self) -> u64 {
131 self.inner
132 .timestamp()
133 .saturating_mul(1000)
134 .saturating_add(self.timestamp_millis_part)
135 }
136
137 pub fn consensus_context(&self) -> Option<TempoConsensusContext> {
139 self.consensus_context
140 }
141
142 pub fn subblocks(&self) -> Vec<RecoveredSubBlock> {
144 (self.subblocks)()
145 }
146}
147
148impl From<EthPayloadAttributes> for TempoPayloadAttributes {
152 fn from(inner: EthPayloadAttributes) -> Self {
153 Self {
154 inner,
155 interrupt: InterruptHandle::default(),
156 timestamp_millis_part: 0,
157 extra_data: Bytes::default(),
158 proposer_public_key: None,
159 consensus_context: None,
160 subblocks: Arc::new(Vec::new),
161 }
162 }
163}
164
165impl PayloadAttributes for TempoPayloadAttributes {
166 fn payload_id(&self, parent_hash: &B256) -> PayloadId {
167 payload_id_from_parent_and_context(parent_hash, self.consensus_context.as_ref())
179 }
180
181 fn timestamp(&self) -> u64 {
182 self.inner.timestamp()
183 }
184
185 fn parent_beacon_block_root(&self) -> Option<B256> {
186 self.inner.parent_beacon_block_root()
187 }
188
189 fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
190 self.inner.withdrawals()
191 }
192
193 fn slot_number(&self) -> Option<u64> {
194 self.inner.slot_number()
195 }
196}
197
198fn payload_id_from_block_hash(block_hash: &B256) -> PayloadId {
200 PayloadId::new(
201 <[u8; 8]>::try_from(&block_hash[0..8])
202 .expect("a 32 byte array always has more than 8 bytes"),
203 )
204}
205
206fn payload_id_from_parent_and_context(
214 parent_hash: &B256,
215 consensus_context: Option<&TempoConsensusContext>,
216) -> PayloadId {
217 let Some(ctx) = consensus_context else {
218 return payload_id_from_block_hash(parent_hash);
219 };
220
221 let mut hasher = Keccak256::new();
222 hasher.update(parent_hash);
223 hasher.update(ctx.epoch.to_be_bytes());
224 hasher.update(ctx.view.to_be_bytes());
225 hasher.update(ctx.parent_view.to_be_bytes());
226 hasher.update(B256::from(&ctx.proposer));
227 let digest = hasher.finalize();
228
229 PayloadId::new(
230 <[u8; 8]>::try_from(&digest[0..8]).expect("a 32 byte array always has more than 8 bytes"),
231 )
232}
233
234fn default_subblocks() -> Arc<dyn Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static> {
235 Arc::new(Vec::new)
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use alloy_rpc_types_eth::Withdrawal;
242 use tempo_primitives::ed25519::PublicKey;
243
244 trait TestExt: Sized {
245 fn random() -> Self;
246 fn with_timestamp(self, millis: u64) -> Self;
247 fn with_subblocks(
248 self,
249 f: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
250 ) -> Self;
251 }
252
253 impl TestExt for TempoPayloadAttributes {
254 fn random() -> Self {
255 Self::new(
256 None,
257 1, 0,
259 Bytes::default(),
260 None,
261 Vec::new,
262 )
263 }
264
265 fn with_timestamp(mut self, millis: u64) -> Self {
266 self.inner.timestamp = millis / 1000;
267 self.timestamp_millis_part = millis % 1000;
268 self
269 }
270
271 fn with_subblocks(
272 mut self,
273 f: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
274 ) -> Self {
275 self.subblocks = Arc::new(f);
276 self
277 }
278 }
279
280 #[test]
281 fn test_interrupt_handle() {
282 let handle = InterruptHandle::default();
284 assert!(!handle.is_interrupted());
285
286 handle.interrupt();
288 assert!(handle.is_interrupted());
289
290 let handle2 = handle.clone();
292 assert!(handle2.is_interrupted());
293
294 let fresh = InterruptHandle::default();
296 let cloned = fresh.clone();
297 assert!(!cloned.is_interrupted());
298 fresh.interrupt();
299 assert!(cloned.is_interrupted()); handle.interrupt();
303 handle.interrupt();
304 assert!(handle.is_interrupted());
305 }
306
307 #[test]
308 fn test_builder_attributes_construction() {
309 let parent = B256::random();
310 let extra_data = Bytes::from(vec![1, 2, 3, 4, 5]);
311
312 let attrs = TempoPayloadAttributes::new(
314 None,
315 1,
316 500, extra_data.clone(),
318 None,
319 Vec::new,
320 );
321 assert_eq!(attrs.extra_data(), &extra_data);
322 assert_eq!(attrs.suggested_fee_recipient, Address::ZERO);
323 assert_eq!(
324 attrs.payload_id(&parent),
325 payload_id_from_block_hash(&parent)
326 );
327 assert_eq!(attrs.timestamp(), 1);
328 assert_eq!(attrs.timestamp_millis_part(), 500);
329
330 assert_eq!(attrs.prev_randao, B256::ZERO);
332 assert_eq!(attrs.parent_beacon_block_root(), Some(B256::ZERO));
333 assert!(attrs.withdrawals().is_some_and(|w| w.is_empty()));
334
335 let attrs2 = TempoPayloadAttributes::new(
337 None,
338 2, 0,
340 Bytes::default(),
341 None,
342 Vec::new,
343 );
344 assert_eq!(attrs2.extra_data(), &Bytes::default());
345 assert_eq!(attrs2.timestamp(), 2);
346 assert_eq!(attrs2.timestamp_millis_part(), 0);
347 }
348
349 #[test]
350 fn test_builder_attributes_interrupt_integration() {
351 let attrs = TempoPayloadAttributes::random();
352
353 assert!(!attrs.is_interrupted());
355
356 let handle = attrs.interrupt_handle().clone();
358 handle.interrupt();
359
360 assert!(attrs.is_interrupted());
362 assert!(handle.is_interrupted());
363
364 let handle2 = attrs.interrupt_handle();
366 assert!(handle2.is_interrupted());
367 }
368
369 #[test]
370 fn test_builder_attributes_timestamp_handling() {
371 let attrs = TempoPayloadAttributes::random().with_timestamp(3000);
373 assert_eq!(attrs.timestamp(), 3);
374 assert_eq!(attrs.timestamp_millis_part(), 0);
375 assert_eq!(attrs.timestamp_millis(), 3000);
376
377 let attrs = TempoPayloadAttributes::random().with_timestamp(3999);
379 assert_eq!(attrs.timestamp(), 3);
380 assert_eq!(attrs.timestamp_millis_part(), 999);
381 assert_eq!(attrs.timestamp_millis(), 3999);
382
383 let attrs = TempoPayloadAttributes::random().with_timestamp(0);
385 assert_eq!(attrs.timestamp(), 0);
386 assert_eq!(attrs.timestamp_millis_part(), 0);
387 assert_eq!(attrs.timestamp_millis(), 0);
388
389 let large_ts = u64::MAX / 1000 * 1000;
391 let attrs = TempoPayloadAttributes::random().with_timestamp(large_ts + 500);
392 assert_eq!(attrs.timestamp_millis_part(), 500);
393 assert!(attrs.timestamp_millis() >= large_ts);
394 }
395
396 #[test]
397 fn test_builder_attributes_subblocks() {
398 use std::sync::atomic::AtomicUsize;
399
400 let call_count = Arc::new(AtomicUsize::new(0));
401 let count_clone = call_count.clone();
402
403 let attrs = TempoPayloadAttributes::random().with_subblocks(move || {
404 count_clone.fetch_add(1, Ordering::SeqCst);
405 Vec::new()
406 });
407
408 assert_eq!(call_count.load(Ordering::SeqCst), 0);
410 let _ = attrs.subblocks();
411 assert_eq!(call_count.load(Ordering::SeqCst), 1);
412 let _ = attrs.subblocks();
413 assert_eq!(call_count.load(Ordering::SeqCst), 2);
414 }
415
416 #[test]
417 fn test_from_eth_payload_builder_attributes() {
418 let eth_attrs = EthPayloadAttributes {
419 timestamp: 1000,
420 suggested_fee_recipient: Address::random(),
421 prev_randao: B256::random(),
422 withdrawals: Some(Default::default()),
423 parent_beacon_block_root: Some(B256::random()),
424 slot_number: None,
425 };
426
427 let tempo_attrs: TempoPayloadAttributes = eth_attrs.clone().into();
428
429 let parent = B256::random();
431 assert_eq!(
432 tempo_attrs.payload_id(&parent),
433 payload_id_from_block_hash(&parent)
434 );
435 assert_eq!(tempo_attrs.timestamp(), eth_attrs.timestamp);
436 assert_eq!(
437 tempo_attrs.suggested_fee_recipient,
438 eth_attrs.suggested_fee_recipient
439 );
440 assert_eq!(tempo_attrs.prev_randao, eth_attrs.prev_randao);
441 assert_eq!(tempo_attrs.withdrawals().as_ref().map(|w| w.len()), Some(0));
442 assert_eq!(
443 tempo_attrs.parent_beacon_block_root(),
444 eth_attrs.parent_beacon_block_root
445 );
446
447 assert_eq!(tempo_attrs.timestamp_millis_part(), 0);
449 assert_eq!(tempo_attrs.extra_data(), &Bytes::default());
450 assert!(!tempo_attrs.is_interrupted());
451 assert!(tempo_attrs.subblocks().is_empty());
452 }
453
454 #[test]
455 fn test_tempo_payload_attributes_serde() {
456 let timestamp = 1234567890;
457 let timestamp_millis_part = 999;
458 let attrs = TempoPayloadAttributes {
459 inner: EthPayloadAttributes {
460 timestamp,
461 prev_randao: B256::ZERO,
462 suggested_fee_recipient: Address::random(),
463 withdrawals: Some(vec![]),
464 parent_beacon_block_root: Some(B256::random()),
465 slot_number: None,
466 },
467 timestamp_millis_part,
468 ..Default::default()
469 };
470
471 let json = serde_json::to_string(&attrs).unwrap();
473 assert!(json.contains("timestampMillisPart"));
474
475 let deserialized: TempoPayloadAttributes = serde_json::from_str(&json).unwrap();
476 assert_eq!(deserialized.inner.timestamp, timestamp);
477 assert_eq!(deserialized.timestamp_millis_part, timestamp_millis_part);
478
479 assert_eq!(attrs.timestamp, timestamp);
481
482 let mut attrs = attrs;
484 attrs.timestamp = 123;
485 assert_eq!(attrs.inner.timestamp, 123);
486 }
487
488 #[test]
489 fn test_tempo_payload_attributes_trait_impl() {
490 let withdrawal_addr = Address::random();
491 let beacon_root = B256::random();
492
493 let attrs = TempoPayloadAttributes {
494 inner: EthPayloadAttributes {
495 timestamp: 9999,
496 prev_randao: B256::ZERO,
497 suggested_fee_recipient: Address::random(),
498 withdrawals: Some(vec![Withdrawal {
499 index: 0,
500 validator_index: 1,
501 address: withdrawal_addr,
502 amount: 500,
503 }]),
504 parent_beacon_block_root: Some(beacon_root),
505 slot_number: None,
506 },
507 timestamp_millis_part: 123,
508 ..Default::default()
509 };
510
511 assert_eq!(PayloadAttributes::timestamp(&attrs), 9999);
513 assert_eq!(attrs.withdrawals().unwrap().len(), 1);
514 assert_eq!(attrs.withdrawals().unwrap()[0].address, withdrawal_addr);
515 assert_eq!(attrs.parent_beacon_block_root(), Some(beacon_root));
516
517 let attrs_none = TempoPayloadAttributes {
519 inner: EthPayloadAttributes {
520 timestamp: 1,
521 prev_randao: B256::ZERO,
522 suggested_fee_recipient: Address::random(),
523 withdrawals: None,
524 parent_beacon_block_root: None,
525 slot_number: None,
526 },
527 timestamp_millis_part: 0,
528 ..Default::default()
529 };
530 assert!(attrs_none.withdrawals().is_none());
531 assert!(attrs_none.parent_beacon_block_root().is_none());
532 }
533
534 #[test]
535 fn payload_id_includes_consensus_context() {
536 let parent = B256::random();
537 let proposer = PublicKey::from_seed([0xab; 32]);
538
539 let mk = |ctx: Option<TempoConsensusContext>| -> PayloadId {
540 let mut attrs = TempoPayloadAttributes::random();
541 attrs.consensus_context = ctx;
542 attrs.payload_id(&parent)
543 };
544
545 let no_ctx = mk(None);
546 let ctx_a = mk(Some(TempoConsensusContext {
547 epoch: 1,
548 view: 1,
549 parent_view: 0,
550 proposer,
551 }));
552 let ctx_b = mk(Some(TempoConsensusContext {
553 epoch: 1,
554 view: 2,
555 parent_view: 1,
556 proposer,
557 }));
558 let ctx_c = mk(Some(TempoConsensusContext {
559 epoch: 2,
560 view: 1,
561 parent_view: 0,
562 proposer,
563 }));
564 let ctx_d = mk(Some(TempoConsensusContext {
565 epoch: 1,
566 view: 1,
567 parent_view: 0,
568 proposer: PublicKey::from_seed([0xcd; 32]),
569 }));
570
571 assert_eq!(no_ctx, payload_id_from_block_hash(&parent));
573
574 let ids = [no_ctx, ctx_a, ctx_b, ctx_c, ctx_d];
577 for i in 0..ids.len() {
578 for j in (i + 1)..ids.len() {
579 assert_ne!(ids[i], ids[j], "payload ids {i} and {j} collide");
580 }
581 }
582
583 let ctx_a_again = mk(Some(TempoConsensusContext {
585 epoch: 1,
586 view: 1,
587 parent_view: 0,
588 proposer,
589 }));
590 assert_eq!(ctx_a, ctx_a_again);
591
592 let other_parent = B256::random();
594 let mut attrs = TempoPayloadAttributes::random();
595 attrs.consensus_context = Some(TempoConsensusContext {
596 epoch: 1,
597 view: 1,
598 parent_view: 0,
599 proposer,
600 });
601 assert_ne!(attrs.payload_id(&parent), attrs.payload_id(&other_parent));
602 }
603}