Validators Exit Bus
It's advised to read What is Lido Oracle mechanism before
Validators Exit Bus is an oracle that ejects Lido validators when the protocol requires additional funds to process user withdrawals.
A report calculation consists of 4 key steps:
- Calculate withdrawals amount to cover with ether.
- Calculate ether rewards prediction per epoch.
- Calculate withdrawal epoch for next validator eligible for exit to cover withdrawal requests if needed
- Prepare validators exit order queue
- Go through the queue until the exited validators’ balances cover all withdrawal requests (considering the predicated final exited balance of each validator).
Placed exit requests via ValidatorsExitBusOracle
should be processed timely according to the ratified Lido on Ethereum Validator Exits Policy V1.0.
See also the provided penalties spec.
Next validator to exit algorithm
The algorithm for the validators exiting is based on the algorithm described on the research forum.
The algorithm is supposed to correct the future number of validators for each Node Operator. Suppose the validators and deposits in-flight of one of the Node Operator are represented in the following form, where validators are sorted by their indexes:
The algorithm assumes that the oldest validators are exited first. Therefore, previously requested validators can be separated to exit by knowing the index of the last requested.
Worth noting, each validator has a status. Some validators may be slashed or be exited without an request from the protocol:
Among all validators the projected ones are the point of interest. They include all active validators and in-flight deposits, but exclude validators whose exit_epoch != FAR_FUTURE_EPOCH
and those validators that were requested to exit.
A few hours later it might look like the following:
Note that th described algorithm is 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 misconception here.
The final exit order predicate sequence:
- Validator whose operator with the lowest number of delayed validators
- Validator whose operator with the highest number of targeted validators to exit
- Validator whose operator with the highest stake weight
- Validator whose operator with the highest number of validators
- Validator with the lowest index
Get information to prepare ordered queue
In order to prepare a queue of validators to exit, the following actions and considerations involved:
- the maximum number of validators that can be requested to exit in one report;
- operator network penetration percent - only if the operator's share is greater than 1%;
- 'exitable' Lido validators;
- fetch node operators stats to sort exitable validators;
- total predictable validators count;
- last requested validators indices.
Report limits
maxValidatorExitRequestsPerReport
- max number of exit requests allowed in report toValidatorsExitBusOracle
fromOracleReportSanityChecker.getOracleReportLimits()
.VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS
- A parameter from fromOracleDaemonConfig
contract used to calculate validators going to exit.NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP
- - A parameter fromOracleDaemonConfig
that is taken into account when determining the penetration of the operator into the network.
Get exitable validators
A validator is 'exitable' if two conditions are strictly have NOT met:
validator.exit_epoch != FAR_FUTURE_EPOCH
andvalidator.index <= last_requested_to_exit_index
.
Node operator stats
Statistics for each node operator, which are needed for sorting their validators in exit order:
- validators count that are not yet in CL
- validators that are in CL and are not yet requested to exit and not on exit
- validators that are in CL and requested to exit but not on exit and not requested to exit recently
- target validators count
- checks whether the target limit flag is enabled
NB: A validator can not be considered as delayed if it was requested to exit in last VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS
slots
Last requested validators indices
The ValidatorsExitBusOracle
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.
Returns the latest validator indices that were requested to exit for the given
operator_indexes
in the given module
. For node operators that were never requested to exit
any validator yet, index is set to -1
.
ValidatorsExitBusOracle.getLastRequestedValidatorIndices(
uint256 moduleId,
uint256[] nodeOpIds
): int256[]
State collection
To find the next validators to exit, Validators Exit Bus Oracle collects the following state from both Ethereum Consensus and Execution layers.
- From OracleDaemonConfig contract:
- PREDICTION_DURATION_IN_SLOTS
- VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS
- From Withdrawal Queue:
- Get total unfinalized withdrawal request amount
- From Lido contract:
- Recent postCLBalance/preCLBalance and withdrawals from Execution Layer Rewards and Withdrawal vaults via events
- 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 Oracle contract:
- Maximum number of exit requests for the current frame
- Recently requested via Exit Bus public keys to exit
Fetching data
Get uncovered withdrawal requests amount of stETH
Collects the amount of stETH in the queue yet to be finalized from WithdrawalQueue.unfinalizedStETH()
Calculate average rewards speed per epoch
Fetches ETHDistributed
and TokenRebased
events from the Lido
contract and calculate average rewards amount per epoch. The rewards prediction period config fetches from the OracleDaemonConfig contract.
To get events in past, addressing the cases where there can be slots with missed block, the next scheme is introduced:
- Get from OracleDaemonConfig contract
PREDICTION_DURATION_IN_SLOTS
value - Get
TokenRebased
events from Lido - Get
ETHDistributed
events from Lido - Group that events by transaction hash
- Collect from events:
total_rewards
aspostCLBalance + withdrawalsWithdrawn - preCLBalance executionLayerRewardsWithdrawn
time_spent
as sum of each eventtimeElapsed
- calculate
rewards_speed_per_epoch
asmax(total_rewards * chain_configs.seconds_per_slot * chain_configs.slots_per_epoch // time_spent, 0)
Calculate epochs to sweep
Average sweep prediction
Predicts the average epochs of the sweep cycle. In the spec: get expected withdrawals, process withdrawals
Withdrawable validators
- Check if
validator
has the 0x01 prefixed "eth1" withdrawal credentials, and - Check if
validator
is partially withdrawable, or - Check if
validator
is fully withdrawable
Predict available ether before next withdrawn
In order to estimate the amount is needed to fully cover the non-finalized withdraw requests, the following values are calculated
- Future rewards
- Future withdrawals amount
- Total available balance
- Validators to eject cummulative amount
- Going to withdrawn balance
To calculate future rewards, it's needed to predict an epoch when all validators in queue and validators_to_eject
will be withdrawn:
- Calculate latest exit epoch number and amount of validators that are exiting in this epoch
- If queue is empty - exit epoch will be calculated as
current epoch + MAX_SEED_LOOK AHEAD + 1
. MAX_SEED_LOOKAHEAD constant needs to mitigate some attacks, more details here - Calculate churn limit - like a rate-limit on changes to the validator set. Minimum is 4 validators per epoch. And recalculates each
CHURN_LIMIT_QUOTIENT = 2**16
. For example when active validators reaches up to 327,680 amount,churn limit
rises to 5, spec - Calculate slots capacity for exit:
remain_exits_capacity_for_epoch=churn_limit - (amount of validators that are exiting in this epoch)
- Calculate epoch to exit all
validators_to_eject_count
:
epochs_required_to_exit_validators = (validators_to_eject_count - remain_exits_capacity_for_epoch) // churn_limit + 1
- So the predictable withdrawable epoch:
withdrawal_epoch=max_exit_epoch_number + epochs_required_to_exit_validators + MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
MIN_VALIDATOR_WITHDRAWABILITY_DELAY here
So now we can calculate what amount (and validators count) is needed to fully cover amount of non-finalized WithdrawQueue requests.
Calculate expected balance to withdraw
Future rewards
future_rewards = (withdrawal_epoch + epochs_to_sweep - blockstamp.ref_epoch ) * rewards_speed_per_epoch
Future withdrawals amount
Get total balance from validators which can be fully withdrawn.
Total available balance
Fetch total balance as sum from:
Lido.getBufferedEther()
+- Balance from
elRewardsVault
+ - Balance from
withdrawalVault
Validators to eject cummulative amount
Get balance from next validator in exit queue.
Validators going to exit
Fetches recently emitted ValidatorExitRequest
events from ValidatorsExitBusOracle
contract and extract pubkeys from them. The delayed timeout config fetches from the OracleDaemonConfig
contract.
Validators requested to exit, but didn't send exit message. In case:
- Activation epoch is not old enough to initiate exit
- Node operator had not enough time to send exit message (VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS)
To get validators, oracle calculates:
lido_validators_by_operator
- Fetches all used Lido keys from Keys API + Fetches all validators at the reference slot and merge them with keysejected_indexes
- get operators with last exited validator indexes from for all staking_modules and node operators viaValidatorsExitBusOracle.getLastRequestedValidatorIndices(module_id, uint256[] nodeOpIds)
recent_pubkeys
- get last requested to exit pubkeys fromValidatorExitRequest
event
For each lido_validators_by_operator
oracle tries to find non exited validators, so:
- if not
validator_asked_to_exit
-> return False - if
is_on_exit
-> return false - if
validator_recently_asked_to_exit
-> return True - if not
validator_eligible_to_exit
-> return True - otherwise return False
Oracle calculates going_to_withdraw_balance
for all non exited validators
Compare expected_balance vs to_withdrawn_balance
Expected balance is:
expected_balance = (
future_withdrawals + # Validators that have withdrawal_epoch
future_rewards + # Rewards we get until last validator in validators_to_eject will be withdrawn
total_available_balance + # Current EL balance (el vault, wc vault, buffered eth)
validator_to_eject_balance_sum + # Validators that we expected to be ejected (requested to exit, not delayed)
going_to_withdraw_balance # validators_to_eject balance
)
First of all, it's checked without exiting the validator, whether the protocol already has enough available ether to cover withdrawal requests in the queue. If yes, then it's not reasonable to exit validators.
If there is not enough, one more validator is considered to be exited and the expected balance gets calculated again. The process continues until the expected balance becomes greater than or equal to the unfinalized withdrawal requests amount.