Skip to content
BaseHub by wbnns Updated

Isthmus L2 Execution Engine

Isthmus reworks several execution-layer rules. The headline change is that the block header’s withdrawalsRoot field now carries the storage root of the L2ToL1MessagePasser predeploy. The upgrade also adjusts deposit-request handling, adds BLS precompiles, and introduces a configurable operator fee.

Like every Base network upgrade, Isthmus turns on at a timestamp. The new L2 block execution rules take effect once L2 Timestamp >= activation time.

L2ToL1MessagePasser Storage Root in Header

Section titled “L2ToL1MessagePasser Storage Root in Header”

From Isthmus activation onward, the L2 block header’s withdrawalsRoot field holds the 32-byte L2ToL1MessagePasser account storage root, taken from the world state identified by the header’s stateRoot. That root is the same value eth_getProof returns for the account at the given block number.

Before Isthmus activates:

  • the L2 block header’s withdrawalsRoot field must be:
    • nil if Canyon has not been activated.
    • keccak256(rlp(empty_string_code)) if Canyon has been activated.
  • the L2 block header’s requestsHash field must be omitted.

After Isthmus activates, an L2 block header is valid if and only if:

  1. The withdrawalsRoot field
    1. Is 32 bytes in length.
    2. Matches the L2ToL1MessagePasser account storage root, as committed to in the storageRoot within the block header
  2. The requestsHash field is equal to sha256('') = 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, indicating no requests in the block.
Byte offsetDescription
[0, 32)L2ToL1MessagePasser account storage root

Generating L2 output roots for historical blocks otherwise requires an archive node, which pushes real cost onto the people who keep the system running. In a world secured by fault proofs that means a proposer needs an archive node to propose an output root at the safe head, and a user proving a withdrawal needs one to confirm that the output root they are proving against is genuinely valid and part of the safe chain. Committing the L2ToL1MessagePasser storage root directly into the withdrawalsRoot field removes that requirement, so both proposers and verifiers can do their work at lower operating cost.

If Isthmus is active at the genesis block, the genesis header’s withdrawalsRoot is set to the L2ToL1MessagePasser account storage root.

While transactions are being validated, the header’s withdrawalsRoot must not be exposed to the EVM or the application layer.

During sync the withdrawals list in the block body is expected to be empty (OP Stack does not use it), so its hash is the MPT root of an empty list. When the synced header chain is verified against the final synced header, the header timestamp decides whether Isthmus is active at that block. If it is, the header’s withdrawalsRoot MPT hash may be any non-null value, since it is expected to hold the L2ToL1MessagePasser storage root.

From Canyon (which brings in Shanghai support) up until Isthmus activation, withdrawalsRoot is set to the MPT root of an empty withdrawals list — the same value as an empty storage root. Withdrawals are captured in the L2 state during this window but are not reflected in withdrawalsRoot, so even when a header carries a non-trivial MPT root in that field before Isthmus, it must not be relied on. Any output-root calculation has to avoid using the header withdrawalsRoot in this range.

There is always nonzero storage in the L2ToL1MessagePasser, because it is a proxied predeploy and stores an implementation address and owner address from genesis. As a result, from Isthmus the withdrawalsRoot is always non-nil and never equal to the MPT root of an empty list.

The withdrawalsRoot field is otherwise unused in Base’s header consensus format and is not slated for any other use. Pointing it at the withdrawal account’s storage root fits Base cleanly and reuses a field that already exists in the L1 header format.

Execution clients keep historical account state in different ways. In a contrived case where Base produced no outbound withdrawal for a long stretch, a node might not retain the L2ToL1MessagePasser account storage root and would then be unable to stay in consensus. In practice most modern clients can reconstruct an account’s storage root at a given block on demand even when they do not persist it directly.

For RPC methods such as eth_simulateV1 that simulate arbitrary transactions across one or more blocks, the header of a simulated block should carry an empty withdrawals root. The same applies whenever the real withdrawals-root value is not readily available.

EIP-6110 moves deposits to the execution layer and defines a new EIP-7685 deposit request of type DEPOSIT_REQUEST_TYPE, which would otherwise show up in the EIP-7685 requests list. Base ignores these: requests generation is modified to leave out EIP-6110 deposit requests. Because the EIP-6110 request type did not exist before Pectra on L1 and Isthmus on L2, no activation time is needed — these deposit-type requests can always be excluded.

The withdrawals list in the block body is encoded as an empty RLP list.

As with the bn256Pairing precompile in the Granite hardfork, EIP-2537 adds a BLS precompile that short-circuits based on input size in the EVM.

