substrate icon indicating copy to clipboard operation
substrate copied to clipboard

Enable an easy way to unbond, if you are not `*exposed*`

Open kianenigma opened this issue 4 years ago • 10 comments

For numerous reasons, we might have nominators who don't end up in the exposure, because of resource limits of the offchain solution submitted to the chain. So the setting is:

  1. They still do get their opinion imposed on the election result.
  2. They cannot be slashed.
  3. Nor can they be rewarded, both because they are not in anyone's Exposure.

We can, in practice, let someone who's unbonding off the hook (and let them not wait the whole BondingDuration) if we know that they are not in the Exposure of any validator, both in the current era and all the previous BondingDuration eras.

With the current storage layout, doing this is quite expensive. I think that transaction would cost a lot -- potentially not even fitting into a single block, so you probably have a better time waiting for the BondingDuration.

That being said, I want to open this meta-issue to bring up that this is a matter that could be improved here. Usually, I would say that this is a matter of UX, and good UX is not the main priority of the protocol. Nonetheless, it would be good to discuss if any other options exist to achieve this, without sacrificing security.

Inspired by comment from @horaciob https://github.com/paritytech/polkadot/issues/2418#issuecomment-804918648

kianenigma avatar Mar 23 '21 15:03 kianenigma

We could touch all nominators after every election to record "when last exposed", not sure what happens currently there.

In principle, we could provide some tool off-chain that crawls the old exposure sets of their current nominations, which plays okay with BEEFY, except all those past block references gets heavy.

burdges avatar Mar 23 '21 15:03 burdges

We could touch all nominators after every election to record "when last exposed", not sure what happens currently there.

That's one way. But of course we don't do this right now, and doing so won't be trivial. We would need to touch tens of thousands of accounts, as opposed to the current validator-major exposures that are stored, which is only a few hundred keys.


Alternatively, we could also store a separate storage item that stored the exposed nominators per era. But this is even more expensive than the previous solution.

All in all, both of these need us to do a lot more work upon finishing an election on the staking side. We can only allow such a thing to happen as a kind of background task (#8197) or if we finish the election multi-block.


@thiolliere one way or another, reducing history depth will help with this. Any reason to keep this at the current, seemingly overkill 84?

kianenigma avatar Jun 22 '21 11:06 kianenigma

I'm not convinced we really need this honestly.. it's definitely nicer ux.. but does it buy us any concrete benefit?

burdges avatar Jun 22 '21 13:06 burdges

For anyone who bonded by mistake, or bonded but is not capable of participating in nomination, this would be useful. It is not important, as you said. Just nice ux.

kianenigma avatar Jun 22 '21 14:06 kianenigma

I see.. yes, we could do bonded and never nominated rather cleanly. We could likely handle bonded but chilled cleanly too. Also bonded but all nominations dropped out or kicked me. It's maybe simpler to cover all special cases like these directly? It's bonded but my nominations never win that sucks, although maybe not wholly impossible even there. Could we just make issues for all special cases and mark them as "good first issue" or whatever?

burdges avatar Jun 22 '21 17:06 burdges

@thiolliere one way or another, reducing history depth will help with this. Any reason to keep this at the current, seemingly overkill 84?

But history depth is not related to bounding duration, as far as I can see. history depth is for giving time to call the reward. And slashing is using information in pallet-session-historical so not related to history depth, no ?

gui1117 avatar Jun 23 '21 07:06 gui1117

@thiolliere one way or another, reducing history depth will help with this. Any reason to keep this at the current, seemingly overkill 84?

But history depth is not related to bounding duration, as far as I can see. history depth is for giving time to call the reward. And slashing is using information in pallet-session-historical so not related to history depth, no ?

I just looked at the fact that to prove that you are not exposed you have to prove that you are not in ErasStakers, and this double map is pruned after HistoryDepth.

Although, I think it is enough to prove that you are not in the BondingDuration last eras of ErasStakers, not the whole thing, so yeah it is unrelated.

NOTE: should we not have an integrity test to assert HistoryDepth > BondingDuration?

kianenigma avatar Jun 23 '21 07:06 kianenigma

NOTE: should we not have an integrity test to assert HistoryDepth > BondingDuration?

I actually think it is correct to have BondingDuration higher than HistoryDepth. BondingDuration store only the hash of the exposure on chain while HistoryDepth store the full exposure, so it is usually sensible to have HistoryDepth < BondingDuration

gui1117 avatar Jun 23 '21 07:06 gui1117

With @rossbulat we came up with a different approach to solving this:

By default, in the current polkadot config, the cost of checking if someone is exposed or not is 28 * 300 = 8400 storage reads. In kusama, it is 28000. The reason is that we have to check every validator, every era. Ross's suggestion was that if we knew historical nomination info, we can only check a smaller subset of validators.

We create a new map, called HistoricalNominations = Map<T::AccountId, BoundedVec<(T::AccountId, EraIndex)>>. The key is the nominator, the value is a vector, representing the union of all of the validators that this nominator has nominated, combined with the era at which this nomination happened.

  1. upon nominate call:
  2. We read this union/vector, and prune the old eras that are old enough. This is one storage read, and a small in-memory pruning.
  3. We compute the new union, and store it*.
  4. the entire existing unstake function remains the same. Instead, upon a new call unstake_fast, we check the exposure of all the validators that exist in the origin's union storage.

Needless to say, for this to work, this union vector will be bounded, e.g. 128. This implies that:

  1. Within any window of 28 days, the union of all the validators that a nominator nominates need to be bounded.
  2. The worse case tx-fee and weight of the unstake_fast will be this bound, multiplied by 28.

The only missing piece here is that I really want to make this opt-in. This functionality is only useful for those who are ACTUALLY not exposed. Within an ideal staking system, we won't have that many of these people around, so constraining everyone for the sake of this small minority is wrong.

kianenigma avatar Jul 31 '22 16:07 kianenigma

To throw another idea out there, we could eliminate the EraIndex in the map if we have a "try before you buy" model, where there is a set limit of 28 eras to quick-unbond from opting in, in the event the nominator is not exposed.

Users can opt-in when they call nominate. HistoricalNominations would then be:

HistoricalNominations = Map<T::AccountId, (when: T::BlockNumber, validators: BoundedVec<T::AccountId>)>

where when is the block they opted-in (batch this call with nominate.

This is more of a convenience function for new users who may not be aware of how nominations work and therefore choose totally inactive nominations. After 28 ears pass, perhaps storage could be pruned somehow within another call.

rossbulat avatar Aug 01 '22 08:08 rossbulat

I’ve thought quite deeply about how a quick unstake system can be introduced in staking. Here are my findings.

A nominator should be able to immediately unstake if their nominations have not been exposed in the last BondingDuration eras. But it is extremely costly to check this. Instead of iterating the entirety of ErasStakers, we could just keep track of a nominator's nominees, and check whether that subset have been exposed in the last BondingDuration eras.

Such a feature will not be useful for everyone, so this should ideally be an opt-in feature.

We want to do this in a way that is easy to clean up stale era data. The most efficient way to do this is by calling a single remove_prefix with the expired era in question.

To achieve this, we introduce 2 new storage items:


New Storage items

A StorageDoubleMap to hold a nominator’s validators per era:

HistoricalNominations:  (Era, Nominator) => [...Validators]  // BoundedVec<64>

We also introduce a StorageMap to keep track of when fast unstake starts, and which eras a nominator sets their nominees to provide a means to refer to the previous storage map:

HistoricalNominatorEras: Nominator => (start_era, [...Eras]) // BoundedVec<28>

We now have a means of tracking which eras nominees are set, and which nominees were set per era, in a way we can easily remove outdated records.


Managing stale storage

We can use clear_era_information to remove HistoricalNominations items that have surpassed BondingDuration eras:

// this ensures there will always be a maximum of 64 * 28 validator exposures to check.
HistoricalNominations::remove_prefix(old_era)

New calls

register_fast_unstake()

  • For new nominators, this should be called before bonding and nominating, as this guarantees there are no outstanding exposures. start_era will be the current era.
  • For existing nominators, they will need to wait at least BondingDuration eras before exposures can be determined. start_era will be the current era + BondingDuration.
  • Initiates HistoricalNominatorEras with an empty array for the era indexes.

unregister_fast_unstake()

  • Removes HistoricalNominatorEras and any HistoricalNominations.

fast_unstake()

  • Aggregates all unique validators of HistoricalNominations and checks whether they have been exposed. If none have been exposed, the fast unstake succeeds, otherwise it fails.

Updates to existing calls

nominate(): Updates HistoricalNominations and HistoricalNominatorEras. Prunes HistoricalNominatorEras of stale eras.

withdraw(): Removes HistoricalNominationsand HistoricalNominatorEras as user is no longer staking.


Shortfalls:

  • No existing historical nominee records: Upon opting in to fast unstake, an existing nominator will still need to wait at least BondingDuration eras before we know whether their nominees have been exposed.
  • Lack of incentive to unregister: A deposit or some sort of incentive to opt-in / keep fast stake active (> deposit = > duration) could be introduced and stored in HistoricalNominatorEras.

Benefits

  • Unstake if not exposed: Ability to call fast_unstake() if no exposures.

Other Possible Benefits:

  • Immediate unstake before nominator's first era: By just having HistoricalNominatorEras, we can derive when a nominator first started nominating and allow them to unstake before they take part in their first election cycle.
  • Inactive nomination pools can offer fast unstake: Pool owners could register to fast unstake and allow any pool member to withdraw their funds if the pool has no exposures.

rossbulat avatar Aug 12 '22 10:08 rossbulat