eip | title | description | author | discussions-to | status | type | category | created | requires |
---|---|---|---|---|---|---|---|---|---|
7251 |
Increase the MAX_EFFECTIVE_BALANCE |
Allow validators to have larger effective balances, while maintaining the 32 ETH lower bound. |
mike (@michaelneuder), Francesco (@fradamt), dapplion (@dapplion), Mikhail (@mkalinin), Aditya (@adiasg), Justin (@justindrake), lightclient (@lightclient) |
Review |
Standards Track |
Core |
2023-06-28 |
7002, 7685 |
Increases the constant MAX_EFFECTIVE_BALANCE
, while keeping the minimum staking balance 32 ETH
. This permits large node operators to consolidate into fewer validators while also allowing solo-stakers to earn compounding rewards and stake in more flexible increments.
As of October 3, 2023, there are currently over 830,000 validators participating in the consensus layer. The size of this set continues to grow due, in part, to the MAX_EFFECTIVE_BALANCE
, which limits the stake of a single validator to 32 ETH
. This leads to large amounts of "redundant validators", which are controlled by a single entity, possibly running on the same beacon node, but with distinct BLS signing keys. The limit on the MAX_EFFECTIVE_BALANCE
is technical debt from the original sharding design, in which subcommittees (not the attesting committee but the committee calculated in is_aggregator
) needed to be majority honest. As a result, keeping the weights of subcommittee members approximately equal reduced the risk of a single large validator containing too much influence. Under the current design, these subcommittees are only used for attestation aggregation, and thus only have a 1/N
honesty assumption.
With the security model of the protocol no longer dependent on a low value for MAX_EFFECTIVE_BALANCE
, we propose raising this value while keeping the minimum validator threshold of 32 ETH
. This increase aims to reduce the validator set size, thereby reducing the number of P2P messages over the network, the number of BLS signatures that need to be aggregated each epoch, and the BeaconState
memory footprint. This change adds value for both small and large validators. Large validators can consolidate to run fewer validators and thus fewer beacon nodes. Small validators now benefit from compounding rewards and the ability to stake in more flexible increments (e.g., the ability to stake 40 ETH
instead of needing to accumulate 64 ETH
to run two validators today).
Name | Value | Comment |
---|---|---|
CONSOLIDATION_REQUEST_TYPE |
0x02 |
The EIP-7685 type prefix for consolidation request |
CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS |
0x |
Where to call and store relevant details about consolidation requests mechanism |
SYSTEM_ADDRESS |
0xfffffffffffffffffffffffffffffffffffffffe |
Address used to invoke system operation on contract |
EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT |
0 |
|
CONSOLIDATION_REQUEST_COUNT_STORAGE_SLOT |
1 |
|
CONSOLIDATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT |
2 |
Pointer to head of the consolidation request message queue |
CONSOLIDATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT |
3 |
Pointer to the tail of the consolidation request message queue |
CONSOLIDATION_REQUEST_QUEUE_STORAGE_OFFSET |
4 |
The start memory slot of the in-state consolidation request message queue |
MAX_CONSOLIDATION_REQUESTS_PER_BLOCK |
1 |
Maximum number of consolidation requests that can be dequeued into a block |
TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK |
1 |
|
MIN_CONSOLIDATION_REQUEST_FEE |
1 |
|
CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION |
17 |
|
EXCESS_INHIBITOR |
1181 |
Excess value used to compute the fee before the first system call |
FORK_TIMESTAMP |
TBD | Mainnet |
Name | Value |
---|---|
COMPOUNDING_WITHDRAWAL_PREFIX |
Bytes1('0x02') |
MIN_ACTIVATION_BALANCE |
Gwei(2**5 * 10**9) (32 ETH) |
MAX_EFFECTIVE_BALANCE |
Gwei(2**11 * 10**9) (2048 ETH) |
The new consolidation request is an EIP-7685 request with type 0x02
consisting of the following fields:
source_address
:Bytes20
source_pubkey
:Bytes48
target_pubkey
:Bytes48
The EIP-7685 encoding of a withdrawal request MUST be computed as the follows:
encoded = CONSOLIDATION_REQUEST_TYPE ++ rlp([source_address, source_pubkey, target_pubkey])
The contract has three different code paths, which can be summarized at a high level as follows:
- Add consolidation request - requires a
96
byte input, concatenated public keys of the source and the target validators. - Excess consolidation requests getter - if the input length is zero, return the current excess consolidation requests count.
- System process - if called by system address, pop off the consolidation requests for the current block from the queue.
If call data input to the contract is exactly 96
bytes, perform the following:
- Ensure enough ETH was sent to cover the current consolidation request fee (
check_fee()
) - Increase consolidation request count by
1
for the current block (increment_count()
) - Insert a consolidation request into the queue for the source address and pubkeys of the source and the target (
insert_withdrawal_request_into_queue()
)
Specifically, the functionality is defined in pseudocode as the function add_consolidation_request()
:
def add_consolidation_request(Bytes48: source_pubkey, Bytes48: target_pubkey):
"""
Add consolidaiton request adds new request to the consolidation request queue, so long as a sufficient fee is provided.
"""
# Verify sufficient fee was provided.
fee = get_fee()
require(msg.value >= fee, 'Insufficient value for fee')
# Increment consolidaiton request count.
count = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_COUNT_STORAGE_SLOT)
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_COUNT_STORAGE_SLOT, count + 1)
# Insert into queue.
queue_tail_index = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT)
queue_storage_slot = CONSOLIDATION_REQUEST_QUEUE_STORAGE_OFFSET + queue_tail_index * 4
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot, msg.sender)
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 1, source_pubkey[0:32])
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2, source_pubkey[32:48] ++ target_pubkey[0:16])
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 3, target_pubkey[16:48])
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT, queue_tail_index + 1)
The following pseudocode can compute the cost an individual consolidation request, given a certain number of excess consolidation requests.
def get_fee() -> int:
excess = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT)
return fake_exponential(
MIN_CONSOLIDATION_REQUEST_FEE,
excess,
CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION
)
def fake_exponential(factor: int, numerator: int, denominator: int) -> int:
i = 1
output = 0
numerator_accum = factor * denominator
while numerator_accum > 0:
output += numerator_accum
numerator_accum = (numerator_accum * numerator) // (denominator * i)
i += 1
return output // denominator
def get_excess_consolidation_requests():
count = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT)
return count
If the contract is called as SYSTEM_ADDRESS
with an empty input data, perform the following:
- The contract's queue is updated based on consolidation requests dequeued and the consolidation requests queue head/tail are reset if the queue has been cleared (
dequeue_consolidation_requests()
) - The contract's excess consolidation requests are updated based on usage in the current block (
update_excess_consolidation_requests()
) - The contract's consolidation requests count is reset to
0
(reset_consolidation_requests_count()
)
Specifically, the functionality is defined in pseudocode as the function process_consolidation_requests()
:
###################
# Public function #
###################
def process_consolidation_requests():
reqs = dequeue_consolidation_requests()
update_excess_consolidation_requests()
reset_consolidation_requests_count()
return reqs
###########
# Helpers #
###########
class ConsolidationRequest(object):
source_address: Bytes20
source_pubkey: Bytes48
target_pubkey: Bytes48
def dequeue_consolidation_requests():
queue_head_index = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT)
queue_tail_index = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT)
num_in_queue = queue_tail_index - queue_head_index
num_dequeued = min(num_in_queue, MAX_CONSOLIDATION_REQUESTS_PER_BLOCK)
reqs = []
for i in range(num_dequeue):
queue_storage_slot = CONSOLIDATION_REQUEST_QUEUE_STORAGE_OFFSET + (queue_head_index + i) * 4
source_address = address(sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot)[0:20])
source_pubkey = (
sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 1)[0:32] + sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2)[0:16]
)
target_pubkey = (
sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2)[16:32] + sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 3)[0:32]
)
req = ConsolidationRequest(
source_address=Bytes20(source_address),
source_pubkey=Bytes48(source_pubkey),
target_pubkey=Bytes48(target_pubkey)
)
reqs.append(req)
new_queue_head_index = queue_head_index + num_dequeued
if new_queue_head_index == queue_tail_index:
# Queue is empty, reset queue pointers
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT, 0)
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_TAIL_STORAGE_SLOT, 0)
else:
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_QUEUE_HEAD_STORAGE_SLOT, new_queue_head_index)
return reqs
def update_excess_consolidation_requests():
previous_excess = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT)
# Check if excess needs to be reset to 0 for first iteration after activation
if previous_excess == EXCESS_INHIBITOR:
previous_excess = 0
count = sload(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_COUNT_STORAGE_SLOT)
new_excess = 0
if previous_excess + count > TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK:
new_excess = previous_excess + count - TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, EXCESS_CONSOLIDATION_REQUESTS_STORAGE_SLOT, new_excess)
def reset_consolidation_requests_count():
sstore(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_COUNT_STORAGE_SLOT, 0)
caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0x98
jumpi
calldatasize
iszero
iszero
push1 0x28
jumpi
push0
sload
push0
mstore
push1 0x20
push0
return
jumpdest
calldatasize
push1 0x60
eq
iszero
push2 0x0144
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 0x0144
jumpi
push1 0x01
sload
push1 0x01
add
push1 0x01
sstore
push1 0x03
sload
dup1
push1 0x04
mul
push1 0x04
add
caller
dup2
sstore
push1 0x01
add
push0
calldataload
dup2
sstore
push1 0x01
add
push1 0x20
calldataload
dup2
sstore
push1 0x01
add
push1 0x40
calldataload
swap1
sstore
push1 0x01
add
push1 0x03
sstore
stop
jumpdest
push1 0x03
sload
push1 0x02
sload
dup1
dup3
sub
dup1
push1 0x01
gt
push1 0xac
jumpi
pop
push1 0x01
jumpdest
push0
jumpdest
dup2
dup2
eq
push1 0xf1
jumpi
dup1
push1 0x74
mul
dup4
dup3
add
push1 0x04
mul
push1 0x04
add
dup1
sload
swap1
push1 0x01
add
dup1
sload
swap1
push1 0x01
add
dup1
sload
swap1
push1 0x01
add
sload
swap3
push1 0x60
shl
dup5
mstore
swap1
dup4
push1 0x14
add
mstore
dup3
push1 0x34
add
mstore
swap1
push1 0x54
add
mstore
push1 0x01
add
push1 0xae
jump
jumpdest
swap2
add
dup1
swap3
eq
push2 0x0103
jumpi
swap1
push1 0x02
sstore
push2 0x010e
jump
jumpdest
swap1
pop
push0
push1 0x02
sstore
push0
push1 0x03
sstore
jumpdest
push0
sload
dup1
push2 0x049d
eq
iszero
push2 0x011d
jumpi
pop
push0
jumpdest
push1 0x01
sload
push1 0x01
dup3
dup3
add
gt
push2 0x0132
jumpi
pop
pop
push0
push2 0x0138
jump
jumpdest
add
push1 0x01
swap1
sub
jumpdest
push0
sstore
push0
push1 0x01
sstore
push1 0x74
mul
push0
return
jumpdest
push0
push0
revert
The consolidation requests contract is deployed like any other smart contract. A special synthetic address is generated by working backwards from the desired deployment transaction:
{
"type": "0x0",
"nonce": "0x0",
"to": null,
"gas": "0x3d090",
"gasPrice": "0xe8d4a51000",
"maxPriorityFeePerGas": null,
"maxFeePerGas": null,
"value": "0x0",
"input": "0x61049d5f5561014880600f5f395ff33373fffffffffffffffffffffffffffffffffffffffe146098573615156028575f545f5260205ff35b36606014156101445760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061014457600154600101600155600354806004026004013381556001015f35815560010160203581556001016040359055600101600355005b6003546002548082038060011160ac575060015b5f5b81811460f15780607402838201600402600401805490600101805490600101805490600101549260601b84529083601401528260340152906054015260010160ae565b9101809214610103579060025561010e565b90505f6002555f6003555b5f548061049d141561011d57505f5b6001546001828201116101325750505f610138565b01600190035b5f555f6001556074025ff35b5f5ffd",
"v": "0x1b",
"r": "0x539",
"s": "0x13370066aa8fbe21ca1511",
"hash": "0x6de32a89ba4c0592fd2453f8838d6bb69d93a102723de5f4b0f046ddcf8b8fa9"
}
Sender: 0xd6e25886D7B986C394156C31a48e84Ee0BA71f72
Address: 0x00b42dbF2194e931E80326D950320f7d9Dbeac02
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 requests validations) clienst software MUST take the following steps:
- Call the contract as
SYSTEM_ADDRESS
and empty input data to trigger the system subroutine execute. - Check that consolidation requests in the EIP-7685 requests list matches the list returned from
dequeue_consolidation_requests()
function of the smart contract respecting the order of the returned requests. If this condition does not hold, the block MUST be deemed invalid.
The defining features of this EIP are:
- Increasing the
MAX_EFFECTIVE_BALANCE
, while creating aMIN_ACTIVATION_BALANCE
. The core feature of allowing variable size validators. - Allowing for multiple validator indices to be combined through the protocol. A mechanism by which large node operators can combine validators without cycling through the exit and activation queues.
- Permitting validators to set custom ceilings for their validator to indicate where the partial withdrawal sweep activates. Allows more flexibility in defining the "ceiling" of a validator's effective balance.
- Adding execution layer partial withdrawals (part of EIP-7002). Allowing Execution Layer messages to trigger partial withdrawals in addition to full exits (e.g., a
100 ETH
validator can remove up to68 ETH
without exiting the validator). - Removing the initial slashing penalty (still in discussion). This reduces the risk of consolidation for large validators.
The Rationale section contains an explanation for each of these proposed core features. A sketch of the resulting changes to the consensus layer is included below.
- Add
COMPOUNDING_WITHDRAWAL_PREFIX
andMIN_ACTIVATION_BALANCE
constants, while updating the value ofMAX_EFFECTIVE_BALANCE
. - Create the
PendingDeposit
container, which is used to track incoming deposits in the weight-based rate limiting mechanism. - Update the
BeaconState
with fields needed for deposit and exit queue weight-based rate limiting. - Modify
is_eligible_for_activation_queue
to check againstMIN_ACTIVATION_BALANCE
rather thanMAX_EFFECTIVE_BALANCE
. - Modify
get_validator_churn_limit
to depend on the validator weight rather than the validator count. - Create a helper
compute_exit_epoch_and_update_churn
to calculate the exit epoch based on the current pending withdrawals. - Modify
initiate_validator_exit
to rate limit the exit queue by balance rather than the number of validators. - Modify
initialize_beacon_state_from_eth1
to useMIN_ACTIVATION_BALANCE
. - Modify
process_registry_updates
to activate all eligible validators. - Add a per-epoch helper,
process_pending_balance_deposits
, to consume some of the pending deposits. - Modify
get_validator_from_deposit
to initialize the effective balance to zero (it's updated by the pending deposit flow). - Modify
apply_deposit
to store incoming deposits instate.pending_balance_deposits
. - Modify
is_aggregator
to be weight-based. - Modify
compute_weak_subjectivity_period
to use the new churn limit function. - Add
has_compounding_withdrawal_credential
to check for the0x02
credential. - Modify
is_fully_withdrawable_validator
to check for compounding credentials. - Add
get_validator_excess_balance
to calculate the excess balance of validators. - Modify
is_partially_withdrawable_validator
to check for excess balance. - Modify
get_expected_withdrawals
to use excess balance.
This EIP aims to reduce the total number of validators without changing anything about the economic security of the protocol. It provides a mechanism by which large node operators who control significant amounts of stake can consolidate into fewer validators. We analyze the reasoning behind each of the core features.
- Increasing the
MAX_EFFECTIVE_BALANCE
, while creating aMIN_ACTIVATION_BALANCE
.- While increasing the
MAX_EFFECTIVE_BALANCE
to allow larger-stake validators, it is important to keep the lower bound of32 ETH
(by introducing a new constant –MIN_ACTIVATION_BALANCE
) to encourage solo-staking.
- While increasing the
- Allowing for multiple validator indices to be combined through the protocol.
- For large staking pools that already control thousands of validators, exiting and re-entering would be extremely slow and costly. The adoption of the EIP will be much higher by allowing in-protocol consolidation.
- Permitting validators to set custom ceilings for their validator to indicate where the partial withdrawal sweep activates.
- To get access to rewards, validators might want the flexibility to set custom ceilings for their effective balance. This gives them more optionality and is a clean way to continue supporting the partial-withdrawal sweep (a gasless way to extract rewards).
- Adding execution layer partial withdrawals (part of EIP-7002).
- For validators that choose to raise their effective balance ceiling, allowing for custom partial withdrawals triggered from the execution layer increases the flexibility of the staking configurations. Validators can choose when and how much they withdraw but will have to pay gas for the EL transaction.
- Removing the initial slashing penalty (still in discussion).
- To encourage consolidation, we could modify the slashing penalties. The biggest hit comes from the initial penalty of
1/32
of the validator's effective balance. Since this scales linearly on the effective balance, the higher-stake validators directly incur higher risk. By changing the scaling properties, we could make consolidation more attractive.
- To encourage consolidation, we could modify the slashing penalties. The biggest hit comes from the initial penalty of
This EIP introduces backward incompatible changes to the block validation rule set on the consensus layer and must be accompanied by a hard fork. These changes do not break anything related to current user activity and experience.
This change modifies committees and churn, but doesn't significantly impact the security properties.
Given full consolidation as the worst case, the probability of an adversarial takeover of a committee remains low. Even in a high consolidation scenario, the required share of honest validators remains well below the 2/3 supermajority needed for finality.
In the original sharding roadmap, subcommittees were required to be secure with extremely high probability. Now with the sole responsibility of attestation aggregation, we only require each committee to have at least one honest aggregator. Currently, aggregators are selected through a VRF lottery, targeting several validator units that can be biased by non-consolidated attackers. This proposal changes the VRF lottery to consider weight, so the probability of having at least one honest aggregator is not worse.
Proposer selection is already weighted by the ratio of their effective balance to MAX_EFFECTIVE_BALANCE
. Due to the lower probabilities, this change will slightly increase the time it takes to calculate the next proposer index.
Sync committee selection is also already weighted by effective balance, so this proposal does not require modifications to the sync protocol. Light clients can still check that a super-majority of participants have signed an update irrespective of their weights since we maintain a weight-based selection probability.
This proposal maintains the activation and exit churn invariants limiting active weight instead of validator count. Balance top-ups are now handled explicitly, being subject to the same activation queue as full deposits.
Copyright and related rights waived via CC0.