Skip to main content

tempo_payload_types/
attrs.rs

1use alloy_primitives::{Address, B256, Bytes};
2use alloy_rpc_types_engine::PayloadId;
3use alloy_rpc_types_eth::Withdrawals;
4use reth_ethereum_engine_primitives::{EthPayloadAttributes, EthPayloadBuilderAttributes};
5use reth_node_api::{PayloadAttributes, PayloadBuilderAttributes};
6use serde::{Deserialize, Serialize};
7use std::{
8    convert::Infallible,
9    sync::{Arc, atomic, atomic::Ordering},
10};
11use tempo_primitives::RecoveredSubBlock;
12
13/// A handle for a payload interrupt flag.
14///
15/// Can be fired using [`InterruptHandle::interrupt`].
16#[derive(Debug, Clone, Default)]
17pub struct InterruptHandle(Arc<atomic::AtomicBool>);
18
19impl InterruptHandle {
20    /// Turns on the interrupt flag on the associated payload.
21    pub fn interrupt(&self) {
22        self.0.store(true, Ordering::Relaxed);
23    }
24
25    /// Returns whether the interrupt flag is set.
26    pub fn is_interrupted(&self) -> bool {
27        self.0.load(Ordering::Relaxed)
28    }
29}
30
31/// Container type for all components required to build a payload.
32///
33/// The `TempoPayloadBuilderAttributes` has an additional feature of interrupting payload.
34///
35/// It also carries DKG data to be included in the block's extra_data field.
36#[derive(derive_more::Debug, Clone)]
37pub struct TempoPayloadBuilderAttributes {
38    inner: EthPayloadBuilderAttributes,
39    interrupt: InterruptHandle,
40    timestamp_millis_part: u64,
41    /// DKG ceremony data to include in the block's extra_data header field.
42    ///
43    /// This is empty when no DKG data is available (e.g., when the DKG manager
44    /// hasn't produced ceremony outcomes yet, or when DKG operations fail).
45    extra_data: Bytes,
46    #[debug(skip)]
47    subblocks: Arc<dyn Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static>,
48}
49
50impl TempoPayloadBuilderAttributes {
51    /// Creates new `TempoPayloadBuilderAttributes` with `inner` attributes.
52    pub fn new(
53        id: PayloadId,
54        parent: B256,
55        suggested_fee_recipient: Address,
56        timestamp_millis: u64,
57        extra_data: Bytes,
58        subblocks: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
59    ) -> Self {
60        let (seconds, millis) = (timestamp_millis / 1000, timestamp_millis % 1000);
61        Self {
62            inner: EthPayloadBuilderAttributes {
63                id,
64                parent,
65                timestamp: seconds,
66                suggested_fee_recipient,
67                prev_randao: B256::ZERO,
68                withdrawals: Withdrawals::default(),
69                parent_beacon_block_root: Some(B256::ZERO),
70            },
71            interrupt: InterruptHandle::default(),
72            timestamp_millis_part: millis,
73            extra_data,
74            subblocks: Arc::new(subblocks),
75        }
76    }
77
78    /// Returns the extra data to be included in the block header.
79    pub fn extra_data(&self) -> &Bytes {
80        &self.extra_data
81    }
82
83    /// Returns the `interrupt` flag. If true, it marks that a payload is requested to stop
84    /// processing any more transactions.
85    pub fn is_interrupted(&self) -> bool {
86        self.interrupt.0.load(Ordering::Relaxed)
87    }
88
89    /// Returns a cloneable [`InterruptHandle`] for turning on the `interrupt` flag.
90    pub fn interrupt_handle(&self) -> &InterruptHandle {
91        &self.interrupt
92    }
93
94    /// Returns the milliseconds portion of the timestamp.
95    pub fn timestamp_millis_part(&self) -> u64 {
96        self.timestamp_millis_part
97    }
98
99    /// Returns the timestamp in milliseconds.
100    pub fn timestamp_millis(&self) -> u64 {
101        self.inner
102            .timestamp()
103            .saturating_mul(1000)
104            .saturating_add(self.timestamp_millis_part)
105    }
106
107    /// Returns the subblocks.
108    pub fn subblocks(&self) -> Vec<RecoveredSubBlock> {
109        (self.subblocks)()
110    }
111}
112
113// Required by reth's e2e-test-utils for integration tests.
114// The test utilities need to convert from standard Ethereum payload attributes
115// to custom chain-specific attributes.
116impl From<EthPayloadBuilderAttributes> for TempoPayloadBuilderAttributes {
117    fn from(inner: EthPayloadBuilderAttributes) -> Self {
118        Self {
119            inner,
120            interrupt: InterruptHandle::default(),
121            timestamp_millis_part: 0,
122            extra_data: Bytes::default(),
123            subblocks: Arc::new(Vec::new),
124        }
125    }
126}
127
128impl PayloadBuilderAttributes for TempoPayloadBuilderAttributes {
129    type RpcPayloadAttributes = TempoPayloadAttributes;
130    type Error = Infallible;
131
132    fn try_new(
133        parent: B256,
134        rpc_payload_attributes: Self::RpcPayloadAttributes,
135        version: u8,
136    ) -> Result<Self, Self::Error>
137    where
138        Self: Sized,
139    {
140        let TempoPayloadAttributes {
141            inner,
142            timestamp_millis_part,
143        } = rpc_payload_attributes;
144        Ok(Self {
145            inner: EthPayloadBuilderAttributes::try_new(parent, inner, version)?,
146            interrupt: InterruptHandle::default(),
147            timestamp_millis_part,
148            extra_data: Bytes::default(),
149            subblocks: Arc::new(Vec::new),
150        })
151    }
152
153    fn payload_id(&self) -> alloy_rpc_types_engine::payload::PayloadId {
154        self.inner.payload_id()
155    }
156
157    fn parent(&self) -> B256 {
158        self.inner.parent()
159    }
160
161    fn timestamp(&self) -> u64 {
162        self.inner.timestamp()
163    }
164
165    fn parent_beacon_block_root(&self) -> Option<B256> {
166        self.inner.parent_beacon_block_root()
167    }
168
169    fn suggested_fee_recipient(&self) -> Address {
170        self.inner.suggested_fee_recipient()
171    }
172
173    fn prev_randao(&self) -> B256 {
174        self.inner.prev_randao()
175    }
176
177    fn withdrawals(&self) -> &Withdrawals {
178        self.inner.withdrawals()
179    }
180}
181
182/// Tempo RPC payload attributes configuration.
183#[derive(Debug, Clone, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut)]
184#[serde(rename_all = "camelCase")]
185pub struct TempoPayloadAttributes {
186    /// Inner [`EthPayloadAttributes`].
187    #[serde(flatten)]
188    #[deref]
189    #[deref_mut]
190    pub inner: EthPayloadAttributes,
191
192    /// Milliseconds portion of the timestamp.
193    #[serde(with = "alloy_serde::quantity")]
194    pub timestamp_millis_part: u64,
195}
196
197impl PayloadAttributes for TempoPayloadAttributes {
198    fn timestamp(&self) -> u64 {
199        self.inner.timestamp()
200    }
201
202    fn withdrawals(&self) -> Option<&Vec<alloy_rpc_types_eth::Withdrawal>> {
203        self.inner.withdrawals()
204    }
205
206    fn parent_beacon_block_root(&self) -> Option<B256> {
207        self.inner.parent_beacon_block_root()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use alloy_rpc_types_eth::Withdrawal;
215
216    trait TestExt: Sized {
217        fn random() -> Self;
218        fn with_timestamp(self, millis: u64) -> Self;
219        fn with_subblocks(
220            self,
221            f: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
222        ) -> Self;
223    }
224
225    impl TestExt for TempoPayloadBuilderAttributes {
226        fn random() -> Self {
227            Self::new(
228                PayloadId::default(),
229                B256::random(),
230                Address::random(),
231                1000,
232                Bytes::default(),
233                Vec::new,
234            )
235        }
236
237        fn with_timestamp(mut self, millis: u64) -> Self {
238            self.inner.timestamp = millis / 1000;
239            self.timestamp_millis_part = millis % 1000;
240            self
241        }
242
243        fn with_subblocks(
244            mut self,
245            f: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
246        ) -> Self {
247            self.subblocks = Arc::new(f);
248            self
249        }
250    }
251
252    #[test]
253    fn test_interrupt_handle() {
254        // Default state
255        let handle = InterruptHandle::default();
256        assert!(!handle.is_interrupted());
257
258        // Interrupt sets flag
259        handle.interrupt();
260        assert!(handle.is_interrupted());
261
262        // Clone shares state
263        let handle2 = handle.clone();
264        assert!(handle2.is_interrupted());
265
266        // New handle via clone before interrupt
267        let fresh = InterruptHandle::default();
268        let cloned = fresh.clone();
269        assert!(!cloned.is_interrupted());
270        fresh.interrupt();
271        assert!(cloned.is_interrupted()); // shared atomic
272
273        // Multiple interrupts are idempotent
274        handle.interrupt();
275        handle.interrupt();
276        assert!(handle.is_interrupted());
277    }
278
279    #[test]
280    fn test_builder_attributes_construction() {
281        let parent = B256::random();
282        let id = PayloadId::new([1, 2, 3, 4, 5, 6, 7, 8]);
283        let recipient = Address::random();
284        let extra_data = Bytes::from(vec![1, 2, 3, 4, 5]);
285        let timestamp_millis = 1500; // 1s + 500ms
286
287        // With extra_data
288        let attrs = TempoPayloadBuilderAttributes::new(
289            id,
290            parent,
291            recipient,
292            timestamp_millis,
293            extra_data.clone(),
294            Vec::new,
295        );
296        assert_eq!(attrs.extra_data(), &extra_data);
297        assert_eq!(attrs.parent(), parent);
298        assert_eq!(attrs.suggested_fee_recipient(), recipient);
299        assert_eq!(attrs.payload_id(), id);
300        assert_eq!(attrs.timestamp(), 1);
301        assert_eq!(attrs.timestamp_millis_part(), 500);
302
303        // Hardcoded in ::new()
304        assert_eq!(attrs.prev_randao(), B256::ZERO);
305        assert_eq!(attrs.parent_beacon_block_root(), Some(B256::ZERO));
306        assert!(attrs.withdrawals().is_empty());
307
308        // Without extra_data
309        let attrs2 = TempoPayloadBuilderAttributes::new(
310            id,
311            parent,
312            recipient,
313            timestamp_millis + 500, // 1.5 seconds + 500ms
314            Bytes::default(),
315            Vec::new,
316        );
317        assert_eq!(attrs2.extra_data(), &Bytes::default());
318        assert_eq!(attrs2.timestamp(), 2);
319        assert_eq!(attrs2.timestamp_millis_part(), 0);
320    }
321
322    #[test]
323    fn test_builder_attributes_interrupt_integration() {
324        let attrs = TempoPayloadBuilderAttributes::random();
325
326        // Initially not interrupted
327        assert!(!attrs.is_interrupted());
328
329        // Get handle and interrupt
330        let handle = attrs.interrupt_handle().clone();
331        handle.interrupt();
332
333        // Both see interrupted state
334        assert!(attrs.is_interrupted());
335        assert!(handle.is_interrupted());
336
337        // Multiple handle accesses return same underlying state
338        let handle2 = attrs.interrupt_handle();
339        assert!(handle2.is_interrupted());
340    }
341
342    #[test]
343    fn test_builder_attributes_timestamp_handling() {
344        // Exact second boundary
345        let attrs = TempoPayloadBuilderAttributes::random().with_timestamp(3000);
346        assert_eq!(attrs.timestamp(), 3);
347        assert_eq!(attrs.timestamp_millis_part(), 0);
348        assert_eq!(attrs.timestamp_millis(), 3000);
349
350        // With milliseconds remainder
351        let attrs = TempoPayloadBuilderAttributes::random().with_timestamp(3999);
352        assert_eq!(attrs.timestamp(), 3);
353        assert_eq!(attrs.timestamp_millis_part(), 999);
354        assert_eq!(attrs.timestamp_millis(), 3999);
355
356        // Zero timestamp
357        let attrs = TempoPayloadBuilderAttributes::random().with_timestamp(0);
358        assert_eq!(attrs.timestamp(), 0);
359        assert_eq!(attrs.timestamp_millis_part(), 0);
360        assert_eq!(attrs.timestamp_millis(), 0);
361
362        // Large timestamp (no overflow due to saturating ops)
363        let large_ts = u64::MAX / 1000 * 1000;
364        let attrs = TempoPayloadBuilderAttributes::random().with_timestamp(large_ts + 500);
365        assert_eq!(attrs.timestamp_millis_part(), 500);
366        assert!(attrs.timestamp_millis() >= large_ts);
367    }
368
369    #[test]
370    fn test_builder_attributes_subblocks() {
371        use std::sync::atomic::AtomicUsize;
372
373        let call_count = Arc::new(AtomicUsize::new(0));
374        let count_clone = call_count.clone();
375
376        let attrs = TempoPayloadBuilderAttributes::random().with_subblocks(move || {
377            count_clone.fetch_add(1, Ordering::SeqCst);
378            Vec::new()
379        });
380
381        // Closure invoked each call
382        assert_eq!(call_count.load(Ordering::SeqCst), 0);
383        let _ = attrs.subblocks();
384        assert_eq!(call_count.load(Ordering::SeqCst), 1);
385        let _ = attrs.subblocks();
386        assert_eq!(call_count.load(Ordering::SeqCst), 2);
387    }
388
389    #[test]
390    fn test_from_eth_payload_builder_attributes() {
391        let eth_attrs = EthPayloadBuilderAttributes {
392            id: PayloadId::new([9, 8, 7, 6, 5, 4, 3, 2]),
393            parent: B256::random(),
394            timestamp: 1000,
395            suggested_fee_recipient: Address::random(),
396            prev_randao: B256::random(),
397            withdrawals: Withdrawals::new(vec![Withdrawal {
398                index: 1,
399                validator_index: 2,
400                address: Address::random(),
401                amount: 100,
402            }]),
403            parent_beacon_block_root: Some(B256::random()),
404        };
405
406        let tempo_attrs: TempoPayloadBuilderAttributes = eth_attrs.clone().into();
407
408        // Inner fields preserved
409        assert_eq!(tempo_attrs.payload_id(), eth_attrs.id);
410        assert_eq!(tempo_attrs.parent(), eth_attrs.parent);
411        assert_eq!(tempo_attrs.timestamp(), eth_attrs.timestamp);
412        assert_eq!(
413            tempo_attrs.suggested_fee_recipient(),
414            eth_attrs.suggested_fee_recipient
415        );
416        assert_eq!(tempo_attrs.prev_randao(), eth_attrs.prev_randao);
417        assert_eq!(tempo_attrs.withdrawals().len(), 1);
418        assert_eq!(
419            tempo_attrs.parent_beacon_block_root(),
420            eth_attrs.parent_beacon_block_root
421        );
422
423        // Tempo-specific defaults
424        assert_eq!(tempo_attrs.timestamp_millis_part(), 0);
425        assert_eq!(tempo_attrs.extra_data(), &Bytes::default());
426        assert!(!tempo_attrs.is_interrupted());
427        assert!(tempo_attrs.subblocks().is_empty());
428    }
429
430    #[test]
431    fn test_try_new_from_rpc_attributes() {
432        let rpc_attrs = TempoPayloadAttributes {
433            inner: EthPayloadAttributes {
434                timestamp: 100,
435                prev_randao: B256::random(),
436                suggested_fee_recipient: Address::random(),
437                withdrawals: Some(vec![]),
438                parent_beacon_block_root: Some(B256::random()),
439            },
440            timestamp_millis_part: 750,
441        };
442
443        let parent = B256::random();
444        let result = TempoPayloadBuilderAttributes::try_new(parent, rpc_attrs, 3);
445        assert!(result.is_ok());
446
447        let attrs = result.unwrap();
448        assert_eq!(attrs.parent(), parent);
449        assert_eq!(attrs.timestamp(), 100);
450        assert_eq!(attrs.timestamp_millis_part(), 750);
451        assert_eq!(attrs.timestamp_millis(), 100_750);
452        assert_eq!(attrs.extra_data(), &Bytes::default());
453        assert!(!attrs.is_interrupted());
454    }
455
456    #[test]
457    fn test_tempo_payload_attributes_serde() {
458        let timestamp = 1234567890;
459        let timestamp_millis_part = 999;
460        let attrs = TempoPayloadAttributes {
461            inner: EthPayloadAttributes {
462                timestamp,
463                prev_randao: B256::ZERO,
464                suggested_fee_recipient: Address::random(),
465                withdrawals: Some(vec![]),
466                parent_beacon_block_root: Some(B256::random()),
467            },
468            timestamp_millis_part,
469        };
470
471        // Roundtrip
472        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        // Deref works
480        assert_eq!(attrs.timestamp, timestamp);
481
482        // DerefMut works
483        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            },
506            timestamp_millis_part: 123,
507        };
508
509        // PayloadAttributes trait methods
510        assert_eq!(PayloadAttributes::timestamp(&attrs), 9999);
511        assert_eq!(attrs.withdrawals().unwrap().len(), 1);
512        assert_eq!(attrs.withdrawals().unwrap()[0].address, withdrawal_addr);
513        assert_eq!(attrs.parent_beacon_block_root(), Some(beacon_root));
514
515        // None cases
516        let attrs_none = TempoPayloadAttributes {
517            inner: EthPayloadAttributes {
518                timestamp: 1,
519                prev_randao: B256::ZERO,
520                suggested_fee_recipient: Address::random(),
521                withdrawals: None,
522                parent_beacon_block_root: None,
523            },
524            timestamp_millis_part: 0,
525        };
526        assert!(attrs_none.withdrawals().is_none());
527        assert!(attrs_none.parent_beacon_block_root().is_none());
528    }
529}