IntersectMBO / cardano-ledger

The ledger implementation and specifications of the Cardano blockchain.
Apache License 2.0
261 stars 157 forks source link

Change to how SPO votes are counted #4645

Closed lehins closed 1 month ago

lehins commented 1 month ago

It has been brought up by a number of community members that the way we currently compute SPO votes is not what everyone expected. Current implementation is such that for HardForkInitiation behavior is exactly the same as for DReps, however for the other three governance actions, that SPOs can vote on, behavior is different for SPOs that did not vote. In other words for these three governance action types:

the default vote counted for SPOs that didn't vote is Abstain, instead of No. The default vote of No is the behavior for DReps, while for SPOs it is default only when HardForkInitiation is proposed.

Current behavior is specified in this portion of the spec and the actual implementation lives here: https://github.com/IntersectMBO/cardano-ledger/blob/8d16d698dd499fd171845da16611c7c7026c1754/eras/conway/impl/src/Cardano/Ledger/Conway/Rules/Ratify.hs#L204-L207

This lead to some concerns about inclusion of SPOs into the future of parameter updates, that is why many have suggested that we should mimic the behavior of DReps exactly. However, that defaulting of SPOs to vote No, has its own concerns. In particular there is a danger if SPOs aren't active enough that they could inadvertently block sensible proposals. Inactivity could be potentially blamed either on indifference or issues with legality for some SPOs.

In order to solve this conundrum we made it the topic of Ledger Working Group Meeting #4 and had a very productive discussion with some community members and came up with an ingenious solution:

Everyone agreed right of the bat that if we had a mechanism for SPOs to choose whether they wanted to default to Abstain instead of No, we would have solved the problem. However making a change to PoolParams at this time is impossible without a new era. Also, whenever this issue of customizing PoolParams has surfaced during Conway development, we knew that making such a change would have been too ambitious of a goal, which would have lead to an extensive delay to the Conway delivery. This has to do with the fact that PoolParams is not a type family yet. Therefore, since adding a custom field to the PoolParams with an optional default vote was not a path that we could choose we almost discarded this idea. It was then that Martin Lang of ATADA Stake Pool proposed an interesting and unexpected solution: we can achieve ability of specifying a default vote for SPOs if we choose to mimic the default delegation of ppRewardAccount that is specified in the PoolParams.

To be more specific the solution to this problem is:

  1. To change the default vote for SPOs to No from Abstain for all of the three proposals in question
  2. Check whether SPO's reward address is delegated to the predefined AlwaysNoConfidence DRep and count default vote as Yes only on NoConfidence proposals for that SPO.
  3. Check whether SPO's reward address is delegated to predefined AlwaysAbstain DRep and count default vote as Abstain for that SPO on all three: NoConfidence, UpdateCommitee and ParameterUpdate proposals.

This is not a terribly complicated logic to implement, which means that we can almost certainly take care of in time for the protocol version 10.0, i.e. the intra-era hard fork, a.k.a Chang+1

WhatisRT commented 1 month ago

I think step 1 is a bad idea, since it still forces participation in governance to return to Abstain. What's wrong with leaving it to default to Abstain? Then SPOs that want to actively participate can change their default and SPOs that don't want to participate don't have to do anything. If you go with the hacky solution for delegation you can just check if there's any DRep delegation in there to default to No.

There's also the issue that this solution means that whatever Ada is associated with that particular stake credential now has to be delegated to a predefined vote. That means that the hacky solution will likely have to be in the code eternally since it affects voting outcomes.

I don't understand the issue with adding a field of type Maybe Vote to PoolParams and just not making it a type family. Is it that it would require CDDL changes?

gitmachtl commented 1 month ago

The issue is that a single SPO pool voting YES can change the whole SPO acceptance to 100%. That was the reason to change it to default NO, so this cannot happen anymore. Setting all to abstain would lead into the same result.

Here is the link to the recording of the meeting to rewatch it: https://drive.google.com/file/d/1_Y0K9mzX6k7gMXDGIB7Gbpgzhj3JlitD/view

WhatisRT commented 1 month ago

Yes, I'm aware of that, here's the counterargument I've given before:

I think the whole 'one SPO can single-handedly approve an action' argument is somewhat overblown. It's rather 'one SPO can single-handedly approve an action if all others ignore it while at the same time at least half of the DReps approve it'. Sounds very far-fetched to me. The real danger is that some SPOs don't pay attention in the time frame where the DReps approve the action and it gets just barely in because of that. But you have to weigh that against forcing exchanges to cast votes.

So let's say that most exchanges don't do anything and 20% of the stake defaults to No now. That means that for the remaining SPOs to pass anything, 62.5% of the votes need to be Yes if nobody abstains. And that number just goes up in the presence of abstention. If 20% abstain as well (as some other exchanges would probably do) you now need 83.3% in favour. Good luck passing anything.

I don't know what will actually happen, but hoping for a good scenario here seems way too risky. Having some extreme looking behaviour right at the start of a proposal that's going to stabilize seems like a much smaller issue than permanently skewing the vote to me.

disassembler commented 1 month ago

My opinion is we go forward with this proposal. Yes, there's nothing that forces an exchange to delegate their rewards address to abstain, but this at least gives them the option to do so. Note that to claim rewards, they already will have to delegate their rewards address anyways, and most likely they'll pick abstain, so that feature alone could get large players to opt-out of voting on anything other than hard fork initiation proposals.

disassembler commented 1 month ago

I do agree that a field on the pool registration certificate is a better design; however we ruled that out months ago because of the time it would take to trickle down the CDDL changes to hw wallets and other tools integrating pool registration certificates.

WhatisRT commented 1 month ago

Note that if we let SPOs pick their default, the initial instability is mitigated even more. If, say, 10% of stake defaults to No, you now need 10% of other stake to stealthily vote to overtake them. At that point this doesn't even seem like a problem to me anymore, and defaulting everyone to No only has the downside of forcing everybody who doesn't want to participate to do something.

kevinhammond commented 1 month ago

Agreed, this seems like a sensible solution. Where it differs, we will make sure the CIP text is consistent with it. Thank you to @gitmachtl for making the suggestion, and to @lehins for finding a way to implement it easily.

lehins commented 1 month ago

I don't understand the issue with adding a field of type Maybe Vote to PoolParams and just not making it a type family. Is it that it would require CDDL changes?

Yes, it would require CDDL changes. For this particular case not making it a type family and adding an optional field would work. Yet again, we do need a new era for this, because we can't change serialization for intra-era hardfork. Maybe, if I knew about this issue early enough we could have solved it in Conway by adding that optional field. But, I only learned about it a week ago.

Cerkoryn commented 1 month ago

I had no idea that SPO votes defaulted to abstain when nothing happened. I just assumed they were treated the same as dReps where inaction means an implicit 'no'. In that case, I have a 4th suggestion @lehins and @disassembler.

Regarding this...

So let's say that most exchanges don't do anything and 20% of the stake defaults to No now. That means that for the remaining SPOs to pass anything, 62.5% of the votes need to be Yes if nobody abstains. And that number just goes up in the presence of abstention. If 20% abstain as well (as some other exchanges would probably do) you now need 83.3% in favour. Good luck passing anything.

I'd previously joined a couple of SPO calls to voice my concern over precisely this issue. My suggestion...

  1. Add a new spoActivity parameter that functions exactly like drepActivity but for SPOs. So if an SPO hasn't voted in that many epochs (currently 20 epochs for dReps), they are switched to abstain and removed from quorum.

That way CEXes or SPOs who don't do anything will simply be removed after a period of time.

Is there any reason why we can't treat both dReps and SPOs the same way? Occam's Razor.

zhekson1 commented 1 month ago

@Cerkoryn The issue I see here is that SPO's will inherently be voting less than dReps, and with a focus on proposals directly relating to infrastructure (as they should be). So if spoActivity countdown is set similarly to drepActivity, it may be the case that most SPOs don't vote for this period, are removed from the quorum, and soon we are back where we started with a very low SPO quorum. If we counteract this by setting spoActivity to much higher than drepActivity, the inverse can quickly become an issue: SPO-relevant parameters progress very slowly due to the high number of (almost always defaulted) "No"s.

IMO giving SPO's more granular optionality in their participation would be more fruitful than trying to arrive at the "right number" for an spoActivity parameter.

Pdest08 commented 1 month ago

Keep it simple: Force SPOs to choose their default when booting up their node

adamrusch commented 1 month ago

I think the whole 'one SPO can single-handedly approve an action' argument is somewhat overblown. It's rather 'one SPO can single-handedly approve an action if all others ignore it while at the same time at least half of the DReps approve it'. Sounds very far-fetched to me. The real danger is that some SPOs don't pay attention in the time frame where the DReps approve the action and it gets just barely in because of that. But you have to weigh that against forcing exchanges to cast votes.

I made a similar argument in the Working Group meeting because I think the possibility of locking up governance over SPO lack of participation is a far greater danger than underrepresentation of SPOs if they don't cast votes.

However, if we have the ability to offer an auto-abstain option with relatively few trade-offs that is a far more elegant solution that keeps default SPO actions in line with DReps and the CC.

What is incredibly shocking here is that we had such a huge discrepancy come up between the specs/implementation and what I see as clear language in the text of the CIP. We spent months socializing the fact that SPOs must cast votes and cannot auto-abstain. We set the SPO related governance parameters based on the understanding that these thresholds must be met as a percentage of the stake held by all stake pools. These things cannot be taken lightly.

Hornan7 commented 1 month ago

Default Abstain without a 'quorum' requirement presents significant risks to what I view as our last line of defense against bad behavior—the SPOs.

What’s being proposed here, in my opinion, offers the best of both worlds, at least for now. I tested a catastrophic scenario on SanchoNet that effectively worked:

Then, 5 minutes before the Epoch boundary:

This scripted approach effectively triggered the CC change without anyone noticing before the calculations occurred, leaving no time for inactive DReps to respond as well.

In conclusion. Ultimately, as inconvenient as it might be, SPOs cannot stall governance by themselves if they dont engage; thresholds can always be changed, requiring only DReps and CC votes. The same applies to changing the guardrails within the constitution itself, which is why I strongly prefer getting back to the Default No with the added voting options for SPOs as proposed here.

-- Mike Hornan 😃👍

lehins commented 1 month ago

Add a new spoActivity parameter that functions exactly like drepActivity but for SPOs. So if an SPO hasn't voted in that many epochs (currently 20 epochs for dReps), they are switched to abstain and removed from quorum.

@Cerkoryn Despite that this would be a feasible solution, it would be pretty complicated to implement. It is one of the more complex features that DReps have. So, we definitely would not be able to implement it in time for the next intra-era hard fork. Moreover, we would have to hard code a value for this new "spoActivity" parameter, because it would not be an actual amendable protocol parameter yet, since we can't add new parameters without a new era. That being said, nothing is preventing us from adding this as a feature in the next era, but that feature would require a whole new CIP.

Is there any reason why we can't treat both dReps and SPOs the same way? Occam's Razor.

Yes, we can't use Occam's Razor, because at this point it is not the simplest solution from technical point of view.

Keep it simple: Force SPOs to choose their default when booting up their node

@Pdest08 Can't be done, because booting of a node is not reflected on chain.

We spent months socializing the fact that SPOs must cast votes and cannot auto-abstain.

@adamrusch I think the real problem here is that the CIP and spec designers had no participation in those months of socializing. If there was better inclusion of architects into discussions with the community, I suspect we would not be discussing this problem right now. In the three years I was with IOG I personally attended only three events that involved members of community. In my opinion all three of those events were incredibly useful for me as the leader of the Ledger team. I even have an example of this, remember @adamrusch how we got a chance to talk about another confusion with respect to the wording about thresholds in particular 51% vs 50% + 1 Lovelace. Maybe no changes to the code came out of it, but it certainly improved the clarity and understanding of the system. Resolution of ambiguities can only happen through proper discussions. So, maybe this is something we can improve on by including developers in community events a lot more often. That is, of course, not my call and not a decision that I can influence. Even without live events, I feel like we are moving more and more towards better communication between developers and community. Working groups meetings and discord channels are a great testament to that. On Monday we were able to get together in the Ledegr Working group meeting and address concerns of the community with a solution that came out of a community member. Which is pretty amazing in my opinion.

I registered an incredibly powerful DRep and voted yes on it, counterbalancing the remaining active DReps.

@Hornan7 That is something you wouldn't be able to do on Mainnet. Although maybe I am wrong and you do have a secret stash with billions of Ada. :wink:

The same applies to changing the guardrails within the constitution itself, which is why I strongly prefer getting back to the Default No with the added voting options for SPOs as proposed here.

So, it seems to me that everyone is so far in favor of this new solution proposed in this ticket. @WhatisRT is the only one that isn't sold on it 100% percent, but I haven't heard from him any viable concerns against this approach either. Therefore, unless anything changes before Chang+1 we are still planning on implementing this solution.

Hornan7 commented 1 month ago

@Hornan7 That is something you wouldn't be able to do on Mainnet. Although maybe I am wrong and you do have a secret stash with billions of Ada. :wink:

With the current amount of ADA delegated to DReps on mainnet (553M live stake), I can assure you that I know entities (not myself) capable of executing exactly that in the first Epoch of Chang +1 if the proposed changes aren't implemented.

While it's unlikely to happen, the mere possibility is still a concern for me. But hey, that's what we're here for, right? 😅😇

lehins commented 1 month ago

With the current amount of ADA delegated to DReps on mainnet (553M live stake)

@Hornan7 Good thing we are not ending bootstrap period now! :smile:

Considering that we've only enabled DReps 3 and a half weeks ago .5B ADA is a really good progress. So, a more realistic estimation of live stake for DReps would be some extrapolation of current progress of DRep registration and delegation, rather than assuming we'll be stuck and half a billion ADA in two months at Chang+1

While it's unlikely to happen, the mere possibility is still a concern for me. But hey, that's what we're here for, right?

I do hear your concern though. Which is why we are changing the behavior. :wink:

Hornan7 commented 1 month ago
  • If you want to revert to a default No, you have to delegate to a DRep, which you might not want to. You can make a 'garbage DRep' to solve this problem, but it's a bit ugly.

On Chang + 1 they will have to delegate their voting power anyway if they want to be able to withdraw their pool reward. So giving them the additional opportunity to passively "auto-abstain" or "No-confidence" with their Stake pool on top of what they already have, sounds right to me.

  • A SPO that doesn't want to vote themself but wants to delegate to a DRep is stuck in a difficult position: either manually vote Abstain on everything, or delegate to AlwaysAbstain and not the the intended DRep. Note that this affects at least the pledge of the pool, other funds could be moved to a different stake credential.

It might not be super popular right now but I have a nice script that can batch vote on all governance actions (votable by SPOs) into 1 single transaction (up to 400 votes per Tx) if they want to delegate to a specific DRep and still actively Abstain with their pool.

WhatisRT commented 3 weeks ago

It's probably too late for this, but after reviewing the code for the spec for this, I think it would have made more sense to use the delegation of the pool itself instead of its stake credential. There are two reasons for this:

As an added bonus, that solution wouldn't have locked up the pledge for a default vote.

lehins commented 3 weeks ago

I think it would have made more sense to use the delegation of the pool itself instead of its stake credential

Could you please elaborate on this. I don't understand what it means "delegation of the pool itself". How can a pool delegate to anything?

lehins commented 3 weeks ago

It's probably too late for this

It's definitely too late for Conway. But, if you have clever ideas that we could use for the next era, I am all ears.

lehins commented 3 weeks ago

Thinking a bit more about this alternative solution I think I am starting to understand what is being suggested. It is phrased a bit ambiguously, but here is my understanding of it.

  1. Pool operator would register a stake credential using the same private key as it is used to register the stake pool.
  2. Whenever we would be looking up a default vote for an SPO, instead of using a reward account from the pool params, we would look if the poolId is also registered as a stake credential.
  3. Then we would resolve its delegation to a DRep and then proceed like we do in the current solution that has already been implemented: namely check if that stake credential (i.e poolId) has delegated to a predefined DRep and use that as a default vote.

@WhatisRT Please correct me if I misunderstood it.

In my opinion it is strictly worse than the current solution of using the reward account specified in the pool params, because current solution provides a motivation for that reward account to be delegated to a DRep. In particular, thinking of Stake Pools that are operated by exchanges, current solution forces them to delegate to a DRep, which is most likely going to be an AlwaysAbstainDRep. If we were to use this hack of using a poolId instead, then this motivation is gone. In fact there would be no motivation for an SPO to register a stake credential using the Pool's Key and we would be back to square one.

With respect to a more permanent solution in the future, it will make more sense to actually add this decision of default vote as a pool parameter, rather than relying on such indirections. Which is something we can target for the next era. Note that during era translation we'll be able to easily populate that new field with the value that is decided by the current mechanism.

So, in the end, I think, our current solution is the best one from all perspectives: