reth_basic_payload_builder/
lib.rs

1//! A basic payload generator for reth.
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
10
11use crate::metrics::PayloadBuilderMetrics;
12use alloy_eips::merge::SLOT_DURATION;
13use alloy_primitives::{B256, U256};
14use futures_core::ready;
15use futures_util::FutureExt;
16use reth_chain_state::CanonStateNotification;
17use reth_payload_builder::{KeepPayloadJobAlive, PayloadId, PayloadJob, PayloadJobGenerator};
18use reth_payload_builder_primitives::PayloadBuilderError;
19use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes, PayloadKind};
20use reth_primitives_traits::{HeaderTy, NodePrimitives, SealedHeader};
21use reth_revm::{cached::CachedReads, cancelled::CancelOnDrop};
22use reth_storage_api::{BlockReaderIdExt, StateProviderFactory};
23use reth_tasks::TaskSpawner;
24use std::{
25    fmt,
26    future::Future,
27    ops::Deref,
28    pin::Pin,
29    sync::Arc,
30    task::{Context, Poll},
31    time::{Duration, SystemTime, UNIX_EPOCH},
32};
33use tokio::{
34    sync::{oneshot, Semaphore},
35    time::{Interval, Sleep},
36};
37use tracing::{debug, trace, warn};
38
39mod better_payload_emitter;
40mod metrics;
41mod stack;
42
43pub use better_payload_emitter::BetterPayloadEmitter;
44pub use stack::PayloadBuilderStack;
45
46/// Helper to access [`NodePrimitives::BlockHeader`] from [`PayloadBuilder::BuiltPayload`].
47pub type HeaderForPayload<P> = <<P as BuiltPayload>::Primitives as NodePrimitives>::BlockHeader;
48
49/// The [`PayloadJobGenerator`] that creates [`BasicPayloadJob`]s.
50#[derive(Debug)]
51pub struct BasicPayloadJobGenerator<Client, Tasks, Builder> {
52    /// The client that can interact with the chain.
53    client: Client,
54    /// The task executor to spawn payload building tasks on.
55    executor: Tasks,
56    /// The configuration for the job generator.
57    config: BasicPayloadJobGeneratorConfig,
58    /// Restricts how many generator tasks can be executed at once.
59    payload_task_guard: PayloadTaskGuard,
60    /// The type responsible for building payloads.
61    ///
62    /// See [`PayloadBuilder`]
63    builder: Builder,
64    /// Stored `cached_reads` for new payload jobs.
65    pre_cached: Option<PrecachedState>,
66}
67
68// === impl BasicPayloadJobGenerator ===
69
70impl<Client, Tasks, Builder> BasicPayloadJobGenerator<Client, Tasks, Builder> {
71    /// Creates a new [`BasicPayloadJobGenerator`] with the given config and custom
72    /// [`PayloadBuilder`]
73    pub fn with_builder(
74        client: Client,
75        executor: Tasks,
76        config: BasicPayloadJobGeneratorConfig,
77        builder: Builder,
78    ) -> Self {
79        Self {
80            client,
81            executor,
82            payload_task_guard: PayloadTaskGuard::new(config.max_payload_tasks),
83            config,
84            builder,
85            pre_cached: None,
86        }
87    }
88
89    /// Returns the maximum duration a job should be allowed to run.
90    ///
91    /// This adheres to the following specification:
92    /// > Client software SHOULD stop the updating process when either a call to engine_getPayload
93    /// > with the build process's payloadId is made or SECONDS_PER_SLOT (12s in the Mainnet
94    /// > configuration) have passed since the point in time identified by the timestamp parameter.
95    ///
96    /// See also <https://github.com/ethereum/execution-apis/blob/431cf72fd3403d946ca3e3afc36b973fc87e0e89/src/engine/paris.md?plain=1#L137>
97    #[inline]
98    fn max_job_duration(&self, unix_timestamp: u64) -> Duration {
99        let duration_until_timestamp = duration_until(unix_timestamp);
100
101        // safety in case clocks are bad
102        let duration_until_timestamp = duration_until_timestamp.min(self.config.deadline * 3);
103
104        self.config.deadline + duration_until_timestamp
105    }
106
107    /// Returns the [Instant](tokio::time::Instant) at which the job should be terminated because it
108    /// is considered timed out.
109    #[inline]
110    fn job_deadline(&self, unix_timestamp: u64) -> tokio::time::Instant {
111        tokio::time::Instant::now() + self.max_job_duration(unix_timestamp)
112    }
113
114    /// Returns a reference to the tasks type
115    pub const fn tasks(&self) -> &Tasks {
116        &self.executor
117    }
118
119    /// Returns the pre-cached reads for the given parent header if it matches the cached state's
120    /// block.
121    fn maybe_pre_cached(&self, parent: B256) -> Option<CachedReads> {
122        self.pre_cached.as_ref().filter(|pc| pc.block == parent).map(|pc| pc.cached.clone())
123    }
124}
125
126// === impl BasicPayloadJobGenerator ===
127
128impl<Client, Tasks, Builder> PayloadJobGenerator
129    for BasicPayloadJobGenerator<Client, Tasks, Builder>
130where
131    Client: StateProviderFactory
132        + BlockReaderIdExt<Header = HeaderForPayload<Builder::BuiltPayload>>
133        + Clone
134        + Unpin
135        + 'static,
136    Tasks: TaskSpawner + Clone + Unpin + 'static,
137    Builder: PayloadBuilder + Unpin + 'static,
138    Builder::Attributes: Unpin + Clone,
139    Builder::BuiltPayload: Unpin + Clone,
140{
141    type Job = BasicPayloadJob<Tasks, Builder>;
142
143    fn new_payload_job(
144        &self,
145        attributes: <Self::Job as PayloadJob>::PayloadAttributes,
146    ) -> Result<Self::Job, PayloadBuilderError> {
147        let parent_header = if attributes.parent().is_zero() {
148            // Use latest header for genesis block case
149            self.client
150                .latest_header()
151                .map_err(PayloadBuilderError::from)?
152                .ok_or_else(|| PayloadBuilderError::MissingParentHeader(B256::ZERO))?
153        } else {
154            // Fetch specific header by hash
155            self.client
156                .sealed_header_by_hash(attributes.parent())
157                .map_err(PayloadBuilderError::from)?
158                .ok_or_else(|| PayloadBuilderError::MissingParentHeader(attributes.parent()))?
159        };
160
161        let config = PayloadConfig::new(Arc::new(parent_header.clone()), attributes);
162
163        let until = self.job_deadline(config.attributes.timestamp());
164        let deadline = Box::pin(tokio::time::sleep_until(until));
165
166        let cached_reads = self.maybe_pre_cached(parent_header.hash());
167
168        let mut job = BasicPayloadJob {
169            config,
170            executor: self.executor.clone(),
171            deadline,
172            // ticks immediately
173            interval: tokio::time::interval(self.config.interval),
174            best_payload: PayloadState::Missing,
175            pending_block: None,
176            cached_reads,
177            payload_task_guard: self.payload_task_guard.clone(),
178            metrics: Default::default(),
179            builder: self.builder.clone(),
180        };
181
182        // start the first job right away
183        job.spawn_build_job();
184
185        Ok(job)
186    }
187
188    fn on_new_state<N: NodePrimitives>(&mut self, new_state: CanonStateNotification<N>) {
189        let mut cached = CachedReads::default();
190
191        // extract the state from the notification and put it into the cache
192        let committed = new_state.committed();
193        let new_execution_outcome = committed.execution_outcome();
194        for (addr, acc) in new_execution_outcome.bundle_accounts_iter() {
195            if let Some(info) = acc.info.clone() {
196                // we want pre cache existing accounts and their storage
197                // this only includes changed accounts and storage but is better than nothing
198                let storage =
199                    acc.storage.iter().map(|(key, slot)| (*key, slot.present_value)).collect();
200                cached.insert_account(addr, info, storage);
201            }
202        }
203
204        self.pre_cached = Some(PrecachedState { block: committed.tip().hash(), cached });
205    }
206}
207
208/// Pre-filled [`CachedReads`] for a specific block.
209///
210/// This is extracted from the [`CanonStateNotification`] for the tip block.
211#[derive(Debug, Clone)]
212pub struct PrecachedState {
213    /// The block for which the state is pre-cached.
214    pub block: B256,
215    /// Cached state for the block.
216    pub cached: CachedReads,
217}
218
219/// Restricts how many generator tasks can be executed at once.
220#[derive(Debug, Clone)]
221pub struct PayloadTaskGuard(Arc<Semaphore>);
222
223impl Deref for PayloadTaskGuard {
224    type Target = Semaphore;
225
226    fn deref(&self) -> &Self::Target {
227        &self.0
228    }
229}
230
231// === impl PayloadTaskGuard ===
232
233impl PayloadTaskGuard {
234    /// Constructs `Self` with a maximum task count of `max_payload_tasks`.
235    pub fn new(max_payload_tasks: usize) -> Self {
236        Self(Arc::new(Semaphore::new(max_payload_tasks)))
237    }
238}
239
240/// Settings for the [`BasicPayloadJobGenerator`].
241#[derive(Debug, Clone)]
242pub struct BasicPayloadJobGeneratorConfig {
243    /// The interval at which the job should build a new payload after the last.
244    interval: Duration,
245    /// The deadline for when the payload builder job should resolve.
246    ///
247    /// By default this is [`SLOT_DURATION`]: 12s
248    deadline: Duration,
249    /// Maximum number of tasks to spawn for building a payload.
250    max_payload_tasks: usize,
251}
252
253// === impl BasicPayloadJobGeneratorConfig ===
254
255impl BasicPayloadJobGeneratorConfig {
256    /// Sets the interval at which the job should build a new payload after the last.
257    pub const fn interval(mut self, interval: Duration) -> Self {
258        self.interval = interval;
259        self
260    }
261
262    /// Sets the deadline when this job should resolve.
263    pub const fn deadline(mut self, deadline: Duration) -> Self {
264        self.deadline = deadline;
265        self
266    }
267
268    /// Sets the maximum number of tasks to spawn for building a payload(s).
269    ///
270    /// # Panics
271    ///
272    /// If `max_payload_tasks` is 0.
273    pub fn max_payload_tasks(mut self, max_payload_tasks: usize) -> Self {
274        assert!(max_payload_tasks > 0, "max_payload_tasks must be greater than 0");
275        self.max_payload_tasks = max_payload_tasks;
276        self
277    }
278}
279
280impl Default for BasicPayloadJobGeneratorConfig {
281    fn default() -> Self {
282        Self {
283            interval: Duration::from_secs(1),
284            // 12s slot time
285            deadline: SLOT_DURATION,
286            max_payload_tasks: 3,
287        }
288    }
289}
290
291/// A basic payload job that continuously builds a payload with the best transactions from the pool.
292///
293/// This type is a [`PayloadJob`] and [`Future`] that terminates when the deadline is reached or
294/// when the job is resolved: [`PayloadJob::resolve`].
295///
296/// This basic job implementation will trigger new payload build task continuously until the job is
297/// resolved or the deadline is reached, or until the built payload is marked as frozen:
298/// [`BuildOutcome::Freeze`]. Once a frozen payload is returned, no additional payloads will be
299/// built and this future will wait to be resolved: [`PayloadJob::resolve`] or terminated if the
300/// deadline is reached..
301#[derive(Debug)]
302pub struct BasicPayloadJob<Tasks, Builder>
303where
304    Builder: PayloadBuilder,
305{
306    /// The configuration for how the payload will be created.
307    config: PayloadConfig<Builder::Attributes, HeaderForPayload<Builder::BuiltPayload>>,
308    /// How to spawn building tasks
309    executor: Tasks,
310    /// The deadline when this job should resolve.
311    deadline: Pin<Box<Sleep>>,
312    /// The interval at which the job should build a new payload after the last.
313    interval: Interval,
314    /// The best payload so far and its state.
315    best_payload: PayloadState<Builder::BuiltPayload>,
316    /// Receiver for the block that is currently being built.
317    pending_block: Option<PendingPayload<Builder::BuiltPayload>>,
318    /// Restricts how many generator tasks can be executed at once.
319    payload_task_guard: PayloadTaskGuard,
320    /// Caches all disk reads for the state the new payloads builds on
321    ///
322    /// This is used to avoid reading the same state over and over again when new attempts are
323    /// triggered, because during the building process we'll repeatedly execute the transactions.
324    cached_reads: Option<CachedReads>,
325    /// metrics for this type
326    metrics: PayloadBuilderMetrics,
327    /// The type responsible for building payloads.
328    ///
329    /// See [`PayloadBuilder`]
330    builder: Builder,
331}
332
333impl<Tasks, Builder> BasicPayloadJob<Tasks, Builder>
334where
335    Tasks: TaskSpawner + Clone + 'static,
336    Builder: PayloadBuilder + Unpin + 'static,
337    Builder::Attributes: Unpin + Clone,
338    Builder::BuiltPayload: Unpin + Clone,
339{
340    /// Spawns a new payload build task.
341    fn spawn_build_job(&mut self) {
342        trace!(target: "payload_builder", id = %self.config.payload_id(), "spawn new payload build task");
343        let (tx, rx) = oneshot::channel();
344        let cancel = CancelOnDrop::default();
345        let _cancel = cancel.clone();
346        let guard = self.payload_task_guard.clone();
347        let payload_config = self.config.clone();
348        let best_payload = self.best_payload.payload().cloned();
349        self.metrics.inc_initiated_payload_builds();
350        let cached_reads = self.cached_reads.take().unwrap_or_default();
351        let builder = self.builder.clone();
352        self.executor.spawn_blocking(Box::pin(async move {
353            // acquire the permit for executing the task
354            let _permit = guard.acquire().await;
355            let args =
356                BuildArguments { cached_reads, config: payload_config, cancel, best_payload };
357            let result = builder.try_build(args);
358            let _ = tx.send(result);
359        }));
360
361        self.pending_block = Some(PendingPayload { _cancel, payload: rx });
362    }
363}
364
365impl<Tasks, Builder> Future for BasicPayloadJob<Tasks, Builder>
366where
367    Tasks: TaskSpawner + Clone + 'static,
368    Builder: PayloadBuilder + Unpin + 'static,
369    Builder::Attributes: Unpin + Clone,
370    Builder::BuiltPayload: Unpin + Clone,
371{
372    type Output = Result<(), PayloadBuilderError>;
373
374    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
375        let this = self.get_mut();
376
377        // check if the deadline is reached
378        if this.deadline.as_mut().poll(cx).is_ready() {
379            trace!(target: "payload_builder", "payload building deadline reached");
380            return Poll::Ready(Ok(()))
381        }
382
383        // check if the interval is reached
384        while this.interval.poll_tick(cx).is_ready() {
385            // start a new job if there is no pending block, we haven't reached the deadline,
386            // and the payload isn't frozen
387            if this.pending_block.is_none() && !this.best_payload.is_frozen() {
388                this.spawn_build_job();
389            }
390        }
391
392        // poll the pending block
393        if let Some(mut fut) = this.pending_block.take() {
394            match fut.poll_unpin(cx) {
395                Poll::Ready(Ok(outcome)) => match outcome {
396                    BuildOutcome::Better { payload, cached_reads } => {
397                        this.cached_reads = Some(cached_reads);
398                        debug!(target: "payload_builder", value = %payload.fees(), "built better payload");
399                        this.best_payload = PayloadState::Best(payload);
400                    }
401                    BuildOutcome::Freeze(payload) => {
402                        debug!(target: "payload_builder", "payload frozen, no further building will occur");
403                        this.best_payload = PayloadState::Frozen(payload);
404                    }
405                    BuildOutcome::Aborted { fees, cached_reads } => {
406                        this.cached_reads = Some(cached_reads);
407                        trace!(target: "payload_builder", worse_fees = %fees, "skipped payload build of worse block");
408                    }
409                    BuildOutcome::Cancelled => {
410                        unreachable!("the cancel signal never fired")
411                    }
412                },
413                Poll::Ready(Err(error)) => {
414                    // job failed, but we simply try again next interval
415                    debug!(target: "payload_builder", %error, "payload build attempt failed");
416                    this.metrics.inc_failed_payload_builds();
417                }
418                Poll::Pending => {
419                    this.pending_block = Some(fut);
420                }
421            }
422        }
423
424        Poll::Pending
425    }
426}
427
428impl<Tasks, Builder> PayloadJob for BasicPayloadJob<Tasks, Builder>
429where
430    Tasks: TaskSpawner + Clone + 'static,
431    Builder: PayloadBuilder + Unpin + 'static,
432    Builder::Attributes: Unpin + Clone,
433    Builder::BuiltPayload: Unpin + Clone,
434{
435    type PayloadAttributes = Builder::Attributes;
436    type ResolvePayloadFuture = ResolveBestPayload<Self::BuiltPayload>;
437    type BuiltPayload = Builder::BuiltPayload;
438
439    fn best_payload(&self) -> Result<Self::BuiltPayload, PayloadBuilderError> {
440        if let Some(payload) = self.best_payload.payload() {
441            Ok(payload.clone())
442        } else {
443            // No payload has been built yet, but we need to return something that the CL then
444            // can deliver, so we need to return an empty payload.
445            //
446            // Note: it is assumed that this is unlikely to happen, as the payload job is
447            // started right away and the first full block should have been
448            // built by the time CL is requesting the payload.
449            self.metrics.inc_requested_empty_payload();
450            self.builder.build_empty_payload(self.config.clone())
451        }
452    }
453
454    fn payload_attributes(&self) -> Result<Self::PayloadAttributes, PayloadBuilderError> {
455        Ok(self.config.attributes.clone())
456    }
457
458    fn payload_timestamp(&self) -> Result<u64, PayloadBuilderError> {
459        Ok(self.config.attributes.timestamp())
460    }
461
462    fn resolve_kind(
463        &mut self,
464        kind: PayloadKind,
465    ) -> (Self::ResolvePayloadFuture, KeepPayloadJobAlive) {
466        let best_payload = self.best_payload.payload().cloned();
467        if best_payload.is_none() && self.pending_block.is_none() {
468            // ensure we have a job scheduled if we don't have a best payload yet and none is active
469            self.spawn_build_job();
470        }
471
472        let maybe_better = self.pending_block.take();
473        let mut empty_payload = None;
474
475        if best_payload.is_none() {
476            debug!(target: "payload_builder", id=%self.config.payload_id(), "no best payload yet to resolve, building empty payload");
477
478            let args = BuildArguments {
479                cached_reads: self.cached_reads.take().unwrap_or_default(),
480                config: self.config.clone(),
481                cancel: CancelOnDrop::default(),
482                best_payload: None,
483            };
484
485            match self.builder.on_missing_payload(args) {
486                MissingPayloadBehaviour::AwaitInProgress => {
487                    debug!(target: "payload_builder", id=%self.config.payload_id(), "awaiting in progress payload build job");
488                }
489                MissingPayloadBehaviour::RaceEmptyPayload => {
490                    debug!(target: "payload_builder", id=%self.config.payload_id(), "racing empty payload");
491
492                    // if no payload has been built yet
493                    self.metrics.inc_requested_empty_payload();
494                    // no payload built yet, so we need to return an empty payload
495                    let (tx, rx) = oneshot::channel();
496                    let config = self.config.clone();
497                    let builder = self.builder.clone();
498                    self.executor.spawn_blocking(Box::pin(async move {
499                        let res = builder.build_empty_payload(config);
500                        let _ = tx.send(res);
501                    }));
502
503                    empty_payload = Some(rx);
504                }
505                MissingPayloadBehaviour::RacePayload(job) => {
506                    debug!(target: "payload_builder", id=%self.config.payload_id(), "racing fallback payload");
507                    // race the in progress job with this job
508                    let (tx, rx) = oneshot::channel();
509                    self.executor.spawn_blocking(Box::pin(async move {
510                        let _ = tx.send(job());
511                    }));
512                    empty_payload = Some(rx);
513                }
514            };
515        }
516
517        let fut = ResolveBestPayload {
518            best_payload,
519            maybe_better,
520            empty_payload: empty_payload.filter(|_| kind != PayloadKind::WaitForPending),
521        };
522
523        (fut, KeepPayloadJobAlive::No)
524    }
525}
526
527/// Represents the current state of a payload being built.
528#[derive(Debug, Clone)]
529pub enum PayloadState<P> {
530    /// No payload has been built yet.
531    Missing,
532    /// The best payload built so far, which may still be improved upon.
533    Best(P),
534    /// The payload is frozen and no further building should occur.
535    ///
536    /// Contains the final payload `P` that should be used.
537    Frozen(P),
538}
539
540impl<P> PayloadState<P> {
541    /// Checks if the payload is frozen.
542    pub const fn is_frozen(&self) -> bool {
543        matches!(self, Self::Frozen(_))
544    }
545
546    /// Returns the payload if it exists (either Best or Frozen).
547    pub const fn payload(&self) -> Option<&P> {
548        match self {
549            Self::Missing => None,
550            Self::Best(p) | Self::Frozen(p) => Some(p),
551        }
552    }
553}
554
555/// The future that returns the best payload to be served to the consensus layer.
556///
557/// This returns the payload that's supposed to be sent to the CL.
558///
559/// If payload has been built so far, it will return that, but it will check if there's a better
560/// payload available from an in progress build job. If so it will return that.
561///
562/// If no payload has been built so far, it will either return an empty payload or the result of the
563/// in progress build job, whatever finishes first.
564#[derive(Debug)]
565pub struct ResolveBestPayload<Payload> {
566    /// Best payload so far.
567    pub best_payload: Option<Payload>,
568    /// Regular payload job that's currently running that might produce a better payload.
569    pub maybe_better: Option<PendingPayload<Payload>>,
570    /// The empty payload building job in progress, if any.
571    pub empty_payload: Option<oneshot::Receiver<Result<Payload, PayloadBuilderError>>>,
572}
573
574impl<Payload> ResolveBestPayload<Payload> {
575    const fn is_empty(&self) -> bool {
576        self.best_payload.is_none() && self.maybe_better.is_none() && self.empty_payload.is_none()
577    }
578}
579
580impl<Payload> Future for ResolveBestPayload<Payload>
581where
582    Payload: Unpin,
583{
584    type Output = Result<Payload, PayloadBuilderError>;
585
586    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
587        let this = self.get_mut();
588
589        // check if there is a better payload before returning the best payload
590        if let Some(fut) = Pin::new(&mut this.maybe_better).as_pin_mut() {
591            if let Poll::Ready(res) = fut.poll(cx) {
592                this.maybe_better = None;
593                if let Ok(Some(payload)) = res.map(|out| out.into_payload())
594                    .inspect_err(|err| warn!(target: "payload_builder", %err, "failed to resolve pending payload"))
595                {
596                    debug!(target: "payload_builder", "resolving better payload");
597                    return Poll::Ready(Ok(payload))
598                }
599            }
600        }
601
602        if let Some(best) = this.best_payload.take() {
603            debug!(target: "payload_builder", "resolving best payload");
604            return Poll::Ready(Ok(best))
605        }
606
607        if let Some(fut) = Pin::new(&mut this.empty_payload).as_pin_mut() {
608            if let Poll::Ready(res) = fut.poll(cx) {
609                this.empty_payload = None;
610                return match res {
611                    Ok(res) => {
612                        if let Err(err) = &res {
613                            warn!(target: "payload_builder", %err, "failed to resolve empty payload");
614                        } else {
615                            debug!(target: "payload_builder", "resolving empty payload");
616                        }
617                        Poll::Ready(res)
618                    }
619                    Err(err) => Poll::Ready(Err(err.into())),
620                }
621            }
622        }
623
624        if this.is_empty() {
625            return Poll::Ready(Err(PayloadBuilderError::MissingPayload))
626        }
627
628        Poll::Pending
629    }
630}
631
632/// A future that resolves to the result of the block building job.
633#[derive(Debug)]
634pub struct PendingPayload<P> {
635    /// The marker to cancel the job on drop
636    _cancel: CancelOnDrop,
637    /// The channel to send the result to.
638    payload: oneshot::Receiver<Result<BuildOutcome<P>, PayloadBuilderError>>,
639}
640
641impl<P> PendingPayload<P> {
642    /// Constructs a `PendingPayload` future.
643    pub const fn new(
644        cancel: CancelOnDrop,
645        payload: oneshot::Receiver<Result<BuildOutcome<P>, PayloadBuilderError>>,
646    ) -> Self {
647        Self { _cancel: cancel, payload }
648    }
649}
650
651impl<P> Future for PendingPayload<P> {
652    type Output = Result<BuildOutcome<P>, PayloadBuilderError>;
653
654    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
655        let res = ready!(self.payload.poll_unpin(cx));
656        Poll::Ready(res.map_err(Into::into).and_then(|res| res))
657    }
658}
659
660/// Static config for how to build a payload.
661#[derive(Clone, Debug)]
662pub struct PayloadConfig<Attributes, Header = alloy_consensus::Header> {
663    /// The parent header.
664    pub parent_header: Arc<SealedHeader<Header>>,
665    /// Requested attributes for the payload.
666    pub attributes: Attributes,
667}
668
669impl<Attributes, Header> PayloadConfig<Attributes, Header>
670where
671    Attributes: PayloadBuilderAttributes,
672{
673    /// Create new payload config.
674    pub const fn new(parent_header: Arc<SealedHeader<Header>>, attributes: Attributes) -> Self {
675        Self { parent_header, attributes }
676    }
677
678    /// Returns the payload id.
679    pub fn payload_id(&self) -> PayloadId {
680        self.attributes.payload_id()
681    }
682}
683
684/// The possible outcomes of a payload building attempt.
685#[derive(Debug)]
686pub enum BuildOutcome<Payload> {
687    /// Successfully built a better block.
688    Better {
689        /// The new payload that was built.
690        payload: Payload,
691        /// The cached reads that were used to build the payload.
692        cached_reads: CachedReads,
693    },
694    /// Aborted payload building because resulted in worse block wrt. fees.
695    Aborted {
696        /// The total fees associated with the attempted payload.
697        fees: U256,
698        /// The cached reads that were used to build the payload.
699        cached_reads: CachedReads,
700    },
701    /// Build job was cancelled
702    Cancelled,
703
704    /// The payload is final and no further building should occur
705    Freeze(Payload),
706}
707
708impl<Payload> BuildOutcome<Payload> {
709    /// Consumes the type and returns the payload if the outcome is `Better`.
710    pub fn into_payload(self) -> Option<Payload> {
711        match self {
712            Self::Better { payload, .. } | Self::Freeze(payload) => Some(payload),
713            _ => None,
714        }
715    }
716
717    /// Returns true if the outcome is `Better`.
718    pub const fn is_better(&self) -> bool {
719        matches!(self, Self::Better { .. })
720    }
721
722    /// Returns true if the outcome is `Aborted`.
723    pub const fn is_aborted(&self) -> bool {
724        matches!(self, Self::Aborted { .. })
725    }
726
727    /// Returns true if the outcome is `Cancelled`.
728    pub const fn is_cancelled(&self) -> bool {
729        matches!(self, Self::Cancelled)
730    }
731
732    /// Applies a fn on the current payload.
733    pub fn map_payload<F, P>(self, f: F) -> BuildOutcome<P>
734    where
735        F: FnOnce(Payload) -> P,
736    {
737        match self {
738            Self::Better { payload, cached_reads } => {
739                BuildOutcome::Better { payload: f(payload), cached_reads }
740            }
741            Self::Aborted { fees, cached_reads } => BuildOutcome::Aborted { fees, cached_reads },
742            Self::Cancelled => BuildOutcome::Cancelled,
743            Self::Freeze(payload) => BuildOutcome::Freeze(f(payload)),
744        }
745    }
746}
747
748/// The possible outcomes of a payload building attempt without reused [`CachedReads`]
749#[derive(Debug)]
750pub enum BuildOutcomeKind<Payload> {
751    /// Successfully built a better block.
752    Better {
753        /// The new payload that was built.
754        payload: Payload,
755    },
756    /// Aborted payload building because resulted in worse block wrt. fees.
757    Aborted {
758        /// The total fees associated with the attempted payload.
759        fees: U256,
760    },
761    /// Build job was cancelled
762    Cancelled,
763    /// The payload is final and no further building should occur
764    Freeze(Payload),
765}
766
767impl<Payload> BuildOutcomeKind<Payload> {
768    /// Attaches the [`CachedReads`] to the outcome.
769    pub fn with_cached_reads(self, cached_reads: CachedReads) -> BuildOutcome<Payload> {
770        match self {
771            Self::Better { payload } => BuildOutcome::Better { payload, cached_reads },
772            Self::Aborted { fees } => BuildOutcome::Aborted { fees, cached_reads },
773            Self::Cancelled => BuildOutcome::Cancelled,
774            Self::Freeze(payload) => BuildOutcome::Freeze(payload),
775        }
776    }
777}
778
779/// A collection of arguments used for building payloads.
780///
781/// This struct encapsulates the essential components and configuration required for the payload
782/// building process. It holds references to the Ethereum client, transaction pool, cached reads,
783/// payload configuration, cancellation status, and the best payload achieved so far.
784#[derive(Debug)]
785pub struct BuildArguments<Attributes, Payload: BuiltPayload> {
786    /// Previously cached disk reads
787    pub cached_reads: CachedReads,
788    /// How to configure the payload.
789    pub config: PayloadConfig<Attributes, HeaderTy<Payload::Primitives>>,
790    /// A marker that can be used to cancel the job.
791    pub cancel: CancelOnDrop,
792    /// The best payload achieved so far.
793    pub best_payload: Option<Payload>,
794}
795
796impl<Attributes, Payload: BuiltPayload> BuildArguments<Attributes, Payload> {
797    /// Create new build arguments.
798    pub const fn new(
799        cached_reads: CachedReads,
800        config: PayloadConfig<Attributes, HeaderTy<Payload::Primitives>>,
801        cancel: CancelOnDrop,
802        best_payload: Option<Payload>,
803    ) -> Self {
804        Self { cached_reads, config, cancel, best_payload }
805    }
806}
807
808/// A trait for building payloads that encapsulate Ethereum transactions.
809///
810/// This trait provides the `try_build` method to construct a transaction payload
811/// using `BuildArguments`. It returns a `Result` indicating success or a
812/// `PayloadBuilderError` if building fails.
813///
814/// Generic parameters `Pool` and `Client` represent the transaction pool and
815/// Ethereum client types.
816pub trait PayloadBuilder: Send + Sync + Clone {
817    /// The payload attributes type to accept for building.
818    type Attributes: PayloadBuilderAttributes;
819    /// The type of the built payload.
820    type BuiltPayload: BuiltPayload;
821
822    /// Tries to build a transaction payload using provided arguments.
823    ///
824    /// Constructs a transaction payload based on the given arguments,
825    /// returning a `Result` indicating success or an error if building fails.
826    ///
827    /// # Arguments
828    ///
829    /// - `args`: Build arguments containing necessary components.
830    ///
831    /// # Returns
832    ///
833    /// A `Result` indicating the build outcome or an error.
834    fn try_build(
835        &self,
836        args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
837    ) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError>;
838
839    /// Invoked when the payload job is being resolved and there is no payload yet.
840    ///
841    /// This can happen if the CL requests a payload before the first payload has been built.
842    fn on_missing_payload(
843        &self,
844        _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
845    ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
846        MissingPayloadBehaviour::RaceEmptyPayload
847    }
848
849    /// Builds an empty payload without any transaction.
850    fn build_empty_payload(
851        &self,
852        config: PayloadConfig<Self::Attributes, HeaderForPayload<Self::BuiltPayload>>,
853    ) -> Result<Self::BuiltPayload, PayloadBuilderError>;
854}
855
856/// Tells the payload builder how to react to payload request if there's no payload available yet.
857///
858/// This situation can occur if the CL requests a payload before the first payload has been built.
859pub enum MissingPayloadBehaviour<Payload> {
860    /// Await the regular scheduled payload process.
861    AwaitInProgress,
862    /// Race the in progress payload process with an empty payload.
863    RaceEmptyPayload,
864    /// Race the in progress payload process with this job.
865    RacePayload(Box<dyn FnOnce() -> Result<Payload, PayloadBuilderError> + Send>),
866}
867
868impl<Payload> fmt::Debug for MissingPayloadBehaviour<Payload> {
869    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
870        match self {
871            Self::AwaitInProgress => write!(f, "AwaitInProgress"),
872            Self::RaceEmptyPayload => {
873                write!(f, "RaceEmptyPayload")
874            }
875            Self::RacePayload(_) => write!(f, "RacePayload"),
876        }
877    }
878}
879
880impl<Payload> Default for MissingPayloadBehaviour<Payload> {
881    fn default() -> Self {
882        Self::RaceEmptyPayload
883    }
884}
885
886/// Checks if the new payload is better than the current best.
887///
888/// This compares the total fees of the blocks, higher is better.
889#[inline(always)]
890pub fn is_better_payload<T: BuiltPayload>(best_payload: Option<&T>, new_fees: U256) -> bool {
891    if let Some(best_payload) = best_payload {
892        new_fees > best_payload.fees()
893    } else {
894        true
895    }
896}
897
898/// Returns the duration until the given unix timestamp in seconds.
899///
900/// Returns `Duration::ZERO` if the given timestamp is in the past.
901fn duration_until(unix_timestamp_secs: u64) -> Duration {
902    let unix_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
903    let timestamp = Duration::from_secs(unix_timestamp_secs);
904    timestamp.saturating_sub(unix_now)
905}