The input-size limits for the BLS precompile contracts are:

  • G1 multiple-scalar-multiply: input_size <= 513760 bytes
  • G2 multiple-scalar-multiply: input_size <= 488448 bytes
  • Pairing check: input_size <= 235008 bytes

The remaining BLS precompiles are fixed-size operations with a fixed gas cost.

On Base, EIP-7685 is no-op’d and requestsHash is always set to sha256(''), as covered in the header validity rules. That also means EIP-6110, EIP-7002, and EIP-7251 are not enabled. After Isthmus activation, the Base execution layer must not run the post-block deposit-contract event filtering (EIP-6110) or the EIP-7002 + EIP-7251 system calls during block sealing.

Users remain free to deploy these contracts permissionlessly, but the Base execution layer gives them no special treatment, and the system calls L1 introduced in Pectra are not considered.

After Isthmus, ExecutionPayload carries an additional withdrawalsRoot field.

From Isthmus onward, engine_newPayloadV4 is used. The executionRequests parameter MUST be an empty array.

Different OP Stack variants consume resources differently and need a more flexible pricing model. To allow more customizable fee structures, Isthmus adds a new term to the fee calculation — the operatorFee — parameterized by two scalars, the operatorFeeScalar and the operatorFeeConstant.

The operator fee is wired directly into the EVM, alongside the standard gas fee and Base’s L1 data fee. It behaves like the EVM’s existing fees, just with a different beneficiary account.

operatorFee = (gas * operatorFeeScalar / 10^6) + operatorFeeConstant

Where:

  • gas is the amount of gas that the transaction used. When calculating the amount of gas that is bought at the beginning of the transaction, this should be the gas_limit. When determining how much gas should be refunded, based off of how much of the gas_limit the transaction used, this should be the gas_used.
  • operatorFeeScalar is a uint32 scalar set by the chain operator, scaled by 1e6.
  • operatorFeeConstant is a uint64 scalar set by the chain operator.

The operator fee’s maximum value fits in 77 bits, which follows from the maximum input parameters:

operatorFee_max = (uint64_max * uint32_max / 10^6) + uint64_max ≈ 7.924660923989131 * 10^22

So implementations that compute it with uint256 types do not need to check for overflow.

Deposit transactions are never charged an operator fee: whatever the operator fee parameters are, it is zero for them. They also receive no operator-fee gas refund, since they never bought that gas in the first place.

Like the EVM’s other fees, the operator fee is charged following this pattern:

  1. During pre-execution validation, the account must have enough ETH to cover the existing worst-case gas + L1 data fees as well as the worst-case operator fee (for deposits, the worst-case fee is 0). To compute this value, use the fee formula with gas set to the gas_limit of the transaction, and add it to the existing worst-case transaction fee.
  2. When buying gas prior to execution, charge the account the worst-case operator fee. To compute this value, use the fee formula with gas set to the gas_limit of the transaction.
  3. After execution, when issuing refunds, transactions that bought operator fee gas should be refunded the operator fee gas that was unused (i.e., the caller should only be charged the effective operator fee.) The refund should be calculated as opFeeRefund = opFeeWorstCase - opFeeActual, where:
    • opFeeWorstCase is as described in #1 + #2.
    • opFeeActual is the amount of the operator fee that was actually used. This value is computed using the fee formula with gas set to the gas_limit - gas_used + refunded_gas. refunded_gas is as described in EIP-3529.
  4. After execution, when rewarding the fee beneficiaries, send the spent operator fee to the operator fee vault. This value is exactly opFeeActual as described above.

Implementations must ensure ETH is neither minted nor destroyed as a result of the operator fee.

Because this extra fee feeds into transaction validity, the transaction pool must reject any transaction whose balance cannot cover the worst-case cost — and that worst-case cost now includes the worst-case operator fee.

operatorFeeScalar and operatorFeeConstant are loaded much like the baseFeeScalar and blobBaseFeeScalar used in the L1Fee calculation. They can be read in two interchangeable ways:

  • read from the deposited L1 attributes (operatorFeeScalar and operatorFeeConstant) of the current L2 block
  • read from the L1 Block Info contract (0x4200000000000000000000000000000000000015)
    • using the respective solidity getter functions (operatorFeeScalar, operatorFeeConstant)
    • using direct storage-reads:
      • Operator fee scalar as big-endian uint32 in slot 8 at offset 0.
      • Operator fee constant as big-endian uint64 in slot 8 at offset 4.

The collected operator fees go to a new vault, the OperatorFeeVault. As with the existing vaults, this is a hardcoded address pointing at a pre-deployed proxy, backed by a FeeVault-based deployment that routes vault funds to L1 securely.

After Isthmus activation, two new fields — operatorFeeScalar and operatorFeeConstant — are added to transaction receipts whenever at least one of them is non-zero.