Skip to content

Conversation

julio4
Copy link
Collaborator

@julio4 julio4 commented Oct 14, 2025

Currently build_payload is defined per platform by taking the payload (as a checkpoint from payload API) and a provider to get access to state:

rblib/src/platform/mod.rs

Lines 104 to 114 in 624ead4

/// Given a payload checkpoint and access to the node state, this method
/// builds a new payload that is ready to be handed back to the CL client as
/// a response to the `ForkchoiceUpdated` request.
fn build_payload<P, Provider>(
payload: Checkpoint<P>,
provider: &Provider,
) -> Result<types::BuiltPayload<P>, PayloadBuilderError>
where
P: traits::PlatformExecBounds<Self>,
Provider: traits::ProviderBounds<Self>;
}

The provider is bounded with ProviderBounds, which is StateProviderFactory + ChainSpecProvider + HeaderProvider. So ProviderBounds is stronger than just a StateProvider bound as the state factory should be able to create state provider for different blocks etc; it's basically a provider to the full node state.

rblib/src/pipelines/mod.rs

Lines 346 to 353 in 624ead4

pub trait ProviderBounds<P: Platform>:
StateProviderFactory
+ ChainSpecProvider<ChainSpec = types::ChainSpec<P>>
+ HeaderProvider<Header = types::Header<P>>
+ Clone
+ Send
+ Sync
+ 'static

In reth codebase the Client trait is a similar bound. Its used in default_ethereum_payload (used in the implementation of build_payload for Ethereum platform):

https://github.com/paradigmxyz/reth/blob/ab2b11f40eed3623219c49022061a11a0b5e2c0c/crates/ethereum/payload/src/lib.rs#L132-L150

Inside the implementation, client is used to get the latest StateProvider client.state_by_block_hash(parent_header.hash()) and to get chain spec client.chain_spec():

https://github.com/paradigmxyz/reth/blob/ab2b11f40eed3623219c49022061a11a0b5e2c0c/crates/ethereum/payload/src/lib.rs#L155
https://github.com/paradigmxyz/reth/blob/ab2b11f40eed3623219c49022061a11a0b5e2c0c/crates/ethereum/payload/src/lib.rs#L175

Similarly, in our own implementation of build_payload for the Optimism platform we use the provider to get latest state provider.state_by_block_hash(payload.block().parent().hash()). The chainspec is accessible with the block context with payload.block().chainspec().

// Top of Block chain state.
let state_provider =
provider.state_by_block_hash(payload.block().parent().hash())?;

chain_spec: block.chainspec().clone(),

So for build_payload, we don’t really need StateProviderFactory + ChainSpecProvider as we already have access to latest StateProvider and chainspec in block context.

Regarding HeaderProvider, it seems to be used only within pipeline API’s PayloadJobGenerator in new_payload_job to request the parent block header to initialize block context.

fn new_payload_job(
&self,
attribs: types::PayloadBuilderAttributes<Plat>,
) -> Result<Self::Job, PayloadBuilderError> {
let header = self
.service
.provider()
.sealed_header_by_hash(attribs.parent())?
.ok_or_else(|| {
PayloadBuilderError::MissingParentHeader(attribs.parent())
})?;
let base_state =
self.service.provider().state_by_block_hash(header.hash())?;
// This is the beginning of the state manipulation API usage from within
// the pipelines API.
let block_ctx = BlockContext::new(
header,
attribs,
base_state,
self.service.chain_spec().clone(),
)
.map_err(PayloadBuilderError::other)?;
Ok(PayloadJob::new(&self.pipeline, block_ctx, &self.service))
}

ProviderBounds is defined as part of the pipeline API and used mostly within the pipeline service to request for node states. I consider pipeline API to be built “on top” of payload/platform API, as a way to abstract building workflows on top of the node. That's why it's hard to get access to it within pipeline/steps, as you’re not supposed to directly read state from the node but instead use checkpoint abstraction.

With this in mind I suggest trying to change build_payload to not take “node provider” but only “latest block state provider”. This would also allows to build payload directly from a checkpoint/from the block context, so within a step payload.build_payload() or ctx.block().build_payload(payload).

This PR shows an example of how it could look like.
Issue: to be able to fully use default_ethereum_payload there's a need for some shims with the latest StateProvider as it expects a StateProviderFactory + ChainSpecProvider. I forked it to change the behavior. We would want to implement a different payload building process anyway (checkpoint aware)

TLDR; lets completely abstract node state from payload/platform and make build_payload takes only StateProvider from BlockContext::base_state

@julio4 julio4 marked this pull request as ready for review October 15, 2025 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant