-
Notifications
You must be signed in to change notification settings - Fork 241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Domains staking: Epoch transition and Pending unlocks #1657
Conversation
f354309
to
c8b3f9c
Compare
We add the operator as next operator under new domain. This operator will start to operate for new domain after the new domain epoch transition.
c8b3f9c
to
0c07426
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense to me in general.
…ication and add optimisations for easier readability
ab50b47
to
4cb987a
Compare
@liuchengxu for now, I have removed the Operator info from the state. I think your attack vector makes it more easy to bloat the state |
4cb987a
to
beb4f72
Compare
let mut total_shares = operator.total_shares; | ||
let mut total_stake = operator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may not need to update these 2 variables in every iteration if we introduce a remaining_stake
and update it instead which is also more straightforward IMHO, non-blocker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the total_stake
here will act as remaining_stake
at the end. As for the total_shares
, not sure what you mean here. We would still need track the remaining shares to calculate the nominator's stake. Am I missing something here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean was something like this:
let total_shares = operator.total_shares;
let total_stake = operator
.current_total_stake
.checked_add(&operator.current_epoch_rewards)
.ok_or(TransitionError::BalanceOverflow)?;
let mut remaining_stake = total_stake;
let freeze_identifier = T::FreezeIdentifier::staking_freeze_id(operator_id);
Nominators::<T>::drain_prefix(operator_id).try_for_each(|(nominator_id, nominator)| {
let nominator_share = Perbill::from_rational(nominator.shares, total_shares);
let nominator_staked_amount = nominator_share.mul_floor(total_stake);
let locked_amount = T::Currency::balance_frozen(&freeze_identifier, &nominator_id);
let amount_to_mint = nominator_staked_amount
.checked_sub(&locked_amount)
.unwrap_or(Zero::zero());
// remove the lock and mint any gains
...
// update pool's remaining stake
remaining_stake = remaining_stake
.checked_sub(&nominator_staked_amount)
.ok_or(TransitionError::BalanceUnderflow)?;
// remove any pending deposits for this nominator and then remove the lock
...
Ok(())
})?;
Currently, we updating total_stake
and total_share
and using them to calculate nominator_staked_amount
at the same time, it is not straightforward to see the calculation is correct IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is incorrect. You suggestion does not account for shares that are burned. If you don't update the shares, then nominator_share
will be always be incorrect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by burned shares? If there are 10 total shares and 2 nominators with 3 and 7 shares respectively, the nominator_share
of them should be 3/10
and 7/10
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rounding loss can be captured by remaining_stake
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but it is not captured when calculating the nominator share. If that is possible, please give me an example here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remaining_stake
is initialized to total_stake
said 100, for every nominator we deduct nominator_staked_amount
(assume rounding loss is 0.1
, the value will be 19.9
, 29.9
, and 49.9
respectively) from remaining_stake
, thus after all deduction the remaining_stake
left 0.3
which is the rounding loss.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is what we get with the current implementation. But you are not calculating share_percentage of each nominator after every deposit Withdraw. You are assuming the share_percentage is not changing which is not true. Here we are calculating nominator's share_percentage after every withdrawal so that (total_shares_left/total_stake_remaining) are upto date instead of pre calculating them earlier to account for those small increments of percentage due to a rounding down in the previous withdrawal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are assuming the share_percentage is not changing which is not true.
The share_percentage
is not (and should) changed even in current impl, the only calculation of share is total_shares = total_shares.checked_sub((&nominator.shares))
which does not bring any rounding loss.
But I do see the difference now, in the current impl nominators can get a cut of the rounding loss. In the above example, after the first nominator is processed, the total_stake
becomes 80.1
and the second nominator can withdraw 80.1 * 3/8
while my proposal is 100 * 3/10
and leave all rounding loss to treasury.
/// Minimum number of blocks after which any finalized withdrawls are released to nominators. | ||
type StakeWithdrawalLockingPeriod: Get<Self::BlockNumber>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use the consensus block for this period? We need to use the domain block instead.
IMO, this locking period of withdrawal is introduced in case there are slashes after the user issue withdrawal, such that we can still deduct from the locking withdrawal. The slash happens if there is fraud proof challenging an invalid ER, and the challenge period of ER is measured by domain block (i.e. BlockTreePruningDepth
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this is per spec where once its moves to unlocking state, it is tracked with consensus chain block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems it takes only StakeEpochDuration
domain blocks (currently set to 5) to move into the unlock state, which is much smaller than BlockTreePruningDepth
(currently set to 256).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the parameterization will be revisited once the block tree PR lands, should be a non-blocker for this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is not about the parameterization, StakeEpochDuration
is expected to be small while BlockTreePruningDepth
is expected to be large.
My concern is in the current implementation a malicious operator can submit a fraudulent ER and then deregister immediately, so they can successfully get away from slashing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great to see concrete example TBH.
In either scenario of domain stall, network delay, and too many bundles to fit in one consensus block, can cause no domain bundle included in a consensus block thus no domain block will be driven from that consensus block, so the consensus chain will progress faster than the domain chain, which means the unlocking period (using consensus block) will progress faster than the challenging period (using domain block).
The following example uses BlockTreePruningDepth = 256
, StakeWithdrawalLockingPeriod = 100
and StakeEpochDuration = 5
as the current implementation:
- In consensus chain
#10
and domain chain#10
, a malicious operator submits a fraudulent ER and a deregister request. - After consensus chain progress
#10 -> #12
and domain chain progress#10 -> #12
, the domain epoch transited the deregistration and reached the unlocking state. - The domain chain stalled at
#12
, but the consensus chain progressed as usual#12 -> #112
, and the deregistration was unlock - The consensus chain receives a fraud proof (which is acceptable until domain block
#266
) but can not slash the malicious operator as it is already deregistered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My issue with using domain blocks is that we would need to track every domains block height in the unlocks list then. Is that a valid concern?
I think it won't bring extra cost compared to the current approach, we just need to change the index of the unlock list from consensus_block_number
to (domain_id, domain_block_number)
unless I miss something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it won't bring extra cost compared to the current approach, we just need to change the index of the unlock list from consensus_block_number to (domain_id, domain_block_number) unless I miss something.
Not just that, but we also need to adjust the other Storage items like PendingWithdrawals. This will be also be scoped such that each with withdrawal is on FIFO order. May not be such a trivial change since the underlying assumption has changed.
StakeEpochDuration = 5
This is not the final number and has a TODO to revisit this number.
Looking at the example there, I dont see that practically but this was the known concern during our offline discussion.
I do not have a strong opinion here but we need to update spec accordingly once we make the decision since calling it Global pending unlocks is confusing as well.
The change will be handled as either a separate PR or part of the epoch PR that uses block tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be also be scoped such that each with withdrawal is on FIFO order.
The domain block number is also monotonically increasing as the consensus block, thus this is preserved.
StakeEpochDuration = 5
This is not the final number and has a TODO to revisit this number.
No matter how large this number can be the malicious operator can always choose to attack at the last few blocks of the current epoch, that is why I use 12 in the above example instead of 15.
The change will be handled as either a separate PR or part of the epoch PR that uses block tree.
I do think it is an issue we need to address but I'm okay with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think it is an issue we need to address
Aboslutely, please start the discussion on the spec v2 regarding or open existing one. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable to me.
/// Minimum number of blocks after which any finalized withdrawls are released to nominators. | ||
type StakeWithdrawalLockingPeriod: Get<Self::BlockNumber>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the parameterization will be revisited once the block tree PR lands, should be a non-blocker for this PR.
This PR brings the following changeset
The first 6 commits add the required functionality for domain epoch transition and Pending unlocks
The remaining two commits connect all the pieces introduced in the previous 6 commits
Would suggest going commit by commit for this one
Closes: #1648
Code contributor checklist: