---
tags: Oracle, withdrawals
title: Ejector oracle
---
# Ejector oracle
## Main cycle
The oracle collects data on the current state, and in the loop finds one most suitable validator to exit, then mutates the state simulating this validator's exit and repeats the loop until the number of withdrawals is enough.
```python
def get_validators_to_exit(report):
state = collect_state(report)
if is_exits_paused(state):
return []
if is_enough_eth_to_cover_demand(state, 0):
return []
validators_ids_to_exit = []
while len(validators_ids_to_exit) < state["max_validators_to_exit"]:
if not has_validators_to_exit(state):
break
predicted_eth = predict_available_eth_before_next_withdrawn(state, report)
if is_enough_eth_to_cover_demand(state, predicted_eth):
break
validator_to_exit = get_next_validator_to_exit(state, report)
validators_ids_to_exit.append(get_validator_ids(validator_to_exit))
simulate_validator_exit(state, validator_to_exit)
return validators_ids_to_exit
```
## State collection
To find the next validators to exit, Ejector Oracle should first collect the following state from Consensus and Execution layers.
- From Consensus Layer node:
- All validators and their states on the reference slot
- From Staking Router:
- Public keys of all Lido validators
- Indices of the last requested validator to exit for each Node Operator
- Validator keys statistics for each Node Operator
- From Withdrawal Queue contract:
- Uncovered withdrawal requests demand
- From Oracle contract:
- Maximum number of exit requests for the current frame
- Recently requested via Exit Bus public keys to exit
- From Lido contract:
- Recent withdrawals from Execution Layer Rewards and Withdrawal vaults
```python
def collect_state(report):
# Fetch data from Consensus Layer node
all_validators = fetch_all_validators(report.ref_slot)
# Fetch data from Keys API
used_lido_pubkeys_by_modules = fetch_used_lido_keys_by_modules(report.ref_block)
lido_modules_dict, lido_pubkeys_dict = build_lido_keys_indexes(used_lido_pubkeys_by_modules)
# Fetch data from Staking Router contract
operators_keys_stats = fetch_operators_keys_stats(lido_modules_dict)
# Fetch data from Withdrawal Queue contract
withdrawal_requests_uncovered_demand = fetch_withdrawal_requests_uncovered_demand(report.ref_block)
# Fetch data from Oracle contract
max_validators_to_exit = fetch_max_validators_to_exit(report.ref_block)
recently_requested_pubkeys_to_exit = fetch_recently_requested_pubkeys_to_exit(report.ref_block)
last_requested_validators_indices = fetch_last_requested_validators_indices(lido_modules_dict)
# Fetch data from Lido contract
predicted_pessimistic_rewards_per_epoch = fetch_predicted_pessimistic_rewards_per_epoch(report.ref_block)
# Filter validators
lido_validators = filter_lido_validators(all_validators, lido_pubkeys_dict)
exitable_lido_validators = filter_exitable_lido_validators(report.ref_epoch, lido_validators, lido_pubkeys_dict, last_requested_validators_indices)
inflight_lido_validators = filter_inflight_lido_validators(report.ref_epoch, lido_validators, lido_pubkeys_dict, last_requested_validators_indices)
# Collect Staking Router state
operators_delays = get_operators_delays_dict(lido_validators, last_requested_validators_indices, recently_requested_pubkeys_to_exit, lido_pubkeys_dict)
operators_state = collect_operators_state(report.ref_epoch, lido_validators, last_requested_validators_indices, operators_delays, lido_pubkeys_dict, operators_keys_stats)
# Collect CL state
sweep_duration_epochs = predict_average_sweep_duration_epochs(all_validators)
total_projected_validators = get_total_projected_validators(all_validators, lido_pubkeys_dict, last_requested_validators_indices)
churn_limit = get_validator_churn_limit(all_validators, report.ref_epoch)
exit_epochs = get_max_exit_epochs(all_validators)
return {
"all_validators": all_validators,
"exitable_lido_validators": exitable_lido_validators,
"inflight_lido_validators": inflight_lido_validators,
"withdrawal_requests_uncovered_demand": withdrawal_requests_uncovered_demand,
"max_validators_to_exit": max_validators_to_exit,
"predicted_pessimistic_rewards_per_epoch": predicted_pessimistic_rewards_per_epoch,
"lido_modules_dict": lido_modules_dict,
"lido_pubkeys_dict": lido_pubkeys_dict,
"operators_state": operators_state,
"operators_keys_stats": operators_keys_stats,
"sweep_duration_epochs": sweep_duration_epochs,
"total_projected_validators": total_projected_validators,
"churn_limit": churn_limit,
"exit_epochs": exit_epochs,
}
```
### Fetching data
#### Validators from CL
Oracle fetches all validators from CL at the reference slot.
```python
def fetch_all_validators(ref_slot):
pass
```
#### Lido Keys
Fetches all used Lido keys from [Keys API](https://github.com/lidofinance/lido-keys-api).
```python
def fetch_used_lido_keys_by_modules(ref_block):
(data, meta) = fetch_keys_from_api_provider()
assert meta["block_number"] > ref_block, "The data is out of date"
return data
```
#### Operators keys stats
Fetches operators' keys statistics for each Staking Module.
```python
def fetch_operators_keys_stats(lido_modules_dict):
"""
function getNodeOperatorReports(
uint256 _stakingModuleId,
uint256[] memory _nodeOperatorIds
)
"""
pass
```
#### Withdrawal requests uncovered demand
Collects data on the available ETH in the protocol buffer, in Withdrawal and Execution Layer Rewards vaults, the number of ETH required to cover withdrawal requests and returns the size of the uncovered balance.
```python
def fetch_withdrawal_requests_uncovered_demand(ref_block):
"""
Pessimistic assumption that requests are finalized 1 to 1,
without taking into account a discount
"""
withdrawal_requests_demand = fetch_withdrawal_requests_demand(ref_block)
withdrawal_vault_balance = fetch_withdrawal_vault_balance(ref_block)
el_rewards_vault_balance = fetch_el_rewards_vault_balance(ref_block)
buffered_eth = fetch_buffered_ether(ref_block)
reserved_buffered_eth = min(withdrawal_requests_demand, buffered_eth)
available_eth_to_cover_demand = withdrawal_vault_balance + el_rewards_vault_balance + reserved_buffered_eth
return max(withdrawal_requests_demand - available_eth_to_cover_demand, 0)
```
#### Report limits
Fetches report limits from the `OracleReportSanityChecker`.
```python
def fetch_max_validators_to_exit(ref_block):
"""
function getOracleReportLimits() public view returns (LimitsList memory)
returns limits["maxValidatorExitRequestsPerReport]
"""
pass
```
#### Recently requested pubkeys to exit
Fetches recently emitted `ValidatorExitRequest` events from `ValidatorsExitBusOracle` contract and extract pubkeys from them. The delayed timeout config fetches from the `OracleDaemonConfig` contract.
==TODO: image for missed blocks==
```python
def fetch_recently_requested_pubkeys_to_exit(ref_block):
timeout = fetch_delayed_validator_timeout(ref_block)
# Rough estimate, since there could be missing blocks in the slots
max_period_block_range = timeout / SECONDS_PER_SLOT
from_block = ref_block - max_period_block_range
to_block = ref_block
events = fetch_exit_requests_events(from_block, to_block)
# Narrow the events range
events = filter_events_by_timestamp(events)
return get_pubkeys_set_from_events(events)
```
#### Predicted rewards per epoch
Fetches `ETHDistributed` and `TokenRebased` events from Lido contract and calculate average rewards amount per epoch. The rewards prediction period config fetches from the `OracleDaemonConfig` contract.
==TODO: take into account the time between reports since there may be missed reports==
```python
def fetch_predicted_pessimistic_rewards_per_epoch(ref_block):
period = fetch_rewards_prediction_period(ref_block)
# Rough estimate, since there could be missing blocks in the slots
max_period_block_range = period / SECONDS_PER_SLOT
from_block = ref_block - max_period_block_range
to_block = ref_block
events = fetch_eth_distributed_events(from_block, to_block)
# Narrow the events range
events = filter_events_by_timestamp(events)
return calc_average_rewards_per_epoch_from_events(events)
```
#### Last requested validators indices
The Withdrawal Queue contract stores the index of the last validator that was requested to exit. Since validators are requested in strict order from the lowest validatorIndex to the highest, the indexes help find all the previously requested validators without fetching all events.
==TODO: validator indexes may be reused in the future==
```python
def fetch_last_requested_validators_indices(lido_modules_dict):
"""
function getLastRequestedValidatorIndices(uint256 moduleId, uint256[] calldata nodeOpIds)
external view returns (int256[] memory)
"""
pass
```
#### Withdrawal requests demand
```python
def fetch_withdrawal_requests_demand(ref_block):
"""
Fetches unfinalized steth from Withdrawal Queue
"""
pass
```
### Lido keys indexes
#### Build Lido keys indexes
Returns dicts to identify Lido validators by pubkeys and `module_id` and `operator_id` by validator pubkey.
```python
def build_lido_keys_indexes(used_lido_pubkeys_by_modules):
lido_modules_dict = get_lido_modules_dict(used_lido_pubkeys_by_modules)
lido_pubkeys_dict = get_lido_pubkeys_dict(used_lido_pubkeys_by_modules)
return (lido_modules_dict, lido_pubkeys_dict)
```
#### Lido keys dicts
```python
def get_lido_modules_dict(keys_by_modules):
modules = {}
for module in keys_by_modules:
module_id = module["module"]["id"]
module_address = module["module"]["stakingModuleAddress"]
modules[module_id] = {
"address": module_address,
"operators": set()
}
for key in module["keys"]:
modules[module_id].add(key["operatorIndex"])
return modules
def get_lido_pubkeys_dict(keys_by_modules):
lido_pubkeys_dict = {}
for sr_module in keys_by_modules:
module_id = sr_module["module"]["id"]
for key in sr_module["keys"]:
pubkey = key["key"]
operator_id = key["operatorIndex"]
lido_pubkeys_dict[pubkey] = (module_id, operator_id)
return lido_pubkeys_dict
```
### Validators filters
#### Filter Lido validators
```python
def filter_lido_validators(all_validators, lido_pubkeys_dict):
return list(filter(lambda validator: (validator["validator"]["pubkey"] in lido_pubkeys_dict), all_validators))
```
#### Filter active Lido validators
```python
def filter_active_lido_validators(epoch, lido_validators):
return list(filter(lambda validator: (is_active_validator(validator), epoch), lido_validators))
```
#### Filter exitable Lido validators
==TODO: exlude validators of inactive operators and stopped modules (do not exclude paused modules)==
```python
def filter_exitable_lido_validators(epoch, lido_validators, lido_pubkeys_dict, last_requested_validators_indices):
def condition(validator):
is_previously_requested = is_previously_requested_to_exit(validator, lido_pubkeys_dict, last_requested_validators_indices)
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#voluntary-exits
is_not_exited = validator["validator"]["exit_epoch"] == FAR_FUTURE_EPOCH
is_active_long_enough = epoch >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
is_exitable = is_not_exited and is_active_long_enough
return not is_previously_requested and is_exitable
return list(filter(condition, lido_validators))
```
#### Filter inflight Lido validators
Returns Lido validators that:
- Were recently requested to exit and the `delayed` deadline hasn't come yet
- Exited but not yet withdrawn
```python
def filter_inflight_lido_validators(epoch, lido_validators, lido_pubkeys_dict, last_requested_validators_indices):
def condition(validator):
is_previously_requested = is_previously_requested_to_exit(validator, lido_pubkeys_dict, last_requested_validators_indices)
is_exited = validator["validator"]["exit_epoch"] != FAR_FUTURE_EPOCH
is_withdrawn = validator["validator"]["withdrawable_epoch"] <= epoch and validator["balance"] == 0
is_exited_and_withdrawable = is_exited and not is_withdrawn
return is_previously_requested or is_exited_and_withdrawable
return list(filter(condition, lido_validators))
```
#### Filter not exited Lido validators
Returns requested but not exited validators. In other words, validators that match stuck, delayed, and in flight exit requests.
```python
def filter_not_exited_lido_validators(lido_validators, lido_pubkeys_dict, last_requested_validators_indices):
def condition(validator):
is_previously_requested = is_previously_requested_to_exit(validator, lido_pubkeys_dict, last_requested_validators_indices)
is_not_exited = validator["validator"]["exit_epoch"] == FAR_FUTURE_EPOCH
return is_previously_requested and is_not_exited
return list(filter(condition, lido_validators))
```
#### Filter delayed Lido validators
Returns delayed, including stuck, validators.
```python
def filter_delayed_lido_validators(not_exited_lido_validators, recently_requested_pubkeys_to_exit):
return list(filter(
lambda validator: (validator["validator"]["pubkey"] not in recently_requested_pubkeys_to_exit),
not_exited_lido_validators
))
```
#### Misc
```python
def get_operator_last_requested_index(validator, lido_pubkeys_dict, last_requested_validators_indices):
"""
Gets last requested validator index for Node Operator or -1 if there were no exit requests
"""
pubkey_operator = get_pubkey_operator(validator, lido_pubkeys_dict)
return last_requested_validators_indices[pubkey_operator]
```
```python
def get_pubkey_operator(lido_validator, lido_pubkeys_dict):
validator_pubkey = lido_validator["validator"]["pubkey"]
pubkey_operator = lido_pubkeys_dict[validator_pubkey]
assert pubkey_operator is not None, "Not a Lido key"
return pubkey_operator
```
```python
def get_validator_ids(lido_validator, lido_pubkeys_dict):
module_id, operator_id = get_pubkey_operator(lido_validator, lido_pubkeys_dict)
validator_index = lido_validator["index"]
return (module_id, operator_id, validator_index)
```
### Operators state
#### Collect operators state
Collects the current state of operators: number of exitable keys, delayed validators, target keys to exit, age of validators.
```python
def collect_operators_state(ref_epoch, lido_validators, last_requested_validators_indices, operators_delays, lido_pubkeys_dict, operators_keys_stats):
operators_dict = {}
for validator in lido_validators:
pubkey_operator = get_pubkey_operator(validator, lido_pubkeys_dict)
is_projected = is_projected_validator(validator, lido_pubkeys_dict, last_requested_validators_indices)
if not operators_dict[pubkey_operator]:
ever_deposited_keys = get_ever_deposited_keys(operators_keys_stats, pubkey_operator)
delayed_validators = get_delayed_validators(operators_delays, operators_keys_stats, pubkey_operator)
targeted_validators = get_target_validators(operators_keys_stats, pubkey_operator)
operators_dict[pubkey_operator] = {
# pre-fill with zero
"projected_validators_total_age": 0,
# pre-fill with ever deposited keys
"projected_validators": ever_deposited_keys,
"delayed_validators": delayed_validators,
"targeted_validators": targeted_validators,
}
# subtract pre-fill if the validator exists
operators_dict[pubkey_operator]["projected_validators"] -= 1
if is_projected:
validator_age = ref_epoch - validator["validator"]["activation_epoch"]
operators_dict[pubkey_operator]["projected_validators_total_age"] += validator_age
operators_dict[pubkey_operator]["projected_validators"] += 1
return operators_dict
```
#### Delayed validator exits
Returns the number of validators that were delayed their exit.
```python
def get_delayed_validators(operators_delays, operators_keys_stats, pubkey_operator):
operator_stats = operators_keys_stats[pubkey_operator]
delayed_validators = operators_delays[pubkey_operator] - operator_stats["refundedKeysKeysCount"]
return max(0, delayed_validators)
```
#### Target validators
Returns the target number of validators for an operator.
```python
def get_target_validators(operators_keys_stats, pubkey_operator):
operator_stats = operators_keys_stats[pubkey_operator]
is_limit_active = operator_stats["isTargetLimitActive"]
target_validators = operator_stats["targetValidatorsCount"]
if not is_limit_active:
return None
return target_validators
```
#### Ever deposited keys
```python
def get_ever_deposited_keys(operators_keys_stats, pubkey_operator):
operator_stats = operators_keys_stats[pubkey_operator]
return operator_stats["validatorsReport"]["totalDeposited"]
```
#### Operator delays
Collects dict with delayed keys for each operator
```python
def get_operators_delays_dict(lido_validators, last_requested_validators_indices, recently_requested_pubkeys_to_exit, lido_pubkeys_dict):
not_exited_lido_validators = filter_not_exited_lido_validators(lido_validators, last_requested_validators_indices)
delayed_lido_validators = filter_delayed_lido_validators(not_exited_lido_validators, recently_requested_pubkeys_to_exit)
operators_delays = {}
for validator in delayed_lido_validators:
pubkey_operator = get_pubkey_operator(validator, lido_pubkeys_dict)
if not operators_delays[pubkey_operator]:
operators_delays[pubkey_operator] = 0
operators_delays[pubkey_operator] += 1
return operators_delays
```
### Exit queue
#### Churn limit
Calculates the churn limit for the current state of the chain. The value is not recalculated when the validators exit since the accuracy is sufficient for our purposes. [Churn limit calculation in the spec](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_validator_churn_limit)
```python
def get_validator_churn_limit(all_validators, ref_epoch):
total_active_validators = get_total_active_validators(all_validators, ref_epoch)
return max(MIN_PER_EPOCH_CHURN_LIMIT, total_active_validators // CHURN_LIMIT_QUOTIENT)
```
#### Exit epochs
Returns an array of maximum exit epochs for the current state of the chain. This data is enough not to recalculate the entire validator set on every validator exit cycle
```python
def get_max_exit_epochs(all_validators):
exit_epochs = [v["validator"]["exit_epoch"] for v in all_validators if v["validator"]["exit_epoch"] != FAR_FUTURE_EPOCH]
max_exit_epoch = max(exit_epochs)
return [epoch for epoch in exit_epochs if epoch == max_exit_epoch]
```
#### Exit queue epoch
Returns the next exit epoch in the exit queue
```python
def get_exit_queue_epoch(exit_epochs, churn_limit, ref_epoch):
exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(ref_epoch)])
exit_queue_churn = len([epoch for epoch in exit_epochs if epoch == exit_queue_epoch])
if exit_queue_churn >= churn_limit:
exit_queue_epoch += 1
return exit_queue_epoch
```
#### Withdrawable queue epoch
Returns the next withdrawable epoch in the exit queue
```python
def get_withdrawable_queue_epoch(exit_epochs, churn_limit, ref_epoch):
return get_exit_queue_epoch(exit_epochs, churn_limit, ref_epoch) + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
```
#### Activation exit epoch
[Method from the spec](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_activation_exit_epoch). Returns the epoch during which validator activations and exits initiated in `epoch` take effect.
```python
def compute_activation_exit_epoch(epoch):
return epoch + 1 + MAX_SEED_LOOKAHEAD
```
#### Validators
```python
def get_total_active_validators(all_validators, ref_epoch):
return len(filter(lambda validator: is_active_validator(validator, ref_epoch), all_validators))
def get_total_projected_validators(all_validators, lido_pubkeys_dict, last_requested_validators_indices):
return len(filter(lambda validator: is_projected_validator(validator, lido_pubkeys_dict, last_requested_validators_indices), all_validators))
```
### Sweep
#### Average sweep prediction
Predicts the average epochs of the sweep cycle. In the spec: [get expected withdrawals](https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#new-get_expected_withdrawals), [process withdrawals](https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#new-process_withdrawals)
```python
def predict_average_sweep_duration_epochs(all_validators, ref_epoch):
total_withdrawable_validators = len(filter(lambda validator: (
is_partially_withdrawable_validator(validator["validator"], validator["balance"]) or
is_fully_withdrawable_validator(validator["validator"], validator["balance"], ref_epoch)
), all_validators))
epochs_for_whole_sweep_cycle = total_withdrawable_validators / MAX_WITHDRAWALS_PER_PAYLOAD / SLOTS_PER_EPOCH
return epochs_for_whole_sweep_cycle // 2
```
#### Withdrawable validators
```python
def has_eth1_withdrawal_credential(validator):
"""
Check if `validator` has an 0x01 prefixed "eth1" withdrawal credential
https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#has_eth1_withdrawal_credential
"""
return validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX
```
```python
def is_fully_withdrawable_validator(validator, balance, epoch):
"""
Check if `validator` is fully withdrawable
https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#is_fully_withdrawable_validator
"""
return (
has_eth1_withdrawal_credential(validator)
and validator.withdrawable_epoch <= epoch
and balance > 0
)
```
```python
def is_partially_withdrawable_validator(validator, balance):
"""
Check if `validator` is partially withdrawable
https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#is_partially_withdrawable_validator
"""
has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE
has_excess_balance = balance > MAX_EFFECTIVE_BALANCE
return has_eth1_withdrawal_credential(validator) and has_max_effective_balance and has_excess_balance
```
## Next validator to exit
The algorithm for the validators exiting is based on [the algorithm described on the research forum](https://research.lido.fi/t/withdrawals-on-validator-exiting-order/3048#combined-approach-17).
The algorithm is supposed to correct the future number of validators for each Node Operator, so we are going to rely on the projected numbers in our calculations. Let's represent the validators and deposits in-flight of one of the Node Operator in the following form, where validators are sorted by their indexes:
<img style="margin: 0 0 15px 0" width="500" src="https://hackmd.io/_uploads/rJkE8zLpj.png"/>
The algorithm assumes that the oldest validators are exited first. Therefore, we can separate previously requested validators to exit by knowing the index of the last requested.
<img style="margin: 0 0 15px 0" width="500" src="https://hackmd.io/_uploads/BJVbwz86s.png"/>
If we look closer, each validator has a status. Some validators may be slashed or be exited without an request from the protocol:
<img style="margin: 0 0 15px 0" width="500" src="https://hackmd.io/_uploads/BJ2qwfLTo.png"/>
Among all validators we are interested in projected. It includes all active validators and in-flight deposits, but excludes validators whose `exit_epoch != FAR_FUTURE_EPOCH` and those validators that were requested to exit.
<img style="margin: 0 0 15px 0" width="500" src="https://hackmd.io/_uploads/BJGVlXUpi.png"/>
A few hours later:
<img style="margin: 0 0 15px 0" width="500" src="https://hackmd.io/_uploads/HkXHx7ITo.png"/>
Note that we are looking for a validator to exit only among those that can be exited, while using the projected number of validators, which includes non-existent yet validators. It's only weights, so there is no conflict here.
Let's now look at the weights themselves, which will be used in sorting validators in each cycle:
### 1. Delayed validators
The number of delayed validators that a Node Operator has. A Node Operator with delayed keys is placed at the end of the exit queue to avoid blocking the queue.
### 2. Targeted validators to exit
The number of targeted validators to exit that a Node Operator has. Calculated as `target limit - projected validators`.
### 3. Stake weight
If the number of projected validators for a Node Operator exceeds 1% of all validators in the chain, the weight is calculated as the average value of `activation_epoch` of its validators that can be exited. For other validators of Node Operators this weight is constant and equals `Infinity`.
The total number of validators in the chain is projected by excluding validators with `exit_epoch != FAR_FUTURE_EPOCH`.
### 4. Number of validators
The weight equals the number of projected validators.
### 5. Activation epoch
The weight equals validator activation epoch. The earliest validators exit first.
### 6. Validator index
Valdiator with the lowest indices exit first. Applies if there are validators activated in the same epoch.
```python
def sort_validators_by_exit_order(state):
lido_pubkeys_dict = state["lido_pubkeys_dict"]
operators_state = state["operators_state"]
total_projected_validators = state["total_projected_validators"]
def condition(validator):
pubkey_operator = get_pubkey_operator(validator, lido_pubkeys_dict)
operator = operators_state[pubkey_operator]
projected_validators_total_age = operator["projected_validators_total_age"]
stake_volume = 100 * operator["projected_validators"] / total_projected_validators
stake_volume_weight = projected_validators_total_age if stake_volume > 1 else 0
targeted_validators_to_exit = max(0, operator["projected_validators"] - operator["targeted_validators"]) if operator["targeted_validators"] else 0
# -1 for reverse order
return (
operator["delayed_validators"],
-1 * targeted_validators_to_exit,
stake_volume_weight,
-1 * operator["projected_validators"],
validator["validator"]["activation_epoch"],
validator["index"]
)
state["exitable_lido_validators"].sort(condition)
```
### Sorting example
<table style="width:100%;display:table;">
<thead>
<tr>
<th colspan="4">Node Operator weights</th>
<th colspan="2">Validator weights</th>
</tr>
<tr>
<th>Delayed ↓</th>
<th>Target ↑</th>
<th>Stake & age ↓</th>
<th>Validators ↑</th>
<th>Activation ↓</th>
<th>Index ↓</th>
</tr>
</thead>
<tbody>
<tr><td>0</td><td>10</td><td>1000</td><td>7000</td><td>500</td><td>47</td></tr>
<tr><td>0</td><td>10</td><td>1000</td><td>7000</td><td>1500</td><td>90</td></tr>
<tr><td colspan="6">...</td></tr>
<tr><td>0</td><td>2</td><td>1200</td><td>6000</td><td>1000</td><td>50</td></tr>
<tr><td>0</td><td>2</td><td>1200</td><td>6000</td><td>1200</td><td>76</td></tr>
<tr><td>0</td><td>2</td><td>1200</td><td>6000</td><td>1400</td><td>81</td></tr>
<tr><td colspan="6">...</td></tr>
<tr><td>0</td><td>0</td><td>998</td><td>7432</td><td>781</td><td>48</td></tr>
<tr><td>0</td><td>0</td><td>998</td><td>7432</td><td>990</td><td>49</td></tr>
<tr><td>0</td><td>0</td><td>998</td><td>7432</td><td>1003</td><td>52</td></tr>
<tr><td colspan="6">...</td></tr>
<tr><td>1</td><td>50</td><td>Infinity</td><td>5</td><td>231</td><td>10</td></tr>
<tr><td colspan="6">...</td></tr>
<tr><td>2</td><td>0</td><td>Infinity</td><td>2</td><td>3210</td><td>1121</td></tr>
<tr><td>2</td><td>0</td><td>Infinity</td><td>2</td><td>3210</td><td>1122</td></tr>
</tbody>
</table>
## Predict available ETH before next withdrawn
Predicts how much ETH the protocol will have at the moment when the closest requested validator to exit can be fully exited and withdrawn. The prediction includes:
- In-flight ETH from validators that are already exited, but haven't been withdrawn yet
- In-flight ETH from validators who have been requested to exit recently, but have not yet changed their status
- Predicted Consensus and Execution layers rewards
However, this prediction is pessimistic, since it does not include ETH in the buffer, which is also used to cover withdrawal requests.
If the available amount of ETH will cover withdrawal requests in the queue, it's not reasonable to exit validators.
```python
def predict_available_eth_before_next_withdrawn(state, report):
predicted_withdrawal_queue_epoch = get_withdrawable_queue_epoch(state["exit_epochs"], state["churn_limit"], report.ref_epoch)
predicted_rewards_eth = predict_rewards_eth_before_next_withdrawn(predicted_withdrawal_queue_epoch, state, report)
inflight_validators_eth = get_inflight_validators_eth(predicted_withdrawal_queue_epoch, state)
return predicted_rewards_eth + inflight_validators_eth
```
```python
def predict_rewards_eth_before_next_withdrawn(withdrawal_queue_epoch, state, report):
pessimistic_rewards_per_epoch = state["predicted_pessimistic_rewards_per_epoch"]
sweep_duration_epochs = state["sweep_duration_epochs"]
withdrawn_epoch = withdrawal_queue_epoch + sweep_duration_epochs
withdrawn_duration_epochs = withdrawn_epoch - report.ref_epoch
return withdrawn_duration_epochs * pessimistic_rewards_per_epoch
```
==TODO: describe filter of slashed validators==
==TODO: put requested but not delayed validators to queue==
```python
def get_inflight_validators_eth(withdrawal_queue_epoch, state):
inflight_lido_validators = state["inflight_lido_validators"]
filtered_validators = filter(
lambda validator: (validator["validator"]["withdrawable_epoch"] <= withdrawal_queue_epoch),
inflight_lido_validators
)
return sum(map(lambda validator: (predict_validator_balance_on_exit(validator)), filtered_validators))
```
```python
def predict_validator_balance_on_exit(validator):
"""
Rewards exceeding MAX_EFFECTIVE_BALANCE are skimmed and counted as CL rewards
"""
return min(validator["balance"], MAX_EFFECTIVE_BALANCE)
```
## Simulate exit
After choosing the validator to exit, we need to simulate its exit in the state, so that in the next cycle it's not counted twice in the Node Operator statistics.
### Simulate validator exit
```python
def simulate_validator_exit(state, report, validator_to_exit):
initiate_validator_exit(state, report, validator_to_exit)
remove_from_exitable_lido_validators(state, validator_to_exit)
decrease_withdrawal_requests_uncovered_demand(state, validator_to_exit)
remove_from_operators_stats(state, report.ref_epoch validator_to_exit)
```
### Initiate validator exit
Updates the validator's `exit_epoch` and `withdrawable_epoch` and increases the exit queue.
```python
def initiate_validator_exit(state, report, validator):
# Compute exit queue epoch
exit_queue_epoch = get_exit_queue_epoch(state["exit_epochs"], state["churn_limit"], report.ref_epoch)
state["exit_epochs"].append(exit_queue_epoch)
# Set validator exit epoch and withdrawable epoch
validator["exit_epoch"] = exit_queue_epoch
validator["withdrawable_epoch"] = validator["exit_epoch"] + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
```
#### Remove from exitable Lido validators
```python
def remove_from_exitable_lido_validators(state, validator_to_exit):
state["exitable_lido_validators"].remove(validator_to_exit)
```
#### Decrease withdrawal requests uncovered demand
```python
def decrease_withdrawal_requests_uncovered_demand(state, validator_to_exit)
state["withdrawal_requests_uncovered_demand"] -= predict_validator_balance_on_exit(validator_to_exit)
```
#### Remove from operators statistics
```python
def remove_from_operators_stats(state, ref_epoch, validator):
pubkey_operator = get_pubkey_operator(validator, state["lido_pubkeys_dict"])
operators_state = state["operators_state"]
operator_state = operators_state[pubkey_operator]
validator_age = ref_epoch - validator["validator"]["activation_epoch"]
operator_state["projected_validators"] -= 1
operator_state["projected_validators_total_age"] -= validator_age
```
## Predicates
```python
def is_not_exited_validator(validator):
return validator["validator"]["exit_epoch"] == FAR_FUTURE_EPOCH
def is_projected_validator(validator, lido_pubkeys_dict, last_requested_validators_indices):
is_previously_requested = is_previously_requested_to_exit(validator, lido_pubkeys_dict, last_requested_validators_indices)
is_not_exited = is_not_exited_validator(validator)
return not is_previously_requested and is_not_exited
def is_active_validator(validator, ref_epoch):
return validator["validator"]["activation_epoch"] <= ref_epoch < validator["validator"]["exit_epoch"]
def is_previously_requested_to_exit(validator, lido_pubkeys_dict, last_requested_validators_indices):
last_requested_validator_index = get_operator_last_requested_index(
validator,
lido_pubkeys_dict,
last_requested_validators_indices
)
return validator["index"] <= last_requested_validator_index
def is_enough_eth_to_cover_demand(state, eth):
return eth >= state["withdrawal_requests_uncovered_demand"]
def is_exits_paused(state):
return state["max_validators_to_exit"] == 0
def has_validators_to_exit(state):
return state["exitable_lido_validators"] > 0
def is_enough_predicted_eth_to_cover_demand(state):
return state["withdrawal_requests_uncovered_demand"] == 0
```