Skip to content
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

Scroll EvmExecutor implementation #5

Closed
Tracked by #16
frisitano opened this issue Oct 1, 2024 · 6 comments · Fixed by #63
Closed
Tracked by #16

Scroll EvmExecutor implementation #5

frisitano opened this issue Oct 1, 2024 · 6 comments · Fixed by #63
Assignees
Milestone

Comments

@frisitano
Copy link
Collaborator

Describe the feature

Overview

To execute blocks using scroll-revm we must implement an EvmExecutor (and associated assets) that wraps scroll-revm and executes blocks. An example of optimism specific Executor assets can be found here. Logic from the stateless-block-verifier can be used as a guide for the block execution logic.

Additional context

No response

@frisitano frisitano moved this to Todo in Reth Oct 1, 2024
@frisitano frisitano added this to Reth Oct 1, 2024
@frisitano frisitano added this to the Milestone 1 milestone Oct 1, 2024
@Thegaram
Copy link

Thegaram commented Oct 3, 2024

It seems there is no EvmExecutor trait yet. Do you mean we should define a ScrollEvmExecutor<EvmConfig> that implements execute_state_transitions, as well as ScrollBlockExecutor etc, following the Eth and Op examples?

@frisitano
Copy link
Collaborator Author

Sorry I should have been explicit when referring to what we need to implement. Here is an example of the OpEvmExecutor. We would also need to implement associated types such as OpBlockExecutor , OpBatchExecutor, OpExecutorProvider and implement the required traits on these types, i.e. Executor, BatchExecutor and BlockExecutorProvider respectively.

@frisitano frisitano mentioned this issue Oct 8, 2024
10 tasks
@greged93
Copy link
Collaborator

greged93 commented Nov 26, 2024

Overview

In order to execute a block using the Scroll block building rules, we implement our own Scroll Executor and BatchExecutor. The abstraction available in Reth and used for Optimism and Ethereum block execution (via the BasicBlockExecutor and the BasicBatchExecutor) is the BlockExecutionStrategy.

Strategy

Implementing this trait allows us to define the following:

  • apply_pre_execution_changes: Applies any necessary changes before executing the block's transactions.
  • execute_transactions: Executes all transactions in the block.
  • apply_post_execution_changes: Applies any necessary changes after executing the block's transactions.
  • validate_block_post_execution: Validate a block with regard to execution results.

apply_pre_execution_changes

This part of the code should contain changes to the state of the chain that should be applied before we run the transactions on the block. For Scroll this doesn’t include anything:

The apply_pre_execution_changes is a noop for Scroll.

edit: Pre execution changes need to apply the Curie hardfork (see #5 (comment)).

execute_transactions

Pre-execution

Before execution, the coinbase of the block needs to be set. By default, this value is set to the beneficiary of the block, but in the case of Scroll this value is in most cases set to 0x0000000000000000000000000000000000000000. Ideally we want this to be configurable and should use the fee vault address 0x5300000000000000000000000000000000000005 as default.

Execution

Execute the transactions, assuming that validations for the body of the block were already performed.

Post-execution

The fork of the revm crate used by Scroll added the l1_block_info field on the InnerEvmContext structure, and this value is set during the loading of the accounts which is always executed by revm. At the end of the execution of the transaction, we can retrieve this value and use it in order to compute the l1_fee for the transaction.
If the transaction is a L1 message transaction, l1_fee is 0.

apply_post_execution_changes

Scroll doesn’t need to handle validator withdrawal from the consensus layer nor EIP-7865 requests so this method is a noop.

validate_block_post_execution

Validates the post execution for the block:

  • validate the receipt root against the root of the header.
  • validate the receipts bloom against the bloom of the block.
  • validate the gas used against the gas used in the header.

We don’t validate the state root which is performed at the Merkle stage or in insert_block_inner for live syncing.

Design

Introduce an execution strategy for Scroll and implement the BlockExecutionStrategy trait:

/// Block execution strategy for Scroll.
pub struct ScrollExecutionStrategy<DB, EvmConfig>
{
    chain_spec: Arc<ChainSpec>,
    evm_config: EvmConfig,
    state: State<DB>,
}

impl<DB, EvmConfig> BlockExecutionStrategy<DB> for EthExecutionStrategy<DB, EvmConfig>
where
    DB: Database<Error: Into<ProviderError> + Display> + ContextFul,
    EvmConfig: ConfigureEvm<Header = alloy_consensus::Header>,
{
    type Error = BlockExecutionError;

    fn init(&mut self, _tx_env_overrides: Box<dyn TxEnvOverrides>) {
    }

    fn apply_pre_execution_changes(
        &mut self,
        _block: &BlockWithSenders,
        _total_difficulty: U256,
    ) -> Result<(), Self::Error> {
        Ok(())
    }

    fn execute_transactions(
        &mut self,
        block: &BlockWithSenders,
        total_difficulty: U256,
    ) -> Result<ExecuteOutput, Self::Error> {
        // Prepare the EVM for the block.
        ...
        // Set the coinbase for the block.
        ...
        for (sender, transaction) in block.transactions_with_sender() {
            // The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior,
            // must be no greater than the block’s gasLimit.
            ...
            
            // Set tx env and execute transaction.
            ...

            let mut l1_fee = 0;
            if !transaction.is_l1_msg() {
                   let mut rlp_bytes = BytesMut::new();
                   transaction.eip2718_encode(&mut rlp_bytes);
                   let l1_block_info = evm.context.evm.inner.l1_block_info.expect("l1 block info exists");
                   l1_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, evm.handler.cfg.spec_id);
            }

            // Accumulate receipts with the l1 fee
            ...
        }
        Ok(ExecuteOutput { receipts, gas_used: cumulative_gas_used })
    }

    fn apply_post_execution_changes(
        &mut self,
        block: &BlockWithSenders,
        total_difficulty: U256,
        receipts: &[Receipt],
    ) -> Result<Requests, Self::Error> {
        Ok(Default::default())
    }

    fn state_ref(&self) -> &State<DB> {
        &self.state
    }

    fn state_mut(&mut self) -> &mut State<DB> {
        &mut self.state
    }

    fn with_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
    }

    fn validate_block_post_execution(
        &self,
        block: &BlockWithSenders,
        receipts: &[Receipt],
        requests: &Requests,
    ) -> Result<(), ConsensusError> {
        if block.gas_used != receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0) {
                return error;
        }
        if chain_spec.is_byzantium_active_at_block(block.header.number) {
                if let Err(error) =
                    verify_receipts_and_bloom(block.header.receipts_root, block.header.logs_bloom, receipts)
                {
                    return error;
                }
        }
    }
}

The easiest way for us to correctly set the coinbase would be to handle this in the ConfigureEvmEnv implementation for Scroll.

pub struct ScrollEvmConfig {
    chain_spec: Arc<ScrollChainSpec>,
}

pub struct ScrollChainSpec {
    ...
    fee_vault_address: Option<Address>
}

impl ConfigureEvmEnv for ScrollEvmConfig {
    type Header = Header;
    type Error = DecodeError;

    ...
    
    fn fill_block_env(&self, block_env: &mut BlockEnv, header: &Self::Header, after_merge: bool) {
        ConfigureEvmEnv::fill_block_env(block_env, header, after_merge);
        if let Some(fee_vault_address) = self.chain_spec.fee_fault_address {
                block_env.coinbase = *fee_vault_address;
        }
    }
}

@Thegaram
Copy link

Before execution, the coinbase of the block needs to be set. By default, this value is set to the beneficiary of the block, but in the case of Scroll this value is in most cases set to 0x0000000000000000000000000000000000000000.

If you mean the coinbase header field, it is currently used by Clique for adding/removing an authorized block producer. But during execution the actual fee recipient is the fee vault: it receives all fees, including base fee, priority fee, and L1 data fee.

Execute the transactions, assuming that validations for the body of the block were already performed.

Where are these validations performed? Would make sense to have something like validate_block_pre_execution (for checking L1 message rules, block size limits, etc.) but it seems this does not exist yet.

At the end of the execution of the transaction, we can retrieve this value and use it in order to compute the l1_fee for the transaction.

I thought all fees are deducted inside revm, no? Is there any fee-related bookkeeping that we need to do in the reth codebase?

Scroll doesn’t need to handle validator withdrawal from the consensus layer nor EIP-7865 requests so this method is a noop.

There was a special state transition during the Curie hard fork (similar to the DAO fork rule) that might need to be added here.

@greged93
Copy link
Collaborator

If you mean the coinbase header field, it is currently used by Clique for adding/removing an authorized block producer. But during execution the actual fee recipient is the fee vault: it receives all fees, including base fee, priority fee, and L1 data fee.

Yes I was hoping to be able to use the coinbase field from the block header, but will introduce a configuration of the fee recipient set to the fee vault.

Where are these validations performed? Would make sense to have something like validate_block_pre_execution (for checking L1 message rules, block size limits, etc.) but it seems this does not exist yet.

As far as I could see, block body validations are performed during Consensus checks. Consensus is ran during live sync but not during backfill. I think this is fine but let me know if you think this could cause an issue.

I thought all fees are deducted inside revm, no? Is there any fee-related bookkeeping that we need to do in the reth codebase?

Right, I should have mentioned this: the computed L1 fee is part of the receipt, not used for fee deduction (performed in revm as you mentioned).

There was a special state transition during the Curie hard fork (similar to the DAO fork rule) that might need to be added here.

Indeed, this would need to be applied in apply_pre_execution_changes as far as I can see from l2geth?

@Thegaram
Copy link

As far as I could see, block body validations are performed during Consensus checks. Consensus is ran during live sync but not during backfill. I think this is fine but let me know if you think this could cause an issue.

I don't think this will cause any issues, thanks for sharing the link.

Indeed, this would need to be applied in apply_pre_execution_changes as far as I can see from l2geth?

Yes, you're right, this should be done pre-execution, not during post-execution as I originally suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants