Skip to main content

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