Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The EXECUTE precompile

Table of Contents

Re-execution vs ZK enforcement

The original proposal for the EXECUTE precompile presented two possible enforcement mechanisms: re-execution and ZK proofs. While the latter requires the L1 ZK-EVM upgrade to take place, the former can potentially be implemented beforehand and set the stage for the ZK version, in a similar way that proto-danksharding was first introduced without PeerDAS.

The re-execution variant would only be able to support optimistic rollups with a bisection protocol that goes down to single or few-L2-blocks sized steps, and that are EVM-equivalent. Today there are three stacks with working bisection protocols, namely Orbit stack (Arbitrum), OP stack (Optimism) and Cartesi. Cartesi is built to run a Linux VM so they wouldn't be able to use the precompile, and Orbit supports Stylus which doesn't make them fully EVM-equivalent, unless a Stylus-less version is implemented, but even in this case it wouldn't be able to support Arbitrum One. OP stack is mostly EVM-equivalent, but still requires heavy modifications to support native execution. It's therefore unclear whether trying to implement the re-execution version of the precompile is worth it, or if it's better to wait for the more powerful ZK version.

While L1 ZK-EVM is not needed for the re-execution version, statelessness is, as we want L1 validators to be able to verify the precompile without having to hold all rollups' state. It's not clear whether the time interval between statelessness and L1 ZK-EVM will be long enough to justify the implementation of the re-execution variant.

(WIP) Specification

def execute(evm: Evm) -> None:
	data = evm.message.data
	...
	charge_gas(...) # TBD
	...

    # Inputs
	chain_id = ... buffer_read(...) # likely hard-coded in a contract
	number = ... buffer_read(...)
	pre_state = ... buffer_read(...)
	post_state = ... buffer_read(...)
	post_receipts = ... buffer_read(...) # TODO: consider for L2->L1 msgs
	block_gas_limit = ... buffer_read(...) # TBD: depends on ZK gas handling
	coinbase = ... buffer_read(...)
    prev_randao = ... buffer_read(...)
    excess_blob_gas = ... buffer_read(...)
	transactions = ... buffer_read(...) # TBD: this should be a ref to blobs
    l1_anchor = ... buffer_read(...) # TBD: arbitrary info that is passed from L1 to L2 storage

    # Disable blob-carrying transactions
    for tx in map(decode_transaction, transactions):
        if isinstance(tx, BlobTransaction):
            raise ExecuteError
	
	block_env = vm.BlockEnvironment(
		chain_id=chain_id,
		state=pre_state,
		block_gas_limit=block_gas_limit,
		block_hashes=..., # TBD: depends how it will look like post-7709
		coinbase=coinbase,
		number=number, # TBD: they probably need to be strictly sequential
		base_fee_per_gas=..., # TBD
		time=..., # TBD: depends if we want to use sequencing or proving time 
		prev_randao=prev_randao, # NOTE: assigning `evm.message.block_env.prev_randao` prevents ahead-of-time sequencing
		excess_blob_gas=excess_blob_gas, # TODO: consider proposals where blob and calldata gas is merged for L2 pricing
		parent_beacon_block_root=... # TBD
    )

    # Handle L1 anchoring
    process_unchecked_system_transaction( # TODO: consider unchecked vs checked and gas accounting if the predeploy is custom
        block_env=block_env,
        target_address=L1_ANCHOR_ADDRESS, # TBD: exact predeploy address + implementation. Also: does it even need to be a fixed address?
        data=l1_anchor # TBD: exact format
    )

	# TODO: decide what to do things that are not valid on a rollup, e.g. blobs
	block_output = apply_body(
		block_env=block_env,
		transactions=transactions,
		withdrawals=() # TODO: consider using this for deposits
	)
	# NOTE: some things might look different with statelessness
	block_state_root = state_root(block_env.state)
	receipt_root = root(block_output.receipts_trie)
	# TODO: consider using requests_hash for withdrawals
	# TODO: consider adding a gas_used top-level param
	
	if block_state_root != post_state:
		raise ExecuteError # TODO: check if this is the proper way to handle errs
	if receipt_root != post_receipts
		raise ExecuteError
	
	evm.output = ... # TBD: maybe block_gas_used?

(WIP) Usage example

The following example shows how an ahead-of-time sequenced rollup can use the EXECUTE precompile to settle blocks.

contract Rollup {

    struct L2Block {
        uint l1AnchorBlock;
        bytes32 blobHash;
        bytes32 prevRandao;
        uint64 excessBlobGas;
    }

	uint64 public constant chainId = 1234321;
	uint public gasLimit;
	
	// latest settled state
	bytes32 public state;
	
	// receipts of latest settled block
	bytes32 public receipts;
	
	// block number to be sequenced next
	uint public nextBlockNumberToSequence;

    // block number to be settled next
    uint public nextBlockNumberToSettle;
	
	// blob refs store (L2 block number, (L1 block number, blob hash))
	mapping(uint => L2Block) public blocks;
	
    // assumes that one blob is one block
	function sequence(uint blobIndex) public {
		blocks[nextBlockNumberToSequence] = L2Block({
            l1AnchorBlock: block.number,
            blobHash: blockhash(index),
            prevRandao: block.prevrandao,
            excessBlobGas: ... // TBD: L1 only exposes block.blobbasefee, not the excess blob gas
        });
        nextBlockNumberToSequence++;
	}
	
	function settle(
		bytes32 _newState,
		bytes32 _receipts,
	) public {
        (uint l1AnchorBlock, bytes32 blobHash, bytes32 prev_randao, uint64 excess_blob_gas) = blocks[nextBlockNumberToSettle];

		EXECUTE(
			chainId,
			nextBlockNumberToSettle,
            l1AnchorBlock,
			state,
			_newState,
			_receipts,
			gasLimit,
			msg.sender,
            prev_randao,
            excess_blob_gas, // TBD
			(l1AnchorBlock, blobHash) // TBD: unclear how to reference past blobs at this point
		)

		state = _newState;
		receipts = _receipts;
        nextBlockNumberToSettle++;
	}
}