Validator ejector is a daemon which loads pre-signed validator exit messages and sends them out when necessary by listening to LidoOracle contract events.
Required:
Optional:
Ejector handles two message formats:
Original:
{
"message": { "epoch": "123", "validator_index": "123" },
"signature": "0x123"
}
Ethdo output:
{
"exit": {
"message": { "epoch": "123", "validator_index": "123" },
"signature": "0x123"
},
"fork_version": "0x02001020"
}
They are supplied as separate .json files in a folder location of which can be set using an environment variable.
For storage safety, messages can be encrypted and Ejector will automatically decrypt messages on startup by using the key from MESSAGES_PASSWORD
environment variable. Encryption/decryption is done following EIP-2335 spec.
Signature is checked for current and previous fork settings, same way Consesus Node does it.
Epoch is not checked separately as it being invalid (eg too early) will fail the signature check.
At the start, all messages are loaded into memory. This allows us not to rely on specific filenames eg 0x123.json
and find messages by validatorIndex inside them instead, providing Node Operators with freedom to choose the names themselves.
If new messages are added while Ejector is running, a restart is needed. Restart is safe as nothing bad will happen if we try to submit exit messages more than one time.
Events are loaded in two stages, both for which block parameters are adjustable via environment variables.
Events will come in rounds, which are 4-6 hours long.
Current event definition:
event ValidatorExitRequest(uint256 indexed stakingModuleId, uint256 indexed nodeOperatorId, bytes validatorPubkey
Events are requested from the Execution Node only for the exact Node Operator for whom id is configured in an environment variable. This allows us to catch and log an error for situations when we should be sending an exit, but don't have a message loaded for that specific validator.
All node requests are done to finalized state (32 blocks after safe state and 64 after head for extra caution).
Default poll parameters are optimised to wake up on new finalized epoch and read its events.
First, on start, events are loaded for a large block amount to compensate for Ejector being switched off. Node Operators can adjust the parameters if it was not running for a long time.
After historical load is done, Ejector will sleep and wake up to load events for a smaller amount of blocks.
When the Ejector thinks an exit should be made, first, validator is status is checked and if it's already exiting, nothing will be done.
switch (status) {
case 'active_exiting':
case 'exited_unslashed':
case 'exited_slashed':
case 'withdrawal_possible': // already exited
case 'withdrawal_done': // already exited
isExiting = true
default:
isExiting = false
}
Then, validator index is found by looking up validator data in the Consensus Node using the public key in exit request. This is done so we can search exit messages in memory.
After that, message is found in memory by looking at validator index inside exit messages.
If it's not found, an error is logged and metric is updated.
At this point, we don't validate message and its signature a second time because node will check its validity and return an error if it's not correct. For example, two hard forks might have happened by the time message needs to be sent out.
Finally, message is sent to the Consensus Node and its response is handled.
Logging is configurable via environment variables.
Vital for debugging eg setting to "debug" to understand why something is not working properly. Especially useful since we might ask Node Operators to provide detailed logs in case of issues.
Simple or JSON. JSON allows easy parsing to filter needed data.
Exact secrets to sanitize from logs. They are optional: some operators would want to sanitize RPC service urls with auth tokens, but for internal node addresses it might not be necessary.
Recommended Use Cases: