1use crate::ValidationLatencyEstimate;
2use alloy_primitives::{Address, B256, Bytes, Keccak256};
3use alloy_rpc_types_engine::PayloadId;
4use alloy_rpc_types_eth::Withdrawal;
5use reth_ethereum_engine_primitives::EthPayloadAttributes;
6use reth_node_api::PayloadAttributes;
7use serde::{Deserialize, Serialize};
8use std::{sync::Arc, time::Duration};
9use tempo_primitives::{RecoveredSubBlock, TempoConsensusContext};
10
11#[derive(
15 derive_more::Debug, Clone, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut,
16)]
17#[serde(rename_all = "camelCase")]
18pub struct TempoPayloadAttributes {
19 #[deref]
21 #[deref_mut]
22 #[serde(flatten)]
23 inner: EthPayloadAttributes,
24 #[serde(skip)]
30 payload_build_budget: Option<Duration>,
31 #[serde(skip)]
36 validation_latency_estimate: Option<ValidationLatencyEstimate>,
37 timestamp_millis_part: u64,
39 extra_data: Bytes,
44 proposer_public_key: Option<B256>,
48 consensus_context: Option<TempoConsensusContext>,
50 #[debug(skip)]
52 #[serde(skip, default = "default_subblocks")]
53 subblocks: Arc<dyn Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static>,
54}
55
56impl Default for TempoPayloadAttributes {
57 fn default() -> Self {
58 Self::from(EthPayloadAttributes::default())
59 }
60}
61
62impl TempoPayloadAttributes {
63 pub fn new(
69 proposer_public_key: Option<B256>,
70 timestamp: u64,
71 timestamp_millis_part: u64,
72 extra_data: Bytes,
73 consensus_context: Option<TempoConsensusContext>,
74 subblocks: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
75 ) -> Self {
76 Self {
77 inner: EthPayloadAttributes {
78 timestamp,
79 suggested_fee_recipient: Address::ZERO,
80 prev_randao: B256::ZERO,
81 withdrawals: Some(Default::default()),
82 parent_beacon_block_root: Some(B256::ZERO),
83 slot_number: None,
84 target_gas_limit: None,
85 },
86 payload_build_budget: None,
87 validation_latency_estimate: None,
88 timestamp_millis_part,
89 extra_data,
90 proposer_public_key,
91 consensus_context,
92 subblocks: Arc::new(subblocks),
93 }
94 }
95
96 pub fn extra_data(&self) -> &Bytes {
98 &self.extra_data
99 }
100
101 pub fn proposer_public_key(&self) -> Option<&B256> {
103 self.proposer_public_key.as_ref()
104 }
105
106 pub fn with_payload_build_budget(mut self, budget: Duration) -> Self {
112 self.payload_build_budget = Some(budget);
113 self
114 }
115
116 pub fn payload_build_budget(&self) -> Option<Duration> {
122 self.payload_build_budget
123 }
124
125 pub fn with_validation_latency_estimate(
127 mut self,
128 estimate: Option<ValidationLatencyEstimate>,
129 ) -> Self {
130 self.validation_latency_estimate = estimate;
131 self
132 }
133
134 pub fn validation_latency_estimate(&self) -> Option<ValidationLatencyEstimate> {
136 self.validation_latency_estimate
137 }
138
139 pub fn timestamp_millis_part(&self) -> u64 {
141 self.timestamp_millis_part
142 }
143
144 pub fn timestamp_millis(&self) -> u64 {
146 self.inner
147 .timestamp()
148 .saturating_mul(1000)
149 .saturating_add(self.timestamp_millis_part)
150 }
151
152 pub fn consensus_context(&self) -> Option<TempoConsensusContext> {
154 self.consensus_context
155 }
156
157 pub fn subblocks(&self) -> Vec<RecoveredSubBlock> {
159 (self.subblocks)()
160 }
161}
162
163impl From<EthPayloadAttributes> for TempoPayloadAttributes {
167 fn from(inner: EthPayloadAttributes) -> Self {
168 Self {
169 inner,
170 payload_build_budget: None,
171 validation_latency_estimate: None,
172 timestamp_millis_part: 0,
173 extra_data: Bytes::default(),
174 proposer_public_key: None,
175 consensus_context: None,
176 subblocks: Arc::new(Vec::new),
177 }
178 }
179}
180
181impl PayloadAttributes for TempoPayloadAttributes {
182 fn payload_id(&self, parent_hash: &B256) -> PayloadId {
183 payload_id_from_parent_and_context(parent_hash, self.consensus_context.as_ref())
195 }
196
197 fn timestamp(&self) -> u64 {
198 self.inner.timestamp()
199 }
200
201 fn parent_beacon_block_root(&self) -> Option<B256> {
202 self.inner.parent_beacon_block_root()
203 }
204
205 fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
206 self.inner.withdrawals()
207 }
208
209 fn slot_number(&self) -> Option<u64> {
210 self.inner.slot_number()
211 }
212}
213
214fn payload_id_from_block_hash(block_hash: &B256) -> PayloadId {
216 PayloadId::new(
217 <[u8; 8]>::try_from(&block_hash[0..8])
218 .expect("a 32 byte array always has more than 8 bytes"),
219 )
220}
221
222fn payload_id_from_parent_and_context(
230 parent_hash: &B256,
231 consensus_context: Option<&TempoConsensusContext>,
232) -> PayloadId {
233 let Some(ctx) = consensus_context else {
234 return payload_id_from_block_hash(parent_hash);
235 };
236
237 let mut hasher = Keccak256::new();
238 hasher.update(parent_hash);
239 hasher.update(ctx.epoch.to_be_bytes());
240 hasher.update(ctx.view.to_be_bytes());
241 hasher.update(ctx.parent_view.to_be_bytes());
242 hasher.update(B256::from(&ctx.proposer));
243 let digest = hasher.finalize();
244
245 PayloadId::new(
246 <[u8; 8]>::try_from(&digest[0..8]).expect("a 32 byte array always has more than 8 bytes"),
247 )
248}
249
250fn default_subblocks() -> Arc<dyn Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static> {
251 Arc::new(Vec::new)
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use alloy_rpc_types_eth::Withdrawal;
258 use tempo_primitives::ed25519::PublicKey;
259
260 trait TestExt: Sized {
261 fn random() -> Self;
262 fn with_timestamp(self, millis: u64) -> Self;
263 fn with_subblocks(
264 self,
265 f: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
266 ) -> Self;
267 }
268
269 impl TestExt for TempoPayloadAttributes {
270 fn random() -> Self {
271 Self::new(
272 None,
273 1, 0,
275 Bytes::default(),
276 None,
277 Vec::new,
278 )
279 }
280
281 fn with_timestamp(mut self, millis: u64) -> Self {
282 self.inner.timestamp = millis / 1000;
283 self.timestamp_millis_part = millis % 1000;
284 self
285 }
286
287 fn with_subblocks(
288 mut self,
289 f: impl Fn() -> Vec<RecoveredSubBlock> + Send + Sync + 'static,
290 ) -> Self {
291 self.subblocks = Arc::new(f);
292 self
293 }
294 }
295
296 #[test]
297 fn test_builder_attributes_construction() {
298 let parent = B256::random();
299 let extra_data = Bytes::from(vec![1, 2, 3, 4, 5]);
300
301 let attrs = TempoPayloadAttributes::new(
303 None,
304 1,
305 500, extra_data.clone(),
307 None,
308 Vec::new,
309 );
310 assert_eq!(attrs.extra_data(), &extra_data);
311 assert_eq!(attrs.suggested_fee_recipient, Address::ZERO);
312 assert_eq!(
313 attrs.payload_id(&parent),
314 payload_id_from_block_hash(&parent)
315 );
316 assert_eq!(attrs.timestamp(), 1);
317 assert_eq!(attrs.timestamp_millis_part(), 500);
318
319 assert_eq!(attrs.prev_randao, B256::ZERO);
321 assert_eq!(attrs.parent_beacon_block_root(), Some(B256::ZERO));
322 assert!(attrs.withdrawals().is_some_and(|w| w.is_empty()));
323
324 let attrs2 = TempoPayloadAttributes::new(
326 None,
327 2, 0,
329 Bytes::default(),
330 None,
331 Vec::new,
332 );
333 assert_eq!(attrs2.extra_data(), &Bytes::default());
334 assert_eq!(attrs2.timestamp(), 2);
335 assert_eq!(attrs2.timestamp_millis_part(), 0);
336 }
337
338 #[test]
339 fn test_builder_attributes_timestamp_handling() {
340 let attrs = TempoPayloadAttributes::random().with_timestamp(3000);
342 assert_eq!(attrs.timestamp(), 3);
343 assert_eq!(attrs.timestamp_millis_part(), 0);
344 assert_eq!(attrs.timestamp_millis(), 3000);
345
346 let attrs = TempoPayloadAttributes::random().with_timestamp(3999);
348 assert_eq!(attrs.timestamp(), 3);
349 assert_eq!(attrs.timestamp_millis_part(), 999);
350 assert_eq!(attrs.timestamp_millis(), 3999);
351
352 let attrs = TempoPayloadAttributes::random().with_timestamp(0);
354 assert_eq!(attrs.timestamp(), 0);
355 assert_eq!(attrs.timestamp_millis_part(), 0);
356 assert_eq!(attrs.timestamp_millis(), 0);
357
358 let large_ts = u64::MAX / 1000 * 1000;
360 let attrs = TempoPayloadAttributes::random().with_timestamp(large_ts + 500);
361 assert_eq!(attrs.timestamp_millis_part(), 500);
362 assert!(attrs.timestamp_millis() >= large_ts);
363 }
364
365 #[test]
366 fn test_builder_attributes_subblocks() {
367 use std::sync::atomic::{AtomicUsize, Ordering};
368
369 let call_count = Arc::new(AtomicUsize::new(0));
370 let count_clone = call_count.clone();
371
372 let attrs = TempoPayloadAttributes::random().with_subblocks(move || {
373 count_clone.fetch_add(1, Ordering::SeqCst);
374 Vec::new()
375 });
376
377 assert_eq!(call_count.load(Ordering::SeqCst), 0);
379 let _ = attrs.subblocks();
380 assert_eq!(call_count.load(Ordering::SeqCst), 1);
381 let _ = attrs.subblocks();
382 assert_eq!(call_count.load(Ordering::SeqCst), 2);
383 }
384
385 #[test]
386 fn test_from_eth_payload_builder_attributes() {
387 let eth_attrs = EthPayloadAttributes {
388 timestamp: 1000,
389 suggested_fee_recipient: Address::random(),
390 prev_randao: B256::random(),
391 withdrawals: Some(Default::default()),
392 parent_beacon_block_root: Some(B256::random()),
393 slot_number: None,
394 target_gas_limit: None,
395 };
396
397 let tempo_attrs: TempoPayloadAttributes = eth_attrs.clone().into();
398
399 let parent = B256::random();
401 assert_eq!(
402 tempo_attrs.payload_id(&parent),
403 payload_id_from_block_hash(&parent)
404 );
405 assert_eq!(tempo_attrs.timestamp(), eth_attrs.timestamp);
406 assert_eq!(
407 tempo_attrs.suggested_fee_recipient,
408 eth_attrs.suggested_fee_recipient
409 );
410 assert_eq!(tempo_attrs.prev_randao, eth_attrs.prev_randao);
411 assert_eq!(tempo_attrs.withdrawals().as_ref().map(|w| w.len()), Some(0));
412 assert_eq!(
413 tempo_attrs.parent_beacon_block_root(),
414 eth_attrs.parent_beacon_block_root
415 );
416
417 assert_eq!(tempo_attrs.timestamp_millis_part(), 0);
419 assert_eq!(tempo_attrs.extra_data(), &Bytes::default());
420 assert!(tempo_attrs.subblocks().is_empty());
421 }
422
423 #[test]
424 fn test_tempo_payload_attributes_serde() {
425 let timestamp = 1234567890;
426 let timestamp_millis_part = 999;
427 let attrs = TempoPayloadAttributes {
428 inner: EthPayloadAttributes {
429 timestamp,
430 prev_randao: B256::ZERO,
431 suggested_fee_recipient: Address::random(),
432 withdrawals: Some(vec![]),
433 parent_beacon_block_root: Some(B256::random()),
434 slot_number: None,
435 target_gas_limit: None,
436 },
437 timestamp_millis_part,
438 ..Default::default()
439 };
440
441 let json = serde_json::to_string(&attrs).unwrap();
443 assert!(json.contains("timestampMillisPart"));
444
445 let deserialized: TempoPayloadAttributes = serde_json::from_str(&json).unwrap();
446 assert_eq!(deserialized.inner.timestamp, timestamp);
447 assert_eq!(deserialized.timestamp_millis_part, timestamp_millis_part);
448
449 assert_eq!(attrs.timestamp, timestamp);
451
452 let mut attrs = attrs;
454 attrs.timestamp = 123;
455 assert_eq!(attrs.inner.timestamp, 123);
456 }
457
458 #[test]
459 fn test_tempo_payload_attributes_trait_impl() {
460 let withdrawal_addr = Address::random();
461 let beacon_root = B256::random();
462
463 let attrs = TempoPayloadAttributes {
464 inner: EthPayloadAttributes {
465 timestamp: 9999,
466 prev_randao: B256::ZERO,
467 suggested_fee_recipient: Address::random(),
468 withdrawals: Some(vec![Withdrawal {
469 index: 0,
470 validator_index: 1,
471 address: withdrawal_addr,
472 amount: 500,
473 }]),
474 parent_beacon_block_root: Some(beacon_root),
475 slot_number: None,
476 target_gas_limit: None,
477 },
478 timestamp_millis_part: 123,
479 ..Default::default()
480 };
481
482 assert_eq!(PayloadAttributes::timestamp(&attrs), 9999);
484 assert_eq!(attrs.withdrawals().unwrap().len(), 1);
485 assert_eq!(attrs.withdrawals().unwrap()[0].address, withdrawal_addr);
486 assert_eq!(attrs.parent_beacon_block_root(), Some(beacon_root));
487
488 let attrs_none = TempoPayloadAttributes {
490 inner: EthPayloadAttributes {
491 timestamp: 1,
492 prev_randao: B256::ZERO,
493 suggested_fee_recipient: Address::random(),
494 withdrawals: None,
495 parent_beacon_block_root: None,
496 slot_number: None,
497 target_gas_limit: None,
498 },
499 timestamp_millis_part: 0,
500 ..Default::default()
501 };
502 assert!(attrs_none.withdrawals().is_none());
503 assert!(attrs_none.parent_beacon_block_root().is_none());
504 }
505
506 #[test]
507 fn payload_id_includes_consensus_context() {
508 let parent = B256::random();
509 let proposer = PublicKey::from_seed([0xab; 32]);
510
511 let mk = |ctx: Option<TempoConsensusContext>| -> PayloadId {
512 let mut attrs = TempoPayloadAttributes::random();
513 attrs.consensus_context = ctx;
514 attrs.payload_id(&parent)
515 };
516
517 let no_ctx = mk(None);
518 let ctx_a = mk(Some(TempoConsensusContext {
519 epoch: 1,
520 view: 1,
521 parent_view: 0,
522 proposer,
523 }));
524 let ctx_b = mk(Some(TempoConsensusContext {
525 epoch: 1,
526 view: 2,
527 parent_view: 1,
528 proposer,
529 }));
530 let ctx_c = mk(Some(TempoConsensusContext {
531 epoch: 2,
532 view: 1,
533 parent_view: 0,
534 proposer,
535 }));
536 let ctx_d = mk(Some(TempoConsensusContext {
537 epoch: 1,
538 view: 1,
539 parent_view: 0,
540 proposer: PublicKey::from_seed([0xcd; 32]),
541 }));
542
543 assert_eq!(no_ctx, payload_id_from_block_hash(&parent));
545
546 let ids = [no_ctx, ctx_a, ctx_b, ctx_c, ctx_d];
549 for i in 0..ids.len() {
550 for j in (i + 1)..ids.len() {
551 assert_ne!(ids[i], ids[j], "payload ids {i} and {j} collide");
552 }
553 }
554
555 let ctx_a_again = mk(Some(TempoConsensusContext {
557 epoch: 1,
558 view: 1,
559 parent_view: 0,
560 proposer,
561 }));
562 assert_eq!(ctx_a, ctx_a_again);
563
564 let other_parent = B256::random();
566 let mut attrs = TempoPayloadAttributes::random();
567 attrs.consensus_context = Some(TempoConsensusContext {
568 epoch: 1,
569 view: 1,
570 parent_view: 0,
571 proposer,
572 });
573 assert_ne!(attrs.payload_id(&parent), attrs.payload_id(&other_parent));
574 }
575}