owned this note
owned this note
Published
Linked with GitHub
### Preface
There are essentially 2 types of withdrawals: a full and a partial one. In the former case the network checks that the validator's withdrawal epoch has been reached and the validator's balance is non-zero. The latter case assumes, the validator's balance is above maximum effective balance for the validator, so the surplus can be withdrawn.
For CSM we expect a node operator / our bot brings a proof of a withdrawal to the module to settle the node operators' bond funds after exiting a validator. Since anyone can bring a proof, it's required to distringuish two types of withdrawals described above, because it's mistakenly to accept a partial withdrawal as an indicator of a withdrawal balance of a validator. The simplest approach is to check the conditions applicable to determine a withdrawal type on-chain. But there's one issue: there is no way to distinguish two sequential full withdrawals in case of any.
### Problem statement
A validator become ineligible for receiving rewards for all its duties but sync committee participation. This specific duty is highly rewardable (and penalized at the same time) and all validators with this duty may get rewards nevertheless their status at the moment. It means that even if a validator is exited and withdrawn, it still can get some rewards.
It's possible because a sync committee assembled by the network at the epoch N becomes active at epoch `N + 256` for the next 256 epochs.
![image](https://hackmd.io/_uploads/HyAHt-5jp.png)
So it requires to wait up to 512 epochs after a validator's exit to make sure it won't receive any rewards afterwards.
Now, let's imagine a case:
![image](https://hackmd.io/_uploads/HyY8FbciT.png)
a validator is assigned to a sync committee bound to some interval **B**. If this validators exits unslashed at some epoch **e**, its withdrawable epoch will be set to some epoch inside the interval **B** (since the minimum delay is 256 epochs). Then, if the network withdraws this validator within some interval **W**, which starts with the validator's withdrawable epoch and ends at last but one epoch of the interval **B**, there's a possibilty for the validator to receive a reward for sync committee participation **after** the withdrawal. If it happens, the validator will be "fully withdrawn" once again in the future.
It's not an issue in most of the cases, because
- sync committee participation for a given validator is pretty [rare](https://eth2book.info/capella/part2/incentives/rewards/#sync-committee-rewards),
- validator should exit before it's sync participation committee becomes active,
- it should be withdrawn before the end of the sync committee period,
- validator shouldn't be shut down after exit/withdrawal.
But is there any case on the mainnet? Here they are:
```sql
SELECT
f_validator_index,
f_balance,
f_effective_balance
FROM
`high-hue-328212.chaind.t_validator_balances`
WHERE
f_epoch > 74240
AND f_effective_balance = 0
AND f_balance > 0;
```
```csv
f_validator_index,f_balance,f_effective_balance
102888,12922648,0
114440,8230095,0
120514,83540119,0
128064,66826537,0
152092,27306605,0
33812,3793432,0
39639,64759802,0
47626,55658804,0
499679,57090240,0
524573,18153155,0
603031,91031300,0
623262,22772972,0
70974,69082975,0
72868,4022346,0
767043,8309011,0
767472,7715467,0
91713,55511645,0
```
Here we can see that `effective_balance` is zero, while the `balance` remains positive, if we look into beaconcha.in and check some of these validators, we can find that it was withdrawn 2 times after `withdrawable_epoch` ([link](https://beaconcha.in/validator/623262#withdrawals)):
![image](https://hackmd.io/_uploads/HyDDtWqjp.png)
It means, there is an open vector for a griefing attack to a node operator: someone can bring a proof of a "full" withdrawal after the withdrawable epoch. The question is how can we assess this issue.
### Proposed solution
The possible solution is to differentiate the value of a withdrawal. For a slashed validator arbitrarily balance can be reported, since all the rewards and penalties will be acquired to the moment of the withdrawal. The reason for that is that its `withdrawable_epoch` will be at least for 8192 epochs after the validator becomes inactive. For a non-slashed validator's withdrawal we can check the `amount` of the withdrawal to be greater than some treshold amount.
We can determine the value of the threshold based on the following statements:
1. The threshold should be low enough to cover a case of a validator being exited by the network with too low effective balance (16 ethers).
2. The threshold should be great enough to cover the case if a validator receives rewards for a sync committee participation after a full withdrawl, as described above.
As shown in the [research](https://hackmd.io/@dgusakov/H1ENnM5j6) the maximum value of rewards an exited validator may get for a sync committee participation is a square root of the sum of effective balances of all validators. It means, we may expect double in rewards if the total staked ether grows at least 4 times.
Given the current conditions a validator can get around 0.18 ether for a whole sync committee interval. A validator can't be chosen for more than 1 interval following its withdrawable epoch. It means we can safely assume it's unlikely to get 1 ether in rewards for a sync commmittee participation in the near future, since it will require at least 915'000'000 more ether to be staked.
Non-slashed validator's exit balance can be lower than 16 ethers because of penalties it will receive for missing attestations and sync committee non-participation. Penalties for a sync committee duties is equal to rewards, so given the previous considerations, we can assume the penalties unlikely to be greater than 1 ether. Attestation penalties can be ommited because they're 100 times lower the penalties for a sync committee duties.
Given all of that we can choose a threshold somewhere in the following range [1;15] ethers, and hence it's proposed to choose **8 ethers** as the value. Below is presented pseudocode describing a flow of accepting a withdrawal.
```python
def submit_full_withdrawal(validator, withdrawal):
if (validator.withdrawable_epoch <= withdrawal.epoch):
if validator.slashed:
accept(withdrawal) and return
elif withdrawal.amount > 8 ether:
accept(withdrawal) and return
raise InvalidWithdrawal()
```
### Credits
- @vladimir-g for the initial heads-up.
- @dgusakov for the max sync committee participation rewards estimation.