-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Design and Implement Engine Driver #5
Comments
First layout of the types and methods for the pub struct ScrollPayloadAttributes {
/// The payload attributes.
payload_attributes: PayloadAttributes,
/// The l1 messages for block building.
l1_messages: Vec<TxL1Message>,
}
/// Information about a block.
pub struct BlockInfo {
/// The block number.
number: u64,
/// The block hash.
hash: B256
}
pub struct EngineDriver<E> {
/// The engine API client.
client: E,
/// The unsafe L2 block info.
unsafe_block_info: BlockInfo,
/// The safe L2 block info.
safe_head: BlockInfo,
/// The finalized L2 block info.
finalized_head: BlockInfo
}
impl<E, P> EngineDriver<E, P>
where
E: EngineApiClient<Types>,
Types: EngineTypes<PayloadAttributes = ScrollPayloadAttributes>,
{
/// Initialize the driver and wait for the Engine server to be ready.
pub fn init_and_wait_for_engine(client: E, unsafe_head: BlockInfo, safe_head: BlockInfo, finalized_head: BlockInfo) -> Self;
/// Handles a execution payload:
/// - Sends the payload to the EL via `engine_newPayloadVx`.
/// - Sets the current fork choice for the EL via `engine_forkchoiceUpdatedVx`.
pub fn handle_execution_payload(&self, execution_payload: ExecutionPayload) -> Result<()>;
/// Handles a payload attributes:
/// - Starts a payload building task on the EL via `engine_forkchoiceUpdatedVx`, passing
/// the provided payload attributes.
/// - Retrieve the payload with `engine_getPayloadVx`.
/// - Sends the constructed payload to the EL via `engine_newPayloadVx`.
/// - Sets the current fork choice for the EL via `engine_forkchoiceUpdatedVx`.
pub fn handle_payload_attributes(&self, payload_attributes: ScrollPayloadAttributes) -> Result<()>;
/// Reorgs the driver by setting the safe and unsafe block info to the finalized info.
pub fn reorg(&mut self);
/// Set the finalized L2 block info.
pub fn set_finalized_block_info(&mut self, finalized: BlockInfo);
/// Set the safe L2 block info.
pub fn set_safe_block_info(&mut self, block: BlockInfo);
/// Set the unsafe L2 block info.
pub fn set_unsafe_block_info(&mut self, block: BlockInfo);
} |
This looks great. One question I have is about reorgs. If I'm not mistaken the scroll chain should never reorg as we only process L1 messages when the messages have been finalized on L1. There have been cases where batches need to be reverted but I'm not sure if this results in a reorg. Let's follow up with other members of scroll to better understand the potential for reorgs. If reorgs is impossible then we can remove the impl<E, P> EngineDriver<E, P>
where
E: EngineApiClient<Types>,
Types: EngineTypes<PayloadAttributes = ScrollPayloadAttributes>,
{
/// Initialize the driver and wait for the Engine server to be ready.
pub fn init_and_wait_for_engine(
client: E,
unsafe_head: BlockInfo,
safe_head: BlockInfo,
finalized_head: BlockInfo,
) -> Self;
/// Handles a execution payload:
/// - Sends the payload to the EL via `engine_newPayloadVx`.
/// - Sets the current fork choice for the EL via `engine_forkchoiceUpdatedVx`.
pub fn handle_execution_payload(&self, execution_payload: ExecutionPayload) -> Result<()>;
/// Handles a payload attributes:
/// - Starts a payload building task on the EL via `engine_forkchoiceUpdatedVx`, passing
/// the provided payload attributes.
/// - Retrieve the payload with `engine_getPayloadVx`.
/// - Sends the constructed payload to the EL via `engine_newPayloadVx`.
/// - Sets the current fork choice for the EL via `engine_forkchoiceUpdatedVx`.
pub fn handle_payload_attributes(
&self,
payload_attributes: ScrollPayloadAttributes,
) -> Result<()>;
/// Set the finalized L2 block info.
pub fn set_finalized_block_info(&mut self, block: BlockInfo);
/// Set the safe L2 block info.
pub fn set_safe_block_info(&mut self, block: BlockInfo);
/// Set the unsafe L2 block info.
pub fn set_unsafe_block_info(&mut self, block: BlockInfo);
} |
Yes true, let's add both. If we agree on the above API I will get started for the implementation |
Looks good to me, feel free to proceed. |
/// The l2 base fee per gas derived from the l1 parent block.
/// https://github.com/scroll-tech/go-ethereum/blob/ac8164f5a4190ff9e536f296195013ea7a4e3e3d/consensus/misc/eip1559.go#L53
base_fee_per_gas: U256 What's the rationale for moving this from EN to RN? If we want to store L2 base fee coefficients in state in the future, it seems simpler to keep this in EN. /// The unsafe L2 block info.
unsafe_block_info: BlockInfo, RN will keep receiving gossiped blocks and feeding these into the EN. Once the provided block is executed, it will be set as pub fn init_and_wait_for_engine(client: E, unsafe_head: BlockInfo, safe_head: BlockInfo, finalized_head: BlockInfo) -> Self; How will we set these params? Simply get them from pub fn handle_execution_payload(&self, execution_payload: ExecutionPayload) -> Result<()>;
pub fn handle_payload_attributes(&self, payload_attributes: ScrollPayloadAttributes) -> Result<()>; Would be useful to sync through the whole process of the RN. During normal operations it will keep receiving L2 blocks from gossip, as well as blocks from L1 sync. If everything is good, then the latter chain will be a prefix of the former. Do we submit both of these using |
We can differentiate between 3 cases:
/// Reorgs the driver by setting the safe and unsafe block info to the finalized info.
pub fn reorg(&mut self); Need to consider more how this will work. Realistically, a component ( |
Afaiu, you need to the parent L1 block info in order to derive the L2 base fee right? In the current state, how would you retrieve this L1 block info in the EN?
Yes this is the path I had in mind.
Yes at start up the Rollup node should initialize the engine driver with the correct heads. If we decide to adopt a "pipeline" path as Kona did, we could have an "initialize" function for the whole pipeline which sets all the value.
Afaiu, if we consider the reorgless path, you don't need to resubmit the execution payload for a finalized head, you can just update the head via If we reorg, the state of the driver will be reset to the finalized head, and the next call to |
You do need to notify the /// Reorgs the driver by rewinding the safe and unsafe block info to the provided block head.
pub fn reorg(&mut self, head: BlockInfo); |
Oh I get your point, you mean most L1 info should be provided through the RN. I generally agree, but the L1 base fee info is already read from state. This info is relayed by another component (
Currently
This is tricky, because we don't get full blocks from the derivation pipeline, only a block without execution results (is that
This boils down to: Which component should be responsible for maintaining this info? Ultimately |
Whilst I agree that it is useful to consider our future ambitions to have unsafe block consolidation and a provable derivation pipeline I would encourage that we primarily focus on Milestone 1 ambitions which is supporting L2 p2p sync as described in #9. As such I would say that the current solution is reasonable whilst being mindful that it is likely to change as we integrate components in the stack and certainly as we proceed with our future milestones. |
Below are three execution flows that should be considered and impact the above design Optimistic syncThe optimistic sync will ingest L2 blocks received via P2P. This path should have the lowest priority: the RN should favor processing L1 derived blocks. sequenceDiagram
participant RN
participant EN
RN ->> RN: receive ExecutionPayload
RN ->> EN: engine_newPayload(ExecutionPayload)
EN ->> RN: {status: VALID, ..}
RN ->> EN: engine_forkChoiceUpdate({head_block_hash: ExecutionPayload.block_hash, ..}, None)
EN ->> RN: {status: VALID, ..}
The engine driver receives the execution payload, sends it to the EN via Pessimistic syncThe pessimistic sync will ingest L1 blocks and derive the payload attributes for building L2 blocks. This path has the highest priority for the RN and L1 derived payload should always be prioritized over P2P payload. In order to avoid redundant execution, the RN node should be able to retrieve past processed execution payloads. This can be achieved by caching execution payloads or fetching blocks via the EN. Attributes matchingIf the L1 derived payload attributes match the retrieved execution payload for the unsafe block at safe head + 1, we can consolidate it by following the below execution path: sequenceDiagram
participant RN
participant EN
RN ->> EN: engine_forkChoiceUpdate({safe_block_hash: (SafeBlock.number + 1).hash, ..}, None)
EN ->> RN: {status: VALID, ..}
Attributes not existing or mismatch (L2 reorg)If the L1 derived payload attributes do no match the retrieved execution payload, we are faced with a L2 reorg and cannot consolidate. A new block should be constructed from the payload, on top of the safe head: sequenceDiagram
participant RN
participant EN
RN ->> RN: receive ScrollPayloadAttributes
RN ->> EN: engine_forkChoiceUpdate(FCU, {no_tx_pool: true, ..attributes})
EN ->> RN: {status: VALID, payload_id}
RN ->> EN: engine_getPayload(payload_id)
EN ->> RN: ExecutionPayload
RN ->> EN: engine_newPayload(ExecutionPayload)
EN ->> RN: {status: VALID, ..}
RN ->> EN: engine_forkChoiceUpdate({safe_block_hash: ExecutionPayload.block_hash, ..}, None)
EN ->> RN: {status: VALID, ..}
The block building process is started by calling OutcomeThe above leads to the following updates:
pub struct ScrollPayloadAttributes {
/// The payload attributes.
payload_attributes: PayloadAttributes,
/// The l1 messages for block building.
l1_messages: Vec<TxL1Message>,
/// An optional array of transaction to be forced included in the block.
transactions: Option<Vec<Transaction>>,
/// Indicates whether the payload building job should happen with or without pool transactions.
no_tx_pool: bool
}
// Implementers of the trait can provide the L2 execution payload for a block id.
pub trait ExecutionPayloadProvider {
execution_payload_by_block(&self, block_id: BlockId) -> Result<ExecutionPayload>;
}
pub struct EngineDriver<EC, P> {
/// The engine API client.
client: EC,
/// The execution payload provider
execution_payload_provider: P,
/// The unsafe L2 block info.
unsafe_block_info: BlockInfo,
/// The safe L2 block info.
safe_head: BlockInfo,
/// The finalized L2 block info.
finalized_head: BlockInfo
} SourcesOpnode: Magi: |
Just to make sure I understand, block building and executing payload attributes derived from L1 are almost exactly the same process? The only difference is the in the latter case the EN will not insert more txs into the block from txpool. The rest (execution, computing state/receipt/tx roots) are the same. |
This is what I understood from Optimism yes. You issue a |
Do we allow RN to run without optimistic sync? In case we do, then |
Great write-up; this looks good to me. Something to note for future reference is that we may need to persist the |
Overview
We should implement a component that is used to drive the execution client via he Engine API.
Engine API reference: https://hackmd.io/@danielrachi/engine_api
The text was updated successfully, but these errors were encountered: