Flexible payout for validator and all nominators#5268
Flexible payout for validator and all nominators#5268shawntabrizi wants to merge 12 commits intoparitytech:masterfrom
Conversation
| ledger.claimed_rewards.retain(|&x| x >= current_era.saturating_sub(history_depth)); | ||
| match ledger.claimed_rewards.binary_search(&era) { | ||
| Ok(_) => Err(Error::<T>::InvalidEraToReward)?, | ||
| Err(pos) => ledger.claimed_rewards.insert(pos, era), |
There was a problem hiding this comment.
This retain + binary search should be combined into a single operation over the vector.
| exposure_clipped.others.sort_unstable_by(|a, b| a.value.cmp(&b.value).reverse()); | ||
| exposure_clipped.others.truncate(clipped_max_len); | ||
| } | ||
| <ErasStakersClipped<T>>::insert(¤t_era, &c, exposure_clipped); |
There was a problem hiding this comment.
There is a dilemma here:
Do we want to keep a copy of the stakers information for every staker for a bunch of eras, or do we want to have it so that some validators invoke reading of a vec which is large.
I think removing this redundant storage item is better, but could be wrong.
frame/staking/src/lib.rs
Outdated
|
|
||
| if nominator_ledger.last_reward.map(|last_reward| last_reward >= era).unwrap_or(false) { | ||
| return Err(Error::<T>::InvalidEraToReward.into()); | ||
| let mut ledger = <Ledger<T>>::get(&validator).ok_or_else(|| Error::<T>::NotController)?; |
There was a problem hiding this comment.
I'm guessing people would actually provide the stash address of the validator, since that's how they nominate them?
frame/staking/src/lib.rs
Outdated
| let max_payout = validator_prefs.max_payout as usize; | ||
| let mut nominators_exposure = exposure.others; | ||
| if nominators_exposure.len() > max_payout { | ||
| nominators_exposure.sort_unstable_by(|a, b| b.value.cmp(&a.value)); | ||
| nominators_exposure.truncate(max_payout); |
There was a problem hiding this comment.
I think your intent is to limit the number of stakers that the transactor pays out, but this logic here prevents people outside of validator_prefs.max_payout from ever claiming their reward, even if they are willing to pay a tx fee to get it. This breaks down to some weird incentives, especially for bigger validators, like setting max_payout to 1 so that only you nominate yourself and if someone else out-nominates you then your incentive is to misbehave and slash them.
There was a problem hiding this comment.
It would be a slightly difficult to allow for an additional payout nominator. It could not be used by the max_payouts people since we do not separately track which payouts they have claimed, and would be at risk of a double payout.
I could make it so it works for anyone not in the max_payouts group, but then the API is a little shitty since it cannot be used by everyone.
I personally don't think things like max_payout = 1(which is inherently a kind of flawed setting) need to be handled well, since we should discourage a validator from selecting this, but there are def solutions if you think it is necessary.
There was a problem hiding this comment.
It's really hard (impossible) to set a proper weight for this function because you don't know max_payout in advance for the validator in question. A validator could set this to be very high and make the nomination accounts to attack the chain.
I would rather see a solution that doesn't give the validator this power, but rather does pay out all nominators by:
- Ordering nominators by exposure.
- Subtracting some fee from the reward.
- Truncate the list by only paying out those for whom reward is greater than zero (or some threshold like
ED/10)
There was a problem hiding this comment.
Why max_payout - why not just payout all? (Excuse my ignorance, sometimes I need a "lemme ask really stupid question" forum)
There was a problem hiding this comment.
I think we are always asking you stupid questions. We are trying to avoid the situation with too many state updates in a single transaction. But I would rather pay out all as long as there is a way to decide if an account is worth paying out.
There was a problem hiding this comment.
It's really hard (impossible) to set a proper weight for this function because you don't know max_payout in advance for the validator in question. A validator could set this to be very high and make the nomination accounts to attack the chain.
This is not really correct. We will likely refactor all extrinsics to include information like this. So in the next iteration, max_payouts will be a value that has to be input into the extrinsic, and matched against what is in storage. If it does not match correctly, then we do nothing.
We will charge the appropriate fee for that action.
There was a problem hiding this comment.
Why max_payout - why not just payout all? (Excuse my ignorance, sometimes I need a "lemme ask really stupid question" forum)
This is an attack vector. A malicious actor can make many many micro nominators to prevent a validator from doing a payout with a reasonable fee.
There was a problem hiding this comment.
This is not really correct. We will likely refactor all extrinsics to include information like this. So in the next iteration,
max_payoutswill be a value that has to be input into the extrinsic, and matched against what is in storage. If it does not match correctly, then we do nothing.We will charge the appropriate fee for that action.
I still don't see how this is a superior solution than deducting a fee from each staker's rewards and truncating the list based on that. You are giving validators the power to truncate the list of rewarded accounts rather than using an economic mechanism. Charging a fee per rewarded account would basically add a cost to nominators for dividing their account up into smaller accounts that all nominate the same target, which is exactly the behavior we want to disincent. Just giving this param to validators seems like a hack to me.
There was a problem hiding this comment.
Ah, I see what you mean, and generally it makes sense, but our weight/fee system isnt quite smart enough to handle this.
The underlying weight of the call needs to appropriately measured/reported for the purposes of forming blocks, and our fee mechanism depends directly on this weight. So even if we do try to "take fees from the rewards" we will still need to increase the weight of the call proportional to the number of people paid out, and ultimately this would need to be known ahead of time, and the fee associated with this will be charged to the sender.
|
|
||
| // Note: if era has no reward to be claimed, era may be future. better not to update | ||
| // `nominator_ledger.last_reward` in this case. | ||
| let era_payout = <ErasValidatorReward<T>>::get(&era) | ||
| .ok_or_else(|| Error::<T>::InvalidEraToReward)?; |
There was a problem hiding this comment.
you removed the comment but the logic still applies, the ? here will return if the era_payout is None
|
additional note: if the fees to claim reward for a validator and its n first nominators is important, maybe nobody wants to claim and one has to, at the end, pay the fee "for other". But this can be solved by paying the fee of claiming reward from inside the reward. A staker would claim the reward, it will first take from the total_reward for the validator and n nominator a small part corresponding to the fee. |
|
I have not updated docs, but the code + tests should tell anyone with an opinion if they like the design or not. Feel free to review, and if it is positive, then I will complete the work needed here. |
|
Superseded by #5406 |
IN PROGRESS: THIS PR NEEDS TO BE POLISHED WITH ADDITIONAL TESTS.
This is a PR which introduces an alternative payout mechanism somewhere in the middle of automatic payouts and lazy payouts.
Main problems to be solved:
Solution:
Create a payout mechanism that pays out to a validator and all nominators for that validator in a single transaction.
Intended Behavior:
Validators are the ones most incentivized to claim rewards as they are the ones most likely to receive the largest cut of the pie. They are also most likely to stay up to date with the network and regularly interact with it through transactions, voting, etc...
Validators want nominators to select them so that they stay in the validator pool.
As a result, we can put the ownership of making payouts to the validators, and allow nominators to get the much preferred "automatic payout"-like system
Changes:
payout_nominatordo_payout_validatoranddo_payout_nominatorlast_rewardsingle value storage tracker withclaimed_rewardsvectorclaimed_rewardsis a vector which keeps track of all the eras a validator has made a claim between the current era and current era - history_depth. This allows for out of order claiming.max_payoutvalidator preference, allowing them to select how many users to pay out to in an era.payout_validatorcallable by anyone where they specify the validator to pay out.ErasStakersClippedwhich redundantly kept the same data around. (Although maybe we should keep it as it would reduce storage read at the time of claiming...)Features:
max_payoutpreference to help in their decision making when selecting nominators. If a validator selectsmax_payout = 1, they are guaranteed to pay a very low fee when claiming rewards, but not many nominators would be incentivized to select them. So a middle ground is good, and the default is currently 64, just like in lazy payouts.TODO:
Get feedback that this is a reasonable direction.
Polish code to make sure it is well optimized for storage/computation.
Benchmark the new extrinsic to compare performance in expected real world scenarios as compared to lazy payouts.
Write comprehensive tests verifying features and functionality.
cc @jacogr @thiolliere @gavofyork