From 35589d35c40576d932923542b31a4d8f7812c3e7 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Mon, 25 Mar 2024 04:48:35 -0600 Subject: [PATCH] Update EIP-7002: convert to system contract Merged by EIP-Bot. --- EIPS/eip-7002.md | 567 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 402 insertions(+), 165 deletions(-) diff --git a/EIPS/eip-7002.md b/EIPS/eip-7002.md index 1ebc0631b856a3..4dc4869004ccee 100644 --- a/EIPS/eip-7002.md +++ b/EIPS/eip-7002.md @@ -2,7 +2,7 @@ eip: 7002 title: Execution layer triggerable exits description: Allows validators to trigger exits via their execution layer (0x01) withdrawal credentials -author: Danny Ryan (@djrtwo), Mikhail Kalinin (@mkalinin), Ansgar Dietrichs (@adietrichs), Hsiao-Wei Wang (@hwwhww) +author: Danny Ryan (@djrtwo), Mikhail Kalinin (@mkalinin), Ansgar Dietrichs (@adietrichs), Hsiao-Wei Wang (@hwwhww), lightclient (@lightclient) discussions-to: https://ethereum-magicians.org/t/eip-7002-execution-layer-triggerable-exits/14195 status: Draft type: Standards Track @@ -12,9 +12,9 @@ created: 2023-05-09 ## Abstract -Adds a new *stateful* precompile that allows validators to trigger exits to the beacon chain from their execution layer (0x01) withdrawal credentials. +Adds a new mechanism to allow validators to trigger exits to the beacon chain from their execution layer (0x01) withdrawal credentials. -These new execution layer exit messages are appended to the execution layer block to reading by the consensus layer. +These new execution layer exit messages are appended to the execution layer block and then processed by the consensus layer. ## Motivation @@ -38,7 +38,8 @@ Note, 0x00 withdrawal credentials can be changed into 0x01 withdrawal credential | Name | Value | Comment | | - | - | - | -| `VALIDATOR_EXIT_PRECOMPILE_ADDRESS` | *TBD* | Where to call and store relevant details about exit mechanism | +| `VALIDATOR_EXIT_ADDRESS` | `0x0f1ee3e66777F27a7703400644C6fCE41527E017` | Where to call and store relevant details about exit mechanism | +| `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` | Address used to invoke system operation on contract | `EXCESS_EXITS_STORAGE_SLOT` | 0 | | | `EXIT_COUNT_STORAGE_SLOT` | 1 | | | `EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT` | 2 | Pointer to head of the exit message queue | @@ -48,7 +49,6 @@ Note, 0x00 withdrawal credentials can be changed into 0x01 withdrawal credential | `TARGET_EXITS_PER_BLOCK` | 2 | | | `MIN_EXIT_FEE` | 1 | | | `EXIT_FEE_UPDATE_FRACTION` | 17 | | -| `EXCESS_RETURN_GAS_STIPEND` | 2300 | | ### Execution layer @@ -72,64 +72,54 @@ rlp_encoded_exit = RLP([ ]) ``` -#### Validator Exit precompile +#### Validator Exit Contract -The precompile requires a single `48` byte input, aliased to `validator_pubkey`. +The contract has three different code paths, which can be summarized at a high level as follows: -`CALL`s to `VALIDATOR_EXIT_PRECOMPILE_ADDRESS` perform the following: +1. Add exit - requires a `48` byte input, aliased to `validator_pubkey`. +2. Excess exits getter - if the input length is zero, return the current excess exits count. +3. System process - if called by system address, pop off the exits for the current block from the queue. + +##### Add Exit + +If call data input to the contract is exactly `48` bytes, perform the following: * Ensure enough ETH was sent to cover the current exit fee (`check_exit_fee()`) * Increase exit count by 1 for the current block (`increment_exit_count()`) * Insert an exit into the queue for the source address and validator pubkey (`insert_exit_to_queue()`) -* Return any unspent ETH in excess of the exit fee with an `EXCESS_RETURN_GAS_STIPEND` gas stipend (`return_excess_payment()`) Specifically, the functionality is defined in pseudocode as the function `trigger_exit()`: ```python -################### -# Public function # -################### - def trigger_exit(Bytes48: validator_pubkey): - check_exit_fee(msg.value) - increment_exit_count() - insert_exit_to_queue(msg.sender, validator_pubkey) - return_excess_payment(msg.value) - -################### -# Primary Helpers # -################### + """ + Trigger exit adds new exit to the exit queue, so long as a sufficient fee is provided. + """ -def check_exit_fee(int: fee_sent): + # Verify sufficient fee was provided. exit_fee = get_exit_fee() - require(fee_sent >= exit_fee, 'Insufficient exit fee') - # Note: consider mapping `MIN_EXIT_FEE` -> 0 fee + require(msg.value >= exit_fee, 'Insufficient exit fee') -def insert_exit_to_queue(address: source_address, Bytes48: validator_pubkey): - queue_tail_index = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT) - # Each exit takes 3 storage slots: 1 for source_address, 2 for validator_pubkey + # Increment exit count. + exit_count = sload(VALIDATOR_EXIT_ADDRESS, EXIT_COUNT_STORAGE_SLOT) + sstore(VALIDATOR_EXIT_ADDRESS, EXIT_COUNT_STORAGE_SLOT, exit_count + 1) + + # Insert into queue. + queue_tail_index = sload(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT) queue_storage_slot = EXIT_MESSAGE_QUEUE_STORAGE_OFFSET + queue_tail_index * 3 - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, queue_storage_slot, source_address) - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, queue_storage_slot + 1, validator_pubkey[0:32]) - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, queue_storage_slot + 2, validator_pubkey[32:48]) - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, queue_tail_index + 1) - -def increment_exit_count(): - exit_count = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_COUNT_STORAGE_SLOT) - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_COUNT_STORAGE_SLOT, exit_count + 1) - -def return_excess_payment(int: fee_sent, address: source_address): - excess_payment = fee_sent - get_exit_fee() - if excess_payment > 0: - (bool sent, bytes memory data) = source_address.call{value: excess_payment, gas: EXCESS_RETURN_GAS_STIPEND}("") - require(sent, "Failed to return excess fee payment") - -###################### -# Additional Helpers # -###################### - + sstore(VALIDATOR_EXIT_ADDRESS, queue_storage_slot, msg.sender) + sstore(VALIDATOR_EXIT_ADDRESS, queue_storage_slot + 1, validator_pubkey[0:32]) + sstore(VALIDATOR_EXIT_ADDRESS, queue_storage_slot + 2, validator_pubkey[32:48]) + sstore(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, queue_tail_index + 1) +``` + +###### Fee calculation + +The following pseudocode can compute the cost an individual exit, given a certain number of excess exits. + +```python def get_exit_fee() -> int: - excess_exits = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXCESS_EXITS_STORAGE_SLOT) + excess_exits = sload(VALIDATOR_EXIT_ADDRESS, EXCESS_EXITS_STORAGE_SLOT) return fake_exponential( MIN_EXIT_FEE, excess_exits, @@ -147,141 +137,402 @@ def fake_exponential(factor: int, numerator: int, denominator: int) -> int: return output // denominator ``` -##### Gas cost +##### Excess Exit Getter -TBD - -Once functionality is reviewed and solidified, we'll estimate the cost of running the above computations fully in the EVM, and then potentially apply some discount due to reduced EVM overhead of being able to execute the above logic natively. - -#### Block structure - -Beginning with the `FORK_BLOCK`, the block body **MUST** be appended with a list of exit operations. RLP encoding of the extended block body structure **MUST** be computed as follows: +When the input to the contract is length zero, interpret this as a get request for the current excess exits count. ```python -block_body_rlp = RLP([ - field_0, - ..., - # Latest block body field before `exits` - field_n, - - [exit_0, ..., exit_k], -]) -``` - -Beginning with the `FORK_BLOCK`, the block header **MUST** be appended with the new **`exits_root`** field. The value of this field is the trie root committing to the list of exits in the block body. **`exits_root`** field value **MUST** be computed as follows: - -```python -def compute_trie_root_from_indexed_data(data): - trie = Trie.from([(i, obj) for i, obj in enumerate(data)]) - return trie.root - -block.header.exits_root = compute_trie_root_from_indexed_data(block.body.exits) -``` - -#### Block validity - -Beginning with the `FORK_BLOCK`, client software **MUST** extend block validity rule set with the following conditions: - -1. Value of **`exits_root`** block header field equals to the trie root committing to the list of exit operations contained in the block. To illustrate: - -```python -def compute_trie_root_from_indexed_data(data): - trie = Trie.from([(i, obj) for i, obj in enumerate(data)]) - return trie.root - -assert block.header.exits_root == compute_trie_root_from_indexed_data(block.body.exits) -``` - -2. The list of exit operations contained in the block body **MUST** be equivalent to list of exits at the head of the exit precompile's exit message queue up to the maximum of `MAX_EXITS_PER_BLOCK`, respecting the order in the queue. This validation **MUST** be run after all transactions in the current block are processed and **MUST** be run before per-block precompile storage calculations (i.e. a call to `update_exit_precompile()`) are performed. To illustrate: - -```python -class ValidatorExit(object): - source_address: Bytes20 - validator_pubkey: Bytes48 - -queue_head_index = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT) -queue_tail_index = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT) -num_exits_in_queue = queue_tail_index - queue_head_index -num_exits_to_dequeue = min(num_exits_in_queue, MAX_EXITS_PER_BLOCK) - -# Retrieve exits from the queue -expected_exits = [] -for i in range(num_exits_to_dequeue): - queue_storage_slot = EXIT_MESSAGE_QUEUE_STORAGE_OFFSET + (queue_head_index + i) * 3 - source_address = address(sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, queue_storage_slot)[0:20]) - validator_pubkey = ( - sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, queue_storage_slot + 1)[0:32] + sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, queue_storage_slot + 2)[0:16] - ) - exit = ValidatorExit( - source_address=Bytes20(source_address), - validator_pubkey=Bytes48(validator_pubkey), - ) - expected_exits.append(exit) - -# Compare retrieved exits to the list in the block body -assert block.body.exits == expected_exits +def get_excess_exits(): + count = sload(VALIDATOR_EXIT_ADDRESS, EXCESS_EXITS_STORAGE_SLOT) + return count ``` -A block that does not satisfy the above conditions **MUST** be deemed invalid. - -#### Block processing +##### System Call -##### Per-block precompile storage calculations +At the end of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. after processing all transactions and after performing the block body exit validations), call the contract as `SYSTEM_ADDRESS` and perform the following: -At the end of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. after processing all transactions and after performing the block body exit validations): +* The exit contract's exit queue is updated based on exits dequeued and the exit queue head/tail are reset if the queue has been cleared (`dequeue_exits()`) +* The exit contracts’s excess exits are updated based on usage in the current block (`update_excess_exits()`) +* The exit contracts's exit count is reset to 0 (`reset_exit_count()`) -* The exit precompile's exit queue is updated based on exits dequeued and the exit queue head/tail are reset if the queue has been cleared (`update_exit_queue()`) -* The exit precompile’s excess exits are updated based on usage in the current block (`update_excess_exits()`) -* The exit precompile's exit count is reset to 0 (`reset_exit_count()`) - -Specifically, the functionality is defined in pseudocode as the function `update_exit_precompile()`: +Specifically, the functionality is defined in pseudocode as the function `read_exits()`: ```python ################### # Public function # ################### -def update_exit_precompile(): - update_exit_queue() +def read_exits(): + exits = dequeue_exits() update_excess_exits() reset_exit_count() + return exits ########### # Helpers # ########### -def update_exit_queue(): - queue_head_index = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT) - queue_tail_index = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT) - +class ValidatorExit(object): + source_address: Bytes20 + validator_pubkey: Bytes48 + +def dequeue_exits(): + queue_head_index = sload(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT) + queue_tail_index = sload(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT) num_exits_in_queue = queue_tail_index - queue_head_index num_exits_dequeued = min(num_exits_in_queue, MAX_EXITS_PER_BLOCK) + + exits = [] + for i in range(num_exits_dequeue): + queue_storage_slot = EXIT_MESSAGE_QUEUE_STORAGE_OFFSET + (queue_head_index + i) * 3 + source_address = address(sload(VALIDATOR_EXIT_ADDRESS, queue_storage_slot)[0:20]) + validator_pubkey = ( + sload(VALIDATOR_EXIT_ADDRESS, queue_storage_slot + 1)[0:32] + sload(VALIDATOR_EXIT_ADDRESS, queue_storage_slot + 2)[0:16] + ) + exit = ValidatorExit( + source_address=Bytes20(source_address), + validator_pubkey=Bytes48(validator_pubkey), + ) + exits.append(exit) + new_queue_head_index = queue_head_index + num_exits_dequeued if new_queue_head_index == queue_tail_index: # Queue is empty, reset queue pointers - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, 0) - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, 0) + sstore(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, 0) + sstore(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, 0) else: - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, new_queue_head_index) + sstore(VALIDATOR_EXIT_ADDRESS, EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, new_queue_head_index) + + return exits def update_excess_exits(): - previous_excess_exits = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXCESS_EXITS_STORAGE_SLOT) - exit_count = sload(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_COUNT_STORAGE_SLOT) + previous_excess_exits = sload(VALIDATOR_EXIT_ADDRESS, EXCESS_EXITS_STORAGE_SLOT) + exit_count = sload(VALIDATOR_EXIT_ADDRESS, EXIT_COUNT_STORAGE_SLOT) new_excess_exits = 0 if previous_excess_exits + exit_count > TARGET_EXITS_PER_BLOCK: new_excess_exits = previous_excess_exits + exit_count - TARGET_EXITS_PER_BLOCK - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXCESS_EXITS_STORAGE_SLOT, new_excess_exits) + sstore(VALIDATOR_EXIT_ADDRESS, EXCESS_EXITS_STORAGE_SLOT, new_excess_exits) def reset_exit_count(): - sstore(VALIDATOR_EXIT_PRECOMPILE_ADDRESS, EXIT_COUNT_STORAGE_SLOT, 0) + sstore(VALIDATOR_EXIT_ADDRESS, EXIT_COUNT_STORAGE_SLOT, 0) ``` +#### Bytecode + +```asm +caller +push20 0xfffffffffffffffffffffffffffffffffffffffe +eq +push1 0x90 +jumpi + +calldatasize +iszero +iszero +push1 0x28 +jumpi + +push0 +sload +push0 +mstore +push1 0x20 +push0 +return + +jumpdest +calldatasize +push1 0x30 +eq +iszero +push2 0x0132 +jumpi + +push1 0x11 +push0 +sload +push1 0x01 +dup3 +mul +push1 0x01 +swap1 +push0 + +jumpdest +push0 +dup3 +gt +iszero +push1 0x59 +jumpi + +dup2 +add +swap1 +dup4 +mul +dup5 +dup4 +mul +swap1 +div +swap2 +push1 0x01 +add +swap2 +swap1 +push1 0x3e +jump + +jumpdest +swap1 +swap4 +swap1 +div +callvalue +lt +push2 0x0132 +jumpi + +push1 0x01 +sload +push1 0x01 +add +push1 0x01 +sstore +push1 0x03 +sload +dup1 +push1 0x03 +mul +push1 0x04 +add +caller +dup2 +sstore +push1 0x01 +add +push0 +calldataload +dup2 +sstore +push1 0x01 +add +push1 0x20 +calldataload +swap1 +sstore +push1 0x01 +add +push1 0x03 +sstore +stop + +jumpdest +push1 0x03 +sload +push1 0x02 +sload +dup1 +dup3 +sub +dup1 +push1 0x10 +gt +push1 0xa4 +jumpi + +pop +push1 0x10 + +jumpdest +push0 + +jumpdest +dup2 +dup2 +eq +push1 0xed +jumpi + +dup1 +push1 0x44 +mul +dup4 +dup3 +add +push1 0x03 +mul +push1 0x04 +add +dup1 +sload +swap1 +push1 0x01 +add +dup1 +sload +swap1 +push1 0x01 +add +sload +swap2 +push1 0x60 +shl +dup2 +push1 0xa0 +shr +or +dup4 +mstore +push1 0x60 +shl +dup2 +push1 0xa0 +shr +or +dup3 +push1 0x20 +add +mstore +push1 0x60 +shl +swap1 +push1 0x40 +add +mstore +push1 0x01 +add +push1 0xa6 +jump + +jumpdest +swap2 +add +dup1 +swap3 +eq +push1 0xfe +jumpi + +swap1 +push1 0x02 +sstore +push2 0x0109 +jump + +jumpdest +swap1 +pop +push0 +push1 0x02 +sstore +push0 +push1 0x03 +sstore + +jumpdest +push0 +sload +push1 0x01 +sload +push1 0x02 +dup3 +dup3 +add +gt +push2 0x0120 +jumpi + +pop +pop +push0 +push2 0x0126 +jump + +jumpdest +add +push1 0x02 +swap1 +sub + +jumpdest +push0 +sstore +push0 +push1 0x01 +sstore +push1 0x44 +mul +push0 +return + +jumpdest +push0 +push0 +revert +``` + +#### Deployment + +The validator exit contract is deployed like any other smart contract. A special synthetic address is generated by working backwards from the desired deployment transaction: + +```json +{ + "type": "0x0", + "nonce": "0x0", + "to": null, + "gas": "0x3d090", + "gasPrice": "0xe8d4a51000", + "maxPriorityFeePerGas": null, + "maxFeePerGas": null, + "value": "0x0", + "input": "0x61013680600a5f395ff33373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b36603014156101325760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565 +b90939004341061013257600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460ed578060440283820160030260040180549060010180549060 +0101549160601b8160a01c17835260601b8160a01c17826020015260601b906040015260010160a6565b910180921460fe5790600255610109565b90505f6002555f6003555b5f546001546002828201116101205750505f610126565b01600290035b5f555f600155604 +4025ff35b5f5ffd", + "v": "0x1b", + "r": "0x539", + "s": "0x1337008ee4a2345a141312", + "hash": "0xa9398c656be137b51d3fbc36b8fac59835a258b2b77ed157dd50b0daa77720eb" +} +``` + +``` +Sender: 0xA5Ed216E97083fA264b34C2F47fbb887E57C3AbA +Address: 0x0f1ee3e66777F27a7703400644C6fCE41527E017 +``` + +#### Block structure + +Beginning with the `FORK_BLOCK`, the block body **MUST** be appended with the list of exit operations returned by `read_exits()`. RLP encoding of the extended block body structure **MUST** be computed as follows: + +```python +block_body_rlp = RLP([ + field_0, + ..., + # Latest block body field before `exits` + field_n, + + [exit_0, ..., exit_k], +]) +``` + +Beginning with the `FORK_BLOCK`, the block header **MUST** be appended with the new **`exits_root`** field. The value of this field is the trie root committing to the list of exits in the block body. **`exits_root`** field value **MUST** be computed as follows: + +```python +def compute_trie_root_from_indexed_data(data): + trie = Trie.from([(i, obj) for i, obj in enumerate(data)]) + return trie.root + +block.header.exits_root = compute_trie_root_from_indexed_data(block.body.exits) +``` ### Consensus layer - +[Full specification](https://github.com/ethereum/consensus-specs/blob/5d80b1954a4b7a121aa36143d50b366727b66cbc/specs/_features/eip7002/beacon-chain.md>) Sketch of spec: @@ -292,45 +543,31 @@ Sketch of spec: ## Rationale -### Stateful precompile - - - -This specification utilizes a *stateful* precompile for simplicity and future-proofness. While precompiles are a well-known quantity, none to date have associated EVM state at the address. - -The alternative designs are (1) to utilize a precompile or opcode for the functionality and write a separate specified space in the EVM -- e.g. `0xFF..FF` -- or (2) to place the required state into the block and require the previous block header as an input into the state transition function (e.g. like [EIP-1559](./eip-1559.md) `base_fee`). - -Alternative design (1) is essentially using a stateful precompile but dissociating the state into a separate address. At first glance, this split appears unnecessarily convoluted when we could store the location of the `CALL` and the associated state in the same address. That said, there might be unexpected engineering constraints around precompiles in existing clients that make this a preferable path. - -Alternative design (2) has two main drawbacks. The first is that with the message queue contains an unbounded amount of state (as opposed to simple the `base_fee` in the similar EIP-1559 design). Additionally, even if the state was constrained to a single variable or two, this design pattern reinforces that the Ethereum state transition function signature be more than `f(pre_state, block) -> post_state` by putting another dependency on the `pre_block_header`. These additional dependencies hinder the elegance of future stateless designs. Providing these dependencies within the EVM state as specified, allows for them to show up naturally in block witnesses. - ### `validator_pubkey` field - - Multiple validators can utilize the same execution layer withdrawal credential, thus the `validator_pubkey` field is utilized to disambiguate which validator is being exited. -Note, `validator_index` also disambiguates validators but is not used because the execution-layer cannot currently trustlessly ascertain this value. +Note, `validator_index` also disambiguates validators. ### Exit message queue -The exit precompile maintains and in-state queue of exit messages to be dequeued each block into the block and thus into the execution layer. +The exit contract maintains and in-state queue of exit messages to be dequeued each block into the block and thus into the execution layer. The number of exits that can be passed into the consensus layer are bound by `MAX_EXITS_PER_BLOCK` to bound the load both on the block size as well as on the consensus layer processing. `16` has been chosen for `MAX_EXITS_PER_BLOCK` to be in line with the bounds of similar operations on the beacon chain -- e.g. `VoluntaryExit` and `Deposit`. -Although there is a maximum number of exits that can passed to the consensus layer each block, the execution layer gas limit can provide for far more calls to the exit precompile at each block. The queue then allows for these calls to successfully be made while still maintaining a system rate limit. +Although there is a maximum number of exits that can passed to the consensus layer each block, the execution layer gas limit can provide for far more calls to the exit contract at each block. The queue then allows for these calls to successfully be made while still maintaining a system rate limit. -The alternative design considered was to have calls to the exit precompile fail after `MAX_EXITS_PER_BLOCK` successful calls were made within the context of a single block. This would eliminate the need for the message queue, but would come at the cost of a bad UX of precompile call failures in times of high exiting. The complexity to mitigate this bad UX is relatively low and is currently favored. +The alternative design considered was to have calls to the exit contract fail after `MAX_EXITS_PER_BLOCK` successful calls were made within the context of a single block. This would eliminate the need for the message queue, but would come at the cost of a bad UX of contract call failures in times of high exiting. The complexity to mitigate this bad UX is relatively low and is currently favored. ### Utilizing `CALL` to return excess payment -Calls to the exit precompile require a fee payment defined by the current state of the precompile. Smart contracts can easily perform a read/calculation to pay the precise fee, whereas EOAs will likely need to compute and send some amount over the current fee at time of signing the transaction. This will result in EOAs having fee payment overages in the normal case. These should be returned to the caller. +Calls to the exit contract require a fee payment defined by the current state of the contract. Smart contracts can easily perform a read/calculation to pay the precise fee, whereas EOAs will likely need to compute and send some amount over the current fee at time of signing the transaction. This will result in EOAs having fee payment overages in the normal case. These should be returned to the caller. -There are two potential designs to return excess fee payments to the caller (1) use an EVM `CALL` with some gas stipend or (2) have special functionality to allow the precompile to "credit" the caller's account with the excess fee. +There are two potential designs to return excess fee payments to the caller (1) use an EVM `CALL` with some gas stipend or (2) have special functionality to allow the contract to "credit" the caller's account with the excess fee. -Option (1) has been selected in the current specification because it utilizes less exceptional functionality and is likely simpler to implement and ensure correctness. The current version sends a gas stipen of 2300. This is following the (outdated) solidity pattern primarily to simplify precompile gas accounting (allowing it to be a fixed instead of dynamic cost). The `CALL` could forward the maximum allowed gas but would then require the cost of the precompile to be dynamic. +Option (1) has been selected in the current specification because it utilizes less exceptional functionality and is likely simpler to implement and ensure correctness. The current version sends a gas stipen of 2300. This is following the (outdated) solidity pattern primarily to simplify contract gas accounting (allowing it to be a fixed instead of dynamic cost). The `CALL` could forward the maximum allowed gas but would then require the cost of the contract to be dynamic. -Option (2) utilizes custom logic (exceptional to base EVM logic) to credit the excess back to the callers balance. This would potentially simplify concerns around precompile gas costs/metering, but at the cost of non-standard EVM complexity. We are open to this path, but want to solicit more input before writing it into the speficiation. +Option (2) utilizes custom logic (exceptional to base EVM logic) to credit the excess back to the callers balance. This would potentially simplify concerns around contract gas costs/metering, but at the cost of non-standard EVM complexity. We are open to this path, but want to solicit more input before writing it into the speficiation. ### Rate limiting using exit fee @@ -338,9 +575,9 @@ Transactions are naturally rate-limited in the execution layer via the gas limit There are two general approaches to combat this griefing -- (a) only allow validators to send such messages and with a limit per time period or (b) utilize an economic method to make such griefing increasingly costly. -Method (a) (not used in this EIP) would require [EIP-4788](./eip-4788.md) (the `BEACON_ROOT` opcode) against which to prove withdrawal credentials in relation to validator pubkeys as well as a data-structure to track exits per-unit-time (e.g. 4 months) to ensure that a validator cannot grief the mechanism by submitting many exits. The downsides of this method are that it requires another cross-layer EIP and that it is of higher cross-layer complexity (e.g. care that might need to be taken in future upgrades if, for example, the shape of the merkle tree of `BEACON_ROOT` changes, then the exit precompile and proof structure might need to be updated). +Method (a) (not used in this EIP) would require [EIP-4788](./eip-4788.md) (the `BEACON_ROOT` opcode) against which to prove withdrawal credentials in relation to validator pubkeys as well as a data-structure to track exits per-unit-time (e.g. 4 months) to ensure that a validator cannot grief the mechanism by submitting many exits. The downsides of this method are that it requires another cross-layer EIP and that it is of higher cross-layer complexity (e.g. care that might need to be taken in future upgrades if, for example, the shape of the merkle tree of `BEACON_ROOT` changes, then the exit contract and proof structure might need to be updated). -Method (b) has been utilized in this EIP to eliminate additional EIP requirements and to reduce cross-layer complexity to allow for correctness of this EIP (now and in the future) to be easier to analyze. The EIP-1559-style mechanism with a dynamically adjusting fee mechanism allows for users to pay `MIN_EXIT_FEE` for exits in the normal case (fewer than 2 per block on average), but scales the fee up exponentially in response to high usage (i.e. potential abuse). +Method (b) has been utilized in this EIP to eliminate additional EIP requirements and to reduce cross-layer complexity to allow for correctness of this EIP (now and in the future) to be easier to analyze. The [EIP-1559](./eip-1559.md)-style mechanism with a dynamically adjusting fee mechanism allows for users to pay `MIN_EXIT_FEE` for exits in the normal case (fewer than 2 per block on average), but scales the fee up exponentially in response to high usage (i.e. potential abuse). ### `TARGET_EXITS_PER_BLOCK` configuration value